diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonUI.java index 94050fa6..7d809aed 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/button/DarkButtonUI.java @@ -75,6 +75,11 @@ public class DarkButtonUI extends BasicButtonUI implements ButtonConstants { return new DarkButtonUI(); } + @Override + public String getPropertyPrefix() { + return super.getPropertyPrefix(); + } + @Override public void installUI(final JComponent c) { button = (AbstractButton) c; diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ButtonGroupInfo.java b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ButtonGroupInfo.java new file mode 100644 index 00000000..deb14131 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ButtonGroupInfo.java @@ -0,0 +1,188 @@ +/* + * 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.togglebutton; + +import javax.swing.*; +import java.awt.*; +import java.util.Enumeration; +import java.util.HashSet; + +public class ButtonGroupInfo { + protected JToggleButton activeButton; + + protected JToggleButton firstButton = null; + protected JToggleButton lastButton = null; + + protected JToggleButton previousButton = null; + protected JToggleButton nextButton = null; + + protected HashSet buttonsInGroup; + protected boolean sourceFound = false; + + public ButtonGroupInfo(final JToggleButton btn) { + activeButton = btn; + buttonsInGroup = new HashSet<>(); + } + + public static boolean isValidToggleButtonObject(final Object obj) { + return obj instanceof JToggleButton + && ((JToggleButton) obj).isEnabled() + && ((JToggleButton) obj).isVisible(); + } + + protected boolean containsInGroup(final JToggleButton button) { + return buttonsInGroup.contains(button); + } + + protected Component getFocusTransferBaseComponent(final boolean next) { + return firstButton; + } + + protected boolean getButtonGroupInfo() { + if (activeButton == null) { + return false; + } + + buttonsInGroup.clear(); + + // Get the button model from the source. + ButtonModel model = activeButton.getModel(); + if (!(model instanceof DefaultButtonModel)) { + return false; + } + + // If the button model is DefaultButtonModel, and use it, otherwise return. + DefaultButtonModel bm = (DefaultButtonModel) model; + + // get the ButtonGroup of the button from the button model + ButtonGroup group = bm.getGroup(); + if (group == null) { + return false; + } + + // Get all the buttons in the group + Enumeration e = group.getElements(); + if (e == null) { + return false; + } + + while (e.hasMoreElements()) { + AbstractButton curElement = e.nextElement(); + if (!isValidToggleButtonObject(curElement)) { + continue; + } + + buttonsInGroup.add((JToggleButton) curElement); + + // If firstBtn is not set yet, curElement is that first button + if (firstButton == null) { + firstButton = (JToggleButton) curElement; + } + + if (activeButton == curElement) { + sourceFound = true; + } else if (!sourceFound) { + // The source has not been yet found and the current element + // is the last previousBtn + previousButton = (JToggleButton) curElement; + } else if (nextButton == null) { + // The source has been found and the current element + // is the next valid button of the list + nextButton = (JToggleButton) curElement; + } + + // Set new last "valid" JToggleButton of the list + lastButton = (JToggleButton) curElement; + } + + return true; + } + + /** + * Find the new radio button that focus needs to be + * moved to in the group, select the button + * + * @param next, indicate if it's arrow up/left or down/right + */ + protected void selectNewButton(final boolean next) { + if (!getButtonGroupInfo()) { + return; + } + + if (sourceFound) { + JToggleButton newSelectedButton; + if (next) { + // Select Next button. Cycle to the first button if the source. + // button is the last of the group. + newSelectedButton = (null == nextButton) ? firstButton : nextButton; + } else { + // Select previous button. Cycle to the last button if the source. + // button is the first button of the group. + newSelectedButton = (null == previousButton) ? lastButton : previousButton; + } + if (newSelectedButton != null && + (newSelectedButton != activeButton)) { + newSelectedButton.requestFocusInWindow(); +// newSelectedButton.setSelected(true); + } + } + } + + /** + * Find the button group the passed in JToggleButton belongs to, and + * move focus to next component of the last button in the group + * or previous component of first button + * + * @param next, indicate if jump to next component or previous + */ + protected void jumpToNextComponent(final boolean next) { + if (!getButtonGroupInfo()) { + // In case the button does not belong to any group, it needs + // to be treated as a component + if (activeButton != null) { + lastButton = activeButton; + firstButton = activeButton; + } else { + return; + } + } + + // Update the component we will use as base to transfer + // focus from + JComponent compTransferFocusFrom = activeButton; + + // If next component in the parent window is not in + // the button group, current active button will be + // base, otherwise, the base will be first or last + // button in the button group + Component focusBase = getFocusTransferBaseComponent(next); + if (focusBase != null) { + if (next) { + KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(focusBase); + } else { + KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase); + } + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonKeyHandler.java b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonKeyHandler.java new file mode 100644 index 00000000..6a3a731e --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonKeyHandler.java @@ -0,0 +1,64 @@ +/* + * 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.togglebutton; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.Set; + +public class DarkToggleButtonKeyHandler implements KeyListener { + + // This listener checks if the key event is a focus traversal key event + // on a radio button, consume the event if so and move the focus + // to next/previous component + public void keyPressed(final KeyEvent e) { + AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e); + if (stroke != null && e.getSource() instanceof JRadioButton) { + JRadioButton source = (JRadioButton) e.getSource(); + boolean next = isFocusTraversalKey(source, + KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + stroke); + if (next || isFocusTraversalKey(source, + KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + stroke)) { + e.consume(); + ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source); + btnGroupInfo.jumpToNextComponent(next); + } + } + } + + private boolean isFocusTraversalKey(final JComponent c, final int id, final AWTKeyStroke stroke) { + Set keys = c.getFocusTraversalKeys(id); + return keys != null && keys.contains(stroke); + } + + public void keyReleased(final KeyEvent e) { + } + + public void keyTyped(final KeyEvent e) { + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonUI.java index 31c10534..d12bfed1 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/DarkToggleButtonUI.java @@ -35,6 +35,7 @@ import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicButtonListener; import java.awt.*; +import java.awt.event.KeyListener; import java.awt.geom.RoundRectangle2D; /** @@ -54,6 +55,7 @@ public class DarkToggleButtonUI extends DarkButtonUI implements ToggleButtonCons protected Color sliderBorderColor; protected Color inactiveSliderBorderColor; protected Color selectedForeground; + protected KeyListener keyListener; public static ComponentUI createUI(final JComponent c) { return new DarkToggleButtonUI(); @@ -75,6 +77,26 @@ public class DarkToggleButtonUI extends DarkButtonUI implements ToggleButtonCons selectedForeground = UIManager.getColor("ToggleButton.selectedForeground"); } + @Override + protected void installListeners(final AbstractButton b) { + super.installListeners(b); + keyListener = createKeyListener(b); + b.addKeyListener(keyListener); + ToggleButtonFocusNavigationActions.installActions(b); + } + + @Override + protected void uninstallListeners(final AbstractButton b) { + super.uninstallListeners(b); + b.removeKeyListener(keyListener); + keyListener = null; + ToggleButtonFocusNavigationActions.uninstallActions(b); + } + + protected KeyListener createKeyListener(final AbstractButton button) { + return new DarkToggleButtonKeyHandler(); + } + @Override protected BasicButtonListener createButtonListener(final AbstractButton b) { return new DarkToggleButtonListener(b, this); diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ToggleButtonFocusNavigationActions.java b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ToggleButtonFocusNavigationActions.java new file mode 100644 index 00000000..ece086a1 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/ToggleButtonFocusNavigationActions.java @@ -0,0 +1,92 @@ +/* + * 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.togglebutton; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +public class ToggleButtonFocusNavigationActions { + + public static void installActions(final AbstractButton button) { + if (button == null) return; + button.getActionMap().put("Previous", new SelectPreviousBtn()); + button.getActionMap().put("Next", new SelectNextBtn()); + + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("UP"), "Previous"); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("DOWN"), "Next"); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("LEFT"), "Previous"); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("RIGHT"), "Next"); + } + + public static void uninstallActions(final AbstractButton button) { + if (button == null) return; + button.getActionMap().remove("Previous"); + button.getActionMap().remove("Next"); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .remove(KeyStroke.getKeyStroke("UP")); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .remove(KeyStroke.getKeyStroke("DOWN")); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .remove(KeyStroke.getKeyStroke("LEFT")); + button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .remove(KeyStroke.getKeyStroke("RIGHT")); + } + + protected static void selectToggleButton(final ActionEvent event, final boolean next) { + // Get the source of the event. + Object eventSrc = event.getSource(); + + // Check whether the source is JRadioButton, it so, whether it is visible + if (!ButtonGroupInfo.isValidToggleButtonObject(eventSrc)) { + return; + } + + ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JToggleButton) eventSrc); + btnGroupInfo.selectNewButton(next); + } + + public static class SelectPreviousBtn extends AbstractAction { + public SelectPreviousBtn() { + super("Previous"); + } + + public void actionPerformed(final ActionEvent e) { + selectToggleButton(e, false); + } + } + + public static class SelectNextBtn extends AbstractAction { + public SelectNextBtn() { + super("Next"); + } + + public void actionPerformed(final ActionEvent e) { + selectToggleButton(e, true); + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/radiobutton/DarkRadioButtonUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/radiobutton/DarkRadioButtonUI.java index 05940d0a..2a491cc4 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/radiobutton/DarkRadioButtonUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/togglebutton/radiobutton/DarkRadioButtonUI.java @@ -24,9 +24,7 @@ package com.github.weisj.darklaf.ui.togglebutton.radiobutton; import com.github.weisj.darklaf.icons.EmptyIcon; -import com.github.weisj.darklaf.ui.togglebutton.DarkToggleButtonUI; -import com.github.weisj.darklaf.ui.togglebutton.ToggleButtonConstants; -import com.github.weisj.darklaf.ui.togglebutton.StateIconUI; +import com.github.weisj.darklaf.ui.togglebutton.*; import com.github.weisj.darklaf.util.GraphicsContext; import com.github.weisj.darklaf.util.GraphicsUtil; import com.github.weisj.darklaf.util.PropertyKey; @@ -35,10 +33,12 @@ import sun.swing.SwingUtilities2; import javax.swing.*; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.IconUIResource; +import javax.swing.plaf.basic.BasicButtonListener; import javax.swing.plaf.basic.BasicHTML; import javax.swing.plaf.metal.MetalRadioButtonUI; import javax.swing.text.View; import java.awt.*; +import java.awt.event.KeyListener; import java.awt.geom.Ellipse2D; import java.awt.geom.RectangularShape; import java.beans.PropertyChangeEvent; @@ -65,6 +65,8 @@ public class DarkRadioButtonUI extends MetalRadioButtonUI implements PropertyCha private Icon radioSelectedIcon; private Icon radioSelectedDisabledIcon; private Icon radioSelectedFocusedIcon; + protected BasicButtonListener buttonListener; + protected KeyListener keyListener; public static ComponentUI createUI(final JComponent c) { @@ -98,10 +100,22 @@ public class DarkRadioButtonUI extends MetalRadioButtonUI implements PropertyCha @Override protected void installListeners(final AbstractButton button) { - super.installListeners(button); + buttonListener = createButtonListener(button); + button.addMouseListener(buttonListener); + button.addMouseMotionListener(buttonListener); + button.addFocusListener(buttonListener); + button.addPropertyChangeListener(buttonListener); + button.addChangeListener(buttonListener); + keyListener = createKeyListener(button); + button.addKeyListener(keyListener); + ToggleButtonFocusNavigationActions.installActions(radioButton); button.addPropertyChangeListener(this); } + protected KeyListener createKeyListener(final AbstractButton button) { + return new DarkToggleButtonKeyHandler(); + } + @Override public void uninstallUI(final JComponent c) { super.uninstallUI(c); @@ -110,7 +124,15 @@ public class DarkRadioButtonUI extends MetalRadioButtonUI implements PropertyCha @Override protected void uninstallListeners(final AbstractButton button) { - super.uninstallListeners(button); + button.removeMouseListener(buttonListener); + button.removeMouseMotionListener(buttonListener); + button.removeFocusListener(buttonListener); + button.removeChangeListener(buttonListener); + button.removePropertyChangeListener(buttonListener); + buttonListener = null; + button.removeKeyListener(keyListener); + keyListener = null; + ToggleButtonFocusNavigationActions.uninstallActions(radioButton); button.removePropertyChangeListener(this); }