Browse Source

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.
pull/214/head
weisj 4 years ago
parent
commit
ec14c8c199
  1. 27
      core/src/main/java/com/github/weisj/darklaf/components/ArrowButton.java
  2. 120
      core/src/main/java/com/github/weisj/darklaf/components/button/JSplitButton.java
  3. 68
      core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonBorder.java
  4. 83
      core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonBorder.java
  5. 86
      core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonListener.java
  6. 305
      core/src/main/java/com/github/weisj/darklaf/ui/splitbutton/DarkSplitButtonUI.java
  7. 9
      core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg
  8. 9
      core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg
  9. 3
      core/src/main/resources/com/github/weisj/darklaf/properties/icons/indicator.properties
  10. 10
      core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties
  11. 74
      core/src/test/java/ui/button/SplitButtonDemo.java

27
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.DimensionUIResource;
import javax.swing.plaf.basic.BasicArrowButton; import javax.swing.plaf.basic.BasicArrowButton;
import com.github.weisj.darklaf.icons.UIAwareIcon;
import com.github.weisj.darklaf.ui.button.DarkButtonUI; import com.github.weisj.darklaf.ui.button.DarkButtonUI;
/** @author Jannis Weis */ /** @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, public static JButton createUpDownArrow(final JComponent parent, final int orientation, final boolean center,
final boolean applyInsetsOnSize, final Insets insets) { final boolean applyInsetsOnSize, final Insets insets) {
UIAwareIcon icon; Icon icon;
switch (orientation) { switch (orientation) {
case NORTH: case NORTH:
icon = (UIAwareIcon) UIManager.getIcon("ArrowButton.up.icon"); icon = UIManager.getIcon("ArrowButton.up.icon");
break; break;
case SOUTH: case SOUTH:
icon = (UIAwareIcon) UIManager.getIcon("ArrowButton.down.icon"); icon = UIManager.getIcon("ArrowButton.down.icon");
break; break;
default: default:
throw new IllegalStateException("Invalid button orientation: " + orientation); 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, 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); private final Insets ins = insets != null ? insets : new Insets(0, 0, 0, 0);
{ {
putClientProperty(DarkButtonUI.KEY_NO_BORDERLESS_OVERWRITE, true); putClientProperty(DarkButtonUI.KEY_NO_BORDERLESS_OVERWRITE, true);
setMargin(new Insets(0, 0, 0, 0));
} }
@Override @Override
public void paint(final Graphics g) { public void paint(final Graphics g) {
int x = (getWidth() - getIcon().getIconWidth()) / 2; Insets margin = getMargin();
int y = direction == NORTH ? getHeight() - getIcon().getIconHeight() + 4 : -4; 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) { 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()); paintTriangle(g, x, y, 0, direction, parent.isEnabled());
} }
@ -82,8 +88,9 @@ public final class ArrowButton implements SwingConstants {
if (!applyInsetsOnSize) { if (!applyInsetsOnSize) {
return new DimensionUIResource(getIcon().getIconWidth(), getIcon().getIconHeight()); return new DimensionUIResource(getIcon().getIconWidth(), getIcon().getIconHeight());
} else { } else {
return new DimensionUIResource(getIcon().getIconWidth() + ins.left + ins.right, Insets i = getInsets();
getIcon().getIconHeight() + ins.top + ins.bottom); return new DimensionUIResource(getIcon().getIconWidth() + i.left + i.right,
getIcon().getIconHeight() + i.top + i.bottom);
} }
} }

120
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 <code>Action</code> supplied.
*
* @param a the <code>Action</code> 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;
}
}

68
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, public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width,
final int height) { final int height) {
if (ButtonConstants.isBorderlessVariant(c)) { if (ButtonConstants.isBorderlessVariant(c)) {
paintBorderlessBorder(c, g, x, y, width, height);
return; return;
} }
Graphics2D g2 = (Graphics2D) g; Graphics2D g2 = (Graphics2D) g;
@ -134,19 +135,35 @@ public class DarkButtonBorder implements Border, UIResource {
int fh = by + bh + borderSize - focusIns.top - focusIns.bottom; int fh = by + bh + borderSize - focusIns.top - focusIns.bottom;
if (paintFocus(c)) { if (paintFocus(c)) {
g.translate(fx, fy); paintFocusBorder(g2, focusArc, borderSize, fx, fy, fw, fh);
PaintUtil.paintFocusBorder(g2, fw, fh, focusArc, borderSize);
g.translate(-fx, -fy);
} }
g2.setColor(getBorderColor(c, focus)); paintLineBorder(c, g2, arc, focus, bx, by, bw, bh);
PaintUtil.paintLineBorder(g2, bx, by, bw, bh, arc);
if (corner != null) { if (corner != null) {
paintNeighbourFocus(g2, c, width, height); paintNeighbourFocus(g2, c, width, height);
} }
config.restore(); 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) { protected void paintNeighbourFocus(final Graphics2D g2, final Component c, final int width, final int height) {
JComponent left = ButtonConstants.getNeighbour(DarkButtonUI.KEY_LEFT_NEIGHBOUR, c); JComponent left = ButtonConstants.getNeighbour(DarkButtonUI.KEY_LEFT_NEIGHBOUR, c);
boolean paintLeft = DarkUIUtil.hasFocus(left); boolean paintLeft = DarkUIUtil.hasFocus(left);
@ -156,9 +173,8 @@ public class DarkButtonBorder implements Border, UIResource {
if (corner != null) ins = corner.maskInsets(ins, borderSize); if (corner != null) ins = corner.maskInsets(ins, borderSize);
int h = height - Math.max(0, getShadowSize(left) - borderSize); int h = height - Math.max(0, getShadowSize(left) - borderSize);
g2.translate(-3 * borderSize + 1, -ins.top); paintFocusBorder(g2, getFocusArc(left), borderSize, -3 * borderSize + 1, -ins.top, 4 * borderSize,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, h + ins.top + ins.bottom, getFocusArc(left), borderSize); h + ins.top + ins.bottom);
g2.translate(-(-3 * borderSize + 1), ins.bottom);
} }
JComponent right = ButtonConstants.getNeighbour(DarkButtonUI.KEY_RIGHT_NEIGHBOUR, c); JComponent right = ButtonConstants.getNeighbour(DarkButtonUI.KEY_RIGHT_NEIGHBOUR, c);
boolean paintRight = DarkUIUtil.hasFocus(right); boolean paintRight = DarkUIUtil.hasFocus(right);
@ -168,9 +184,8 @@ public class DarkButtonBorder implements Border, UIResource {
if (corner != null) ins = corner.maskInsets(ins, borderSize); if (corner != null) ins = corner.maskInsets(ins, borderSize);
int h = height - Math.max(0, getShadowSize(right) - borderSize); int h = height - Math.max(0, getShadowSize(right) - borderSize);
g2.translate(width - borderSize - 1, -ins.top); paintFocusBorder(g2, getFocusArc(right), borderSize, width - borderSize - 1, -ins.top, 4 * borderSize,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, h + ins.top + ins.bottom, getFocusArc(right), borderSize); h + ins.top + ins.bottom);
g2.translate(-(width - borderSize - 1), ins.top);
} }
JComponent top = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_NEIGHBOUR, c); 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); Insets ins = new Insets(0, 0, 0, 0);
if (corner != null) ins = corner.maskInsets(ins, borderSize); if (corner != null) ins = corner.maskInsets(ins, borderSize);
g2.translate(-ins.left, -3 * borderSize + 1); paintFocusBorder(g2, getFocusArc(top), borderSize, -ins.left, -3 * borderSize + 1,
PaintUtil.paintFocusBorder(g2, width + ins.right + ins.left, 4 * borderSize, getFocusArc(top), borderSize); width + ins.right + ins.left, 4 * borderSize);
g2.translate(ins.left, -(-3 * borderSize + 1));
} }
JComponent topLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_LEFT_NEIGHBOUR, c); JComponent topLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_LEFT_NEIGHBOUR, c);
boolean paintTopLeft = DarkUIUtil.hasFocus(topLeft); boolean paintTopLeft = DarkUIUtil.hasFocus(topLeft);
if (paintTopLeft) { if (paintTopLeft) {
g2.translate(-3 * borderSize + 1, -3 * borderSize + 1); paintFocusBorder(g2, getFocusArc(topLeft), borderSize, -3 * borderSize + 1, -3 * borderSize + 1,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(topLeft), borderSize); 4 * borderSize, 4 * borderSize);
g2.translate(-(-3 * borderSize + 1), -(-3 * borderSize + 1));
} }
JComponent topRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_RIGHT_NEIGHBOUR, c); JComponent topRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_TOP_RIGHT_NEIGHBOUR, c);
boolean paintTopRight = DarkUIUtil.hasFocus(topRight); boolean paintTopRight = DarkUIUtil.hasFocus(topRight);
if (paintTopRight) { if (paintTopRight) {
g2.translate(width - borderSize - 1, -3 * borderSize + 1); paintFocusBorder(g2, getFocusArc(topRight), borderSize, width - borderSize - 1, -3 * borderSize + 1,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(topRight), borderSize); 4 * borderSize, 4 * borderSize);
g2.translate(-(width - borderSize - 1), -(-3 * borderSize + 1));
} }
JComponent bottom = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_NEIGHBOUR, c); 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); Insets ins = new Insets(0, 0, 0, 0);
if (corner != null) ins = corner.maskInsets(ins, borderSize); if (corner != null) ins = corner.maskInsets(ins, borderSize);
g2.translate(-ins.left, height - borderSize - 1); paintFocusBorder(g2, getFocusArc(bottom), borderSize, -ins.left, height - borderSize - 1,
PaintUtil.paintFocusBorder(g2, width + ins.left + ins.right, 4 * borderSize, getFocusArc(bottom), width + ins.left + ins.right, 4 * borderSize);
borderSize);
g2.translate(ins.left, -(height - borderSize - 1));
} }
JComponent bottomLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_LEFT_NEIGHBOUR, c); JComponent bottomLeft = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_LEFT_NEIGHBOUR, c);
boolean paintBottomLeft = DarkUIUtil.hasFocus(bottomLeft); boolean paintBottomLeft = DarkUIUtil.hasFocus(bottomLeft);
if (paintBottomLeft) { if (paintBottomLeft) {
g2.translate(-3 * borderSize + 1, height - borderSize - 1); paintFocusBorder(g2, getFocusArc(bottomLeft), borderSize, -3 * borderSize + 1, height - borderSize - 1,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(bottomLeft), borderSize); 4 * borderSize, 4 * borderSize);
g2.translate(-(-3 * borderSize + 1), -(height - borderSize - 1));
} }
JComponent bottomRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_RIGHT_NEIGHBOUR, c); JComponent bottomRight = ButtonConstants.getNeighbour(DarkButtonUI.KEY_BOTTOM_RIGHT_NEIGHBOUR, c);
boolean paintBottomRight = DarkUIUtil.hasFocus(bottomRight); boolean paintBottomRight = DarkUIUtil.hasFocus(bottomRight);
if (paintBottomRight) { if (paintBottomRight) {
g2.translate(width - borderSize - 1, height - borderSize - 1); paintFocusBorder(g2, getFocusArc(bottomRight), borderSize, width - borderSize - 1, height - borderSize - 1,
PaintUtil.paintFocusBorder(g2, 4 * borderSize, 4 * borderSize, getFocusArc(bottomRight), borderSize); 4 * borderSize, 4 * borderSize);
g2.translate(-(width - borderSize - 1), -(height - borderSize - 1));
} }
} }

83
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;
}
}

86
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();
}
}
}

305
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;
}
}
}
}
}

9
core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdown.svg

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<defs id="colors">
<linearGradient id="Icons.dropdown.color">
<stop offset="0" stop-color="#AFB1B3"/>
<stop offset="1" stop-color="#AFB1B3"/>
</linearGradient>
</defs>
<path fill="url(#Icons.dropdown.color)" fill-rule="evenodd" d="M13 16l3-3h-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

9
core/src/main/resources/com/github/weisj/darklaf/icons/indicator/dropdownDisabled.svg

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<defs id="colors">
<linearGradient id="Icons.dropdownDisabled.color">
<stop offset="0" stop-color="#6E6E6E"/>
<stop offset="1" stop-color="#6E6E6E"/>
</linearGradient>
</defs>
<path fill="url(#Icons.dropdownDisabled.color)" fill-rule="evenodd" d="M13 16l3-3h-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

3
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.speaker4.volumeColor = %menuIconEnabled
Icons.speaker4Disabled.color = %menuIconDisabled Icons.speaker4Disabled.color = %menuIconDisabled
Icons.speaker4Disabled.volumeColor = %menuIconDisabled Icons.speaker4Disabled.volumeColor = %menuIconDisabled
Icons.dropdown.color = %menuIconEnabled
Icons.dropdownDisabled.color = %menuIconDisabled

10
core/src/main/resources/com/github/weisj/darklaf/properties/ui/misc.properties

@ -36,3 +36,13 @@ CloseButton.closeHoverIcon = navigation/closeHovered.svg[themed]
HelpButton.helpIcon = menu/help.svg[themed] HelpButton.helpIcon = menu/help.svg[themed]
HelpButton.helpHighlightIcon = menu/helpHighlight.svg[themed] HelpButton.helpHighlightIcon = menu/helpHighlight.svg[themed]
HelpButton.helpDisabledIcon = menu/helpDisabled.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)

74
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";
}
}
Loading…
Cancel
Save