From 2a76a235484b7a36b35235e42ee3e4ef4a8d6aa6 Mon Sep 17 00:00:00 2001 From: Ron Young Date: Thu, 3 Jan 2019 11:06:10 -0600 Subject: [PATCH] Add RuneliteColorPicker Co-authored-by: psikoi --- .../runelite/client/config/ConfigManager.java | 3 +- .../client/plugins/config/ConfigPanel.java | 51 +-- .../ui/components/colorpicker/ColorPanel.java | 207 ++++++++++ .../colorpicker/ColorValuePanel.java | 143 +++++++ .../colorpicker/ColorValueSlider.java | 111 ++++++ .../ui/components/colorpicker/HuePanel.java | 151 ++++++++ .../components/colorpicker/PreviewPanel.java | 65 ++++ .../colorpicker/RuneliteColorPicker.java | 357 ++++++++++++++++++ 8 files changed, 1062 insertions(+), 26 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValuePanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValueSlider.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/HuePanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/PreviewPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/RuneliteColorPicker.java diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index f59dc6acbf..2cde4900ad 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -60,6 +60,7 @@ import net.runelite.api.events.ConfigChanged; import net.runelite.client.RuneLite; import net.runelite.client.account.AccountSession; import net.runelite.client.eventbus.EventBus; +import net.runelite.client.util.ColorUtil; import net.runelite.http.api.config.ConfigClient; import net.runelite.http.api.config.ConfigEntry; import net.runelite.http.api.config.Configuration; @@ -482,7 +483,7 @@ public class ConfigManager } if (type == Color.class) { - return Color.decode(str); + return ColorUtil.fromString(str); } if (type == Dimension.class) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index f35d131515..b71d5e8b0e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java @@ -48,11 +48,8 @@ import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; -import javax.swing.JColorChooser; import javax.swing.JComboBox; -import javax.swing.JComponent; import javax.swing.JFormattedTextField; -import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; @@ -63,6 +60,7 @@ import javax.swing.JTextArea; import javax.swing.ScrollPaneConstants; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; @@ -90,6 +88,8 @@ import net.runelite.client.ui.PluginPanel; import net.runelite.client.ui.components.ComboBoxListRenderer; import net.runelite.client.ui.components.IconButton; import net.runelite.client.ui.components.IconTextField; +import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; +import net.runelite.client.util.ColorUtil; import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; @@ -400,47 +400,48 @@ public class ConfigPanel extends PluginPanel String existing = configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName()); Color existingColor; - JButton colorPicker; + JButton colorPickerBtn; if (existing == null) { existingColor = Color.BLACK; - colorPicker = new JButton("Pick a color"); + colorPickerBtn = new JButton("Pick a color"); } else { - existingColor = Color.decode(existing); - colorPicker = new JButton("#" + Integer.toHexString(existingColor.getRGB()).substring(2).toUpperCase()); + existingColor = ColorUtil.fromString(existing); + colorPickerBtn = new JButton(ColorUtil.toHexColor(existingColor).toUpperCase()); } - colorPicker.setFocusable(false); - colorPicker.setBackground(existingColor); - colorPicker.addMouseListener(new MouseAdapter() + colorPickerBtn.setFocusable(false); + colorPickerBtn.setBackground(existingColor); + colorPickerBtn.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - final JFrame parent = new JFrame(); - JColorChooser jColorChooser = new JColorChooser(existingColor); - jColorChooser.getSelectionModel().addChangeListener(e1 -> + RuneliteColorPicker colorPicker = new RuneliteColorPicker(SwingUtilities.windowForComponent(ConfigPanel.this), + colorPickerBtn.getBackground(), cid.getItem().name(), cid.getAlpha() == null); + colorPicker.setLocation(getLocationOnScreen()); + colorPicker.setOnColorChange(c -> { - colorPicker.setBackground(jColorChooser.getColor()); - colorPicker.setText("#" + Integer.toHexString(jColorChooser.getColor().getRGB()).substring(2).toUpperCase()); + colorPickerBtn.setBackground(c); + colorPickerBtn.setText(ColorUtil.toHexColor(c).toUpperCase()); }); - parent.addWindowListener(new WindowAdapter() + + colorPicker.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - changeConfiguration(listItem, config, jColorChooser, cd, cid); + changeConfiguration(listItem, config, colorPicker, cd, cid); } }); - parent.add(jColorChooser); - parent.pack(); - parent.setVisible(true); + colorPicker.setVisible(true); } }); - item.add(colorPicker, BorderLayout.EAST); + + item.add(colorPickerBtn, BorderLayout.EAST); } if (cid.getType() == Dimension.class) @@ -556,7 +557,7 @@ public class ConfigPanel extends PluginPanel scrollPane.getVerticalScrollBar().setValue(0); } - private void changeConfiguration(PluginListItem listItem, Config config, JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid) + private void changeConfiguration(PluginListItem listItem, Config config, Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) { final ConfigItem configItem = cid.getItem(); @@ -588,10 +589,10 @@ public class ConfigPanel extends PluginPanel JTextComponent textField = (JTextComponent) component; configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), textField.getText()); } - else if (component instanceof JColorChooser) + else if (component instanceof RuneliteColorPicker) { - JColorChooser jColorChooser = (JColorChooser) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), String.valueOf(jColorChooser.getColor().getRGB())); + RuneliteColorPicker colorPicker = (RuneliteColorPicker) component; + configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), colorPicker.getSelectedColor().getRGB() + ""); } else if (component instanceof JComboBox) { diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorPanel.java new file mode 100644 index 0000000000..210de8f799 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorPanel.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.components.colorpicker; + +import com.google.common.primitives.Ints; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.image.BufferedImage; +import java.util.function.Consumer; +import javax.swing.JPanel; +import lombok.Setter; + +public class ColorPanel extends JPanel +{ + private static final int SELECTOR_RADIUS = 7; + + private final int size; + + private BufferedImage image; + private Point targetPosition; + private int selectedY; + private boolean forceRedraw; + + @Setter + private Consumer onColorChange; + + ColorPanel(int size) + { + this.size = size; + this.image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); + this.targetPosition = new Point(size, 0); + setPreferredSize(new Dimension(size, size)); + + addMouseMotionListener(new MouseMotionAdapter() + { + @Override + public void mouseDragged(MouseEvent me) + { + moveTarget(me.getX(), me.getY(), true); + } + }); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseReleased(MouseEvent me) + { + moveTarget(me.getX(), me.getY(), true); + } + + @Override + public void mousePressed(MouseEvent me) + { + moveTarget(me.getX(), me.getY(), true); + } + }); + } + + /* + * Sets the gradient's base hue index. + */ + void setBaseColor(int selectedY) + { + if (this.selectedY == selectedY) + { + return; + } + + this.selectedY = selectedY; + redrawGradient(); + + if (onColorChange != null) + { + onColorChange.accept(colorAt(targetPosition.x, targetPosition.y)); + } + + paintImmediately(0, 0, size, size); + } + + /* + * Move the indicator to the closest color without firing change event. + */ + void moveToClosestColor(int y, Color color) + { + Point closest = closestPointToColor(color); + if (this.selectedY == y && closest.x == targetPosition.x && closest.y == targetPosition.y) + { + return; + } + + this.selectedY = y; + redrawGradient(); + moveTarget(closest.x, closest.y, false); + } + + /* + * Calculates the closest point to a given color. + */ + private Point closestPointToColor(Color target) + { + float[] hsb = Color.RGBtoHSB(target.getRed(), target.getGreen(), target.getBlue(), null); + int offSize = size - 1; + + return new Point((int) (hsb[1] * offSize), offSize - (int) (hsb[2] * offSize)); + } + + /** + * Moves the target (selector) to a specified x,y coordinates. + */ + private void moveTarget(int x, int y, boolean shouldUpdate) + { + if (targetPosition.x == x && targetPosition.y == y && !forceRedraw) + { + return; + } + + x = Ints.constrainToRange(x, 0, size - 1); + y = Ints.constrainToRange(y, 0, size - 1); + + targetPosition = new Point(x, y); + paintImmediately(0, 0, size, size); + + if (onColorChange != null && shouldUpdate) + { + onColorChange.accept(colorAt(x, y)); + } + forceRedraw = false; + } + + @Override + public void paint(Graphics g) + { + // Paint the gradient + g.drawImage(this.image, 0, 0, null); + + int targetX = targetPosition.x - (SELECTOR_RADIUS / 2); + int targetY = targetPosition.y - (SELECTOR_RADIUS / 2); + + // Paint the target (selector) + g.setColor(Color.WHITE); + g.fillOval(targetX, targetY, SELECTOR_RADIUS, SELECTOR_RADIUS); + g.setColor(Color.BLACK); + g.drawOval(targetX, targetY, SELECTOR_RADIUS, SELECTOR_RADIUS); + } + + /* + * Draws a 3-color gradient based on white, black, and current hue index. + */ + private void redrawGradient() + { + Color primaryRight = Color.getHSBColor(1f - this.selectedY / (float) (size - 1), 1, 1); + Graphics2D g = image.createGraphics(); + GradientPaint primary = new GradientPaint( + 0f, 0f, Color.WHITE, + size - 1, 0f, primaryRight); + GradientPaint shade = new GradientPaint( + 0f, 0f, new Color(0, 0, 0, 0), + 0f, size - 1, Color.BLACK); + g.setPaint(primary); + g.fillRect(0, 0, size, size); + g.setPaint(shade); + g.fillRect(0, 0, size, size); + + g.dispose(); + forceRedraw = true; + } + + /* + * Determines which color is displayed on the gradient at x,y. + */ + private Color colorAt(int x, int y) + { + x = Ints.constrainToRange(x, 0, size - 1); + y = Ints.constrainToRange(y, 0, size - 1); + return new Color(image.getRGB(x, y)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValuePanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValuePanel.java new file mode 100644 index 0000000000..08110b01b6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValuePanel.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.components.colorpicker; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.util.function.Consumer; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DocumentFilter; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.util.ColorUtil; + +public class ColorValuePanel extends JPanel +{ + private static final int DEFAULT_VALUE = ColorUtil.MAX_RGB_VALUE; + + private ColorValueSlider slider = new ColorValueSlider(); + private JTextField input = new JTextField(); + + private Consumer onValueChanged; + + void setOnValueChanged(Consumer c) + { + onValueChanged = c; + slider.setOnValueChanged(c); + } + + ColorValuePanel(String labelText) + { + setLayout(new BorderLayout(10, 0)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + input.setBackground(ColorScheme.DARKER_GRAY_COLOR); + input.setPreferredSize(new Dimension(35, 30)); + input.setBorder(new EmptyBorder(5, 5, 5, 5)); + ((AbstractDocument) input.getDocument()).setDocumentFilter(new DocumentFilter() + { + @Override + public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String str, AttributeSet attrs) + throws BadLocationException + { + try + { + String text = RuneliteColorPicker.getReplacedText(fb, offset, length, str); + + int value = Integer.parseInt(text); + if (value < ColorUtil.MIN_RGB_VALUE || value > ColorUtil.MAX_RGB_VALUE) + { + Toolkit.getDefaultToolkit().beep(); + return; + } + + super.replace(fb, offset, length, str, attrs); + } + catch (NumberFormatException e) + { + Toolkit.getDefaultToolkit().beep(); + } + } + }); + + input.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + updateText(); + } + }); + + input.addActionListener(a -> updateText()); + + JLabel label = new JLabel(labelText); + label.setPreferredSize(new Dimension(45, 0)); + label.setForeground(Color.WHITE); + + slider.setBackground(ColorScheme.DARK_GRAY_COLOR); + slider.setBorder(new EmptyBorder(0, 0, 5, 0)); + slider.setPreferredSize(new Dimension(ColorUtil.MAX_RGB_VALUE + ColorValueSlider.KNOB_WIDTH, 30)); + + update(DEFAULT_VALUE); + add(label, BorderLayout.WEST); + add(slider, BorderLayout.CENTER); + add(input, BorderLayout.EAST); + } + + private void updateText() + { + int value = Integer.parseInt(input.getText()); + + update(value); + if (onValueChanged != null) + { + onValueChanged.accept(getValue()); + } + } + + public void update(int value) + { + value = ColorUtil.constrainValue(value); + + slider.setValue(value); + input.setText(value + ""); + } + + public int getValue() + { + return slider.getValue(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValueSlider.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValueSlider.java new file mode 100644 index 0000000000..12b5fb7cc5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/ColorValueSlider.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.components.colorpicker; + +import com.google.common.primitives.Ints; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.function.Consumer; +import javax.swing.JPanel; +import lombok.Setter; +import net.runelite.client.util.ColorUtil; + +public class ColorValueSlider extends JPanel +{ + static final int KNOB_WIDTH = 4; + + private static final int KNOB_HEIGHT = 14; + private static final Color TRACK_COLOR = new Color(20, 20, 20); + private static final Color KNOB_COLOR = new Color(150, 150, 150); + + private int value = ColorUtil.MAX_RGB_VALUE + KNOB_WIDTH; + + @Setter + private Consumer onValueChanged; + + ColorValueSlider() + { + addMouseMotionListener(new MouseMotionAdapter() + { + @Override + public void mouseDragged(MouseEvent me) + { + moveTarget(me.getX(), true); + } + }); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseReleased(MouseEvent me) + { + moveTarget(me.getX(), true); + } + + @Override + public void mousePressed(MouseEvent me) + { + moveTarget(me.getX(), true); + } + }); + } + + public void setValue(int x) + { + moveTarget(x + KNOB_WIDTH, false); + } + + private void moveTarget(int x, boolean shouldUpdate) + { + value = Ints.constrainToRange(x, ColorUtil.MIN_RGB_VALUE + KNOB_WIDTH, ColorUtil.MAX_RGB_VALUE + KNOB_WIDTH); + paintImmediately(0, 0, this.getWidth(), this.getHeight()); + + if (shouldUpdate && onValueChanged != null) + { + onValueChanged.accept(getValue()); + } + } + + @Override + public void paint(Graphics g) + { + super.paint(g); + + g.setColor(TRACK_COLOR); + g.fillRect(0, this.getHeight() / 2 - 2, this.getWidth() - KNOB_WIDTH, 5); + + g.setColor(KNOB_COLOR); + g.fillRect(value - KNOB_WIDTH / 2, this.getHeight() / 2 - KNOB_HEIGHT / 2, KNOB_WIDTH, KNOB_HEIGHT); + } + + int getValue() + { + return value - KNOB_WIDTH; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/HuePanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/HuePanel.java new file mode 100644 index 0000000000..392255daa7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/HuePanel.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.components.colorpicker; + +import com.google.common.primitives.Ints; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.function.Consumer; +import javax.swing.JPanel; +import lombok.Getter; +import lombok.Setter; + +public class HuePanel extends JPanel +{ + private static final int PANEL_WIDTH = 15; + private static final int KNOB_HEIGHT = 4; + + private final int height; + + @Getter + private int selectedY; + + @Setter + private Consumer onColorChange; + + HuePanel(int height) + { + this.height = height; + setPreferredSize(new Dimension(PANEL_WIDTH, height)); + + addMouseMotionListener(new MouseMotionAdapter() + { + @Override + public void mouseDragged(MouseEvent me) + { + moveSelector(me.getY()); + } + }); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseReleased(MouseEvent me) + { + moveSelector(me.getY()); + } + + @Override + public void mousePressed(MouseEvent me) + { + moveSelector(me.getY()); + } + }); + } + + /** + * Repaint slider with closest guess for y value on slider. + */ + public void select(Color color) + { + this.selectedY = closestYToColor(color); + paintImmediately(0, 0, PANEL_WIDTH, height); + } + + /** + * Moves the selector to a specified y coordinate. + */ + private void moveSelector(int y) + { + y = Ints.constrainToRange(y, 0, height - 1); + if (y == this.selectedY) + { + return; + } + + this.selectedY = y; + paintImmediately(0, 0, PANEL_WIDTH, height); + if (this.onColorChange != null) + { + this.onColorChange.accept(y); + } + } + + /** + * Calculates a close y value for the given target color. + */ + private int closestYToColor(Color target) + { + float[] hsb = Color.RGBtoHSB(target.getRed(), target.getGreen(), target.getBlue(), null); + float hue = hsb[0]; + + int offHeight = height - 1; + + return Math.round(offHeight - hue * offHeight); + } + + @Override + public void paint(Graphics g) + { + // Paint the gradient + for (int y = 0; y < height; y++) + { + g.setColor(colorAt(y)); + g.fillRect(0, y, PANEL_WIDTH, 1); + } + + final int halfKnob = KNOB_HEIGHT / 2; + + // Paint the selector + g.setColor(Color.WHITE); + g.fillRect(0, selectedY - 1, PANEL_WIDTH, KNOB_HEIGHT); + g.setColor(Color.BLACK); + g.drawLine(0, selectedY - halfKnob, PANEL_WIDTH, selectedY - halfKnob); + g.drawLine(0, selectedY + halfKnob, PANEL_WIDTH, selectedY + halfKnob); + } + + /** + * Calculate hue color for current hue index. + */ + private Color colorAt(int y) + { + return Color.getHSBColor(1 - (float) y / (height - 1), 1, 1); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/PreviewPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/PreviewPanel.java new file mode 100644 index 0000000000..e602d4de50 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/PreviewPanel.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.runelite.client.ui.components.colorpicker; + +import java.awt.Color; +import java.awt.Graphics; +import javax.swing.JPanel; +import lombok.Getter; + +class PreviewPanel extends JPanel +{ + private static final int CHECKER_SIZE = 10; + + @Getter + private Color color; + + void setColor(Color c) + { + this.color = c; + this.paintImmediately(0, 0, this.getWidth(), this.getHeight()); + } + + @Override + public void paint(Graphics g) + { + super.paint(g); + + for (int x = 0; x < getWidth(); x += CHECKER_SIZE) + { + for (int y = 0; y < getHeight(); y += CHECKER_SIZE) + { + int val = (x / CHECKER_SIZE + y / CHECKER_SIZE) % 2; + g.setColor(val == 0 ? Color.LIGHT_GRAY : Color.WHITE); + g.fillRect(x, y, CHECKER_SIZE, CHECKER_SIZE); + } + } + + g.setColor(color); + g.fillRect(0, 0, this.getWidth(), this.getHeight()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/RuneliteColorPicker.java b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/RuneliteColorPicker.java new file mode 100644 index 0000000000..81d6780d70 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/colorpicker/RuneliteColorPicker.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Ron Young + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui.components.colorpicker; + +import com.google.common.base.Strings; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.function.Consumer; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.DocumentFilter; +import lombok.Getter; +import lombok.Setter; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.util.ColorUtil; +import org.pushingpixels.substance.internal.SubstanceSynapse; + +public class RuneliteColorPicker extends JDialog +{ + private final static int FRAME_WIDTH = 400; + private final static int FRAME_HEIGHT = 380; + private final static int TONE_PANEL_SIZE = 160; + + private final static String BLANK_HEX = "#000"; + + private final ColorPanel colorPanel = new ColorPanel(TONE_PANEL_SIZE); + private final HuePanel huePanel = new HuePanel(TONE_PANEL_SIZE); + private final PreviewPanel afterPanel = new PreviewPanel(); + + private final ColorValuePanel redSlider = new ColorValuePanel("Red"); + private final ColorValuePanel greenSlider = new ColorValuePanel("Green"); + private final ColorValuePanel blueSlider = new ColorValuePanel("Blue"); + private final ColorValuePanel alphaSlider = new ColorValuePanel("Opacity"); + + private final JTextField hexInput = new JTextField(); + + private final boolean alphaHidden; + + @Getter + private Color selectedColor; + + @Setter + private Consumer onColorChange; + + public RuneliteColorPicker(Window parent, Color previousColor, String title, boolean alphaHidden) + { + super(parent, "RuneLite Color Picker - " + title, ModalityType.MODELESS); + + this.selectedColor = previousColor; + + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setResizable(false); + setSize(FRAME_WIDTH, FRAME_HEIGHT); + setBackground(ColorScheme.PROGRESS_COMPLETE_COLOR); + + JPanel content = new JPanel(new BorderLayout()); + content.putClientProperty(SubstanceSynapse.COLORIZATION_FACTOR, 1.0); + content.setBorder(new EmptyBorder(15, 15, 15, 15)); + + JPanel colorSelection = new JPanel(new BorderLayout(15, 0)); + + JPanel leftPanel = new JPanel(new BorderLayout(15, 0)); + leftPanel.add(huePanel, BorderLayout.WEST); + leftPanel.add(colorPanel, BorderLayout.CENTER); + + JPanel rightPanel = new JPanel(); + rightPanel.setLayout(new GridBagLayout()); + GridBagConstraints cx = new GridBagConstraints(); + + + cx.insets = new Insets(0, 0, 0, 0); + JLabel old = new JLabel("Previous"); + old.setHorizontalAlignment(JLabel.CENTER); + + JLabel next = new JLabel(" Current "); + next.setHorizontalAlignment(JLabel.CENTER); + + PreviewPanel beforePanel = new PreviewPanel(); + beforePanel.setColor(previousColor); + afterPanel.setColor(previousColor); + + JPanel hexContainer = new JPanel(new GridBagLayout()); + + JLabel hexLabel = new JLabel("#"); + hexInput.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + JLabel label = new JLabel("Hex color"); + label.setVerticalAlignment(JLabel.BOTTOM); + + cx.weightx = 0; + cx.fill = GridBagConstraints.BOTH; + cx.insets = new Insets(0, 1, 0, 1); + hexContainer.add(hexLabel, cx); + + cx.weightx = 1; + cx.fill = GridBagConstraints.HORIZONTAL; + cx.gridwidth = GridBagConstraints.REMAINDER; + hexContainer.add(hexInput, cx); + + cx.fill = GridBagConstraints.BOTH; + cx.gridwidth = GridBagConstraints.RELATIVE; + cx.weightx = 1; + cx.weighty = 1; + cx.gridy = 0; + cx.gridx = 0; + rightPanel.add(old, cx); + + cx.gridx++; + rightPanel.add(next, cx); + + cx.gridx = 0; + cx.gridy++; + cx.gridwidth = GridBagConstraints.RELATIVE; + cx.fill = GridBagConstraints.BOTH; + rightPanel.add(beforePanel, cx); + + cx.gridx++; + rightPanel.add(afterPanel, cx); + + cx.gridwidth = GridBagConstraints.REMAINDER; + cx.gridx = 0; + cx.gridy++; + rightPanel.add(label, cx); + + cx.gridy++; + cx.fill = GridBagConstraints.HORIZONTAL; + rightPanel.add(hexContainer, cx); + + JPanel slidersContainer = new JPanel(new GridLayout(4, 1, 0, 10)); + slidersContainer.setBorder(new EmptyBorder(15, 0, 0, 0)); + + slidersContainer.add(redSlider); + slidersContainer.add(greenSlider); + slidersContainer.add(blueSlider); + slidersContainer.add(alphaSlider); + + this.alphaHidden = alphaHidden; + if (alphaHidden) + { + alphaSlider.setVisible(false); + setSize(FRAME_WIDTH, FRAME_HEIGHT - 40); + } + + colorSelection.add(leftPanel, BorderLayout.WEST); + colorSelection.add(rightPanel, BorderLayout.CENTER); + colorSelection.add(slidersContainer, BorderLayout.SOUTH); + + content.add(colorSelection, BorderLayout.NORTH); + + setContentPane(content); + + // Reset selected color when clicking the old color + beforePanel.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent e) + { + if (!alphaHidden) + { + alphaSlider.update(beforePanel.getColor().getAlpha()); + } + colorChange(beforePanel.getColor()); + updatePanels(); + } + }); + + huePanel.setOnColorChange((y) -> + { + colorPanel.setBaseColor(y); + updateText(); + }); + + colorPanel.setOnColorChange(this::colorChange); + + ((AbstractDocument) hexInput.getDocument()).setDocumentFilter(new DocumentFilter() + { + @Override + public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String str, AttributeSet attrs) + throws BadLocationException + { + str = str.replaceAll("#|0x", ""); + String text = RuneliteColorPicker.getReplacedText(fb, offset, length, str); + + if (!ColorUtil.isHex(text)) + { + Toolkit.getDefaultToolkit().beep(); + return; + } + + super.replace(fb, offset, length, str, attrs); + } + }); + hexInput.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + updateHex(); + } + }); + hexInput.addActionListener((ActionEvent e) -> updateHex()); + + redSlider.setOnValueChanged(i -> + { + colorChange(new Color(i, selectedColor.getGreen(), selectedColor.getBlue())); + updatePanels(); + }); + + greenSlider.setOnValueChanged(i -> + { + colorChange(new Color(selectedColor.getRed(), i, selectedColor.getBlue())); + updatePanels(); + }); + + blueSlider.setOnValueChanged(i -> + { + colorChange(new Color(selectedColor.getRed(), selectedColor.getGreen(), i)); + updatePanels(); + }); + + alphaSlider.setOnValueChanged(i -> + colorChange(new Color(selectedColor.getRed(), selectedColor.getGreen(), selectedColor.getBlue(), i))); + + updatePanels(); + updateText(); + } + + private void updatePanels() + { + huePanel.select(selectedColor); + colorPanel.moveToClosestColor(huePanel.getSelectedY(), selectedColor); + } + + private void updateText() + { + String hex = alphaHidden ? ColorUtil.colorToHexCode(this.getSelectedColor()) : ColorUtil.colorToAlphaHexCode(this.getSelectedColor()); + hexInput.setText(hex.toUpperCase()); + afterPanel.setColor(selectedColor); + + redSlider.update(this.selectedColor.getRed()); + greenSlider.update(this.selectedColor.getGreen()); + blueSlider.update(this.selectedColor.getBlue()); + if (!alphaHidden) + { + alphaSlider.update(this.selectedColor.getAlpha()); + } + } + + private void colorChange(Color newColor) + { + if (newColor == this.selectedColor) + { + return; + } + + this.selectedColor = newColor; + + // Finally, before firing the color changed event, apply any transparency + // that might have been edited by the user + if (this.selectedColor.getAlpha() != this.alphaSlider.getValue()) + { + this.selectedColor = new Color( + selectedColor.getRed(), + selectedColor.getGreen(), + selectedColor.getBlue(), + alphaSlider.getValue()); + } + + updateText(); + + if (onColorChange != null) + { + onColorChange.accept(this.selectedColor); + } + } + + /** + * Parses the hex input, updating the color with the new values. + */ + private void updateHex() + { + String hex = hexInput.getText(); + if (Strings.isNullOrEmpty(hex)) + { + hex = BLANK_HEX; + } + + Color color = ColorUtil.fromHex(hex); + if (color == null) + { + return; + } + + if (!alphaHidden && ColorUtil.isAlphaHex(hex)) + { + alphaSlider.update(color.getAlpha()); + } + + colorChange(color); + updatePanels(); + } + + /** + * Gets the whole string from the passed DocumentFilter replace. + */ + static String getReplacedText(DocumentFilter.FilterBypass fb, int offset, int length, String str) + throws BadLocationException + { + Document doc = fb.getDocument(); + StringBuilder sb = new StringBuilder(doc.getText(0, doc.getLength())); + sb.replace(offset, offset + length, str); + + return sb.toString(); + } +} \ No newline at end of file