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. 28
      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.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);
}
}

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,
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);
}
}

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

28
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)

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