From ec14c8c199f3d69680f7517f8ede6c56954f45d0 Mon Sep 17 00:00:00 2001 From: weisj Date: Wed, 21 Oct 2020 21:21:23 +0200 Subject: [PATCH] Introduce a split button which - opens a popup menu if no default action has been assigned. - displays a separate drop down button to access an action menu next to the button if a default actions has been set. --- .../weisj/darklaf/components/ArrowButton.java | 27 +- .../components/button/JSplitButton.java | 120 +++++++ .../darklaf/ui/button/DarkButtonBorder.java | 68 ++-- .../ui/splitbutton/DarkSplitButtonBorder.java | 83 +++++ .../splitbutton/DarkSplitButtonListener.java | 86 +++++ .../ui/splitbutton/DarkSplitButtonUI.java | 305 ++++++++++++++++++ .../darklaf/icons/indicator/dropdown.svg | 9 + .../icons/indicator/dropdownDisabled.svg | 9 + .../properties/icons/indicator.properties | 3 + .../darklaf/properties/ui/misc.properties | 28 +- .../test/java/ui/button/SplitButtonDemo.java | 74 +++++ 11 files changed, 763 insertions(+), 49 deletions(-) create mode 100644 core/src/main/java/com/github/weisj/darklaf/components/button/JSplitButton.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonBorder.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonListener.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonUI.java create mode 100644 core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg create mode 100644 core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg create mode 100644 core/src/test/java/ui/button/SplitButtonDemo.java diff --git a/core/src/main/java/com/github/weisj/darklaf/components/ArrowButton.java b/core/src/main/java/com/github/weisj/darklaf/components/ArrowButton.java index a558ca91..c01dd1f8 100644 --- a/core/src/main/java/com/github/weisj/darklaf/components/ArrowButton.java +++ b/core/src/main/java/com/github/weisj/darklaf/components/ArrowButton.java @@ -27,7 +27,6 @@ import javax.swing.*; import javax.swing.plaf.DimensionUIResource; import javax.swing.plaf.basic.BasicArrowButton; -import com.github.weisj.darklaf.icons.UIAwareIcon; import com.github.weisj.darklaf.ui.button.DarkButtonUI; /** @author Jannis Weis */ @@ -41,18 +40,18 @@ public final class ArrowButton implements SwingConstants { public static JButton createUpDownArrow(final JComponent parent, final int orientation, final boolean center, final boolean applyInsetsOnSize, final Insets insets) { - UIAwareIcon icon; + Icon icon; switch (orientation) { case NORTH: - icon = (UIAwareIcon) UIManager.getIcon("ArrowButton.up.icon"); + icon = UIManager.getIcon("ArrowButton.up.icon"); break; case SOUTH: - icon = (UIAwareIcon) UIManager.getIcon("ArrowButton.down.icon"); + icon = UIManager.getIcon("ArrowButton.down.icon"); break; default: throw new IllegalStateException("Invalid button orientation: " + orientation); } - return createUpDownArrow(parent, icon, icon.getDual(), orientation, center, applyInsetsOnSize, insets); + return createUpDownArrow(parent, icon, icon, orientation, center, applyInsetsOnSize, insets); } public static JButton createUpDownArrow(final JComponent parent, final Icon activeIcon, final Icon inactiveIcon, @@ -61,15 +60,22 @@ public final class ArrowButton implements SwingConstants { private final Insets ins = insets != null ? insets : new Insets(0, 0, 0, 0); { putClientProperty(DarkButtonUI.KEY_NO_BORDERLESS_OVERWRITE, true); + setMargin(new Insets(0, 0, 0, 0)); } @Override public void paint(final Graphics g) { - int x = (getWidth() - getIcon().getIconWidth()) / 2; - int y = direction == NORTH ? getHeight() - getIcon().getIconHeight() + 4 : -4; + Insets margin = getMargin(); + int w = getWidth() - margin.left - margin.right; + int h = getHeight() - margin.top - margin.bottom; + int x = margin.left + (w - getIcon().getIconWidth()) / 2; + int y; if (center) { - y = (getHeight() - getIcon().getIconHeight()) / 2; + y = (h - getIcon().getIconHeight()) / 2; + } else { + y = direction == NORTH ? h - getIcon().getIconHeight() + 4 : -4; } + y += margin.top; paintTriangle(g, x, y, 0, direction, parent.isEnabled()); } @@ -82,8 +88,9 @@ public final class ArrowButton implements SwingConstants { if (!applyInsetsOnSize) { return new DimensionUIResource(getIcon().getIconWidth(), getIcon().getIconHeight()); } else { - return new DimensionUIResource(getIcon().getIconWidth() + ins.left + ins.right, - getIcon().getIconHeight() + ins.top + ins.bottom); + Insets i = getInsets(); + return new DimensionUIResource(getIcon().getIconWidth() + i.left + i.right, + getIcon().getIconHeight() + i.top + i.bottom); } } diff --git a/core/src/main/java/com/github/weisj/darklaf/components/button/JSplitButton.java b/core/src/main/java/com/github/weisj/darklaf/components/button/JSplitButton.java new file mode 100644 index 00000000..987245f9 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/button/JSplitButton.java @@ -0,0 +1,120 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.components.button; + +import java.awt.event.ActionListener; + +import javax.swing.*; + +public class JSplitButton extends JButton { + + public static final String KEY_ACTION_ADDED = "addedAction"; + public static final String KEY_ACTION_REMOVED = "removedAction"; + + private JPopupMenu actionMenu; + + /** + * Creates a button with no set text or icon. + */ + public JSplitButton() { + super(); + } + + /** + * Creates a button with an icon. + * + * @param icon the Icon image to display on the button + */ + public JSplitButton(final Icon icon) { + super(icon); + } + + /** + * Creates a button with text. + * + * @param text the text of the button + */ + public JSplitButton(final String text) { + super(text); + } + + /** + * Creates a button where properties are taken from the Action supplied. + * + * @param a the Action used to specify the new button + * + * @since 1.3 + */ + public JSplitButton(final Action a) { + super(a); + } + + /** + * Creates a button with initial text and an icon. + * + * @param text the text of the button + * @param icon the Icon image to display on the button + */ + public JSplitButton(final String text, final Icon icon) { + super(text, icon); + } + + public int getActionCount() { + return listenerList.getListenerCount(ActionListener.class); + } + + @Override + public String getUIClassID() { + return "SplitButtonUI"; + } + + @Override + public void addActionListener(final ActionListener l) { + super.addActionListener(l); + firePropertyChange(KEY_ACTION_ADDED, null, l); + } + + @Override + public void removeActionListener(final ActionListener l) { + super.removeActionListener(l); + firePropertyChange(KEY_ACTION_REMOVED, l, null); + } + + public JPopupMenu getActionMenu() { + if (actionMenu == null) { + actionMenu = new JPopupMenu(); + } + return actionMenu; + } + + @Override + public void updateUI() { + super.updateUI(); + if (actionMenu != null) { + SwingUtilities.updateComponentTreeUI(actionMenu); + } + } + + public void setActionMenu(final JPopupMenu actionMenu) { + this.actionMenu = actionMenu; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonBorder.java b/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonBorder.java index e3184f90..dd11ff22 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonBorder.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonBorder.java @@ -102,6 +102,7 @@ public class DarkButtonBorder implements Border, UIResource { public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) { if (ButtonConstants.isBorderlessVariant(c)) { + paintBorderlessBorder(c, g, x, y, width, height); return; } Graphics2D g2 = (Graphics2D) g; @@ -134,19 +135,35 @@ public class DarkButtonBorder implements Border, UIResource { int fh = by + bh + borderSize - focusIns.top - focusIns.bottom; if (paintFocus(c)) { - g.translate(fx, fy); - PaintUtil.paintFocusBorder(g2, fw, fh, focusArc, borderSize); - g.translate(-fx, -fy); + paintFocusBorder(g2, focusArc, borderSize, fx, fy, fw, fh); } - g2.setColor(getBorderColor(c, focus)); - PaintUtil.paintLineBorder(g2, bx, by, bw, bh, arc); + paintLineBorder(c, g2, arc, focus, bx, by, bw, bh); + if (corner != null) { paintNeighbourFocus(g2, c, width, height); } config.restore(); } + protected void paintBorderlessBorder(final Component c, final Graphics g, final int x, final int y, final int width, + final int height) { + + } + + protected void paintLineBorder(final Component c, final Graphics2D g2, final int arc, final boolean focus, + final int bx, final int by, final int bw, final int bh) { + g2.setColor(getBorderColor(c, focus)); + PaintUtil.paintLineBorder(g2, bx, by, bw, bh, arc); + } + + protected void paintFocusBorder(final Graphics2D g2, final int focusArc, final int borderSize, final int fx, + final int fy, final int fw, final int fh) { + g2.translate(fx, fy); + PaintUtil.paintFocusBorder(g2, fw, fh, focusArc, borderSize); + g2.translate(-fx, -fy); + } + protected void paintNeighbourFocus(final Graphics2D g2, final Component c, final int width, final int height) { JComponent left = ButtonConstants.getNeighbour(DarkButtonUI.KEY_LEFT_NEIGHBOUR, c); boolean paintLeft = DarkUIUtil.hasFocus(left); @@ -156,9 +173,8 @@ public class DarkButtonBorder implements Border, UIResource { if (corner != null) ins = corner.maskInsets(ins, borderSize); int h = height - Math.max(0, getShadowSize(left) - borderSize); - g2.translate(-3 * borderSize + 1, -ins.top); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, h + ins.top + ins.bottom, getFocusArc(left), borderSize); - g2.translate(-(-3 * borderSize + 1), ins.bottom); + paintFocusBorder(g2, getFocusArc(left), borderSize, -3 * borderSize + 1, -ins.top, 4 * borderSize, + h + ins.top + ins.bottom); } JComponent right = ButtonConstants.getNeighbour(DarkButtonUI.KEY_RIGHT_NEIGHBOUR, c); boolean paintRight = DarkUIUtil.hasFocus(right); @@ -168,9 +184,8 @@ public class DarkButtonBorder implements Border, UIResource { if (corner != null) ins = corner.maskInsets(ins, borderSize); int h = height - Math.max(0, getShadowSize(right) - borderSize); - g2.translate(width - borderSize - 1, -ins.top); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, h + ins.top + ins.bottom, getFocusArc(right), borderSize); - g2.translate(-(width - borderSize - 1), ins.top); + paintFocusBorder(g2, getFocusArc(right), borderSize, width - borderSize - 1, -ins.top, 4 * borderSize, + h + ins.top + ins.bottom); } JComponent top = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_NEIGHBOUR, c); @@ -180,25 +195,22 @@ public class DarkButtonBorder implements Border, UIResource { Insets ins = new Insets(0, 0, 0, 0); if (corner != null) ins = corner.maskInsets(ins, borderSize); - g2.translate(-ins.left, -3 * borderSize + 1); - PaintUtil.paintFocusBorder(g2, width + ins.right + ins.left, 4 * borderSize, getFocusArc(top), borderSize); - g2.translate(ins.left, -(-3 * borderSize + 1)); + paintFocusBorder(g2, getFocusArc(top), borderSize, -ins.left, -3 * borderSize + 1, + width + ins.right + ins.left, 4 * borderSize); } JComponent topLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_LEFT_NEIGHBOUR, c); boolean paintTopLeft = DarkUIUtil.hasFocus(topLeft); if (paintTopLeft) { - g2.translate(-3 * borderSize + 1, -3 * borderSize + 1); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(topLeft), borderSize); - g2.translate(-(-3 * borderSize + 1), -(-3 * borderSize + 1)); + paintFocusBorder(g2, getFocusArc(topLeft), borderSize, -3 * borderSize + 1, -3 * borderSize + 1, + 4 * borderSize, 4 * borderSize); } JComponent topRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_RIGHT_NEIGHBOUR, c); boolean paintTopRight = DarkUIUtil.hasFocus(topRight); if (paintTopRight) { - g2.translate(width - borderSize - 1, -3 * borderSize + 1); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(topRight), borderSize); - g2.translate(-(width - borderSize - 1), -(-3 * borderSize + 1)); + paintFocusBorder(g2, getFocusArc(topRight), borderSize, width - borderSize - 1, -3 * borderSize + 1, + 4 * borderSize, 4 * borderSize); } JComponent bottom = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_NEIGHBOUR, c); @@ -208,26 +220,22 @@ public class DarkButtonBorder implements Border, UIResource { Insets ins = new Insets(0, 0, 0, 0); if (corner != null) ins = corner.maskInsets(ins, borderSize); - g2.translate(-ins.left, height - borderSize - 1); - PaintUtil.paintFocusBorder(g2, width + ins.left + ins.right, 4 * borderSize, getFocusArc(bottom), - borderSize); - g2.translate(ins.left, -(height - borderSize - 1)); + paintFocusBorder(g2, getFocusArc(bottom), borderSize, -ins.left, height - borderSize - 1, + width + ins.left + ins.right, 4 * borderSize); } JComponent bottomLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_LEFT_NEIGHBOUR, c); boolean paintBottomLeft = DarkUIUtil.hasFocus(bottomLeft); if (paintBottomLeft) { - g2.translate(-3 * borderSize + 1, height - borderSize - 1); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(bottomLeft), borderSize); - g2.translate(-(-3 * borderSize + 1), -(height - borderSize - 1)); + paintFocusBorder(g2, getFocusArc(bottomLeft), borderSize, -3 * borderSize + 1, height - borderSize - 1, + 4 * borderSize, 4 * borderSize); } JComponent bottomRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_RIGHT_NEIGHBOUR, c); boolean paintBottomRight = DarkUIUtil.hasFocus(bottomRight); if (paintBottomRight) { - g2.translate(width - borderSize - 1, height - borderSize - 1); - PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(bottomRight), borderSize); - g2.translate(-(width - borderSize - 1), -(height - borderSize - 1)); + paintFocusBorder(g2, getFocusArc(bottomRight), borderSize, width - borderSize - 1, height - borderSize - 1, + 4 * borderSize, 4 * borderSize); } } diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonBorder.java b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonBorder.java new file mode 100644 index 00000000..54cf6c34 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonBorder.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.splitbutton; + +import java.awt.*; + +import javax.swing.*; + +import com.github.weisj.darklaf.components.button.JSplitButton; +import com.github.weisj.darklaf.ui.button.ButtonConstants; +import com.github.weisj.darklaf.ui.button.DarkButtonBorder; +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class DarkSplitButtonBorder extends DarkButtonBorder { + + @Override + protected void paintBorderlessBorder(final Component c, final Graphics g, final int x, final int y, final int width, + final int height) { + if (showSplit(c)) { + paintDivider(c, g, x, y, width, height); + } + super.paintBorderlessBorder(c, g, x, y, width, height); + } + + @Override + protected void paintLineBorder(final Component c, final Graphics2D g2, final int arc, final boolean focus, + final int bx, final int by, final int bw, final int bh) { + if (showSplit(c)) { + paintDivider(c, g2, bx, by, bw, bh); + } + super.paintLineBorder(c, g2, arc, focus, bx, by, bw, bh); + } + + protected void paintDivider(final Component c, final Graphics g, final int x, final int y, final int width, + final int height) { + if (!(c instanceof JSplitButton)) return; + Component arrowButton = ((JSplitButton) c).getComponent(0); + if (arrowButton == null) return; + boolean ltr = c.getComponentOrientation().isLeftToRight(); + int splitPos = ltr ? arrowButton.getX() : arrowButton.getX() + arrowButton.getWidth() - 1; + + DarkSplitButtonUI ui = DarkUIUtil.getUIOfType(((JSplitButton) c).getUI(), DarkSplitButtonUI.class); + if (ui != null && ui.getDrawOutline(c)) { + boolean armed = ui.isArmedBorderless(ui.splitButton) + || (ui.useArrowButton() && ui.isArmedBorderless(ui.arrowButton)); + g.setColor(ui.getBorderlessOutline(armed)); + } else { + g.setColor(getBorderColor(c, false)); + } + + g.fillRect(splitPos, y, 1, height); + } + + protected boolean showSplit(final Component c) { + boolean hasDefaultAction = c instanceof JSplitButton && ((JSplitButton) c).getActionCount() > 1; + boolean borderless = ButtonConstants.isBorderlessVariant(c); + if (!borderless) return hasDefaultAction; + if (hasDefaultAction && ButtonConstants.isBorderlessVariant(c)) { + DarkSplitButtonUI ui = DarkUIUtil.getUIOfType(((JSplitButton) c).getUI(), DarkSplitButtonUI.class); + return ui != null && ui.isRolloverBorderless((AbstractButton) c); + } + return false; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonListener.java b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonListener.java new file mode 100644 index 00000000..653cf9e6 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonListener.java @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.splitbutton; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.github.weisj.darklaf.components.button.JSplitButton; +import com.github.weisj.darklaf.ui.WidgetPopupHelper; +import com.github.weisj.darklaf.ui.button.ButtonConstants; + +public class DarkSplitButtonListener implements ActionListener, PropertyChangeListener, ChangeListener { + + + private final DarkSplitButtonUI ui; + + public DarkSplitButtonListener(final DarkSplitButtonUI ui) { + this.ui = ui; + } + + @Override + public void actionPerformed(final ActionEvent e) { + if (e.getSource() == ui.splitButton && ui.useArrowButton()) return; + JPopupMenu actionMenu = ui.splitButton.getActionMenu(); + if (actionMenu.isVisible()) { + actionMenu.setVisible(false); + } else { + boolean splitButton = e.getSource() == ui.splitButton; + actionMenu.setPreferredSize(null); + Dimension size = actionMenu.getPreferredSize(); + Rectangle popupBounds = + WidgetPopupHelper.getPopupBounds(ui.splitButton, actionMenu, size, splitButton, !splitButton); + if (splitButton) { + actionMenu.setPreferredSize(popupBounds.getSize()); + } + actionMenu.show(ui.splitButton, popupBounds.x, popupBounds.y); + } + } + + + @Override + public void propertyChange(final PropertyChangeEvent evt) { + String key = evt.getPropertyName(); + if (JSplitButton.KEY_ACTION_ADDED.equals(key) || JSplitButton.KEY_ACTION_REMOVED.equals(key)) { + ui.updateDefaultAction(); + ui.splitButton.doLayout(); + ui.splitButton.repaint(); + } else if (ButtonConstants.KEY_THIN.equals(key)) { + ui.updateArrowMargin(); + } + } + + @Override + public void stateChanged(final ChangeEvent e) { + ui.splitButton.repaint(); + if (!ui.splitButton.hasFocus() && ui.arrowButton.getModel().isPressed()) { + ui.splitButton.requestFocusInWindow(); + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonUI.java new file mode 100644 index 00000000..f242aefc --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonUI.java @@ -0,0 +1,305 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.splitbutton; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.plaf.ComponentUI; + +import com.github.weisj.darklaf.components.ArrowButton; +import com.github.weisj.darklaf.components.button.JSplitButton; +import com.github.weisj.darklaf.icons.ToggleIcon; +import com.github.weisj.darklaf.ui.button.ButtonConstants; +import com.github.weisj.darklaf.ui.button.DarkButtonUI; +import com.github.weisj.darklaf.ui.popupmenu.DarkPopupMenuUI; +import com.github.weisj.darklaf.util.PropertyUtil; + +public class DarkSplitButtonUI extends DarkButtonUI { + + protected JSplitButton splitButton; + protected AbstractButton arrowButton; + private DarkSplitButtonListener arrowButtonListener; + private Icon overlayIcon; + private Icon overlayDisabledIcon; + + private ToggleIcon arrowToggleIcon; + private ToggleIcon arrowDisabledToggleIcon; + + private Insets arrowInsets; + private Insets arrowInsetsThin; + private final Insets arrowButtonMargin = new Insets(0, 0, 0, 0); + + public static ComponentUI createUI(final JComponent c) { + return new DarkSplitButtonUI(); + } + + + @Override + public void installUI(final JComponent c) { + splitButton = (JSplitButton) c; + super.installUI(c); + } + + @Override + public void uninstallUI(final JComponent c) { + super.uninstallUI(c); + splitButton = null; + } + + @Override + protected void installDefaults(final AbstractButton b) { + super.installDefaults(b); + overlayIcon = UIManager.getIcon("SplitButton.overlayIcon"); + overlayDisabledIcon = UIManager.getIcon("SplitButton.overlayDisabledIcon"); + arrowInsets = UIManager.getInsets("SplitButton.arrowInsets"); + arrowInsetsThin = UIManager.getInsets("SplitButton.arrowThinInsets"); + Icon arrowIcon = UIManager.getIcon("SplitButton.arrowIcon"); + Icon arrowIconDisabled = UIManager.getIcon("SplitButton.arrowIconDisabled"); + Icon arrowIconThin = UIManager.getIcon("SplitButton.arrowThinIcon"); + Icon arrowIconThinDisabled = UIManager.getIcon("SplitButton.arrowThinIconDisabled"); + + arrowToggleIcon = new ToggleIcon(arrowIcon, arrowIconThin); + arrowDisabledToggleIcon = new ToggleIcon(arrowIconDisabled, arrowIconThinDisabled); + + PropertyUtil.installBorder(splitButton, new DarkSplitButtonBorder()); + arrowButton = createArrowButton(); + configureArrowButton(arrowButton); + splitButton.add(arrowButton); + updateArrowMargin(); + updateDefaultAction(); + } + + protected void configureArrowButton(final AbstractButton button) { + button.setRequestFocusEnabled(false); + button.setInheritsPopupMenu(true); + button.resetKeyboardActions(); + button.setEnabled(splitButton.isEnabled()); + button.putClientProperty(DarkPopupMenuUI.KEY_CONSUME_EVENT_ON_CLOSE, true); + } + + @Override + protected void installListeners(final AbstractButton b) { + super.installListeners(b); + arrowButtonListener = createArrowButtonListener(); + arrowButton.addActionListener(arrowButtonListener); + splitButton.addActionListener(arrowButtonListener); + arrowButton.getModel().addChangeListener(arrowButtonListener); + splitButton.addPropertyChangeListener(arrowButtonListener); + } + + @Override + protected void uninstallListeners(final AbstractButton b) { + super.uninstallListeners(b); + arrowButton.removeActionListener(arrowButtonListener); + splitButton.removeActionListener(arrowButtonListener); + arrowButton.getModel().removeActionListener(arrowButtonListener); + splitButton.removePropertyChangeListener(arrowButtonListener); + arrowButtonListener = null; + } + + @Override + protected void uninstallDefaults(final AbstractButton b) { + super.uninstallDefaults(b); + splitButton.remove(arrowButton); + arrowButton = null; + } + + protected void updateArrowMargin() { + if (ButtonConstants.isThin(splitButton)) { + arrowButtonMargin.set(arrowInsetsThin.top, arrowInsetsThin.left, arrowInsetsThin.bottom, + arrowInsetsThin.right); + arrowToggleIcon.setChooseAlternativeIcon(true); + } else { + arrowButtonMargin.set(arrowInsets.top, arrowInsets.left, arrowInsets.bottom, arrowInsets.right); + arrowToggleIcon.setChooseAlternativeIcon(false); + } + splitButton.doLayout(); + } + + protected DarkSplitButtonListener createArrowButtonListener() { + return new DarkSplitButtonListener(this); + } + + protected AbstractButton createArrowButton() { + AbstractButton b = ArrowButton.createUpDownArrow(splitButton, arrowToggleIcon, arrowDisabledToggleIcon, + SwingConstants.SOUTH, true, true, arrowButtonMargin); + b.setRolloverEnabled(true); + return b; + } + + @Override + public Dimension getPreferredSize(final JComponent c) { + Dimension dim = super.getPreferredSize(c); + if (useArrowButton()) { + Dimension arrowSize = arrowButton.getPreferredSize(); + dim.width += arrowSize.width; + dim.height = Math.max(dim.height, arrowSize.height); + } + return dim; + } + + @Override + protected LayoutManager createLayout() { + return new DarkSplitButtonLayout(); + } + + protected boolean useArrowButton() { + return arrowButton != null && splitButton.getActionCount() > 1; + } + + @Override + protected void paintIcon(final Graphics g, final AbstractButton b, final JComponent c) { + super.paintIcon(g, b, c); + if (b.getIcon() != null && !useArrowButton()) { + Icon overlay = b.isEnabled() ? overlayIcon : overlayDisabledIcon; + overlay.paintIcon(c, g, iconRect.x + iconRect.width - overlay.getIconWidth(), + iconRect.y + iconRect.height - overlay.getIconHeight()); + } + } + + @Override + public boolean isRolloverBorderless(final AbstractButton b) { + return super.isRolloverBorderless(b) || (useArrowButton() && arrowButton.getModel().isRollover()); + } + + @Override + protected void paintDarklafBorderBgImpl(final AbstractButton c, final Graphics2D g, final boolean showShadow, + final int shadow, final int effectiveArc, final Rectangle bgRect) { + super.paintDarklafBorderBgImpl(c, g, showShadow, shadow, effectiveArc, bgRect); + if (useArrowButton()) { + boolean isDefault = splitButton.isDefaultButton(); + boolean enabled = splitButton.isEnabled(); + boolean rollover = c.isRolloverEnabled() && arrowButton.getModel().isRollover(); + boolean clicked = arrowButton.getModel().isArmed(); + g.setColor(getBackgroundColor(splitButton, isDefault, rollover, clicked, enabled)); + Shape clip = g.getClip(); + boolean ltr = c.getComponentOrientation().isLeftToRight(); + if (ltr) { + g.clipRect(arrowButton.getX(), 0, button.getWidth(), button.getHeight()); + } else { + g.clipRect(0, 0, arrowButton.getX() + arrowButton.getWidth(), button.getHeight()); + } + paintBackgroundRect(g, effectiveArc, bgRect); + g.setClip(clip); + } + } + + protected void setArmedClip(final AbstractButton c, final Graphics g) { + boolean ltr = c.getComponentOrientation().isLeftToRight(); + boolean arrowArmed = arrowButton.getModel().isRollover(); + if (ltr) { + if (arrowArmed) { + g.clipRect(arrowButton.getX(), 0, splitButton.getWidth(), splitButton.getHeight()); + } else { + g.clipRect(0, 0, arrowButton.getX(), splitButton.getHeight()); + } + } else { + if (arrowArmed) { + g.clipRect(0, 0, arrowButton.getX() + arrowButton.getWidth(), splitButton.getHeight()); + } else { + g.clipRect(arrowButton.getX() + arrowButton.getWidth(), 0, splitButton.getWidth(), + splitButton.getHeight()); + } + } + } + + @Override + protected void paintBorderlessBackgroundImpl(final AbstractButton b, final Graphics2D g, final int arc, final int x, + final int y, final int w, final int h) { + boolean splitArmed = splitButton.getModel().isArmed(); + boolean arrowArmed = arrowButton.getModel().isArmed(); + Shape clip = g.getClip(); + if (splitArmed) { + super.paintBorderlessBackgroundImpl(arrowButton, g, arc, x, y, w, h); + setArmedClip(splitButton, g); + super.paintBorderlessBackgroundImpl(splitButton, g, arc, x, y, w, h); + } else if (arrowArmed) { + super.paintBorderlessBackgroundImpl(splitButton, g, arc, x, y, w, h); + setArmedClip(splitButton, g); + super.paintBorderlessBackgroundImpl(arrowButton, g, arc, x, y, w, h); + } else { + super.paintBorderlessBackgroundImpl(b, g, arc, x, y, w, h); + } + g.setClip(clip); + } + + @Override + protected void paintBorderlessRectangularBackgroundIml(final AbstractButton b, final Graphics2D g, final int x, + final int y, final int w, final int h) { + boolean splitArmed = splitButton.getModel().isArmed(); + boolean arrowArmed = splitButton.getModel().isArmed(); + Shape clip = g.getClip(); + if (splitArmed) { + super.paintBorderlessRectangularBackgroundIml(arrowButton, g, x, y, w, h); + setArmedClip(splitButton, g); + super.paintBorderlessRectangularBackgroundIml(splitButton, g, x, y, w, h); + } else if (arrowArmed) { + super.paintBorderlessRectangularBackgroundIml(splitButton, g, x, y, w, h); + setArmedClip(splitButton, g); + super.paintBorderlessRectangularBackgroundIml(arrowButton, g, x, y, w, h); + } else { + super.paintBorderlessRectangularBackgroundIml(b, g, x, y, w, h); + } + g.setClip(clip); + } + + public void updateDefaultAction() { + arrowButton.setVisible(useArrowButton()); + splitButton.putClientProperty(DarkPopupMenuUI.KEY_CONSUME_EVENT_ON_CLOSE, !useArrowButton()); + } + + protected class DarkSplitButtonLayout extends DarkButtonLayout { + + @Override + public void layoutContainer(final Container parent) { + super.layoutContainer(parent); + if (useArrowButton()) { + Insets ins = parent.getInsets(); + Dimension arrowSize = arrowButton.getPreferredSize(); + boolean ltr = splitButton.getComponentOrientation().isLeftToRight(); + if (ltr) { + arrowButton.setBounds(parent.getWidth() - ins.right - arrowSize.width, 0, + arrowSize.width + ins.right, parent.getHeight()); + arrowButton.setMargin(new Insets(ins.top, 0, ins.bottom, ins.right)); + } else { + arrowButton.setBounds(ins.left, ins.top, arrowSize.width, + parent.getHeight() - ins.top - ins.bottom); + arrowButton.setMargin(new Insets(ins.top, ins.left, ins.bottom, 0)); + } + } + } + + @Override + protected void prepareContentRects(final AbstractButton b, final int width, final int height) { + super.prepareContentRects(b, width, height); + if (useArrowButton()) { + Dimension arrowSize = arrowButton.getPreferredSize(); + boolean ltr = splitButton.getComponentOrientation().isLeftToRight(); + viewRect.width -= arrowSize.width; + if (!ltr) { + viewRect.x += arrowSize.width; + } + } + } + } +} diff --git a/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg b/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg new file mode 100644 index 00000000..12c05bc7 --- /dev/null +++ b/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg b/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg new file mode 100644 index 00000000..45378f70 --- /dev/null +++ b/core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/src/main/resources/com/github/weisj/darklaf/properties/icons/indicator.properties b/core/src/main/resources/com/github/weisj/darklaf/properties/icons/indicator.properties index e58cf2c3..80b7a588 100644 --- a/core/src/main/resources/com/github/weisj/darklaf/properties/icons/indicator.properties +++ b/core/src/main/resources/com/github/weisj/darklaf/properties/icons/indicator.properties @@ -48,3 +48,6 @@ Icons.speaker4.color = %menuIconEnabled Icons.speaker4.volumeColor = %menuIconEnabled Icons.speaker4Disabled.color = %menuIconDisabled Icons.speaker4Disabled.volumeColor = %menuIconDisabled + +Icons.dropdown.color = %menuIconEnabled +Icons.dropdownDisabled.color = %menuIconDisabled diff --git a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties index 89343fe4..716d370d 100644 --- a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties +++ b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties @@ -24,15 +24,25 @@ # # suppress inspection "UnusedProperty" for whole file # -ThemeSettings.icon = menu/themeSettings.svg[themed] +ThemeSettings.icon = menu/themeSettings.svg[themed] -LoadIndicator.stepWorkingIcon = progress/stepWorking.svg[themed] -LoadIndicator.stepPassiveIcon = progress/stepPassive.svg[themed] +LoadIndicator.stepWorkingIcon = progress/stepWorking.svg[themed] +LoadIndicator.stepPassiveIcon = progress/stepPassive.svg[themed] -CloseButton.closeIcon = navigation/close.svg[themed] -CloseButton.closeDisabledIcon = navigation/closeDisabled.svg[themed] -CloseButton.closeHoverIcon = navigation/closeHovered.svg[themed] +CloseButton.closeIcon = navigation/close.svg[themed] +CloseButton.closeDisabledIcon = navigation/closeDisabled.svg[themed] +CloseButton.closeHoverIcon = navigation/closeHovered.svg[themed] -HelpButton.helpIcon = menu/help.svg[themed] -HelpButton.helpHighlightIcon = menu/helpHighlight.svg[themed] -HelpButton.helpDisabledIcon = menu/helpDisabled.svg[themed] +HelpButton.helpIcon = menu/help.svg[themed] +HelpButton.helpHighlightIcon = menu/helpHighlight.svg[themed] +HelpButton.helpDisabledIcon = menu/helpDisabled.svg[themed] + +SplitButtonUI = com.github.weisj.darklaf.ui.splitbutton.DarkSplitButtonUI +SplitButton.arrowInsets = 4,4,4,4 +SplitButton.arrowThinInsets = 2,0,2,0 +SplitButton.overlayIcon = indicator/dropdown.svg[themed] +SplitButton.overlayDisabledIcon = indicator/dropdownDisabled.svg[themed] +SplitButton.arrowIcon = navigation/arrow/thick/arrowDown.svg[themed] +SplitButton.arrowDisabledIcon = navigation/arrow/thick/arrowDownDisabled.svg[themed] +SplitButton.arrowThinIcon = navigation/arrow/thick/arrowDown.svg[themed](14,14) +SplitButton.arrowThinDisabledIcon = navigation/arrow/thick/arrowDownDisabled.svg[themed](14,14) diff --git a/core/src/test/java/ui/button/SplitButtonDemo.java b/core/src/test/java/ui/button/SplitButtonDemo.java new file mode 100644 index 00000000..7438045d --- /dev/null +++ b/core/src/test/java/ui/button/SplitButtonDemo.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package ui.button; + +import java.awt.event.ActionListener; + +import javax.swing.*; + +import ui.ComponentDemo; +import ui.DemoResources; + +import com.github.weisj.darklaf.components.button.JSplitButton; + +public class SplitButtonDemo extends ButtonDemo { + + public static void main(final String[] args) { + ComponentDemo.showDemo(new SplitButtonDemo()); + } + + @Override + protected JButton createButton() { + Icon icon = DemoResources.FOLDER_ICON; + JSplitButton button = new JSplitButton("Split Button", icon); + JPopupMenu menu = button.getActionMenu(); + for (int i = 0; i < 5; i++) { + menu.add("Item " + i); + } + return button; + } + + @Override + protected void addCheckBoxControls(final JPanel controlPanel, final JButton button) { + super.addCheckBoxControls(controlPanel, button); + controlPanel.add(new JCheckBox("Default action set") { + private final ActionListener l = ee -> { + }; + + { + addActionListener(e -> { + if (isSelected()) { + button.addActionListener(l); + } else { + button.removeActionListener(l); + } + }); + } + }); + + } + + @Override + public String getTitle() { + return "Split Button Demo"; + } +}