diff --git a/CHANGELOG.md b/CHANGELOG.md index 40410106..3c842f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ FlatLaf Change Log #### New features and improvements +- FileChooser: Added (optional) shortcuts panel. On Windows it contains "Recent + Items", "Desktop", "Documents", "This PC" and "Network". On macOS and Linux it + is empty/hidden. (issue #100) - Button and ToggleButton: Added missing foreground colors for hover, pressed, focused and selected states. (issue #535) - Table: Optionally paint alternating rows below table if table is smaller than diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatFileChooserUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatFileChooserUI.java index 6b4a749f..9ae03f41 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatFileChooserUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatFileChooserUI.java @@ -19,11 +19,21 @@ package com.formdev.flatlaf.ui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.RenderingHints; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.io.File; +import java.lang.reflect.Method; +import java.util.function.Function; import javax.swing.AbstractButton; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; @@ -34,12 +44,16 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.SwingConstants; import javax.swing.UIManager; +import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileView; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.metal.MetalFileChooserUI; import javax.swing.table.TableCellRenderer; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.ScaledImageIcon; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -133,12 +147,21 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault FileChooser.listViewActionLabelText String * @uiDefault FileChooser.detailsViewActionLabelText String * + * + * + * @uiDefault FileChooser.shortcuts.buttonSize Dimension optional; default is 84,64 + * @uiDefault FileChooser.shortcuts.iconSize Dimension optional; default is 32,32 + * @uiDefault FileChooser.shortcuts.filesFunction Function + * @uiDefault FileChooser.shortcuts.displayNameFunction Function + * @uiDefault FileChooser.shortcuts.iconFunction Function + * * @author Karl Tauber */ public class FlatFileChooserUI extends MetalFileChooserUI { private final FlatFileView fileView = new FlatFileView(); + private FlatShortcutsPanel shortcutsPanel; public static ComponentUI createUI( JComponent c ) { return new FlatFileChooserUI( (JFileChooser) c ); @@ -153,6 +176,25 @@ public class FlatFileChooserUI super.installComponents( fc ); patchUI( fc ); + + if( !UIManager.getBoolean( "FileChooser.noPlacesBar" ) ) { // same as in Windows L&F + FlatShortcutsPanel panel = createShortcutsPanel( fc ); + if( panel.getComponentCount() > 0 ) { + shortcutsPanel = panel; + fc.add( shortcutsPanel, BorderLayout.LINE_START ); + fc.addPropertyChangeListener( shortcutsPanel ); + } + } + } + + @Override + public void uninstallComponents( JFileChooser fc ) { + super.uninstallComponents( fc ); + + if( shortcutsPanel != null ) { + fc.removePropertyChangeListener( shortcutsPanel ); + shortcutsPanel = null; + } } private void patchUI( JFileChooser fc ) { @@ -192,6 +234,25 @@ public class FlatFileChooserUI } catch( ArrayIndexOutOfBoundsException ex ) { // ignore } + + // put north, center and south components into a new panel so that + // the shortcuts panel (at west) gets full height + LayoutManager layout = fc.getLayout(); + if( layout instanceof BorderLayout ) { + BorderLayout borderLayout = (BorderLayout) layout; + borderLayout.setHgap( 8 ); + + Component north = borderLayout.getLayoutComponent( BorderLayout.NORTH ); + Component center = borderLayout.getLayoutComponent( BorderLayout.CENTER ); + Component south = borderLayout.getLayoutComponent( BorderLayout.SOUTH ); + if( north != null && center != null && south != null ) { + JPanel p = new JPanel( new BorderLayout( 0, 11 ) ); + p.add( north, BorderLayout.NORTH ); + p.add( center, BorderLayout.CENTER ); + p.add( south, BorderLayout.SOUTH ); + fc.add( p, BorderLayout.CENTER ); + } + } } @Override @@ -250,9 +311,19 @@ public class FlatFileChooserUI return p; } + /** @since 2.3 */ + protected FlatShortcutsPanel createShortcutsPanel( JFileChooser fc ) { + return new FlatShortcutsPanel( fc ); + } + @Override public Dimension getPreferredSize( JComponent c ) { - return UIScale.scale( super.getPreferredSize( c ) ); + Dimension prefSize = super.getPreferredSize( c ); + Dimension minSize = getMinimumSize( c ); + int shortcutsPanelWidth = (shortcutsPanel != null) ? shortcutsPanel.getPreferredSize().width : 0; + return new Dimension( + Math.max( prefSize.width, minSize.width + shortcutsPanelWidth ), + Math.max( prefSize.height, minSize.height ) ); } @Override @@ -316,4 +387,234 @@ public class FlatFileChooserUI return icon; } } + + //---- class FlatShortcutsPanel ------------------------------------------- + + /** @since 2.3 */ + public static class FlatShortcutsPanel + extends JToolBar + implements PropertyChangeListener + { + private final JFileChooser fc; + + private final Dimension buttonSize; + private final Dimension iconSize; + private final Function filesFunction; + private final Function displayNameFunction; + private final Function iconFunction; + + protected final File[] files; + protected final JToggleButton[] buttons; + protected final ButtonGroup buttonGroup; + + @SuppressWarnings( "unchecked" ) + public FlatShortcutsPanel( JFileChooser fc ) { + super( JToolBar.VERTICAL ); + this.fc = fc; + setFloatable( false ); + + buttonSize = UIScale.scale( getUIDimension( "FileChooser.shortcuts.buttonSize", 84, 64 ) ); + iconSize = getUIDimension( "FileChooser.shortcuts.iconSize", 32, 32 ); + + filesFunction = (Function) UIManager.get( "FileChooser.shortcuts.filesFunction" ); + displayNameFunction = (Function) UIManager.get( "FileChooser.shortcuts.displayNameFunction" ); + iconFunction = (Function) UIManager.get( "FileChooser.shortcuts.iconFunction" ); + + FileSystemView fsv = fc.getFileSystemView(); + File[] files = getChooserShortcutPanelFiles( fsv ); + if( filesFunction != null ) + files = filesFunction.apply( files ); + this.files = files; + + // create toolbar buttons + buttons = new JToggleButton[files.length]; + buttonGroup = new ButtonGroup(); + for( int i = 0; i < files.length; i++ ) { + // wrap drive path + if( fsv.isFileSystemRoot( files[i] ) ) + files[i] = fsv.createFileObject( files[i].getAbsolutePath() ); + + File file = files[i]; + String name = getDisplayName( fsv, file ); + Icon icon = getIcon( fsv, file ); + + // remove path from name + int lastSepIndex = name.lastIndexOf( File.separatorChar ); + if( lastSepIndex >= 0 && lastSepIndex < name.length() - 1 ) + name = name.substring( lastSepIndex + 1 ); + + // scale icon (if necessary) + if( icon instanceof ImageIcon ) + icon = new ScaledImageIcon( (ImageIcon) icon, iconSize.width, iconSize.height ); + else if( icon != null ) + icon = new ShortcutIcon( icon, iconSize.width, iconSize.height ); + + // create button + JToggleButton button = createButton( name, icon ); + button.addActionListener( e -> { + fc.setCurrentDirectory( file ); + } ); + + add( button ); + buttonGroup.add( button ); + buttons[i] = button; + } + + directoryChanged( fc.getCurrentDirectory() ); + } + + private Dimension getUIDimension( String key, int defaultWidth, int defaultHeight ) { + Dimension size = UIManager.getDimension( key ); + if( size == null ) + size = new Dimension( defaultWidth, defaultHeight ); + return size; + } + + protected JToggleButton createButton( String name, Icon icon ) { + JToggleButton button = new JToggleButton( name, icon ); + button.setVerticalTextPosition( SwingConstants.BOTTOM ); + button.setHorizontalTextPosition( SwingConstants.CENTER ); + button.setAlignmentX( Component.CENTER_ALIGNMENT ); + button.setIconTextGap( 0 ); + button.setPreferredSize( buttonSize ); + button.setMaximumSize( buttonSize ); + return button; + } + + protected File[] getChooserShortcutPanelFiles( FileSystemView fsv ) { + try { + if( SystemInfo.isJava_12_orLater ) { + Method m = fsv.getClass().getMethod( "getChooserShortcutPanelFiles" ); + File[] files = (File[]) m.invoke( fsv ); + + // on macOS and Linux, files consists only of the user home directory + if( files.length == 1 && files[0].equals( new File( System.getProperty( "user.home" ) ) ) ) + files = new File[0]; + + return files; + } else if( SystemInfo.isWindows ) { + Class cls = Class.forName( "sun.awt.shell.ShellFolder" ); + Method m = cls.getMethod( "get", String.class ); + return (File[]) m.invoke( null, "fileChooserShortcutPanelFolders" ); + } + } catch( IllegalAccessException ex ) { + // do not log because access may be denied via VM option '--illegal-access=deny' + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + + // fallback + return new File[0]; + } + + protected String getDisplayName( FileSystemView fsv, File file ) { + if( displayNameFunction != null ) { + String name = displayNameFunction.apply( file ); + if( name != null ) + return name; + } + + return fsv.getSystemDisplayName( file ); + } + + protected Icon getIcon( FileSystemView fsv, File file ) { + if( iconFunction != null ) { + Icon icon = iconFunction.apply( file ); + if( icon != null ) + return icon; + } + + // Java 17+ supports getting larger system icons + try { + if( SystemInfo.isJava_17_orLater ) { + Method m = fsv.getClass().getMethod( "getSystemIcon", File.class, int.class, int.class ); + return (Icon) m.invoke( fsv, file, iconSize.width, iconSize.height ); + } else if( iconSize.width > 16 || iconSize.height > 16 ) { + Class cls = Class.forName( "sun.awt.shell.ShellFolder" ); + if( cls.isInstance( file ) ) { + Method m = file.getClass().getMethod( "getIcon", boolean.class ); + m.setAccessible( true ); + Image image = (Image) m.invoke( file, true ); + if( image != null ) + return new ImageIcon( image ); + } + } + } catch( IllegalAccessException ex ) { + // do not log because access may be denied via VM option '--illegal-access=deny' + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + + // get system icon in default size 16x16 + return fsv.getSystemIcon( file ); + } + + protected void directoryChanged( File file ) { + if( file != null ) { + String absolutePath = file.getAbsolutePath(); + for( int i = 0; i < files.length; i++ ) { + // also compare path because otherwise selecting "Documents" + // in "Look in" combobox would not select "Documents" shortcut item + if( files[i].equals( file ) || files[i].getAbsolutePath().equals( absolutePath ) ) { + buttons[i].setSelected( true ); + return; + } + } + } + + buttonGroup.clearSelection(); + } + + @Override + public void propertyChange( PropertyChangeEvent e ) { + switch( e.getPropertyName() ) { + case JFileChooser.DIRECTORY_CHANGED_PROPERTY: + directoryChanged( fc.getCurrentDirectory() ); + break; + } + } + } + + //---- class ShortcutIcon ------------------------------------------------- + + private static class ShortcutIcon + implements Icon + { + private final Icon icon; + private final int iconWidth; + private final int iconHeight; + + ShortcutIcon( Icon icon, int iconWidth, int iconHeight ) { + this.icon = icon; + this.iconWidth = iconWidth; + this.iconHeight = iconHeight; + } + + @Override + public void paintIcon( Component c, Graphics g, int x, int y ) { + Graphics2D g2 = (Graphics2D) g.create(); + try { + // set rendering hint for the case that the icon is a bitmap (not used for vector icons) + g2.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC ); + + double scale = (double) getIconWidth() / (double) icon.getIconWidth(); + g2.translate( x, y ); + g2.scale( scale, scale ); + + icon.paintIcon( c, g2, 0, 0 ); + } finally { + g2.dispose(); + } + } + + @Override + public int getIconWidth() { + return UIScale.scale( iconWidth ); + } + + @Override + public int getIconHeight() { + return UIScale.scale( iconHeight ); + } + } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java index 60a827af..8678afcd 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java @@ -50,6 +50,7 @@ public class SystemInfo public static final long javaVersion; public static final boolean isJava_9_orLater; public static final boolean isJava_11_orLater; + /** @since 2.3 */ public static final boolean isJava_12_orLater; public static final boolean isJava_15_orLater; /** @since 2 */ public static final boolean isJava_17_orLater; /** @since 2 */ public static final boolean isJava_18_orLater; @@ -95,6 +96,7 @@ public class SystemInfo javaVersion = scanVersion( System.getProperty( "java.version" ) ); isJava_9_orLater = (javaVersion >= toVersion( 9, 0, 0, 0 )); isJava_11_orLater = (javaVersion >= toVersion( 11, 0, 0, 0 )); + isJava_12_orLater = (javaVersion >= toVersion( 12, 0, 0, 0 )); isJava_15_orLater = (javaVersion >= toVersion( 15, 0, 0, 0 )); isJava_17_orLater = (javaVersion >= toVersion( 17, 0, 0, 0 )); isJava_18_orLater = (javaVersion >= toVersion( 18, 0, 0, 0 )); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatChooserTest.java index 0a4b5b1b..a15020b8 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatChooserTest.java @@ -16,7 +16,12 @@ package com.formdev.flatlaf.testing; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Function; import javax.swing.*; +import com.formdev.flatlaf.icons.FlatFileChooserHomeFolderIcon; import net.miginfocom.swing.*; /** @@ -28,6 +33,24 @@ public class FlatChooserTest public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatChooserTest" ); + + UIManager.put( "FileChooser.shortcuts.filesFunction", (Function) files -> { + ArrayList list = new ArrayList<>( Arrays.asList( files ) ); + list.add( 0, new File( System.getProperty( "user.home" ) ) ); + return list.toArray( new File[list.size()] ); + } ); + + UIManager.put( "FileChooser.shortcuts.displayNameFunction", (Function) file -> { + if( file.getAbsolutePath().equals( System.getProperty( "user.home" ) ) ) + return "Home"; + return null; + } ); + UIManager.put( "FileChooser.shortcuts.iconFunction", (Function) file -> { + if( file.getAbsolutePath().equals( System.getProperty( "user.home" ) ) ) + return new FlatFileChooserHomeFolderIcon(); + return null; + } ); + frame.showFrame( FlatChooserTest::new ); } ); } 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 166e07ca..1ce4fc69 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 @@ -265,6 +265,8 @@ FileChooser.homeFolderIcon FileChooser.listViewIcon FileChooser.newFolderIcon FileChooser.readOnly +FileChooser.shortcuts.buttonSize +FileChooser.shortcuts.iconSize FileChooser.upFolderIcon FileChooser.useSystemExtensionHiding FileChooser.usesSingleFilePane