diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c0cb40..101e4f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ FlatLaf Change Log #### New features and improvements - Menus: Improved usability of submenus. (PR #490; issue #247) +- Menus: Scroll large menus using mouse wheel or up/down arrows. (issue #225) - Linux: Support using custom window decorations. Enable with `JFrame.setDefaultLookAndFeelDecorated(true)` and `JDialog.setDefaultLookAndFeelDecorated(true)` before creating a window. diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java index 365306ed..3c0014ab 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java @@ -16,18 +16,58 @@ package com.formdev.flatlaf.ui; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; import java.awt.Container; import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Insets; import java.awt.LayoutManager; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.beans.PropertyChangeListener; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.BoxLayout; +import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JViewport; +import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; +import javax.swing.Popup; +import javax.swing.PopupFactory; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.event.MenuKeyEvent; +import javax.swing.event.MenuKeyListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import javax.swing.plaf.ButtonUI; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; +import javax.swing.plaf.basic.BasicComboPopup; +import javax.swing.plaf.basic.BasicMenuItemUI; import javax.swing.plaf.basic.BasicPopupMenuUI; import javax.swing.plaf.basic.DefaultMenuLayout; +import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.util.LoggingFacade; @@ -41,12 +81,22 @@ import com.formdev.flatlaf.util.LoggingFacade; * @uiDefault PopupMenu.foreground Color * @uiDefault PopupMenu.border Border * + * + * + * @uiDefault Component.arrowType String chevron (default) or triangle + * @uiDefault PopupMenu.scrollArrowColor Color + * @uiDefault PopupMenu.hoverScrollArrowBackground Color optional + * * @author Karl Tauber */ public class FlatPopupMenuUI extends BasicPopupMenuUI implements StyleableUI { + /** @since 2.1 */ @Styleable protected String arrowType; + /** @since 2.1 */ @Styleable protected Color scrollArrowColor; + /** @since 2.1 */ @Styleable protected Color hoverScrollArrowBackground; + private PropertyChangeListener propertyChangeListener; private Map oldStyleValues; private AtomicBoolean borderShared; @@ -74,11 +124,23 @@ public class FlatPopupMenuUI public void installDefaults() { super.installDefaults(); + arrowType = UIManager.getString( "Component.arrowType" ); + scrollArrowColor = UIManager.getColor( "PopupMenu.scrollArrowColor" ); + hoverScrollArrowBackground = UIManager.getColor( "PopupMenu.hoverScrollArrowBackground" ); + LayoutManager layout = popupMenu.getLayout(); if( layout == null || layout instanceof UIResource ) popupMenu.setLayout( new FlatMenuLayout( popupMenu, BoxLayout.Y_AXIS ) ); } + @Override + protected void uninstallDefaults() { + super.uninstallDefaults(); + + scrollArrowColor = null; + hoverScrollArrowBackground = null; + } + @Override protected void installListeners() { super.installListeners(); @@ -122,6 +184,52 @@ public class FlatPopupMenuUI return FlatStylingSupport.getAnnotatedStyleableInfos( this, popupMenu.getBorder() ); } + @Override + public Popup getPopup( JPopupMenu popup, int x, int y ) { + // do not add scroller to combobox popups or to popups that already have a scroll pane + if( popup instanceof BasicComboPopup || + (popup.getComponentCount() > 0 && popup.getComponent( 0 ) instanceof JScrollPane) ) + return super.getPopup( popup, x, y ); + + // do not add scroller if popup fits into screen + Dimension prefSize = popup.getPreferredSize(); + int screenHeight = getScreenHeightAt( x, y ); + if( prefSize.height <= screenHeight ) + return super.getPopup( popup, x, y ); + + // create scroller + FlatPopupScroller scroller = new FlatPopupScroller( popup ); + scroller.setPreferredSize( new Dimension( prefSize.width, screenHeight ) ); + + // create popup + PopupFactory popupFactory = PopupFactory.getSharedInstance(); + return popupFactory.getPopup( popup.getInvoker(), scroller, x, y ); + } + + private int getScreenHeightAt( int x, int y ) { + // find GraphicsConfiguration at popup location (similar to JPopupMenu.getCurrentGraphicsConfiguration()) + GraphicsConfiguration gc = null; + for( GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices() ) { + if( device.getType() == GraphicsDevice.TYPE_RASTER_SCREEN ) { + GraphicsConfiguration dgc = device.getDefaultConfiguration(); + if( dgc.getBounds().contains( x, y ) ) { + gc = dgc; + break; + } + } + } + if( gc == null && popupMenu.getInvoker() != null ) + gc = popupMenu.getInvoker().getGraphicsConfiguration(); + + // compute screen height + // (always subtract screen insets because there is no API to detect whether + // the popup can overlap the taskbar; see JPopupMenu.canPopupOverlapTaskBar()) + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Rectangle screenBounds = (gc != null) ? gc.getBounds() : new Rectangle( toolkit.getScreenSize() ); + Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets( gc ); + return screenBounds.height - screenInsets.top - screenInsets.bottom; + } + //---- class FlatMenuLayout ----------------------------------------------- protected static class FlatMenuLayout @@ -138,4 +246,206 @@ public class FlatPopupMenuUI return super.preferredLayoutSize( target ); } } + + //---- class FlatPopupScroller -------------------------------------------- + + private class FlatPopupScroller + extends JPanel + implements MouseWheelListener, PopupMenuListener, MenuKeyListener + { + private final JPopupMenu popup; + + private final JScrollPane scrollPane; + private final JButton scrollUpButton; + private final JButton scrollDownButton; + private int unitIncrement; + + FlatPopupScroller( JPopupMenu popup ) { + super( new BorderLayout() ); + this.popup = popup; + + // this panel is required to avoid that JPopupMenu.setLocation() will be invoked + // while scrolling, because this would call JPopupMenu.showPopup() + JPanel view = new JPanel( new BorderLayout() ); + view.add( popup, BorderLayout.CENTER ); + + scrollPane = new JScrollPane( view, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPane.setBorder( null ); + + scrollUpButton = new ArrowButton( SwingConstants.NORTH ); + scrollDownButton = new ArrowButton( SwingConstants.SOUTH ); + + add( scrollPane, BorderLayout.CENTER ); + add( scrollUpButton, BorderLayout.NORTH ); + add( scrollDownButton, BorderLayout.SOUTH ); + + setBackground( popup.getBackground() ); + setBorder( popup.getBorder() ); + popup.setBorder( null ); + + popup.addPopupMenuListener( this ); + popup.addMouseWheelListener( this ); + popup.addMenuKeyListener( this ); + + updateArrowButtons(); + } + + void scroll( int unitsToScroll ) { + if( unitIncrement == 0 ) + unitIncrement = new JMenuItem( "X" ).getPreferredSize().height; + + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + int newY = viewPosition.y + (unitIncrement * unitsToScroll); + if( newY < 0 ) + newY = 0; + else + newY = Math.min( newY, viewport.getViewSize().height - viewport.getExtentSize().height ); + viewport.setViewPosition( new Point( viewPosition.x, newY ) ); + + updateArrowButtons(); + } + + void updateArrowButtons() { + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + + scrollUpButton.setVisible( viewPosition.y > 0 ); + scrollDownButton.setVisible( viewPosition.y < viewport.getViewSize().height - viewport.getExtentSize().height ); + } + + //---- interface PopupMenuListener ---- + + @Override + public void popupMenuWillBecomeInvisible( PopupMenuEvent e ) { + // restore popup border + popup.setBorder( getBorder() ); + + popup.removePopupMenuListener( this ); + popup.removeMouseWheelListener( this ); + popup.removeMenuKeyListener( this ); + } + + @Override public void popupMenuWillBecomeVisible( PopupMenuEvent e ) {} + @Override public void popupMenuCanceled( PopupMenuEvent e ) {} + + //---- interface MouseWheelListener ---- + + /** + * Scroll when user rotates mouse wheel. + */ + @Override + public void mouseWheelMoved( MouseWheelEvent e ) { + // convert mouse location before scrolling + Point mouseLocation = SwingUtilities.convertPoint( (Component) e.getSource(), e.getPoint(), this ); + + // scroll + scroll( e.getUnitsToScroll() ); + + // select menu item at mouse location + Component c = SwingUtilities.getDeepestComponentAt( this, mouseLocation.x, mouseLocation.y ); + if( c instanceof JMenuItem ) { + ButtonUI ui = ((JMenuItem)c).getUI(); + if( ui instanceof BasicMenuItemUI ) + MenuSelectionManager.defaultManager().setSelectedPath( ((BasicMenuItemUI)ui).getPath() ); + } + + // this avoids that the popup is closed when running on Java 8 + // https://bugs.openjdk.java.net/browse/JDK-8075063 + e.consume(); + } + + //---- interface MenuKeyListener ---- + + /** + * Scroll when user presses Up or Down keys. + */ + @Override + public void menuKeyPressed( MenuKeyEvent e ) { + // use invokeLater() because menu selection is not yet updated because + // this listener is invoked before another listener that updates the menu selection + EventQueue.invokeLater( () -> { + if( !isDisplayable() ) + return; + + MenuElement[] path = MenuSelectionManager.defaultManager().getSelectedPath(); + if( path.length == 0 ) + return; + + // scroll selected menu item to visible area + Component c = path[path.length - 1].getComponent(); + JViewport viewport = scrollPane.getViewport(); + Point pt = SwingUtilities.convertPoint( c, 0, 0, viewport ); + viewport.scrollRectToVisible( new Rectangle( pt, c.getSize() ) ); + + // update arrow buttons + boolean upVisible = scrollUpButton.isVisible(); + updateArrowButtons(); + if( !upVisible && scrollUpButton.isVisible() ) { + // if "up" button becomes visible, make sure that bottom menu item stays visible + Point viewPosition = viewport.getViewPosition(); + int newY = viewPosition.y + scrollUpButton.getPreferredSize().height; + viewport.setViewPosition( new Point( viewPosition.x, newY ) ); + } + } ); + } + + @Override public void menuKeyTyped( MenuKeyEvent e ) {} + @Override public void menuKeyReleased( MenuKeyEvent e ) {} + + //---- class ArrowButton ---------------------------------------------- + + private class ArrowButton + extends FlatArrowButton + implements MouseListener, ActionListener + { + private Timer timer; + + ArrowButton( int direction ) { + super( direction, arrowType, scrollArrowColor, null, null, hoverScrollArrowBackground, null, null ); + + addMouseListener( this ); + } + + @Override + public void paint( Graphics g ) { + // always fill background to paint over border on HiDPI screens + g.setColor( popup.getBackground() ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + + super.paint( g ); + } + + //---- interface MouseListener ---- + + @Override public void mouseClicked( MouseEvent e ) {} + @Override public void mousePressed( MouseEvent e ) {} + @Override public void mouseReleased( MouseEvent e ) {} + + @Override + public void mouseEntered( MouseEvent e ) { + if( timer == null ) + timer = new Timer( 50, this ); + timer.start(); + } + + @Override + public void mouseExited( MouseEvent e ) { + if( timer != null ) + timer.stop(); + } + + //---- interface ActionListener ---- + + @Override + public void actionPerformed( ActionEvent e ) { + if( timer != null && !isDisplayable() ) { + timer.stop(); + return; + } + + scroll( direction == SwingConstants.NORTH ? -1 : 1 ); + } + } + } } diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties index 895f5888..91126b86 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties @@ -252,6 +252,7 @@ Popup.dropShadowOpacity = 0.25 #---- PopupMenu ---- PopupMenu.borderColor = tint(@background,17%) +PopupMenu.hoverScrollArrowBackground = lighten(@background,5%) #---- ProgressBar ---- diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index 766bf944..daaa2e76 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -500,6 +500,7 @@ Popup.dropShadowInsets = -4,-4,4,4 PopupMenu.border = com.formdev.flatlaf.ui.FlatPopupMenuBorder PopupMenu.borderInsets = 4,1,4,1 PopupMenu.background = @menuBackground +PopupMenu.scrollArrowColor = @buttonArrowColor #---- PopupMenuSeparator ---- diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties index 6473b89a..488f991c 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties @@ -259,6 +259,7 @@ Popup.dropShadowOpacity = 0.15 #---- PopupMenu ---- PopupMenu.borderColor = shade(@background,28%) +PopupMenu.hoverScrollArrowBackground = darken(@background,5%) #---- ProgressBar ---- diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyleableInfo.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyleableInfo.java index 4ca37a48..1dd5c188 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyleableInfo.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyleableInfo.java @@ -422,6 +422,10 @@ public class TestFlatStyleableInfo FlatPopupMenuUI ui = (FlatPopupMenuUI) c.getUI(); Map> expected = expectedMap( + "arrowType", String.class, + "scrollArrowColor", Color.class, + "hoverScrollArrowBackground", Color.class, + "borderInsets", Insets.class, "borderColor", Color.class ); diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java index a8c51801..c85be429 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java @@ -565,6 +565,10 @@ public class TestFlatStyling JPopupMenu c = new JPopupMenu(); FlatPopupMenuUI ui = (FlatPopupMenuUI) c.getUI(); + ui.applyStyle( "arrowType: chevron" ); + ui.applyStyle( "scrollArrowColor: #fff" ); + ui.applyStyle( "hoverScrollArrowBackground: #fff" ); + ui.applyStyle( "borderInsets: 1,2,3,4" ); ui.applyStyle( "borderColor: #fff" ); diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 63d3cb00..8aa4acb9 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -464,6 +464,7 @@ class DemoFrame JMenuItem projectViewMenuItem = new JMenuItem(); JMenuItem structureViewMenuItem = new JMenuItem(); JMenuItem propertiesViewMenuItem = new JMenuItem(); + scrollingPopupMenu = new JMenu(); JMenuItem menuItem2 = new JMenuItem(); htmlMenuItem = new JMenuItem(); JRadioButtonMenuItem radioButtonMenuItem1 = new JRadioButtonMenuItem(); @@ -668,6 +669,12 @@ class DemoFrame } viewMenu.add(menu1); + //======== scrollingPopupMenu ======== + { + scrollingPopupMenu.setText("Scrolling Popup Menu"); + } + viewMenu.add(scrollingPopupMenu); + //---- menuItem2 ---- menuItem2.setText("Disabled Item"); menuItem2.setEnabled(false); @@ -889,6 +896,12 @@ class DemoFrame copyMenuItem.addActionListener( new DefaultEditorKit.CopyAction() ); pasteMenuItem.addActionListener( new DefaultEditorKit.PasteAction() ); + scrollingPopupMenu.add( "Large menus are scrollable" ); + scrollingPopupMenu.add( "Use mouse wheel to scroll" ); + scrollingPopupMenu.add( "Or use up/down arrows at top/bottom" ); + for( int i = 1; i <= 100; i++ ) + scrollingPopupMenu.add( "Item " + i ); + if( FlatLaf.supportsNativeWindowDecorations() || (SystemInfo.isLinux && JFrame.isDefaultLookAndFeelDecorated()) ) { if( SystemInfo.isLinux ) unsupported( windowDecorationsCheckBoxMenuItem ); @@ -934,6 +947,7 @@ class DemoFrame // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JMenuItem exitMenuItem; + private JMenu scrollingPopupMenu; private JMenuItem htmlMenuItem; private JMenu fontMenu; private JMenu optionsMenu; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 65bb5670..f561a71a 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -282,6 +282,13 @@ new FormModel { addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) } ) } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "scrollingPopupMenu" + "text": "Scrolling Popup Menu" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) add( new FormComponent( "javax.swing.JMenuItem" ) { name: "menuItem2" "text": "Disabled Item" diff --git a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt index 700e8f6a..1a170334 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt @@ -737,6 +737,8 @@ PopupMenu.borderInsets 4,1,4,1 javax.swing.plaf.InsetsUIResource [UI] PopupMenu.consumeEventOnClose false PopupMenu.font [active] $defaultFont [UI] PopupMenu.foreground #bbbbbb HSL 0 0 73 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.hoverScrollArrowBackground #484c4e HSL 200 4 29 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.scrollArrowColor #9b9b9b HSL 0 0 61 javax.swing.plaf.ColorUIResource [UI] #---- PopupMenuSeparator ---- diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt index 69c1f517..31cece69 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt @@ -742,6 +742,8 @@ PopupMenu.borderInsets 4,1,4,1 javax.swing.plaf.InsetsUIResource [UI] PopupMenu.consumeEventOnClose false PopupMenu.font [active] $defaultFont [UI] PopupMenu.foreground #000000 HSL 0 0 0 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.hoverScrollArrowBackground #e5e5e5 HSL 0 0 90 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.scrollArrowColor #666666 HSL 0 0 40 javax.swing.plaf.ColorUIResource [UI] #---- PopupMenuSeparator ---- diff --git a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt index 1ccc3ef4..4e3aa1ed 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt @@ -752,6 +752,8 @@ PopupMenu.borderInsets 4,1,4,1 javax.swing.plaf.InsetsUIResource [UI] PopupMenu.consumeEventOnClose false PopupMenu.font [active] $defaultFont [UI] PopupMenu.foreground #ff0000 HSL 0 100 50 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.hoverScrollArrowBackground #00ff00 HSL 120 100 50 javax.swing.plaf.ColorUIResource [UI] +PopupMenu.scrollArrowColor #0000ff HSL 240 100 50 javax.swing.plaf.ColorUIResource [UI] #---- PopupMenuSeparator ---- diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.java index 399b41a3..848700e7 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.java @@ -80,13 +80,24 @@ public class FlatMenusTest FlatLaf.updateUI(); } - private void showPopupMenuButtonActionPerformed(ActionEvent e) { + private void showPopupMenuButton(ActionEvent e) { Component invoker = (Component) e.getSource(); PopupMenu popupMenu = new PopupMenu(); popupMenu.applyComponentOrientation( getComponentOrientation() ); popupMenu.show( invoker, 0, invoker.getHeight() ); } + private void showScrollingPopupMenu(ActionEvent e) { + Component invoker = (Component) e.getSource(); + JPopupMenu popupMenu = new JPopupMenu(); + for( int i = 1; i <= 100; i++ ) { + popupMenu.add( "menu item " + i + (i % 5 == 0 ? " test" : "") ) + .addActionListener( e2 -> System.out.println( ((JMenuItem)e2.getSource()).getText() ) ); + } + popupMenu.applyComponentOrientation( getComponentOrientation() ); + popupMenu.show( invoker, 0, invoker.getHeight() ); + } + private void largerChanged() { LargerMenuItem.useLargerSize = largerCheckBox.isSelected(); menuBar2.revalidate(); @@ -230,6 +241,7 @@ public class FlatMenusTest JRadioButtonMenuItem radioButtonMenuItem11 = new JRadioButtonMenuItem(); JLabel popupMenuLabel = new JLabel(); JButton showPopupMenuButton = new JButton(); + showScrollingPopupMenuButton = new JButton(); armedCheckBox = new JCheckBox(); underlineCheckBox = new JCheckBox(); popupMenubackgroundCheckBox = new JCheckBox(); @@ -839,9 +851,14 @@ public class FlatMenusTest //---- showPopupMenuButton ---- showPopupMenuButton.setText("show JPopupMenu"); - showPopupMenuButton.addActionListener(e -> showPopupMenuButtonActionPerformed(e)); + showPopupMenuButton.addActionListener(e -> showPopupMenuButton(e)); add(showPopupMenuButton, "cell 1 2"); + //---- showScrollingPopupMenuButton ---- + showScrollingPopupMenuButton.setText("show scrolling JPopupMenu"); + showScrollingPopupMenuButton.addActionListener(e -> showScrollingPopupMenu(e)); + add(showScrollingPopupMenuButton, "cell 2 2"); + //---- armedCheckBox ---- armedCheckBox.setText("armed"); armedCheckBox.setMnemonic('A'); @@ -884,6 +901,7 @@ public class FlatMenusTest private JMenuBar menuBar2; private JCheckBox largerCheckBox; private JCheckBox accelCheckBox; + private JButton showScrollingPopupMenuButton; private JCheckBox armedCheckBox; private JCheckBox underlineCheckBox; private JCheckBox popupMenubackgroundCheckBox; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.jfd index 98071c78..6897ecce 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMenusTest.jfd @@ -629,10 +629,20 @@ new FormModel { add( new FormComponent( "javax.swing.JButton" ) { name: "showPopupMenuButton" "text": "show JPopupMenu" - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showPopupMenuButtonActionPerformed", true ) ) + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showPopupMenuButton", true ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "showScrollingPopupMenuButton" + "text": "show scrolling JPopupMenu" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showScrollingPopupMenu", true ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "armedCheckBox" "text": "armed" diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties index 77bd40a8..820d5436 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties @@ -276,6 +276,8 @@ Popup.dropShadowInsets = -6,6,6,6 #---- PopupMenu ---- PopupMenu.borderColor = #00f +PopupMenu.scrollArrowColor = #00f +PopupMenu.hoverScrollArrowBackground = #0f0 #---- PopupMenuSeparator ---- diff --git a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt index c06f6c8d..2cc6eacc 100644 --- a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt +++ b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt @@ -575,6 +575,8 @@ PopupMenu.borderInsets PopupMenu.consumeEventOnClose PopupMenu.font PopupMenu.foreground +PopupMenu.hoverScrollArrowBackground +PopupMenu.scrollArrowColor PopupMenu.selectedWindowInputMapBindings PopupMenu.selectedWindowInputMapBindings.RightToLeft PopupMenuSeparator.height