diff --git a/build.gradle b/build.gradle index 9034a484..dfbc78d1 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,8 @@ tasks.withType(JavaCompile) { options.compilerArgs += [ '--add-exports=java.desktop/sun.awt=ALL-UNNAMED', '--add-exports=java.desktop/com.sun.java.swing=ALL-UNNAMED', - '--add-exports=java.desktop/sun.swing=ALL-UNNAMED' + '--add-exports=java.desktop/sun.swing=ALL-UNNAMED', + '--add-exports=java.desktop/sun.awt.shell=ALL-UNNAMED' ] } } diff --git a/src/main/java/com/weis/darklaf/components/OverlayScrollPane.java b/src/main/java/com/weis/darklaf/components/OverlayScrollPane.java index ce87f7c1..78600f19 100644 --- a/src/main/java/com/weis/darklaf/components/OverlayScrollPane.java +++ b/src/main/java/com/weis/darklaf/components/OverlayScrollPane.java @@ -40,7 +40,6 @@ public class OverlayScrollPane extends JLayeredPane { protected final OScrollPane scrollPane; private final ControlPanel controlPanel; - private Insets barInsets; /** * Creates a JScrollIndicator that displays the contents of the specified component, @@ -76,9 +75,7 @@ public class OverlayScrollPane extends JLayeredPane { * @param hsbPolicy an integer that specifies the horizontal scrollbar policy */ public OverlayScrollPane(final JComponent view, final int vsbPolicy, final int hsbPolicy) { - setBorder(null); scrollPane = createScrollPane(view, vsbPolicy, hsbPolicy); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); add(scrollPane, JLayeredPane.DEFAULT_LAYER); controlPanel = new ControlPanel(scrollPane); @@ -106,47 +103,32 @@ public class OverlayScrollPane extends JLayeredPane { scrollPane.setSize(getSize()); controlPanel.setSize(getSize()); scrollPane.doLayout(); - } @Override - public Dimension getPreferredSize() { - return scrollPane.getPreferredSize(); } public void setVerticalScrollBarPolicy(final int policy) { scrollPane.setVerticalScrollBarPolicy(policy); controlPanel.showVerticalScrollBar(policy != JScrollPane.VERTICAL_SCROLLBAR_NEVER); } @Override - public void setPreferredSize(final Dimension preferredSize) { - super.setPreferredSize(preferredSize); - scrollPane.setPreferredSize(preferredSize); - } - - public void setHorizontalScrollBarPolicy(final int policy) { - scrollPane.setHorizontalScrollBarPolicy(policy); - controlPanel.showHorizontalScrollBar(policy != JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - } - - @Contract(pure = true) - @NotNull - public JScrollBar getVerticalScrollBar() { - return scrollPane.verticalScrollBar; - } - - @Contract(pure = true) - @NotNull - public JScrollBar getHorizontalScrollBar() { - return scrollPane.horizontalScrollBar; - } - - public void setViewportView(final Component c) { - scrollPane.setViewportView(c); + public Dimension getPreferredSize() { + return scrollPane.getPreferredSize(); } private static final class PopupScrollBar extends JScrollBar { - private PopupScrollBar(final int direction) { + private final JScrollPane pane; + + private PopupScrollBar(final int direction, final JScrollPane pane) { super(direction); + this.pane = pane; + putClientProperty("JScrollBar.fastWheelScrolling", true); setOpaque(false); } + + @Contract(pure = true) + @Override + public boolean isOpaque() { + return false; + } } protected static class OScrollPane extends JScrollPane { @@ -218,10 +200,12 @@ public class OverlayScrollPane extends JLayeredPane { */ public void setUI(final ScrollPaneUI ui) { if (verticalScrollBar == null) { - verticalScrollBar = new PopupScrollBar(JScrollBar.VERTICAL); + verticalScrollBar = new PopupScrollBar(JScrollBar.VERTICAL, this); + verticalScrollBar.putClientProperty("JScrollBar.scrollPaneParent", this); } if (horizontalScrollBar == null) { - horizontalScrollBar = new PopupScrollBar(JScrollBar.HORIZONTAL); + horizontalScrollBar = new PopupScrollBar(JScrollBar.HORIZONTAL, this); + horizontalScrollBar.putClientProperty("JScrollBar.scrollPaneParent", this); } super.setUI(ui); SwingUtilities.invokeLater(() -> { @@ -243,6 +227,33 @@ public class OverlayScrollPane extends JLayeredPane { } } + @Override + public void setPreferredSize(final Dimension preferredSize) { + super.setPreferredSize(preferredSize); + scrollPane.setPreferredSize(preferredSize); + } + + public void setHorizontalScrollBarPolicy(final int policy) { + scrollPane.setHorizontalScrollBarPolicy(policy); + controlPanel.showHorizontalScrollBar(policy != JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + @Contract(pure = true) + @NotNull + public JScrollBar getVerticalScrollBar() { + return scrollPane.verticalScrollBar; + } + + @Contract(pure = true) + @NotNull + public JScrollBar getHorizontalScrollBar() { + return scrollPane.horizontalScrollBar; + } + + public void setViewportView(final Component c) { + scrollPane.setViewportView(c); + } + private final class ControlPanel extends JPanel { private boolean showVertical; @@ -281,20 +292,14 @@ public class OverlayScrollPane extends JLayeredPane { scrollPane.horizontalScrollBar.setVisible(show); } - @NotNull - private Rectangle getVerticalBounds() { - var bounds = OverlayScrollPane.this.getBounds(); - var verticalSize = scrollPane.verticalScrollBar.getPreferredSize(); - return new Rectangle(bounds.width - verticalSize.width, 0, - verticalSize.width, bounds.height); - } - - @NotNull - private Rectangle getHorizontalBounds() { - var bounds = OverlayScrollPane.this.getBounds(); - var horizontalSize = scrollPane.horizontalScrollBar.getPreferredSize(); - return new Rectangle(0, bounds.height - horizontalSize.height, - bounds.width, horizontalSize.height); + @Override + public boolean contains(final int x, final int y) { + if (scrollPane.horizontalScrollBar.isVisible() + && scrollPane.horizontalScrollBar.getBounds().contains(x, y)) { + return true; + } + return scrollPane.verticalScrollBar.isVisible() + && scrollPane.verticalScrollBar.getBounds().contains(x, y); } } diff --git a/src/main/java/com/weis/darklaf/components/ShadowButton.java b/src/main/java/com/weis/darklaf/components/ShadowButton.java new file mode 100644 index 00000000..2084e249 --- /dev/null +++ b/src/main/java/com/weis/darklaf/components/ShadowButton.java @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.components; + +import javax.swing.*; + +public class ShadowButton extends JButton { + + public ShadowButton(final Icon icon) { + super(icon); + init(); + } + + private void init() { + setRolloverEnabled(true); + setOpaque(false); + putClientProperty("JButton.variant", "shadow"); + putClientProperty("JButton.square", Boolean.TRUE); + } + + public ShadowButton(final Action action) { + super(action); + init(); + } +} diff --git a/src/main/java/com/weis/darklaf/components/TooltipAwareButton.java b/src/main/java/com/weis/darklaf/components/TooltipAwareButton.java new file mode 100644 index 00000000..f125ff10 --- /dev/null +++ b/src/main/java/com/weis/darklaf/components/TooltipAwareButton.java @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.components; + +import com.weis.darklaf.components.alignment.Alignment; +import com.weis.darklaf.components.tooltip.ToolTipContext; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; + +public class TooltipAwareButton extends JButton { + + private final ToolTipContext context = new ToolTipContext(this) + .setAlignment(Alignment.CENTER) + .setCenterAlignment(Alignment.SOUTH); + + public TooltipAwareButton() { + this(null, null); + } + + public TooltipAwareButton(final String text, final Icon icon) { + super(text, icon); + } + + public TooltipAwareButton(final Icon icon) { + this(null, icon); + } + + public TooltipAwareButton(final String text) { + this(text, null); + } + + public TooltipAwareButton(final Action a) { + super(a); + } + + @Override + public Point getToolTipLocation(final MouseEvent event) { + return context.getToolTipLocation(event); + } + + @Override + public JToolTip createToolTip() { + return context.getToolTip(); + } +} diff --git a/src/main/java/com/weis/darklaf/components/TooltipAwareToggleButton.java b/src/main/java/com/weis/darklaf/components/TooltipAwareToggleButton.java new file mode 100644 index 00000000..6e15f52b --- /dev/null +++ b/src/main/java/com/weis/darklaf/components/TooltipAwareToggleButton.java @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.components; + +import com.weis.darklaf.components.alignment.Alignment; +import com.weis.darklaf.components.tooltip.ToolTipContext; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; + +public class TooltipAwareToggleButton extends JToggleButton { + + private final ToolTipContext context = new ToolTipContext(this) + .setAlignment(Alignment.CENTER) + .setCenterAlignment(Alignment.SOUTH); + + public TooltipAwareToggleButton() { + this(null, null); + } + + public TooltipAwareToggleButton(final String text, final Icon icon) { + super(text, icon); + } + + public TooltipAwareToggleButton(final Icon icon) { + this(null, icon); + } + + public TooltipAwareToggleButton(final String text) { + this(text, null); + } + + public TooltipAwareToggleButton(final Action a) { + super(a); + } + + @Override + public Point getToolTipLocation(final MouseEvent event) { + return context.getToolTipLocation(event); + } + + @Override + public JToolTip createToolTip() { + return context.getToolTip(); + } +} diff --git a/src/main/java/com/weis/darklaf/ui/button/DarkToggleButtonUI.java b/src/main/java/com/weis/darklaf/ui/button/DarkToggleButtonUI.java index 9bc2b8b9..36a4a108 100644 --- a/src/main/java/com/weis/darklaf/ui/button/DarkToggleButtonUI.java +++ b/src/main/java/com/weis/darklaf/ui/button/DarkToggleButtonUI.java @@ -82,7 +82,9 @@ public class DarkToggleButtonUI extends DarkButtonUI { public Dimension getPreferredSize(final JComponent c) { Dimension d = super.getPreferredSize(c); - d.width += SLIDER_WIDTH + DarkButtonBorder.BORDER_SIZE; + if (isSlider(c)) { + d.width += SLIDER_WIDTH + DarkButtonBorder.BORDER_SIZE; + } return d; } @@ -129,20 +131,43 @@ public class DarkToggleButtonUI extends DarkButtonUI { } } - protected Color getBackgroundColor(@NotNull final JComponent c) { - if ((c instanceof JToggleButton && ((JToggleButton) c).isSelected())) { - return UIManager.getColor("Button.activeFillColor"); + private void paintSlider(@NotNull final Graphics2D g, final AbstractButton c) { + var bounds = getSliderBounds(c); + g.translate(bounds.x, bounds.y); + Shape slider = new RoundRectangle2D.Float(0, 0, bounds.width, bounds.height, + bounds.height, bounds.height); + + if (c.hasFocus()) { + g.translate(-BSIZE, -BSIZE); + DarkUIUtil.paintFocusBorder(g, bounds.width + 2 * BSIZE, bounds.height + 2 * BSIZE, + (float) ((bounds.height + 2 * BSIZE) / 2.0 + 2), true); + g.translate(BSIZE, BSIZE); + } + + g.setColor(getBackgroundColor(c)); + g.fill(slider); + g.setColor(getToggleBorderColor(c)); + g.draw(slider); + g.setColor(getSliderColor(c)); + + if (c.isSelected()) { + g.fill(new Ellipse2D.Float( + bounds.width - bounds.height + 1, 1, bounds.height - 1.5f, bounds.height - 1.5f)); } else { - return UIManager.getColor("Button.inactiveFillColor"); + g.fill(new Ellipse2D.Float(1, 1, bounds.height - 1.5f, bounds.height - 1.5f)); } + g.translate(-bounds.x, -bounds.y); } - @Override - public boolean contains(@NotNull final JComponent c, final int x, final int y) { - if (!(x >= 0 && x <= c.getWidth() && y >= 0 && y <= c.getHeight())) return false; - var bounds = getSliderBounds(c); - return new RoundRectangle2D.Float(bounds.x, bounds.y, bounds.width, bounds.height, - bounds.height, bounds.height).contains(x, y); + protected Color getBackgroundColor(@NotNull final JComponent c) { + if (c instanceof JToggleButton && c.isEnabled()) { + if (((JToggleButton) c).isSelected()) { + return UIManager.getColor("ToggleButton.activeFillColor"); + } else { + return UIManager.getColor("ToggleButton.inactiveFillColor"); + } + } + return super.getBackgroundColor(c); } @NotNull @@ -162,31 +187,13 @@ public class DarkToggleButtonUI extends DarkButtonUI { && "slider".equals(c.getClientProperty("ToggleButton.variant")); } - private void paintSlider(@NotNull final Graphics2D g, final AbstractButton c) { + @Override + public boolean contains(@NotNull final JComponent c, final int x, final int y) { + if (!isSlider(c)) return super.contains(c, x, y); + if (!(x >= 0 && x <= c.getWidth() && y >= 0 && y <= c.getHeight())) return false; var bounds = getSliderBounds(c); - g.translate(bounds.x, bounds.y); - Shape slider = new RoundRectangle2D.Float(0, 0, bounds.width, bounds.height, - bounds.height, bounds.height); - g.setColor(getBackgroundColor(c)); - g.fill(slider); - - if (c.hasFocus()) { - g.translate(-BSIZE, -BSIZE); - DarkUIUtil.paintFocusBorder(g, bounds.width + 2 * BSIZE, bounds.height + 2 * BSIZE, - (float) (bounds.height / 2.0) + 2, true); - g.translate(BSIZE, BSIZE); - } - - g.setColor(getToggleBorderColor(c)); - g.draw(slider); - g.setColor(getSliderColor(c)); - if (c.isSelected()) { - g.fill(new Ellipse2D.Float( - bounds.width - bounds.height + 1, 1, bounds.height - 1.5f, bounds.height - 1.5f)); - } else { - g.fill(new Ellipse2D.Float(1, 1, bounds.height - 1.5f, bounds.height - 1.5f)); - } - g.translate(-bounds.x, -bounds.y); + return new RoundRectangle2D.Float(bounds.x, bounds.y, bounds.width, bounds.height, + bounds.height, bounds.height).contains(x, y); } private static Color getToggleBorderColor(@NotNull final AbstractButton b) { diff --git a/src/main/java/com/weis/darklaf/ui/checkbox/DarkCheckBoxMenuItemUI.java b/src/main/java/com/weis/darklaf/ui/checkbox/DarkCheckBoxMenuItemUI.java index 83a172c8..190a4ef6 100644 --- a/src/main/java/com/weis/darklaf/ui/checkbox/DarkCheckBoxMenuItemUI.java +++ b/src/main/java/com/weis/darklaf/ui/checkbox/DarkCheckBoxMenuItemUI.java @@ -56,7 +56,7 @@ public class DarkCheckBoxMenuItemUI extends DarkMenuItemUIBase { Graphics2D g = (Graphics2D) g2; GraphicsContext config = GraphicsUtil.setupStrokePainting(g); var rect = lr.getCheckRect(); - getCheckBoxIcon(lh.getMenuItem()).paintIcon(lh.getMenuItem(), g2, rect.x, rect.y); + getCheckBoxIcon(lh.getMenuItem()).paintIcon(lh.getMenuItem(), g2, rect.x - 1, rect.y); config.restore(); g.setColor(foreground); } diff --git a/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserListViewBorder.java b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserListViewBorder.java new file mode 100644 index 00000000..e9c12c7d --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserListViewBorder.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.filechooser; + +import com.weis.darklaf.components.border.MutableLineBorder; + +import javax.swing.*; +import java.awt.*; + +public class DarkFileChooserListViewBorder extends MutableLineBorder.UIResource { + + public DarkFileChooserListViewBorder() { + super(1, 1, 1, 1, Color.BLACK); + } + + @Override + protected Color getColor() { + return UIManager.getColor("FileChooser.borderColor"); + } +} diff --git a/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUI.java b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUI.java new file mode 100644 index 00000000..12ab8243 --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUI.java @@ -0,0 +1,294 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.filechooser; + +import com.weis.darklaf.components.TooltipAwareButton; +import com.weis.darklaf.components.TooltipAwareToggleButton; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import sun.swing.FilePane; + +import javax.accessibility.AccessibleContext; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.filechooser.FileSystemView; +import javax.swing.plaf.ComponentUI; +import java.awt.*; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.io.File; + +public class DarkFileChooserUI extends DarkFileChooserUIBridge { + + public DarkFileChooserUI(final JFileChooser b) { + super(b); + } + + @NotNull + @Contract("_ -> new") + public static ComponentUI createUI(final JComponent c) { + return new DarkFileChooserUI((JFileChooser) c); + } + + @Override + public void installComponents(final JFileChooser fc) { + FileSystemView fsv = fc.getFileSystemView(); + + fc.setBorder(new EmptyBorder(12, 12, 11, 11)); + fc.setLayout(new BorderLayout(0, 11)); + + filePane = new DarkFilePane(new MetalFileChooserUIAccessor()); + fc.addPropertyChangeListener(filePane); + + // ********************************* // + // **** Construct the top panel **** // + // ********************************* // + + // Directory manipulation buttons + JPanel topPanel = new JPanel(new BorderLayout(11, 0)); + JPanel topButtonPanel = new JPanel(); + topButtonPanel.setLayout(new BoxLayout(topButtonPanel, BoxLayout.LINE_AXIS)); + topPanel.add(topButtonPanel, BorderLayout.AFTER_LINE_ENDS); + + // Add the top panel to the fileChooser + fc.add(topPanel, BorderLayout.NORTH); + + // ComboBox Label + lookInLabel = new JLabel(lookInLabelText); + lookInLabel.setDisplayedMnemonic(lookInLabelMnemonic); + topPanel.add(lookInLabel, BorderLayout.BEFORE_LINE_BEGINS); + + // CurrentDir ComboBox + directoryComboBox = new JComboBox<>() { + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + // Must be small enough to not affect total width. + d.width = 150; + return d; + } + }; + directoryComboBox.putClientProperty(AccessibleContext.ACCESSIBLE_DESCRIPTION_PROPERTY, + lookInLabelText); + lookInLabel.setLabelFor(directoryComboBox); + directoryComboBoxModel = createDirectoryComboBoxModel(fc); + directoryComboBox.setModel(directoryComboBoxModel); + directoryComboBox.addActionListener(directoryComboBoxAction); + directoryComboBox.setRenderer(createDirectoryComboBoxRenderer(fc)); + directoryComboBox.setAlignmentX(JComponent.LEFT_ALIGNMENT); + directoryComboBox.setAlignmentY(JComponent.TOP_ALIGNMENT); + directoryComboBox.setMaximumRowCount(8); + + topPanel.add(directoryComboBox, BorderLayout.CENTER); + + // Up Button + JButton upFolderButton = new TooltipAwareButton(getChangeToParentDirectoryAction()); + upFolderButton.setText(null); + upFolderButton.setIcon(upFolderIcon); + upFolderButton.setToolTipText(upFolderToolTipText); + upFolderButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + upFolderAccessibleName); + upFolderButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + upFolderButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + upFolderButton.setMargin(shrinkwrap); + + topButtonPanel.add(upFolderButton); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // Home Button + File homeDir = fsv.getHomeDirectory(); + String toolTipText = homeFolderToolTipText; + + + JButton b = new TooltipAwareButton(homeFolderIcon); + b.setToolTipText(toolTipText); + b.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + homeFolderAccessibleName); + b.setAlignmentX(JComponent.LEFT_ALIGNMENT); + b.setAlignmentY(JComponent.CENTER_ALIGNMENT); + b.setMargin(shrinkwrap); + + b.addActionListener(getGoHomeAction()); + topButtonPanel.add(b); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // New Directory Button + if (!UIManager.getBoolean("FileChooser.readOnly")) { + b = new TooltipAwareButton(filePane.getNewFolderAction()); + b.setText(null); + b.setIcon(newFolderIcon); + b.setToolTipText(newFolderToolTipText); + b.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + newFolderAccessibleName); + b.setAlignmentX(JComponent.LEFT_ALIGNMENT); + b.setAlignmentY(JComponent.CENTER_ALIGNMENT); + b.setMargin(shrinkwrap); + } + topButtonPanel.add(b); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // View button group + ButtonGroup viewButtonGroup = new ButtonGroup(); + + // List Button + listViewButton = new TooltipAwareToggleButton(listViewIcon); + listViewButton.putClientProperty("JButton.square", Boolean.TRUE); + listViewButton.setToolTipText(listViewButtonToolTipText); + listViewButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + listViewButtonAccessibleName); + + + listViewButton.setSelected(true); + listViewButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + listViewButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + listViewButton.setMargin(shrinkwrap); + listViewButton.addActionListener(filePane.getViewTypeAction(FilePane.VIEWTYPE_LIST)); + topButtonPanel.add(listViewButton); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + viewButtonGroup.add(listViewButton); + + // Details Button + detailsViewButton = new TooltipAwareToggleButton(detailsViewIcon); + detailsViewButton.putClientProperty("JButton.square", Boolean.TRUE); + detailsViewButton.setToolTipText(detailsViewButtonToolTipText); + detailsViewButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + detailsViewButtonAccessibleName); + detailsViewButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + detailsViewButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + detailsViewButton.setMargin(shrinkwrap); + detailsViewButton.addActionListener(filePane.getViewTypeAction(FilePane.VIEWTYPE_DETAILS)); + topButtonPanel.add(detailsViewButton); + viewButtonGroup.add(detailsViewButton); + + topButtonPanel.add(Box.createGlue()); + + filePane.addPropertyChangeListener(e -> { + if ("viewType".equals(e.getPropertyName())) { + int viewType = filePane.getViewType(); + switch (viewType) { + case FilePane.VIEWTYPE_LIST: + listViewButton.setSelected(true); + break; + + case FilePane.VIEWTYPE_DETAILS: + detailsViewButton.setSelected(true); + break; + } + } + }); + + // ************************************** // + // ******* Add the directory pane ******* // + // ************************************** // + fc.add(getAccessoryPanel(), BorderLayout.AFTER_LINE_ENDS); + JComponent accessory = fc.getAccessory(); + if (accessory != null) { + getAccessoryPanel().add(accessory); + } + filePane.setPreferredSize(LIST_PREF_SIZE); + fc.add(filePane, BorderLayout.CENTER); + + // ********************************** // + // **** Construct the bottom panel ** // + // ********************************** // + JPanel bottomPanel = getBottomPanel(); + bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS)); + fc.add(bottomPanel, BorderLayout.SOUTH); + + // FileName label and textfield + JPanel fileNamePanel = new JPanel(); + fileNamePanel.setLayout(new BoxLayout(fileNamePanel, BoxLayout.LINE_AXIS)); + bottomPanel.add(fileNamePanel); + bottomPanel.add(Box.createRigidArea(vstrut5)); + + fileNameLabel = new AlignedLabel(); + populateFileNameLabel(); + fileNamePanel.add(fileNameLabel); + + @SuppressWarnings("serial") // anonymous class + JTextField tmp2 = new JTextField(35) { + public Dimension getMaximumSize() { + return new Dimension(Short.MAX_VALUE, super.getPreferredSize().height); + } + }; + fileNameTextField = tmp2; + fileNamePanel.add(fileNameTextField); + fileNameLabel.setLabelFor(fileNameTextField); + fileNameTextField.addFocusListener( + new FocusAdapter() { + public void focusGained(final FocusEvent e) { + if (!getFileChooser().isMultiSelectionEnabled()) { + filePane.clearSelection(); + } + } + } + ); + if (fc.isMultiSelectionEnabled()) { + setFileName(fileNameString(fc.getSelectedFiles())); + } else { + setFileName(fileNameString(fc.getSelectedFile())); + } + + + // Filetype label and combobox + JPanel filesOfTypePanel = new JPanel(); + filesOfTypePanel.setLayout(new BoxLayout(filesOfTypePanel, BoxLayout.LINE_AXIS)); + bottomPanel.add(filesOfTypePanel); + + AlignedLabel filesOfTypeLabel = new AlignedLabel(filesOfTypeLabelText); + filesOfTypeLabel.setDisplayedMnemonic(filesOfTypeLabelMnemonic); + filesOfTypePanel.add(filesOfTypeLabel); + + filterComboBoxModel = createFilterComboBoxModel(); + fc.addPropertyChangeListener(filterComboBoxModel); + filterComboBox = new JComboBox<>(filterComboBoxModel); + if (filterComboBox.getItemCount() == 0) { + filterComboBox.setEnabled(false); + } + filterComboBox.putClientProperty(AccessibleContext.ACCESSIBLE_DESCRIPTION_PROPERTY, + filesOfTypeLabelText); + filesOfTypeLabel.setLabelFor(filterComboBox); + filterComboBox.setRenderer(createFilterComboBoxRenderer()); + filesOfTypePanel.add(filterComboBox); + + // buttons + getButtonPanel().setLayout(new ButtonAreaLayout()); + + approveButton = new TooltipAwareButton(getApproveButtonText(fc)); + // Note: Metal does not use mnemonics for approve and cancel + approveButton.addActionListener(getApproveSelectionAction()); + approveButton.setToolTipText(getApproveButtonToolTipText(fc)); + getButtonPanel().add(approveButton); + + cancelButton = new TooltipAwareButton(cancelButtonText); + cancelButton.setToolTipText(cancelButtonToolTipText); + cancelButton.addActionListener(getCancelSelectionAction()); + getButtonPanel().add(cancelButton); + + if (fc.getControlButtonsAreShown()) { + addControlButtons(); + } + + groupLabels(new AlignedLabel[]{fileNameLabel, filesOfTypeLabel}); + } +} diff --git a/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUIBridge.java b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUIBridge.java new file mode 100644 index 00000000..32627945 --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUIBridge.java @@ -0,0 +1,1390 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.filechooser; + +import sun.awt.shell.ShellFolder; +import sun.swing.FilePane; +import sun.swing.SwingUtilities2; + +import javax.accessibility.AccessibleContext; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileSystemView; +import javax.swing.plaf.ActionMapUIResource; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.basic.BasicDirectoryModel; +import javax.swing.plaf.basic.BasicFileChooserUI; +import javax.swing.plaf.metal.MetalFileChooserUI; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; +import java.util.Vector; + + +/** + * Metal L&F implementation of a FileChooser. + * + * @author Jeff Dinkins + */ +public class DarkFileChooserUIBridge extends BasicFileChooserUI { + + // Much of the Metal UI for JFilechooser is just a copy of + // the windows implementation, but using Metal themed buttons, lists, + // icons, etc. We are planning a complete rewrite, and hence we've + // made most things in this class protected. + + protected static final Dimension hstrut5 = new Dimension(5, 1); + protected static final Dimension hstrut11 = new Dimension(11, 1); + protected static final Dimension vstrut5 = new Dimension(1, 5); + protected static final Insets shrinkwrap = new Insets(0, 0, 0, 0); + static final int space = 10; + // Preferred and Minimum sizes for the dialog box + protected static int PREF_WIDTH = 500; + protected static int PREF_HEIGHT = 326; + protected static Dimension PREF_SIZE = new Dimension(PREF_WIDTH, PREF_HEIGHT); + protected static int MIN_WIDTH = 500; + protected static int MIN_HEIGHT = 326; + protected static int LIST_PREF_WIDTH = 405; + protected static int LIST_PREF_HEIGHT = 135; + protected static Dimension LIST_PREF_SIZE = new Dimension(LIST_PREF_WIDTH, LIST_PREF_HEIGHT); + protected JLabel lookInLabel; + protected JComboBox directoryComboBox; + protected DirectoryComboBoxModel directoryComboBoxModel; + protected Action directoryComboBoxAction = new DirectoryComboBoxAction(); + protected FilterComboBoxModel filterComboBoxModel; + protected JTextField fileNameTextField; + protected DarkFilePaneUIBridge filePane; + protected JToggleButton listViewButton; + protected JToggleButton detailsViewButton; + protected JButton approveButton; + protected JButton cancelButton; + protected JPanel buttonPanel; + protected JPanel bottomPanel; + protected JComboBox filterComboBox; + // Labels, mnemonics, and tooltips (oh my!) + protected int lookInLabelMnemonic = 0; + protected String lookInLabelText = null; + protected String saveInLabelText = null; + protected int fileNameLabelMnemonic = 0; + protected String fileNameLabelText = null; + protected int folderNameLabelMnemonic = 0; + protected String folderNameLabelText = null; + protected int filesOfTypeLabelMnemonic = 0; + protected String filesOfTypeLabelText = null; + protected String upFolderToolTipText = null; + protected String upFolderAccessibleName = null; + protected String homeFolderToolTipText = null; + protected String homeFolderAccessibleName = null; + protected String newFolderToolTipText = null; + protected String newFolderAccessibleName = null; + protected String listViewButtonToolTipText = null; + protected String listViewButtonAccessibleName = null; + protected String detailsViewButtonToolTipText = null; + protected String detailsViewButtonAccessibleName = null; + protected AlignedLabel fileNameLabel; + + /** + * Constructs a new instance of {@code MetalFileChooserUI}. + * + * @param filechooser a {@code JFileChooser} + */ + public DarkFileChooserUIBridge(final JFileChooser filechooser) { + super(filechooser); + } + + /** + * Constructs a new instance of {@code MetalFileChooserUI}. + * + * @param c a component + * @return a new instance of {@code MetalFileChooserUI} + */ + public static ComponentUI createUI(final JComponent c) { + return new MetalFileChooserUI((JFileChooser) c); + } + + public void installUI(final JComponent c) { + super.installUI(c); + } + + public void uninstallUI(final JComponent c) { + // Remove listeners + c.removePropertyChangeListener(filterComboBoxModel); + c.removePropertyChangeListener(filePane); + cancelButton.removeActionListener(getCancelSelectionAction()); + approveButton.removeActionListener(getApproveSelectionAction()); + fileNameTextField.removeActionListener(getApproveSelectionAction()); + + if (filePane != null) { + filePane.uninstallUI(); + filePane = null; + } + + super.uninstallUI(c); + } + + public void installComponents(final JFileChooser fc) { + FileSystemView fsv = fc.getFileSystemView(); + + fc.setBorder(new EmptyBorder(12, 12, 11, 11)); + fc.setLayout(new BorderLayout(0, 11)); + + filePane = new DarkFilePane(new MetalFileChooserUIAccessor()); + fc.addPropertyChangeListener(filePane); + + // ********************************* // + // **** Construct the top panel **** // + // ********************************* // + + // Directory manipulation buttons + JPanel topPanel = new JPanel(new BorderLayout(11, 0)); + JPanel topButtonPanel = new JPanel(); + topButtonPanel.setLayout(new BoxLayout(topButtonPanel, BoxLayout.LINE_AXIS)); + topPanel.add(topButtonPanel, BorderLayout.AFTER_LINE_ENDS); + + // Add the top panel to the fileChooser + fc.add(topPanel, BorderLayout.NORTH); + + // ComboBox Label + lookInLabel = new JLabel(lookInLabelText); + lookInLabel.setDisplayedMnemonic(lookInLabelMnemonic); + topPanel.add(lookInLabel, BorderLayout.BEFORE_LINE_BEGINS); + + // CurrentDir ComboBox + @SuppressWarnings("serial") // anonymous class + JComboBox tmp1 = new JComboBox<>() { + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + // Must be small enough to not affect total width. + d.width = 150; + return d; + } + }; + directoryComboBox = tmp1; + directoryComboBox.putClientProperty(AccessibleContext.ACCESSIBLE_DESCRIPTION_PROPERTY, + lookInLabelText); + directoryComboBox.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE); + lookInLabel.setLabelFor(directoryComboBox); + directoryComboBoxModel = createDirectoryComboBoxModel(fc); + directoryComboBox.setModel(directoryComboBoxModel); + directoryComboBox.addActionListener(directoryComboBoxAction); + directoryComboBox.setRenderer(createDirectoryComboBoxRenderer(fc)); + directoryComboBox.setAlignmentX(JComponent.LEFT_ALIGNMENT); + directoryComboBox.setAlignmentY(JComponent.TOP_ALIGNMENT); + directoryComboBox.setMaximumRowCount(8); + + topPanel.add(directoryComboBox, BorderLayout.CENTER); + + // Up Button + JButton upFolderButton = new JButton(getChangeToParentDirectoryAction()); + upFolderButton.setText(null); + upFolderButton.setIcon(upFolderIcon); + upFolderButton.setToolTipText(upFolderToolTipText); + upFolderButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + upFolderAccessibleName); + upFolderButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + upFolderButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + upFolderButton.setMargin(shrinkwrap); + + topButtonPanel.add(upFolderButton); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // Home Button + File homeDir = fsv.getHomeDirectory(); + String toolTipText = homeFolderToolTipText; + + + JButton b = new JButton(homeFolderIcon); + b.setToolTipText(toolTipText); + b.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + homeFolderAccessibleName); + b.setAlignmentX(JComponent.LEFT_ALIGNMENT); + b.setAlignmentY(JComponent.CENTER_ALIGNMENT); + b.setMargin(shrinkwrap); + + b.addActionListener(getGoHomeAction()); + topButtonPanel.add(b); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // New Directory Button + if (!UIManager.getBoolean("FileChooser.readOnly")) { + b = new JButton(filePane.getNewFolderAction()); + b.setText(null); + b.setIcon(newFolderIcon); + b.setToolTipText(newFolderToolTipText); + b.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + newFolderAccessibleName); + b.setAlignmentX(JComponent.LEFT_ALIGNMENT); + b.setAlignmentY(JComponent.CENTER_ALIGNMENT); + b.setMargin(shrinkwrap); + } + topButtonPanel.add(b); + topButtonPanel.add(Box.createRigidArea(hstrut5)); + + // View button group + ButtonGroup viewButtonGroup = new ButtonGroup(); + + // List Button + listViewButton = new JToggleButton(listViewIcon); + listViewButton.setToolTipText(listViewButtonToolTipText); + listViewButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + listViewButtonAccessibleName); + listViewButton.setSelected(true); + listViewButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + listViewButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + listViewButton.setMargin(shrinkwrap); + listViewButton.addActionListener(filePane.getViewTypeAction(FilePane.VIEWTYPE_LIST)); + topButtonPanel.add(listViewButton); + viewButtonGroup.add(listViewButton); + + // Details Button + detailsViewButton = new JToggleButton(detailsViewIcon); + detailsViewButton.setToolTipText(detailsViewButtonToolTipText); + detailsViewButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, + detailsViewButtonAccessibleName); + detailsViewButton.setAlignmentX(JComponent.LEFT_ALIGNMENT); + detailsViewButton.setAlignmentY(JComponent.CENTER_ALIGNMENT); + detailsViewButton.setMargin(shrinkwrap); + detailsViewButton.addActionListener(filePane.getViewTypeAction(FilePane.VIEWTYPE_DETAILS)); + topButtonPanel.add(detailsViewButton); + viewButtonGroup.add(detailsViewButton); + + filePane.addPropertyChangeListener(new PropertyChangeListener() { + public void propertyChange(final PropertyChangeEvent e) { + if ("viewType".equals(e.getPropertyName())) { + int viewType = filePane.getViewType(); + switch (viewType) { + case FilePane.VIEWTYPE_LIST: + listViewButton.setSelected(true); + break; + + case FilePane.VIEWTYPE_DETAILS: + detailsViewButton.setSelected(true); + break; + } + } + } + }); + + // ************************************** // + // ******* Add the directory pane ******* // + // ************************************** // + fc.add(getAccessoryPanel(), BorderLayout.AFTER_LINE_ENDS); + JComponent accessory = fc.getAccessory(); + if (accessory != null) { + getAccessoryPanel().add(accessory); + } + filePane.setPreferredSize(LIST_PREF_SIZE); + fc.add(filePane, BorderLayout.CENTER); + + // ********************************** // + // **** Construct the bottom panel ** // + // ********************************** // + JPanel bottomPanel = getBottomPanel(); + bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS)); + fc.add(bottomPanel, BorderLayout.SOUTH); + + // FileName label and textfield + JPanel fileNamePanel = new JPanel(); + fileNamePanel.setLayout(new BoxLayout(fileNamePanel, BoxLayout.LINE_AXIS)); + bottomPanel.add(fileNamePanel); + bottomPanel.add(Box.createRigidArea(vstrut5)); + + fileNameLabel = new AlignedLabel(); + populateFileNameLabel(); + fileNamePanel.add(fileNameLabel); + + @SuppressWarnings("serial") // anonymous class + JTextField tmp2 = new JTextField(35) { + public Dimension getMaximumSize() { + return new Dimension(Short.MAX_VALUE, super.getPreferredSize().height); + } + }; + fileNameTextField = tmp2; + fileNamePanel.add(fileNameTextField); + fileNameLabel.setLabelFor(fileNameTextField); + fileNameTextField.addFocusListener( + new FocusAdapter() { + public void focusGained(final FocusEvent e) { + if (!getFileChooser().isMultiSelectionEnabled()) { + filePane.clearSelection(); + } + } + } + ); + if (fc.isMultiSelectionEnabled()) { + setFileName(fileNameString(fc.getSelectedFiles())); + } else { + setFileName(fileNameString(fc.getSelectedFile())); + } + + + // Filetype label and combobox + JPanel filesOfTypePanel = new JPanel(); + filesOfTypePanel.setLayout(new BoxLayout(filesOfTypePanel, BoxLayout.LINE_AXIS)); + bottomPanel.add(filesOfTypePanel); + + AlignedLabel filesOfTypeLabel = new AlignedLabel(filesOfTypeLabelText); + filesOfTypeLabel.setDisplayedMnemonic(filesOfTypeLabelMnemonic); + filesOfTypePanel.add(filesOfTypeLabel); + + filterComboBoxModel = createFilterComboBoxModel(); + fc.addPropertyChangeListener(filterComboBoxModel); + filterComboBox = new JComboBox<>(filterComboBoxModel); + filterComboBox.putClientProperty(AccessibleContext.ACCESSIBLE_DESCRIPTION_PROPERTY, + filesOfTypeLabelText); + filesOfTypeLabel.setLabelFor(filterComboBox); + filterComboBox.setRenderer(createFilterComboBoxRenderer()); + filesOfTypePanel.add(filterComboBox); + + // buttons + getButtonPanel().setLayout(new ButtonAreaLayout()); + + approveButton = new JButton(getApproveButtonText(fc)); + // Note: Metal does not use mnemonics for approve and cancel + approveButton.addActionListener(getApproveSelectionAction()); + approveButton.setToolTipText(getApproveButtonToolTipText(fc)); + getButtonPanel().add(approveButton); + + cancelButton = new JButton(cancelButtonText); + cancelButton.setToolTipText(cancelButtonToolTipText); + cancelButton.addActionListener(getCancelSelectionAction()); + getButtonPanel().add(cancelButton); + + if (fc.getControlButtonsAreShown()) { + addControlButtons(); + } + + groupLabels(new AlignedLabel[]{fileNameLabel, filesOfTypeLabel}); + } + + public void uninstallComponents(final JFileChooser fc) { + fc.removeAll(); + bottomPanel = null; + buttonPanel = null; + } + + protected void installListeners(final JFileChooser fc) { + super.installListeners(fc); + ActionMap actionMap = getActionMap(); + SwingUtilities.replaceUIActionMap(fc, actionMap); + } + + protected void installStrings(final JFileChooser fc) { + super.installStrings(fc); + + Locale l = fc.getLocale(); + + lookInLabelMnemonic = getMnemonic("FileChooser.lookInLabelMnemonic", l); + lookInLabelText = UIManager.getString("FileChooser.lookInLabelText", l); + saveInLabelText = UIManager.getString("FileChooser.saveInLabelText", l); + + fileNameLabelMnemonic = getMnemonic("FileChooser.fileNameLabelMnemonic", l); + fileNameLabelText = UIManager.getString("FileChooser.fileNameLabelText", l); + folderNameLabelMnemonic = getMnemonic("FileChooser.folderNameLabelMnemonic", l); + folderNameLabelText = UIManager.getString("FileChooser.folderNameLabelText", l); + + filesOfTypeLabelMnemonic = getMnemonic("FileChooser.filesOfTypeLabelMnemonic", l); + filesOfTypeLabelText = UIManager.getString("FileChooser.filesOfTypeLabelText", l); + + upFolderToolTipText = UIManager.getString("FileChooser.upFolderToolTipText", l); + upFolderAccessibleName = UIManager.getString("FileChooser.upFolderAccessibleName", l); + + homeFolderToolTipText = UIManager.getString("FileChooser.homeFolderToolTipText", l); + homeFolderAccessibleName = UIManager.getString("FileChooser.homeFolderAccessibleName", l); + + newFolderToolTipText = UIManager.getString("FileChooser.newFolderToolTipText", l); + newFolderAccessibleName = UIManager.getString("FileChooser.newFolderAccessibleName", l); + + listViewButtonToolTipText = UIManager.getString("FileChooser.listViewButtonToolTipText", l); + listViewButtonAccessibleName = UIManager.getString("FileChooser.listViewButtonAccessibleName", l); + + detailsViewButtonToolTipText = UIManager.getString("FileChooser.detailsViewButtonToolTipText", l); + detailsViewButtonAccessibleName = UIManager.getString("FileChooser.detailsViewButtonAccessibleName", l); + } + + protected Integer getMnemonic(final String key, final Locale l) { + return SwingUtilities2.getUIDefaultsInt(key, l); + } + + /* + * Listen for filechooser property changes, such as + * the selected file changing, or the type of the dialog changing. + */ + public PropertyChangeListener createPropertyChangeListener(final JFileChooser fc) { + return new PropertyChangeListener() { + public void propertyChange(final PropertyChangeEvent e) { + String s = e.getPropertyName(); + if (s.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) { + doSelectedFileChanged(e); + } else if (s.equals(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY)) { + doSelectedFilesChanged(e); + } else if (s.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY)) { + doDirectoryChanged(e); + } else if (s.equals(JFileChooser.FILE_FILTER_CHANGED_PROPERTY)) { + doFilterChanged(e); + } else if (s.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)) { + doFileSelectionModeChanged(e); + } else if (s.equals(JFileChooser.ACCESSORY_CHANGED_PROPERTY)) { + doAccessoryChanged(e); + } else if (s.equals(JFileChooser.APPROVE_BUTTON_TEXT_CHANGED_PROPERTY) || + s.equals(JFileChooser.APPROVE_BUTTON_TOOL_TIP_TEXT_CHANGED_PROPERTY)) { + doApproveButtonTextChanged(e); + } else if (s.equals(JFileChooser.DIALOG_TYPE_CHANGED_PROPERTY)) { + doDialogTypeChanged(e); + } else if (s.equals(JFileChooser.APPROVE_BUTTON_MNEMONIC_CHANGED_PROPERTY)) { + doApproveButtonMnemonicChanged(e); + } else if (s.equals(JFileChooser.CONTROL_BUTTONS_ARE_SHOWN_CHANGED_PROPERTY)) { + doControlButtonsChanged(e); + } else if (s.equals("componentOrientation")) { + ComponentOrientation o = (ComponentOrientation) e.getNewValue(); + JFileChooser cc = (JFileChooser) e.getSource(); + if (o != e.getOldValue()) { + cc.applyComponentOrientation(o); + } + } else if (s == "FileChooser.useShellFolder") { + doDirectoryChanged(e); + } else if (s.equals("ancestor")) { + if (e.getOldValue() == null && e.getNewValue() != null) { + // Ancestor was added, set initial focus + fileNameTextField.selectAll(); + fileNameTextField.requestFocus(); + } + } + } + }; + } + + protected void doSelectedFileChanged(final PropertyChangeEvent e) { + File f = (File) e.getNewValue(); + JFileChooser fc = getFileChooser(); + if (f != null + && ((fc.isFileSelectionEnabled() && !f.isDirectory()) + || (f.isDirectory() && fc.isDirectorySelectionEnabled()))) { + + setFileName(fileNameString(f)); + } + } + + protected void doSelectedFilesChanged(final PropertyChangeEvent e) { + File[] files = (File[]) e.getNewValue(); + JFileChooser fc = getFileChooser(); + if (files != null + && files.length > 0 + && (files.length > 1 || fc.isDirectorySelectionEnabled() || !files[0].isDirectory())) { + setFileName(fileNameString(files)); + } + } + + protected void doDirectoryChanged(final PropertyChangeEvent e) { + JFileChooser fc = getFileChooser(); + FileSystemView fsv = fc.getFileSystemView(); + + clearIconCache(); + File currentDirectory = fc.getCurrentDirectory(); + if (currentDirectory != null) { + directoryComboBoxModel.addItem(currentDirectory); + + if (fc.isDirectorySelectionEnabled() && !fc.isFileSelectionEnabled()) { + if (fsv.isFileSystem(currentDirectory)) { + setFileName(currentDirectory.getPath()); + } else { + setFileName(null); + } + } + } + } + + protected void doFilterChanged(final PropertyChangeEvent e) { + clearIconCache(); + } + + protected void doFileSelectionModeChanged(final PropertyChangeEvent e) { + if (fileNameLabel != null) { + populateFileNameLabel(); + } + clearIconCache(); + + JFileChooser fc = getFileChooser(); + File currentDirectory = fc.getCurrentDirectory(); + if (currentDirectory != null + && fc.isDirectorySelectionEnabled() + && !fc.isFileSelectionEnabled() + && fc.getFileSystemView().isFileSystem(currentDirectory)) { + + setFileName(currentDirectory.getPath()); + } else { + setFileName(null); + } + } + + protected void doAccessoryChanged(final PropertyChangeEvent e) { + if (getAccessoryPanel() != null) { + if (e.getOldValue() != null) { + getAccessoryPanel().remove((JComponent) e.getOldValue()); + } + JComponent accessory = (JComponent) e.getNewValue(); + if (accessory != null) { + getAccessoryPanel().add(accessory, BorderLayout.CENTER); + } + } + } + + protected void doApproveButtonTextChanged(final PropertyChangeEvent e) { + JFileChooser chooser = getFileChooser(); + approveButton.setText(getApproveButtonText(chooser)); + approveButton.setToolTipText(getApproveButtonToolTipText(chooser)); + } + + protected void doDialogTypeChanged(final PropertyChangeEvent e) { + JFileChooser chooser = getFileChooser(); + approveButton.setText(getApproveButtonText(chooser)); + approveButton.setToolTipText(getApproveButtonToolTipText(chooser)); + if (chooser.getDialogType() == JFileChooser.SAVE_DIALOG) { + lookInLabel.setText(saveInLabelText); + } else { + lookInLabel.setText(lookInLabelText); + } + } + + protected void doApproveButtonMnemonicChanged(final PropertyChangeEvent e) { + // Note: Metal does not use mnemonics for approve and cancel + } + + protected void doControlButtonsChanged(final PropertyChangeEvent e) { + if (getFileChooser().getControlButtonsAreShown()) { + addControlButtons(); + } else { + removeControlButtons(); + } + } + + /** + * Removes control buttons from bottom panel. + */ + protected void removeControlButtons() { + getBottomPanel().remove(getButtonPanel()); + } + + public String getFileName() { + if (fileNameTextField != null) { + return fileNameTextField.getText(); + } else { + return null; + } + } + + public void setFileName(final String filename) { + if (fileNameTextField != null) { + fileNameTextField.setText(filename); + } + } + + /** + * Returns the directory name. + * + * @return the directory name + */ + public String getDirectoryName() { + // PENDING(jeff) - get the name from the directory combobox + return null; + } + + /* The following methods are used by the PropertyChange Listener */ + + /** + * Sets the directory name. + * + * @param dirname the directory name + */ + public void setDirectoryName(final String dirname) { + // PENDING(jeff) - set the name in the directory combobox + } + + public void rescanCurrentDirectory(final JFileChooser fc) { + filePane.rescanCurrentDirectory(); + } + + public void ensureFileIsVisible(final JFileChooser fc, final File f) { + filePane.ensureFileIsVisible(fc, f); + } + + protected JButton getApproveButton(final JFileChooser fc) { + return approveButton; + } + + /** + * Creates a selection listener for the list of files and directories. + * + * @param fc a JFileChooser + * @return a ListSelectionListener + */ + public ListSelectionListener createListSelectionListener(final JFileChooser fc) { + return super.createListSelectionListener(fc); + } + + /** + * Property to remember whether a directory is currently selected in the UI. + * This is normally called by the UI on a selection event. + * + * @param directorySelected if a directory is currently selected. + * @since 1.4 + */ + protected void setDirectorySelected(final boolean directorySelected) { + super.setDirectorySelected(directorySelected); + JFileChooser chooser = getFileChooser(); + if (directorySelected) { + if (approveButton != null) { + approveButton.setText(directoryOpenButtonText); + approveButton.setToolTipText(directoryOpenButtonToolTipText); + } + } else { + if (approveButton != null) { + approveButton.setText(getApproveButtonText(chooser)); + approveButton.setToolTipText(getApproveButtonToolTipText(chooser)); + } + } + } + + /** + * Returns an instance of {@code ActionMap}. + * + * @return an instance of {@code ActionMap} + */ + protected ActionMap getActionMap() { + return createActionMap(); + } + + /** + * Constructs an instance of {@code ActionMap}. + * + * @return an instance of {@code ActionMap} + */ + protected ActionMap createActionMap() { + ActionMap map = new ActionMapUIResource(); + FilePane.addActionsToMap(map, filePane.getActions()); + return map; + } + + /** + * Constructs a new instance of {@code DataModel} for {@code DirectoryComboBox}. + * + * @param fc a {@code JFileChooser} + * @return a new instance of {@code DataModel} for {@code DirectoryComboBox} + */ + protected DirectoryComboBoxModel createDirectoryComboBoxModel(final JFileChooser fc) { + return new DirectoryComboBoxModel(); + } + + /** + * Constructs a new instance of {@code DirectoryComboBoxRenderer}. + * + * @param fc a {@code JFileChooser} + * @return a new instance of {@code DirectoryComboBoxRenderer} + */ + protected DefaultListCellRenderer createDirectoryComboBoxRenderer(final JFileChooser fc) { + return new DirectoryComboBoxRenderer(); + } + + /** + * Returns the bottom panel. + * + * @return the bottom panel + */ + protected JPanel getBottomPanel() { + if (bottomPanel == null) { + bottomPanel = new JPanel(); + } + return bottomPanel; + } + + protected void populateFileNameLabel() { + if (getFileChooser().getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY) { + fileNameLabel.setText(folderNameLabelText); + fileNameLabel.setDisplayedMnemonic(folderNameLabelMnemonic); + } else { + fileNameLabel.setText(fileNameLabelText); + fileNameLabel.setDisplayedMnemonic(fileNameLabelMnemonic); + } + } + + protected String fileNameString(final File[] files) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; files != null && i < files.length; i++) { + if (i > 0) { + sb.append(" "); + } + if (files.length > 1) { + sb.append("\""); + } + sb.append(fileNameString(files[i])); + if (files.length > 1) { + sb.append("\""); + } + } + return sb.toString(); + } + + protected String fileNameString(final File file) { + if (file == null) { + return null; + } else { + JFileChooser fc = getFileChooser(); + if ((fc.isDirectorySelectionEnabled() && !fc.isFileSelectionEnabled()) || + (fc.isDirectorySelectionEnabled() && fc.isFileSelectionEnabled() + && fc.getFileSystemView().isFileSystemRoot(file))) { + return file.getPath(); + } else { + return file.getName(); + } + } + } + + /** + * Constructs a {@code DataModel} for types {@code ComboBox}. + * + * @return a {@code DataModel} for types {@code ComboBox} + */ + protected FilterComboBoxModel createFilterComboBoxModel() { + return new FilterComboBoxModel(); + } + + /** + * Constructs a {@code Renderer} for types {@code ComboBox}. + * + * @return a {@code Renderer} for types {@code ComboBox} + */ + protected FilterComboBoxRenderer createFilterComboBoxRenderer() { + return new FilterComboBoxRenderer(); + } + + /** + * Returns the button panel. + * + * @return the button panel + */ + protected JPanel getButtonPanel() { + if (buttonPanel == null) { + buttonPanel = new JPanel(); + } + return buttonPanel; + } + + /** + * Adds control buttons to bottom panel. + */ + protected void addControlButtons() { + getBottomPanel().add(getButtonPanel()); + } + + protected static void groupLabels(final AlignedLabel[] group) { + for (int i = 0; i < group.length; i++) { + group[i].group = group; + } + } + + /** + * Constructs a details view. + * + * @param fc a {@code JFileChooser} + * @return the list + */ + protected JPanel createList(final JFileChooser fc) { + return filePane.createList(); + } + + /** + * Constructs a details view. + * + * @param fc a {@code JFileChooser} + * @return the details view + */ + protected JPanel createDetailsView(final JFileChooser fc) { + return filePane.createDetailsView(); + } + + /** + * Returns the preferred size of the specified + * JFileChooser. + * The preferred size is at least as large, + * in both height and width, + * as the preferred size recommended + * by the file chooser's layout manager. + * + * @param c a JFileChooser + * @return a Dimension specifying the preferred + * width and height of the file chooser + */ + @Override + public Dimension getPreferredSize(final JComponent c) { + int prefWidth = PREF_SIZE.width; + Dimension d = c.getLayout().preferredLayoutSize(c); + if (d != null) { + return new Dimension(d.width < prefWidth ? prefWidth : d.width, + d.height < PREF_SIZE.height ? PREF_SIZE.height : d.height); + } else { + return new Dimension(prefWidth, PREF_SIZE.height); + } + } + + /** + * Returns the minimum size of the JFileChooser. + * + * @param c a JFileChooser + * @return a Dimension specifying the minimum + * width and height of the file chooser + */ + @Override + public Dimension getMinimumSize(final JComponent c) { + return new Dimension(MIN_WIDTH, MIN_HEIGHT); + } + + /** + * Returns the maximum size of the JFileChooser. + * + * @param c a JFileChooser + * @return a Dimension specifying the maximum + * width and height of the file chooser + */ + @Override + public Dimension getMaximumSize(final JComponent c) { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * Invokes when {@code ListSelectionEvent} occurs. + * + * @param e an instance of {@code ListSelectionEvent} + */ + public void valueChanged(final ListSelectionEvent e) { + JFileChooser fc = getFileChooser(); + File f = fc.getSelectedFile(); + if (!e.getValueIsAdjusting() && f != null && !getFileChooser().isTraversable(f)) { + setFileName(fileNameString(f)); + } + } + + /** + * Render different type sizes and styles. + */ + @SuppressWarnings("serial") // Superclass is not serializable across versions + public static class FilterComboBoxRenderer extends DefaultListCellRenderer { + public Component getListCellRendererComponent(final JList list, + final Object value, final int index, final boolean isSelected, + final boolean cellHasFocus) { + + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof FileFilter) { + setText(((FileFilter) value).getDescription()); + } + setOpaque(false); + + return this; + } + } + + /** + * ButtonAreaLayout behaves in a similar manner to + * FlowLayout. It lays out all components from left to + * right, flushed right. The widths of all components will be set + * to the largest preferred size width. + */ + protected static class ButtonAreaLayout implements LayoutManager { + protected int hGap = 5; + protected int topMargin = 17; + + public void addLayoutComponent(final String string, final Component comp) { + } + + public void removeLayoutComponent(final Component c) { + } + + public Dimension preferredLayoutSize(final Container c) { + return minimumLayoutSize(c); + } + + public Dimension minimumLayoutSize(final Container c) { + if (c != null) { + Component[] children = c.getComponents(); + + if (children != null && children.length > 0) { + int numChildren = children.length; + int height = 0; + Insets cInsets = c.getInsets(); + int extraHeight = topMargin + cInsets.top + cInsets.bottom; + int extraWidth = cInsets.left + cInsets.right; + int maxWidth = 0; + + for (int counter = 0; counter < numChildren; counter++) { + Dimension aSize = children[counter].getPreferredSize(); + height = Math.max(height, aSize.height); + maxWidth = Math.max(maxWidth, aSize.width); + } + return new Dimension(extraWidth + numChildren * maxWidth + + (numChildren - 1) * hGap, + extraHeight + height); + } + } + return new Dimension(0, 0); + } + + public void layoutContainer(final Container container) { + Component[] children = container.getComponents(); + + if (children != null && children.length > 0) { + int numChildren = children.length; + Dimension[] sizes = new Dimension[numChildren]; + Insets insets = container.getInsets(); + int yLocation = insets.top + topMargin; + int maxWidth = 0; + + for (int counter = 0; counter < numChildren; counter++) { + sizes[counter] = children[counter].getPreferredSize(); + maxWidth = Math.max(maxWidth, sizes[counter].width); + } + int xLocation, xOffset; + if (container.getComponentOrientation().isLeftToRight()) { + xLocation = container.getSize().width - insets.left - maxWidth; + xOffset = hGap + maxWidth; + } else { + xLocation = insets.left; + xOffset = -(hGap + maxWidth); + } + for (int counter = numChildren - 1; counter >= 0; counter--) { + children[counter].setBounds(xLocation, yLocation, + maxWidth, sizes[counter].height); + xLocation -= xOffset; + } + } + } + } + + protected class MetalFileChooserUIAccessor implements DarkFilePaneUIBridge.FileChooserUIAccessor { + public JFileChooser getFileChooser() { + return DarkFileChooserUIBridge.this.getFileChooser(); + } + + public BasicDirectoryModel getModel() { + return DarkFileChooserUIBridge.this.getModel(); + } + + public JPanel createList() { + return DarkFileChooserUIBridge.this.createList(getFileChooser()); + } + + public JPanel createDetailsView() { + return DarkFileChooserUIBridge.this.createDetailsView(getFileChooser()); + } + + public boolean isDirectorySelected() { + return DarkFileChooserUIBridge.this.isDirectorySelected(); + } + + public File getDirectory() { + return DarkFileChooserUIBridge.this.getDirectory(); + } + + public Action getChangeToParentDirectoryAction() { + return DarkFileChooserUIBridge.this.getChangeToParentDirectoryAction(); + } + + public Action getApproveSelectionAction() { + return DarkFileChooserUIBridge.this.getApproveSelectionAction(); + } + + public Action getNewFolderAction() { + return DarkFileChooserUIBridge.this.getNewFolderAction(); + } + + public MouseListener createDoubleClickListener(final JList list) { + return DarkFileChooserUIBridge.this.createDoubleClickListener(getFileChooser(), + list); + } + + public ListSelectionListener createListSelectionListener() { + return DarkFileChooserUIBridge.this.createListSelectionListener(getFileChooser()); + } + } + + /** + * Obsolete class, not used in this version. + * + * @deprecated As of JDK version 9. Obsolete class. + */ + @Deprecated(since = "9") + protected class SingleClickListener extends MouseAdapter { + /** + * Constructs an instance of {@code SingleClickListener}. + * + * @param list an instance of {@code JList} + */ + public SingleClickListener(final JList list) { + } + } + + /** + * Obsolete class, not used in this version. + * + * @deprecated As of JDK version 9. Obsolete class. + */ + @Deprecated(since = "9") + @SuppressWarnings("serial") // Superclass is not serializable across versions + protected class FileRenderer extends DefaultListCellRenderer { + } + + // + // Renderer for DirectoryComboBox + // + @SuppressWarnings("serial") + // Superclass is not serializable across versions + class DirectoryComboBoxRenderer extends DefaultListCellRenderer { + IndentIcon ii = new IndentIcon(); + + public Component getListCellRendererComponent(final JList list, final Object value, + final int index, final boolean isSelected, + final boolean cellHasFocus) { + + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value == null) { + setText(""); + return this; + } + File directory = (File) value; + setText(getFileChooser().getName(directory)); + Icon icon = getFileChooser().getIcon(directory); + ii.icon = icon; + ii.depth = directoryComboBoxModel.getDepth(index); + setIcon(ii); + + return this; + } + } + + class IndentIcon implements Icon { + + Icon icon = null; + int depth = 0; + + public void paintIcon(final Component c, final Graphics g, final int x, final int y) { + if (c.getComponentOrientation().isLeftToRight()) { + icon.paintIcon(c, g, x + depth * space, y); + } else { + icon.paintIcon(c, g, x, y); + } + } + + public int getIconWidth() { + return icon.getIconWidth() + depth * space; + } + + public int getIconHeight() { + return icon.getIconHeight(); + } + + } + + /** + * Data model for a type-face selection combo-box. + */ + @SuppressWarnings("serial") // Superclass is not serializable across versions + protected class DirectoryComboBoxModel extends AbstractListModel implements ComboBoxModel { + Vector directories = new Vector(); + int[] depths = null; + File selectedDirectory = null; + JFileChooser chooser = getFileChooser(); + FileSystemView fsv = chooser.getFileSystemView(); + + /** + * Constructs an instance of {@code DirectoryComboBoxModel}. + */ + public DirectoryComboBoxModel() { + // Add the current directory to the model, and make it the + // selectedDirectory + File dir = getFileChooser().getCurrentDirectory(); + if (dir != null) { + addItem(dir); + } + } + + /** + * Adds the directory to the model and sets it to be selected, + * additionally clears out the previous selected directory and + * the paths leading up to it, if any. + */ + protected void addItem(final File directory) { + + if (directory == null) { + return; + } + + boolean useShellFolder = FilePane.usesShellFolder(chooser); + + directories.clear(); + + File[] baseFolders = (useShellFolder) + ? (File[]) ShellFolder.get("fileChooserComboBoxFolders") + : fsv.getRoots(); + directories.addAll(Arrays.asList(baseFolders)); + + // Get the canonical (full) path. This has the side + // benefit of removing extraneous chars from the path, + // for example /foo/bar/ becomes /foo/bar + File canonical; + try { + canonical = ShellFolder.getNormalizedFile(directory); + } catch (IOException e) { + // Maybe drive is not ready. Can't abort here. + canonical = directory; + } + + // create File instances of each directory leading up to the top + try { + File sf = useShellFolder ? ShellFolder.getShellFolder(canonical) + : canonical; + File f = sf; + Vector path = new Vector(10); + do { + path.addElement(f); + } while ((f = f.getParentFile()) != null); + + int pathCount = path.size(); + // Insert chain at appropriate place in vector + for (int i = 0; i < pathCount; i++) { + f = path.get(i); + if (directories.contains(f)) { + int topIndex = directories.indexOf(f); + for (int j = i - 1; j >= 0; j--) { + directories.insertElementAt(path.get(j), topIndex + i - j); + } + break; + } + } + calculateDepths(); + setSelectedItem(sf); + } catch (FileNotFoundException ex) { + calculateDepths(); + } + } + + protected void calculateDepths() { + depths = new int[directories.size()]; + for (int i = 0; i < depths.length; i++) { + File dir = directories.get(i); + File parent = dir.getParentFile(); + depths[i] = 0; + if (parent != null) { + for (int j = i - 1; j >= 0; j--) { + if (parent.equals(directories.get(j))) { + depths[i] = depths[j] + 1; + break; + } + } + } + } + } + + /** + * Returns the depth of {@code i}-th file. + * + * @param i an index + * @return the depth of {@code i}-th file + */ + public int getDepth(final int i) { + return (depths != null && i >= 0 && i < depths.length) ? depths[i] : 0; + } + + public int getSize() { + return directories.size(); + } + + public void setSelectedItem(final Object selectedDirectory) { + this.selectedDirectory = (File) selectedDirectory; + fireContentsChanged(this, -1, -1); + } + + public Object getElementAt(final int index) { + return directories.elementAt(index); + } + + public Object getSelectedItem() { + return selectedDirectory; + } + + + } + + /** + * Data model for a type-face selection combo-box. + */ + @SuppressWarnings("serial") // Same-version serialization only + protected class FilterComboBoxModel extends AbstractListModel implements ComboBoxModel, PropertyChangeListener { + + /** + * An array of file filters. + */ + protected FileFilter[] filters; + + /** + * Constructs an instance of {@code FilterComboBoxModel}. + */ + protected FilterComboBoxModel() { + super(); + filters = getFileChooser().getChoosableFileFilters(); + } + + public void propertyChange(final PropertyChangeEvent e) { + String prop = e.getPropertyName(); + if (prop == JFileChooser.CHOOSABLE_FILE_FILTER_CHANGED_PROPERTY) { + filters = (FileFilter[]) e.getNewValue(); + fireContentsChanged(this, -1, -1); + } else if (prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY) { + fireContentsChanged(this, -1, -1); + } + } + + public void setSelectedItem(final Object filter) { + if (filter != null) { + getFileChooser().setFileFilter((FileFilter) filter); + fireContentsChanged(this, -1, -1); + } + } + + public Object getSelectedItem() { + // Ensure that the current filter is in the list. + // NOTE: we shouldnt' have to do this, since JFileChooser adds + // the filter to the choosable filters list when the filter + // is set. Lets be paranoid just in case someone overrides + // setFileFilter in JFileChooser. + FileFilter currentFilter = getFileChooser().getFileFilter(); + boolean found = false; + if (currentFilter != null) { + for (FileFilter filter : filters) { + if (filter == currentFilter) { + found = true; + } + } + if (found == false) { + getFileChooser().addChoosableFileFilter(currentFilter); + } + } + return getFileChooser().getFileFilter(); + } + + public int getSize() { + if (filters != null) { + return filters.length; + } else { + return 0; + } + } + + public Object getElementAt(final int index) { + if (index > getSize() - 1) { + // This shouldn't happen. Try to recover gracefully. + return getFileChooser().getFileFilter(); + } + if (filters != null) { + return filters[index]; + } else { + return null; + } + } + } + + /** + * Acts when DirectoryComboBox has changed the selected item. + */ + @SuppressWarnings("serial") // Superclass is not serializable across versions + protected class DirectoryComboBoxAction extends AbstractAction { + + /** + * Constructs a new instance of {@code DirectoryComboBoxAction}. + */ + protected DirectoryComboBoxAction() { + super("DirectoryComboBoxAction"); + } + + public void actionPerformed(final ActionEvent e) { + directoryComboBox.hidePopup(); + File f = (File) directoryComboBox.getSelectedItem(); + if (!getFileChooser().getCurrentDirectory().equals(f)) { + getFileChooser().setCurrentDirectory(f); + } + } + } + + @SuppressWarnings("serial") // Superclass is not serializable across versions + protected class AlignedLabel extends JLabel { + protected AlignedLabel[] group; + protected int maxWidth = 0; + + AlignedLabel() { + super(); + setAlignmentX(JComponent.LEFT_ALIGNMENT); + } + + + AlignedLabel(final String text) { + super(text); + setAlignmentX(JComponent.LEFT_ALIGNMENT); + } + + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + // Align the width with all other labels in group. + return new Dimension(getMaxWidth() + 11, d.height); + } + + protected int getMaxWidth() { + if (maxWidth == 0 && group != null) { + int max = 0; + for (int i = 0; i < group.length; i++) { + max = Math.max(group[i].getSuperPreferredWidth(), max); + } + for (int i = 0; i < group.length; i++) { + group[i].maxWidth = max; + } + } + return maxWidth; + } + + protected int getSuperPreferredWidth() { + return super.getPreferredSize().width; + } + } +} diff --git a/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePane.java b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePane.java new file mode 100644 index 00000000..f1f8d90f --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePane.java @@ -0,0 +1,434 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.filechooser; + +import com.weis.darklaf.components.OverlayScrollPane; +import com.weis.darklaf.ui.table.TextFieldTableCellEditorBorder; +import com.weis.darklaf.util.DarkUIUtil; +import sun.awt.AWTAccessor; +import sun.swing.SwingUtilities2; + +import javax.accessibility.AccessibleContext; +import javax.swing.*; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.TableModelEvent; +import javax.swing.table.TableCellRenderer; +import javax.swing.text.Position; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.function.Supplier; + +public class DarkFilePane extends DarkFilePaneUIBridge { + + + public DarkFilePane(final FileChooserUIAccessor fileChooserUIAccessor) { + super(fileChooserUIAccessor); + } + + @Override + protected void installDefaults() { + super.installDefaults(); + kiloByteString = UIManager.getString("FileChooser.fileSizeKiloBytes"); + megaByteString = UIManager.getString("FileChooser.fileSizeMegaBytes"); + gigaByteString = UIManager.getString("FileChooser.fileSizeGigaBytes"); + editCell = new JTextField(); + editCell.setBorder(new TextFieldTableCellEditorBorder()); + editCell.putClientProperty("JTextField.listCellEditor", true); + } + + public JPanel createList() { + JPanel p = new JPanel(new BorderLayout()); + final JFileChooser fileChooser = getFileChooser(); + + @SuppressWarnings("serial") // anonymous class + final JList list = new JList<>() { + public int getNextMatch(final String prefix, final int startIndex, final Position.Bias bias) { + ListModel model = getModel(); + int max = model.getSize(); + if (prefix == null || startIndex < 0 || startIndex >= max) { + throw new IllegalArgumentException(); + } + // start search from the next element before/after the selected element + boolean backwards = (bias == Position.Bias.Backward); + for (int i = startIndex; backwards ? i >= 0 : i < max; i += (backwards ? -1 : 1)) { + String filename = fileChooser.getName((File) model.getElementAt(i)); + if (filename.regionMatches(true, 0, prefix, 0, prefix.length())) { + return i; + } + } + return -1; + } + }; + list.setCellRenderer(new FileRenderer()); + list.setLayoutOrientation(JList.VERTICAL_WRAP); + + // 4835633 : tell BasicListUI that this is a file list + list.putClientProperty("List.isFileList", Boolean.TRUE); + list.putClientProperty("JList.fullRowSelection", fullRowSelection); + + if (listViewWindowsStyle) { + list.addFocusListener(repaintListener); + } + + updateListRowCount(list); + + getModel().addListDataListener(new ListDataListener() { + public void intervalAdded(final ListDataEvent e) { + updateListRowCount(list); + } + + public void intervalRemoved(final ListDataEvent e) { + updateListRowCount(list); + } + + public void contentsChanged(final ListDataEvent e) { + if (isShowing()) { + clearSelection(); + } + updateListRowCount(list); + } + }); + + getModel().addPropertyChangeListener(this); + + if (fileChooser.isMultiSelectionEnabled()) { + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } else { + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + list.setModel(new SortableListModel()); + + list.addListSelectionListener(createListSelectionListener()); + list.addMouseListener(getMouseHandler()); + + OverlayScrollPane overlayScrollPane = new OverlayScrollPane(list); + JScrollPane scrollPane = overlayScrollPane.getScrollPane(); + if (listViewBackground != null) { + list.setBackground(listViewBackground); + } + if (listViewBorder != null) { + scrollPane.setBorder(listViewBorder); + } + + list.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, filesListAccessibleName); + + p.add(overlayScrollPane, BorderLayout.CENTER); + return p; + } + + @Override + public JPanel createDetailsView() { + final JFileChooser chooser = getFileChooser(); + + JPanel p = new JPanel(new BorderLayout()); + + @SuppressWarnings("serial") // anonymous class + final JTable detailsTable = new JTable(getDetailsTableModel()) { + public void tableChanged(final TableModelEvent e) { + super.tableChanged(e); + + if (e.getFirstRow() == TableModelEvent.HEADER_ROW) { + // update header with possibly changed column set + updateDetailsColumnModel(this); + } + } + + // Handle Escape key events here + protected boolean processKeyBinding(final KeyStroke ks, final KeyEvent e, + final int condition, final boolean pressed) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && getCellEditor() == null) { + // We are not editing, forward to filechooser. + chooser.dispatchEvent(e); + return true; + } + return super.processKeyBinding(ks, e, condition, pressed); + } + }; + int rowHeight = UIManager.getInt("FileChooser.rowHeight"); + if (rowHeight > 0) { + detailsTable.setRowHeight(rowHeight); + } + detailsTable.setRowSorter(getRowSorter()); + detailsTable.setAutoCreateColumnsFromModel(false); + detailsTable.setComponentOrientation(chooser.getComponentOrientation()); + detailsTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + detailsTable.setRowSelectionAllowed(true); + detailsTable.setShowGrid(false); + detailsTable.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); + detailsTable.addKeyListener(detailsKeyListener); + detailsTable.putClientProperty("JTable.rowFocusBorder", true); + detailsTable.putClientProperty("JTable.fileChooserParent", (Supplier) this::getFileChooser); + detailsTable.putClientProperty("JTable.fileNameColumnIndex", COLUMN_FILENAME); + + + Font font = list.getFont(); + detailsTable.setFont(font); + + TableCellRenderer headerRenderer = + new AlignableTableHeaderRenderer(detailsTable.getTableHeader().getDefaultRenderer()); + detailsTable.getTableHeader().setDefaultRenderer(headerRenderer); + TableCellRenderer cellRenderer = new DetailsTableCellRenderer(chooser); + detailsTable.setDefaultRenderer(Object.class, cellRenderer); + + if (getFileChooser().isMultiSelectionEnabled()) { + detailsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } else { + detailsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + detailsTable.addMouseListener(getMouseHandler()); + + // 4835633 : tell BasicTableUI that this is a file list + detailsTable.putClientProperty("Table.isFileList", Boolean.TRUE); + + if (listViewWindowsStyle) { + detailsTable.addFocusListener(repaintListener); + } + + // TAB/SHIFT-TAB should transfer focus and ENTER should select an item. + // We don't want them to navigate within the table + ActionMap am = SwingUtilities.getUIActionMap(detailsTable); + am.remove("selectNextRowCell"); + am.remove("selectPreviousRowCell"); + am.remove("selectNextColumnCell"); + am.remove("selectPreviousColumnCell"); + detailsTable.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + null); + detailsTable.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + null); + + OverlayScrollPane overlayScrollPane = new OverlayScrollPane(detailsTable); + JScrollPane scrollPane = overlayScrollPane.getScrollPane(); + scrollPane.setComponentOrientation(chooser.getComponentOrientation()); + LookAndFeel.installColors(scrollPane.getViewport(), "Table.background", "Table.foreground"); + + // Adjust width of first column so the table fills the viewport when + // first displayed (temporary listener). + scrollPane.addComponentListener(new ComponentAdapter() { + public void componentResized(final ComponentEvent e) { + JScrollPane sp = (JScrollPane) e.getComponent(); + fixNameColumnWidth(sp.getViewport().getSize().width); + sp.removeComponentListener(this); + } + }); + + detailsTable.setForeground(list.getForeground()); + detailsTable.setBackground(list.getBackground()); + + if (listViewBorder != null) { + scrollPane.setBorder(listViewBorder); + } + p.add(overlayScrollPane, BorderLayout.CENTER); + + detailsTableModel.fireTableStructureChanged(); + + detailsTable.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, filesDetailsAccessibleName); + + return p; + } + + protected void cancelEdit() { + if (editFile != null) { + editFile = null; + list.remove(editCell); + list.putClientProperty("JList.isEditing", false); + repaint(); + } else if (detailsTable != null && detailsTable.isEditing()) { + detailsTable.getCellEditor().cancelCellEditing(); + } + } + + protected void editFileName(final int index) { + JFileChooser chooser = getFileChooser(); + File currentDirectory = chooser.getCurrentDirectory(); + + if (readOnly || !canWrite(currentDirectory, chooser)) { + return; + } + + ensureIndexIsVisible(index); + switch (viewType) { + case VIEWTYPE_LIST: + editFile = (File) getModel().getElementAt(getRowSorter().convertRowIndexToModel(index)); + Rectangle r = list.getCellBounds(index, index); + if (editCell == null) { + editCell = new JTextField(); + editCell.setName("Tree.cellEditor"); + editCell.addActionListener(new EditActionListener()); + editCell.addFocusListener(editorFocusListener); + editCell.setNextFocusableComponent(list); + } + list.add(editCell); + editCell.setText(chooser.getName(editFile)); + ComponentOrientation orientation = list.getComponentOrientation(); + editCell.setComponentOrientation(orientation); + + Icon icon = chooser.getIcon(editFile); + + // PENDING - grab padding (4) below from defaults table. + int editX = icon == null ? 20 : icon.getIconWidth() + 4; + + int gap = 0; + var renderer = list.getCellRenderer(); + if (renderer instanceof JLabel) { + gap = ((JLabel) renderer).getIconTextGap() - 1; + } + if (orientation.isLeftToRight()) { + editCell.setBounds(editX + r.x + gap, r.y, r.width - editX - gap, r.height); + } else { + editCell.setBounds(r.x, r.y, r.width - editX - gap, r.height); + } + list.putClientProperty("JList.isEditing", true); + editCell.requestFocus(); + editCell.selectAll(); + break; + + case VIEWTYPE_DETAILS: + detailsTable.editCellAt(index, COLUMN_FILENAME); + break; + } + } + + public JPopupMenu getComponentPopupMenu() { + JPopupMenu popupMenu = getFileChooser().getComponentPopupMenu(); + if (popupMenu != null) { + return popupMenu; + } + + JMenu viewMenu = getViewMenu(); + if (contextMenu == null) { + contextMenu = new JPopupMenu(); + if (viewMenu != null) { + contextMenu.add(viewMenu); + if (listViewWindowsStyle) { + contextMenu.addSeparator(); + } + } + ActionMap actionMap = getActionMap(); + Action refreshAction = actionMap.get(ACTION_REFRESH); + Action newFolderAction = actionMap.get(ACTION_NEW_FOLDER); + Action renameAction = actionMap.get(ACTION_EDIT_FILE_NAME); + if (refreshAction != null) { + contextMenu.add(refreshAction); + } + if (renameAction != null) { + var menuItem = new JMenuItem(renameAction); + menuItem.setText("Rename"); + contextMenu.add(menuItem); + } + if (newFolderAction != null) { + contextMenu.add(newFolderAction); + } + } + if (viewMenu != null) { + viewMenu.getPopupMenu().setInvoker(viewMenu); + } + return contextMenu; + } + + @Override + protected Handler getMouseHandler() { + if (handler == null) { + handler = new DarkHandler(); + } + return handler; + } + + protected class DarkHandler extends Handler { + + @Override + public void mouseClicked(MouseEvent evt) { + JComponent source = (JComponent) evt.getSource(); + + int index; + if (source instanceof JList) { + index = list.locationToIndex(evt.getPoint()); + } else if (source instanceof JTable) { + JTable table = (JTable) source; + Point p = evt.getPoint(); + index = table.rowAtPoint(p); + + boolean pointOutsidePrefSize = SwingUtilities2.pointOutsidePrefSize(table, index, + table.columnAtPoint(p), p); + + if (pointOutsidePrefSize && !fullRowSelection) { + return; + } + + // Translate point from table to list + if (index >= 0 && list != null && listSelectionModel.isSelectedIndex(index)) { + + // Make a new event with the list as source, placing the + // click in the corresponding list cell. + Rectangle r = list.getCellBounds(index, index); + MouseEvent newEvent = new MouseEvent(list, evt.getID(), + evt.getWhen(), evt.getModifiersEx(), + r.x + 1, r.y + r.height / 2, + evt.getXOnScreen(), + evt.getYOnScreen(), + evt.getClickCount(), evt.isPopupTrigger(), + evt.getButton()); + AWTAccessor.MouseEventAccessor meAccessor = AWTAccessor.getMouseEventAccessor(); + meAccessor.setCausedByTouchEvent(newEvent, + meAccessor.isCausedByTouchEvent(evt)); + evt = newEvent; + } + } else { + return; + } + + if (index >= 0 && SwingUtilities.isLeftMouseButton(evt)) { + JFileChooser fc = getFileChooser(); + + // For single click, we handle editing file name + if (evt.getClickCount() == 1 && source instanceof JList) { + if ((!fc.isMultiSelectionEnabled() || fc.getSelectedFiles().length <= 1) + && listSelectionModel.isSelectedIndex(index) + && getEditIndex() == index && editFile == null + && DarkUIUtil.isOverText(evt, index, list)) { + editFileName(index); + } else { + setEditIndex(index); + } + } else if (evt.getClickCount() == 2) { + // on double click (open or drill down one directory) be + // sure to clear the edit index + resetEditIndex(); + } + } + + // Forward event to Basic + if (getDoubleClickListener() != null) { + list.putClientProperty("List.isFileList", false); + getDoubleClickListener().mouseClicked(evt); + list.putClientProperty("List.isFileList", true); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePaneUIBridge.java b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePaneUIBridge.java new file mode 100644 index 00000000..4e768bcb --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePaneUIBridge.java @@ -0,0 +1,2060 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.filechooser; + +import com.weis.darklaf.ui.list.DarkListCellRenderer; +import com.weis.darklaf.ui.table.DarkTableCellEditor; +import com.weis.darklaf.ui.table.DarkTableCellRenderer; +import com.weis.darklaf.ui.table.TextFieldTableCellEditorBorder; +import org.jetbrains.annotations.NotNull; +import sun.awt.AWTAccessor; +import sun.awt.shell.ShellFolder; +import sun.awt.shell.ShellFolderColumnInfo; +import sun.swing.FilePane; +import sun.swing.SwingUtilities2; + +import javax.accessibility.AccessibleContext; +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.RowSorterEvent; +import javax.swing.event.RowSorterListener; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.filechooser.FileSystemView; +import javax.swing.plaf.basic.BasicDirectoryModel; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableColumnModel; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; +import javax.swing.text.Position; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.FileNotFoundException; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.Callable; + +/** + * WARNING: This class is an implementation detail and is only + * public so that it can be used by two packages. You should NOT consider + * this public API. + *

+ * This component is intended to be used in a subclass of + * javax.swing.plaf.basic.BasicFileChooserUI. It realies heavily on the + * implementation of BasicFileChooserUI, and is intended to be API compatible + * with earlier implementations of MetalFileChooserUI and WindowsFileChooserUI. + * + * @author Leif Samuelsson + */ +public class DarkFilePaneUIBridge extends JPanel implements PropertyChangeListener { + + // Constants for actions. These are used for the actions' ACTION_COMMAND_KEY + // and as keys in the action maps for FilePane and the corresponding UI classes + + public static final String ACTION_APPROVE_SELECTION = "approveSelection"; + public static final String ACTION_CANCEL = "cancelSelection"; + public static final String ACTION_EDIT_FILE_NAME = "editFileName"; + public static final String ACTION_REFRESH = "refresh"; + public static final String ACTION_CHANGE_TO_PARENT_DIRECTORY = "Go Up"; + public static final String ACTION_NEW_FOLDER = "New Folder"; + public static final String ACTION_VIEW_LIST = "viewTypeList"; + public static final String ACTION_VIEW_DETAILS = "viewTypeDetails"; + // "enums" for setViewType() + public static final int VIEWTYPE_LIST = 0; + public static final int VIEWTYPE_DETAILS = 1; + protected static final int VIEWTYPE_COUNT = 2; + protected static final Cursor waitCursor = + Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); + protected static final int COLUMN_FILENAME = 0; + protected static final int COLUMN_SIZE = 1; + protected static FocusListener repaintListener = new FocusListener() { + public void focusGained(final FocusEvent fe) { + repaintSelection(fe.getSource()); + } + + public void focusLost(final FocusEvent fe) { + repaintSelection(fe.getSource()); + } + + protected void repaintSelection(final Object source) { + if (source instanceof JList) { + repaintListSelection((JList) source); + } else if (source instanceof JTable) { + repaintTableSelection((JTable) source); + } + } + + protected void repaintListSelection(final JList list) { + int[] indices = list.getSelectedIndices(); + for (int i : indices) { + Rectangle bounds = list.getCellBounds(i, i); + list.repaint(bounds); + } + } + + protected void repaintTableSelection(final JTable table) { + int minRow = table.getSelectionModel().getMinSelectionIndex(); + int maxRow = table.getSelectionModel().getMaxSelectionIndex(); + if (minRow == -1 || maxRow == -1) { + return; + } + + int col0 = table.convertColumnIndexToView(COLUMN_FILENAME); + + Rectangle first = table.getCellRect(minRow, col0, false); + Rectangle last = table.getCellRect(maxRow, col0, false); + Rectangle dirty = first.union(last); + table.repaint(dirty); + } + }; + protected Action[] actions; + protected int viewType = -1; + protected JPanel[] viewPanels = new JPanel[VIEWTYPE_COUNT]; + protected JPanel currentViewPanel; + protected String[] viewTypeActionNames; + protected String filesListAccessibleName = null; + protected String filesDetailsAccessibleName = null; + protected JPopupMenu contextMenu; + protected JMenu viewMenu; + protected String viewMenuLabelText; + protected String refreshActionLabelText; + protected String newFolderActionLabelText; + protected String kiloByteString; + protected String megaByteString; + protected String gigaByteString; + protected String renameErrorTitleText; + protected String renameErrorText; + protected String renameErrorFileExistsText; + protected boolean smallIconsView = false; + protected Border listViewBorder; + protected Color listViewBackground; + protected boolean listViewWindowsStyle; + protected boolean readOnly; + protected boolean fullRowSelection = true; + + protected ListSelectionModel listSelectionModel; + protected JList list; + protected JTable detailsTable; + // Provides a way to recognize a newly created folder, so it can + // be selected when it appears in the model. + protected File newFolderFile; + // Used for accessing methods in the corresponding UI class + protected FileChooserUIAccessor fileChooserUIAccessor; + protected DetailsTableModel detailsTableModel; + protected DetailsTableRowSorter rowSorter; + protected final KeyListener detailsKeyListener = new KeyAdapter() { + protected final long timeFactor; + + protected final StringBuilder typedString = new StringBuilder(); + + protected long lastTime = 1000L; + + { + Long l = (Long) UIManager.get("Table.timeFactor"); + timeFactor = (l != null) ? l : 1000L; + } + + /** + * Moves the keyboard focus to the first element whose prefix matches + * the sequence of alphanumeric keys pressed by the user with delay + * less than value of timeFactor. Subsequent same key + * presses move the keyboard focus to the next object that starts with + * the same letter until another key is pressed, then it is treated + * as the prefix with appropriate number of the same letters followed + * by first typed another letter. + */ + public void keyTyped(final KeyEvent e) { + BasicDirectoryModel model = getModel(); + int rowCount = model.getSize(); + + if (detailsTable == null || rowCount == 0 || + e.isAltDown() || e.isControlDown() || e.isMetaDown()) { + return; + } + + InputMap inputMap = detailsTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + KeyStroke key = KeyStroke.getKeyStrokeForEvent(e); + + if (inputMap != null && inputMap.get(key) != null) { + return; + } + + int startIndex = detailsTable.getSelectionModel().getLeadSelectionIndex(); + + if (startIndex < 0) { + startIndex = 0; + } + + if (startIndex >= rowCount) { + startIndex = rowCount - 1; + } + + char c = e.getKeyChar(); + + long time = e.getWhen(); + + if (time - lastTime < timeFactor) { + if (typedString.length() == 1 && typedString.charAt(0) == c) { + // Subsequent same key presses move the keyboard focus to the next + // object that starts with the same letter. + startIndex++; + } else { + typedString.append(c); + } + } else { + startIndex++; + + typedString.setLength(0); + typedString.append(c); + } + + lastTime = time; + + if (startIndex >= rowCount) { + startIndex = 0; + } + + // Find next file + int index = getNextMatch(startIndex, rowCount - 1); + + if (index < 0 && startIndex > 0) { // wrap + index = getNextMatch(0, startIndex - 1); + } + + if (index >= 0) { + detailsTable.getSelectionModel().setSelectionInterval(index, index); + + Rectangle cellRect = detailsTable.getCellRect(index, detailsTable.convertColumnIndexToView(COLUMN_FILENAME), false); + detailsTable.scrollRectToVisible(cellRect); + } + } + + protected int getNextMatch(final int startIndex, final int finishIndex) { + BasicDirectoryModel model = getModel(); + JFileChooser fileChooser = getFileChooser(); + DetailsTableRowSorter rowSorter = getRowSorter(); + + String prefix = typedString.toString().toLowerCase(); + + // Search element + for (int index = startIndex; index <= finishIndex; index++) { + File file = (File) model.getElementAt(rowSorter.convertRowIndexToModel(index)); + + String fileName = fileChooser.getName(file).toLowerCase(); + + if (fileName.startsWith(prefix)) { + return index; + } + } + + return -1; + } + }; + protected DetailsTableCellEditor tableCellEditor; + protected Action newFolderAction; + protected Handler handler; + int lastIndex = -1; + File editFile = null; + JTextField editCell = null; + protected FocusListener editorFocusListener = new FocusAdapter() { + public void focusLost(final FocusEvent e) { + if (!e.isTemporary()) { + applyEdit(); + } + } + }; + + public DarkFilePaneUIBridge(final FileChooserUIAccessor fileChooserUIAccessor) { + super(new BorderLayout()); + + this.fileChooserUIAccessor = fileChooserUIAccessor; + + installDefaults(); + createActionMap(); + } + + protected static void recursivelySetInheritsPopupMenu(final Container container, final boolean b) { + if (container instanceof JComponent) { + ((JComponent) container).setInheritsPopupMenu(b); + } + int n = container.getComponentCount(); + for (int i = 0; i < n; i++) { + recursivelySetInheritsPopupMenu((Container) container.getComponent(i), b); + } + } + + public static void addActionsToMap(final ActionMap map, final Action[] actions) { + if (map != null && actions != null) { + for (Action a : actions) { + String cmd = (String) a.getValue(Action.ACTION_COMMAND_KEY); + if (cmd == null) { + cmd = (String) a.getValue(Action.NAME); + } + map.put(cmd, a); + } + } + } + + public static boolean canWrite(final File f, final JFileChooser chooser) { + // Return false for non FileSystem files or if file doesn't exist. + if (!f.exists()) { + return false; + } + + try { + if (f instanceof ShellFolder) { + return f.canWrite(); + } else { + if (usesShellFolder(chooser)) { + try { + return ShellFolder.getShellFolder(f).canWrite(); + } catch (FileNotFoundException ex) { + // File doesn't exist + return false; + } + } else { + // Ordinary file + return f.canWrite(); + } + } + } catch (SecurityException e) { + return false; + } + } + + /** + * Returns true if specified FileChooser should use ShellFolder + */ + public static boolean usesShellFolder(final JFileChooser chooser) { + Boolean prop = (Boolean) chooser.getClientProperty("FileChooser.useShellFolder"); + + return prop == null ? chooser.getFileSystemView().equals(FileSystemView.getFileSystemView()) + : prop.booleanValue(); + } + + public void uninstallUI() { + if (getModel() != null) { + getModel().removePropertyChangeListener(this); + } + } + + protected BasicDirectoryModel getModel() { + return fileChooserUIAccessor.getModel(); + } + + public int getViewType() { + return viewType; + } + + public void setViewType(final int viewType) { + if (viewType == this.viewType) { + return; + } + + int oldValue = this.viewType; + this.viewType = viewType; + + JPanel createdViewPanel = null; + Component newFocusOwner = null; + + switch (viewType) { + case VIEWTYPE_LIST: + if (viewPanels[viewType] == null) { + createdViewPanel = fileChooserUIAccessor.createList(); + if (createdViewPanel == null) { + createdViewPanel = createList(); + } + + list = findChildComponent(createdViewPanel, JList.class); + if (listSelectionModel == null) { + listSelectionModel = list.getSelectionModel(); + if (detailsTable != null) { + detailsTable.setSelectionModel(listSelectionModel); + } + } else { + list.setSelectionModel(listSelectionModel); + } + } + list.setLayoutOrientation(JList.VERTICAL_WRAP); + newFocusOwner = list; + break; + + case VIEWTYPE_DETAILS: + if (viewPanels[viewType] == null) { + createdViewPanel = fileChooserUIAccessor.createDetailsView(); + if (createdViewPanel == null) { + createdViewPanel = createDetailsView(); + } + + detailsTable = findChildComponent(createdViewPanel, JTable.class); + if (listSelectionModel != null) { + detailsTable.setSelectionModel(listSelectionModel); + } + } + newFocusOwner = detailsTable; + break; + } + + if (createdViewPanel != null) { + viewPanels[viewType] = createdViewPanel; + recursivelySetInheritsPopupMenu(createdViewPanel, true); + } + + boolean isFocusOwner = false; + + if (currentViewPanel != null) { + Component owner = DefaultKeyboardFocusManager. + getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + isFocusOwner = owner == detailsTable || owner == list; + + remove(currentViewPanel); + } + + currentViewPanel = viewPanels[viewType]; + add(currentViewPanel, BorderLayout.CENTER); + + if (isFocusOwner && newFocusOwner != null) { + newFocusOwner.requestFocusInWindow(); + } + + revalidate(); + repaint(); + updateViewMenu(); + firePropertyChange("viewType", oldValue, viewType); + } + + public Action getViewTypeAction(final int viewType) { + return new ViewTypeAction(viewType); + } + + protected void installDefaults() { + Locale l = getFileChooser().getLocale(); + + listViewBorder = UIManager.getBorder("FileChooser.listViewBorder"); + listViewBackground = UIManager.getColor("FileChooser.listViewBackground"); + listViewWindowsStyle = UIManager.getBoolean("FileChooser.listViewWindowsStyle"); + readOnly = UIManager.getBoolean("FileChooser.readOnly"); + + // TODO: On windows, get the following localized strings from the OS + + viewMenuLabelText = + UIManager.getString("FileChooser.viewMenuLabelText", l); + refreshActionLabelText = + UIManager.getString("FileChooser.refreshActionLabelText", l); + newFolderActionLabelText = + UIManager.getString("FileChooser.newFolderActionLabelText", l); + + viewTypeActionNames = new String[VIEWTYPE_COUNT]; + viewTypeActionNames[VIEWTYPE_LIST] = + UIManager.getString("FileChooser.listViewActionLabelText", l); + viewTypeActionNames[VIEWTYPE_DETAILS] = + UIManager.getString("FileChooser.detailsViewActionLabelText", l); + + kiloByteString = UIManager.getString("FileChooser.fileSizeKiloBytes", l); + megaByteString = UIManager.getString("FileChooser.fileSizeMegaBytes", l); + gigaByteString = UIManager.getString("FileChooser.fileSizeGigaBytes", l); + fullRowSelection = UIManager.getBoolean("FileView.fullRowSelection"); + + filesListAccessibleName = UIManager.getString("FileChooser.filesListAccessibleName", l); + filesDetailsAccessibleName = UIManager.getString("FileChooser.filesDetailsAccessibleName", l); + + renameErrorTitleText = UIManager.getString("FileChooser.renameErrorTitleText", l); + renameErrorText = UIManager.getString("FileChooser.renameErrorText", l); + renameErrorFileExistsText = UIManager.getString("FileChooser.renameErrorFileExistsText", l); + } + + /** + * Fetches the command list for the FilePane. These commands + * are useful for binding to events, such as in a keymap. + * + * @return the command list + */ + public Action[] getActions() { + if (actions == null) { + @SuppressWarnings("serial") + // JDK-implementation class + class FilePaneAction extends AbstractAction { + FilePaneAction(final String name) { + this(name, name); + } + + FilePaneAction(final String name, final String cmd) { + super(name); + putValue(Action.ACTION_COMMAND_KEY, cmd); + } + + public void actionPerformed(final ActionEvent e) { + String cmd = (String) getValue(Action.ACTION_COMMAND_KEY); + + if (Objects.equals(cmd, ACTION_CANCEL)) { + if (editFile != null) { + cancelEdit(); + } else { + getFileChooser().cancelSelection(); + } + } else if (Objects.equals(cmd, ACTION_EDIT_FILE_NAME)) { + JFileChooser fc = getFileChooser(); + int index = listSelectionModel.getMinSelectionIndex(); + if (index >= 0 && editFile == null && + (!fc.isMultiSelectionEnabled() || + fc.getSelectedFiles().length <= 1)) { + + editFileName(index); + } + } else if (Objects.equals(cmd, ACTION_REFRESH)) { + getFileChooser().rescanCurrentDirectory(); + } + } + + public boolean isEnabled() { + String cmd = (String) getValue(Action.ACTION_COMMAND_KEY); + if (Objects.equals(cmd, ACTION_CANCEL)) { + return getFileChooser().isEnabled(); + } else if (Objects.equals(cmd, ACTION_EDIT_FILE_NAME)) { + return !readOnly && getFileChooser().isEnabled(); + } else { + return true; + } + } + } + + ArrayList actionList = new ArrayList<>(8); + Action action; + + actionList.add(new FilePaneAction(ACTION_CANCEL)); + actionList.add(new FilePaneAction(ACTION_EDIT_FILE_NAME)); + actionList.add(new FilePaneAction(refreshActionLabelText, ACTION_REFRESH)); + + action = fileChooserUIAccessor.getApproveSelectionAction(); + if (action != null) { + actionList.add(action); + } + action = fileChooserUIAccessor.getChangeToParentDirectoryAction(); + if (action != null) { + actionList.add(action); + } + action = getNewFolderAction(); + if (action != null) { + actionList.add(action); + } + action = getViewTypeAction(VIEWTYPE_LIST); + if (action != null) { + actionList.add(action); + } + action = getViewTypeAction(VIEWTYPE_DETAILS); + if (action != null) { + actionList.add(action); + } + actions = actionList.toArray(new Action[0]); + } + + return Arrays.copyOf(actions, actions.length); + } + + protected void createActionMap() { + addActionsToMap(super.getActionMap(), getActions()); + } + + protected void updateListRowCount(final JList list) { + if (smallIconsView) { + list.setVisibleRowCount(getModel().getSize() / 3); + } else { + list.setVisibleRowCount(-1); + } + } + + public JPanel createList() { + JPanel p = new JPanel(new BorderLayout()); + final JFileChooser fileChooser = getFileChooser(); + + @SuppressWarnings("serial") // anonymous class + final JList list = new JList<>() { + public int getNextMatch(final String prefix, final int startIndex, final Position.Bias bias) { + ListModel model = getModel(); + int max = model.getSize(); + if (prefix == null || startIndex < 0 || startIndex >= max) { + throw new IllegalArgumentException(); + } + // start search from the next element before/after the selected element + boolean backwards = (bias == Position.Bias.Backward); + for (int i = startIndex; backwards ? i >= 0 : i < max; i += (backwards ? -1 : 1)) { + String filename = fileChooser.getName((File) model.getElementAt(i)); + if (filename.regionMatches(true, 0, prefix, 0, prefix.length())) { + return i; + } + } + return -1; + } + }; + list.setCellRenderer(new FileRenderer()); + list.setLayoutOrientation(JList.VERTICAL_WRAP); + + // 4835633 : tell BasicListUI that this is a file list + list.putClientProperty("List.isFileList", Boolean.TRUE); + + if (listViewWindowsStyle) { + list.addFocusListener(repaintListener); + } + + updateListRowCount(list); + + getModel().addListDataListener(new ListDataListener() { + public void intervalAdded(final ListDataEvent e) { + updateListRowCount(list); + } + + public void intervalRemoved(final ListDataEvent e) { + updateListRowCount(list); + } + + public void contentsChanged(final ListDataEvent e) { + if (isShowing()) { + clearSelection(); + } + updateListRowCount(list); + } + }); + + getModel().addPropertyChangeListener(this); + + if (fileChooser.isMultiSelectionEnabled()) { + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } else { + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + list.setModel(new SortableListModel()); + + list.addListSelectionListener(createListSelectionListener()); + list.addMouseListener(getMouseHandler()); + + JScrollPane scrollPane = new JScrollPane(list); + if (listViewBackground != null) { + list.setBackground(listViewBackground); + } + if (listViewBorder != null) { + scrollPane.setBorder(listViewBorder); + } + + list.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, filesListAccessibleName); + + p.add(scrollPane, BorderLayout.CENTER); + return p; + } + + protected DetailsTableModel getDetailsTableModel() { + if (detailsTableModel == null) { + detailsTableModel = new DetailsTableModel(getFileChooser()); + } + return detailsTableModel; + } + + protected void updateDetailsColumnModel(final JTable table) { + if (table != null) { + ShellFolderColumnInfo[] columns = detailsTableModel.getColumns(); + + TableColumnModel columnModel = new DefaultTableColumnModel(); + for (int i = 0; i < columns.length; i++) { + ShellFolderColumnInfo dataItem = columns[i]; + TableColumn column = new TableColumn(i); + + String title = dataItem.getTitle(); + if (title != null && title.startsWith("FileChooser.") && title.endsWith("HeaderText")) { + // the column must have a string resource that we try to get + String uiTitle = UIManager.getString(title, table.getLocale()); + if (uiTitle != null) { + title = uiTitle; + } + } + column.setHeaderValue(title); + + Integer width = dataItem.getWidth(); + if (width != null) { + column.setPreferredWidth(width); + // otherwise we let JTable to decide the actual width + } + + columnModel.addColumn(column); + } + + // Install cell editor for editing file name + if (!readOnly && columnModel.getColumnCount() > COLUMN_FILENAME) { + columnModel.getColumn(COLUMN_FILENAME). + setCellEditor(getDetailsTableCellEditor()); + } + + table.setColumnModel(columnModel); + table.getColumnModel().setColumnMargin(0); + } + } + + protected DetailsTableCellEditor getDetailsTableCellEditor() { + if (tableCellEditor == null) { + tableCellEditor = new DetailsTableCellEditor(new JTextField() { + { + super.setBorder(new TextFieldTableCellEditorBorder()); + } + + @Override + public void setBorder(final Border border) { + } + }); + } + return tableCellEditor; + } + + public JPanel createDetailsView() { + final JFileChooser chooser = getFileChooser(); + + JPanel p = new JPanel(new BorderLayout()); + + @SuppressWarnings("serial") // anonymous class + final JTable detailsTable = new JTable(getDetailsTableModel()) { + public void tableChanged(final TableModelEvent e) { + super.tableChanged(e); + + if (e.getFirstRow() == TableModelEvent.HEADER_ROW) { + // update header with possibly changed column set + updateDetailsColumnModel(this); + } + } + + // Handle Escape key events here + protected boolean processKeyBinding(final KeyStroke ks, final KeyEvent e, final int condition, final boolean pressed) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && getCellEditor() == null) { + // We are not editing, forward to filechooser. + chooser.dispatchEvent(e); + return true; + } + return super.processKeyBinding(ks, e, condition, pressed); + } + }; + + detailsTable.setRowSorter(getRowSorter()); + detailsTable.setAutoCreateColumnsFromModel(false); + detailsTable.setComponentOrientation(chooser.getComponentOrientation()); + detailsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + detailsTable.setShowGrid(false); + detailsTable.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); + detailsTable.addKeyListener(detailsKeyListener); + + Font font = list.getFont(); + detailsTable.setFont(font); + + TableCellRenderer headerRenderer = + new AlignableTableHeaderRenderer(detailsTable.getTableHeader().getDefaultRenderer()); + detailsTable.getTableHeader().setDefaultRenderer(headerRenderer); + TableCellRenderer cellRenderer = new DetailsTableCellRenderer(chooser); + detailsTable.setDefaultRenderer(Object.class, cellRenderer); + + // So that drag can be started on a mouse press + detailsTable.getColumnModel().getSelectionModel(). + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + detailsTable.addMouseListener(getMouseHandler()); + // No need to addListSelectionListener because selections are forwarded + // to our JList. + + // 4835633 : tell BasicTableUI that this is a file list + detailsTable.putClientProperty("Table.isFileList", Boolean.TRUE); + + if (listViewWindowsStyle) { + detailsTable.addFocusListener(repaintListener); + } + + // TAB/SHIFT-TAB should transfer focus and ENTER should select an item. + // We don't want them to navigate within the table + ActionMap am = SwingUtilities.getUIActionMap(detailsTable); + am.remove("selectNextRowCell"); + am.remove("selectPreviousRowCell"); + am.remove("selectNextColumnCell"); + am.remove("selectPreviousColumnCell"); + detailsTable.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + null); + detailsTable.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + null); + + JScrollPane scrollpane = new JScrollPane(detailsTable); + scrollpane.setComponentOrientation(chooser.getComponentOrientation()); + LookAndFeel.installColors(scrollpane.getViewport(), "Table.background", "Table.foreground"); + + // Adjust width of first column so the table fills the viewport when + // first displayed (temporary listener). + scrollpane.addComponentListener(new ComponentAdapter() { + public void componentResized(final ComponentEvent e) { + JScrollPane sp = (JScrollPane) e.getComponent(); + fixNameColumnWidth(sp.getViewport().getSize().width); + sp.removeComponentListener(this); + } + }); + + // 4835633. + // If the mouse is pressed in the area below the Details view table, the + // event is not dispatched to the Table MouseListener but to the + // scrollpane. Listen for that here so we can clear the selection. + scrollpane.addMouseListener(new MouseAdapter() { + public void mousePressed(final MouseEvent e) { + JScrollPane jsp = ((JScrollPane) e.getComponent()); + JTable table = (JTable) jsp.getViewport().getView(); + + if (!e.isShiftDown() || table.getSelectionModel().getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) { + clearSelection(); + TableCellEditor tce = table.getCellEditor(); + if (tce != null) { + tce.stopCellEditing(); + } + } + } + }); + + detailsTable.setForeground(list.getForeground()); + detailsTable.setBackground(list.getBackground()); + + if (listViewBorder != null) { + scrollpane.setBorder(listViewBorder); + } + p.add(scrollpane, BorderLayout.CENTER); + + detailsTableModel.fireTableStructureChanged(); + + detailsTable.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, filesDetailsAccessibleName); + + return p; + } // createDetailsView + + protected void fixNameColumnWidth(final int viewWidth) { + TableColumn nameCol = detailsTable.getColumnModel().getColumn(COLUMN_FILENAME); + int tableWidth = detailsTable.getPreferredSize().width; + + if (tableWidth < viewWidth) { + nameCol.setPreferredWidth(nameCol.getPreferredWidth() + viewWidth - tableWidth); + } + } + + /** + * Creates a selection listener for the list of files and directories. + * + * @return a ListSelectionListener + */ + public ListSelectionListener createListSelectionListener() { + return fileChooserUIAccessor.createListSelectionListener(); + } + + protected int getEditIndex() { + return lastIndex; + } + + protected void setEditIndex(final int i) { + lastIndex = i; + } + + protected void resetEditIndex() { + lastIndex = -1; + } + + protected void cancelEdit() { + if (editFile != null) { + editFile = null; + list.remove(editCell); + repaint(); + } else if (detailsTable != null && detailsTable.isEditing()) { + detailsTable.getCellEditor().cancelCellEditing(); + } + } + + /** + * @param index visual index of the file to be edited + */ + @SuppressWarnings("deprecation") + protected void editFileName(final int index) { + JFileChooser chooser = getFileChooser(); + File currentDirectory = chooser.getCurrentDirectory(); + + if (readOnly || !canWrite(currentDirectory, chooser)) { + return; + } + + ensureIndexIsVisible(index); + switch (viewType) { + case VIEWTYPE_LIST: + editFile = (File) getModel().getElementAt(getRowSorter().convertRowIndexToModel(index)); + Rectangle r = list.getCellBounds(index, index); + if (editCell == null) { + editCell = new JTextField(); + editCell.setName("Tree.cellEditor"); + editCell.addActionListener(new EditActionListener()); + editCell.addFocusListener(editorFocusListener); + editCell.setNextFocusableComponent(list); + } + list.add(editCell); + editCell.setText(chooser.getName(editFile)); + ComponentOrientation orientation = list.getComponentOrientation(); + editCell.setComponentOrientation(orientation); + + Icon icon = chooser.getIcon(editFile); + + // PENDING - grab padding (4) below from defaults table. + int editX = icon == null ? 20 : icon.getIconWidth() + 4; + + if (orientation.isLeftToRight()) { + editCell.setBounds(editX + r.x, r.y, r.width - editX, r.height); + } else { + editCell.setBounds(r.x, r.y, r.width - editX, r.height); + } + editCell.requestFocus(); + editCell.selectAll(); + break; + + case VIEWTYPE_DETAILS: + detailsTable.editCellAt(index, COLUMN_FILENAME); + break; + } + } + + protected void applyEdit() { + if (editFile != null && editFile.exists()) { + JFileChooser chooser = getFileChooser(); + String oldDisplayName = chooser.getName(editFile); + String oldFileName = editFile.getName(); + String newDisplayName = editCell.getText().trim(); + String newFileName; + + if (!newDisplayName.equals(oldDisplayName)) { + newFileName = newDisplayName; + //Check if extension is hidden from user + int i1 = oldFileName.length(); + int i2 = oldDisplayName.length(); + if (i1 > i2 && oldFileName.charAt(i2) == '.') { + newFileName = newDisplayName + oldFileName.substring(i2); + } + + // rename + FileSystemView fsv = chooser.getFileSystemView(); + File f2 = fsv.createFileObject(editFile.getParentFile(), newFileName); + if (f2.exists()) { + JOptionPane.showMessageDialog(chooser, MessageFormat.format(renameErrorFileExistsText, oldFileName), + renameErrorTitleText, JOptionPane.ERROR_MESSAGE); + } else { + if (getModel().renameFile(editFile, f2)) { + if (fsv.isParent(chooser.getCurrentDirectory(), f2)) { + if (chooser.isMultiSelectionEnabled()) { + chooser.setSelectedFiles(new File[]{f2}); + } else { + chooser.setSelectedFile(f2); + } + } else { + //Could be because of delay in updating Desktop folder + //chooser.setSelectedFile(null); + } + } else { + JOptionPane.showMessageDialog(chooser, MessageFormat.format(renameErrorText, oldFileName), + renameErrorTitleText, JOptionPane.ERROR_MESSAGE); + } + } + } + } + if (detailsTable != null && detailsTable.isEditing()) { + detailsTable.getCellEditor().stopCellEditing(); + } + cancelEdit(); + } + + @SuppressWarnings("serial") // anonymous class inside + public Action getNewFolderAction() { + if (!readOnly && newFolderAction == null) { + newFolderAction = new AbstractAction(newFolderActionLabelText) { + protected Action basicNewFolderAction; + + // Initializer + { + putValue(Action.ACTION_COMMAND_KEY, FilePane.ACTION_NEW_FOLDER); + + File currentDirectory = getFileChooser().getCurrentDirectory(); + if (currentDirectory != null) { + setEnabled(canWrite(currentDirectory, getFileChooser())); + } + } + + public void actionPerformed(final ActionEvent ev) { + if (basicNewFolderAction == null) { + basicNewFolderAction = fileChooserUIAccessor.getNewFolderAction(); + } + JFileChooser fc = getFileChooser(); + File oldFile = fc.getSelectedFile(); + basicNewFolderAction.actionPerformed(ev); + File newFile = fc.getSelectedFile(); + if (newFile != null && !newFile.equals(oldFile) && newFile.isDirectory()) { + newFolderFile = newFile; + } + } + }; + } + return newFolderAction; + } + + @SuppressWarnings("deprecation") + void setFileSelected() { + if (getFileChooser().isMultiSelectionEnabled() && !isDirectorySelected()) { + File[] files = getFileChooser().getSelectedFiles(); // Should be selected + Object[] selectedObjects = list.getSelectedValues(); // Are actually selected + + listSelectionModel.setValueIsAdjusting(true); + try { + int lead = listSelectionModel.getLeadSelectionIndex(); + int anchor = listSelectionModel.getAnchorSelectionIndex(); + + Arrays.sort(files); + Arrays.sort(selectedObjects); + + int shouldIndex = 0; + int actuallyIndex = 0; + + // Remove files that shouldn't be selected and add files which should be selected + // Note: Assume files are already sorted in compareTo order. + while (shouldIndex < files.length && + actuallyIndex < selectedObjects.length) { + int comparison = files[shouldIndex].compareTo((File) selectedObjects[actuallyIndex]); + if (comparison < 0) { + doSelectFile(files[shouldIndex++]); + } else if (comparison > 0) { + doDeselectFile(selectedObjects[actuallyIndex++]); + } else { + // Do nothing + shouldIndex++; + actuallyIndex++; + } + + } + + while (shouldIndex < files.length) { + doSelectFile(files[shouldIndex++]); + } + + while (actuallyIndex < selectedObjects.length) { + doDeselectFile(selectedObjects[actuallyIndex++]); + } + + // restore the anchor and lead + if (listSelectionModel instanceof DefaultListSelectionModel) { + ((DefaultListSelectionModel) listSelectionModel). + moveLeadSelectionIndex(lead); + listSelectionModel.setAnchorSelectionIndex(anchor); + } + } finally { + listSelectionModel.setValueIsAdjusting(false); + } + } else { + JFileChooser chooser = getFileChooser(); + File f; + if (isDirectorySelected()) { + f = getDirectory(); + } else { + f = chooser.getSelectedFile(); + } + int i; + if (f != null && (i = getModel().indexOf(f)) >= 0) { + int viewIndex = getRowSorter().convertRowIndexToView(i); + listSelectionModel.setSelectionInterval(viewIndex, viewIndex); + ensureIndexIsVisible(viewIndex); + } else { + clearSelection(); + } + } + } + + protected void doSelectFile(final File fileToSelect) { + int index = getModel().indexOf(fileToSelect); + // could be missed in the current directory if it changed + if (index >= 0) { + index = getRowSorter().convertRowIndexToView(index); + listSelectionModel.addSelectionInterval(index, index); + } + } + + protected void doDeselectFile(final Object fileToDeselect) { + int index = getRowSorter().convertRowIndexToView( + getModel().indexOf(fileToDeselect)); + listSelectionModel.removeSelectionInterval(index, index); + } + + protected void doSelectedFileChanged(final PropertyChangeEvent e) { + applyEdit(); + File f = (File) e.getNewValue(); + JFileChooser fc = getFileChooser(); + if (f != null + && ((fc.isFileSelectionEnabled() && !f.isDirectory()) + || (f.isDirectory() && fc.isDirectorySelectionEnabled()))) { + + setFileSelected(); + } + } + + protected void doSelectedFilesChanged(final PropertyChangeEvent e) { + applyEdit(); + File[] files = (File[]) e.getNewValue(); + JFileChooser fc = getFileChooser(); + if (files != null + && files.length > 0 + && (files.length > 1 || fc.isDirectorySelectionEnabled() || !files[0].isDirectory())) { + setFileSelected(); + } + } + + protected void doDirectoryChanged(final PropertyChangeEvent e) { + getDetailsTableModel().updateColumnInfo(); + + JFileChooser fc = getFileChooser(); + FileSystemView fsv = fc.getFileSystemView(); + + applyEdit(); + resetEditIndex(); + ensureIndexIsVisible(0); + File currentDirectory = fc.getCurrentDirectory(); + if (currentDirectory != null) { + if (!readOnly) { + getNewFolderAction().setEnabled(canWrite(currentDirectory, getFileChooser())); + } + fileChooserUIAccessor.getChangeToParentDirectoryAction().setEnabled(!fsv.isRoot(currentDirectory)); + } + if (list != null) { + list.clearSelection(); + } + } + + protected void doFilterChanged(final PropertyChangeEvent e) { + applyEdit(); + resetEditIndex(); + clearSelection(); + } + + protected void doFileSelectionModeChanged(final PropertyChangeEvent e) { + applyEdit(); + resetEditIndex(); + clearSelection(); + } + + protected void doMultiSelectionChanged(final PropertyChangeEvent e) { + if (getFileChooser().isMultiSelectionEnabled()) { + listSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } else { + listSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + clearSelection(); + getFileChooser().setSelectedFiles(null); + } + } + + /* + * Listen for filechooser property changes, such as + * the selected file changing, or the type of the dialog changing. + */ + public void propertyChange(final PropertyChangeEvent e) { + if (viewType == -1) { + setViewType(VIEWTYPE_LIST); + } + + String s = e.getPropertyName(); + if (s.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) { + doSelectedFileChanged(e); + } else if (s.equals(JFileChooser.SELECTED_FILES_CHANGED_PROPERTY)) { + doSelectedFilesChanged(e); + } else if (s.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY)) { + doDirectoryChanged(e); + } else if (s.equals(JFileChooser.FILE_FILTER_CHANGED_PROPERTY)) { + doFilterChanged(e); + } else if (s.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)) { + doFileSelectionModeChanged(e); + } else if (s.equals(JFileChooser.MULTI_SELECTION_ENABLED_CHANGED_PROPERTY)) { + doMultiSelectionChanged(e); + } else if (s.equals(JFileChooser.CANCEL_SELECTION)) { + applyEdit(); + } else if (s.equals("busy")) { + setCursor((Boolean) e.getNewValue() ? waitCursor : null); + } else if (s.equals("componentOrientation")) { + ComponentOrientation o = (ComponentOrientation) e.getNewValue(); + JFileChooser cc = (JFileChooser) e.getSource(); + if (o != e.getOldValue()) { + cc.applyComponentOrientation(o); + } + if (detailsTable != null) { + detailsTable.setComponentOrientation(o); + detailsTable.getParent().getParent().setComponentOrientation(o); + } + } + } + + public void ensureFileIsVisible(final JFileChooser fc, final File f) { + int modelIndex = getModel().indexOf(f); + if (modelIndex >= 0) { + ensureIndexIsVisible(getRowSorter().convertRowIndexToView(modelIndex)); + } + } + + protected void ensureIndexIsVisible(final int i) { + if (i >= 0) { + if (list != null) { + list.ensureIndexIsVisible(i); + } + if (detailsTable != null) { + detailsTable.scrollRectToVisible(detailsTable.getCellRect(i, COLUMN_FILENAME, true)); + } + } + } + + protected DetailsTableRowSorter getRowSorter() { + if (rowSorter == null) { + rowSorter = new DetailsTableRowSorter(); + } + return rowSorter; + } + + /* The following methods are used by the PropertyChange Listener */ + + public void rescanCurrentDirectory() { + getModel().validateFileCache(); + } + + public void clearSelection() { + if (listSelectionModel != null) { + listSelectionModel.clearSelection(); + if (listSelectionModel instanceof DefaultListSelectionModel) { + ((DefaultListSelectionModel) listSelectionModel).moveLeadSelectionIndex(-1); + listSelectionModel.setAnchorSelectionIndex(-1); + } + } + } + + public JPopupMenu getComponentPopupMenu() { + JPopupMenu popupMenu = getFileChooser().getComponentPopupMenu(); + if (popupMenu != null) { + return popupMenu; + } + + JMenu viewMenu = getViewMenu(); + if (contextMenu == null) { + contextMenu = new JPopupMenu(); + if (viewMenu != null) { + contextMenu.add(viewMenu); + if (listViewWindowsStyle) { + contextMenu.addSeparator(); + } + } + ActionMap actionMap = getActionMap(); + Action refreshAction = actionMap.get(ACTION_REFRESH); + Action newFolderAction = actionMap.get(ACTION_NEW_FOLDER); + if (refreshAction != null) { + contextMenu.add(refreshAction); + if (listViewWindowsStyle && newFolderAction != null) { + contextMenu.addSeparator(); + } + } + if (newFolderAction != null) { + contextMenu.add(newFolderAction); + } + } + if (viewMenu != null) { + viewMenu.getPopupMenu().setInvoker(viewMenu); + } + return contextMenu; + } + + protected JFileChooser getFileChooser() { + return fileChooserUIAccessor.getFileChooser(); + } + + public JMenu getViewMenu() { + if (viewMenu == null) { + viewMenu = new JMenu(viewMenuLabelText); + ButtonGroup viewButtonGroup = new ButtonGroup(); + + for (int i = 0; i < VIEWTYPE_COUNT; i++) { + JRadioButtonMenuItem mi = + new JRadioButtonMenuItem(new ViewTypeAction(i)); + viewButtonGroup.add(mi); + viewMenu.add(mi); + } + updateViewMenu(); + } + return viewMenu; + } + + protected void updateViewMenu() { + if (viewMenu != null) { + Component[] comps = viewMenu.getMenuComponents(); + for (Component comp : comps) { + if (comp instanceof JRadioButtonMenuItem) { + JRadioButtonMenuItem mi = (JRadioButtonMenuItem) comp; + if (((ViewTypeAction) mi.getAction()).viewType == viewType) { + mi.setSelected(true); + } + } + } + } + } + + protected Handler getMouseHandler() { + if (handler == null) { + handler = new Handler(); + } + return handler; + } + + /** + * Property to remember whether a directory is currently selected in the UI. + * + * @return true iff a directory is currently selected. + */ + protected boolean isDirectorySelected() { + return fileChooserUIAccessor.isDirectorySelected(); + } + + /** + * Property to remember the directory that is currently selected in the UI. + * + * @return the value of the directory property + */ + protected File getDirectory() { + return fileChooserUIAccessor.getDirectory(); + } + + protected T findChildComponent(final Container container, final Class cls) { + int n = container.getComponentCount(); + for (int i = 0; i < n; i++) { + Component comp = container.getComponent(i); + if (cls.isInstance(comp)) { + return cls.cast(comp); + } else if (comp instanceof Container) { + T c = findChildComponent((Container) comp, cls); + if (c != null) { + return c; + } + } + } + return null; + } + + // This interface is used to access methods in the FileChooserUI + // that are not public. + public interface FileChooserUIAccessor { + JFileChooser getFileChooser(); + + BasicDirectoryModel getModel(); + + JPanel createList(); + + JPanel createDetailsView(); + + boolean isDirectorySelected(); + + File getDirectory(); + + Action getApproveSelectionAction(); + + Action getChangeToParentDirectoryAction(); + + Action getNewFolderAction(); + + MouseListener createDoubleClickListener(JList list); + + ListSelectionListener createListSelectionListener(); + } + + @SuppressWarnings("serial") + // JDK-implementation class + class ViewTypeAction extends AbstractAction { + protected int viewType; + + ViewTypeAction(final int viewType) { + super(viewTypeActionNames[viewType]); + this.viewType = viewType; + + String cmd; + switch (viewType) { + case VIEWTYPE_LIST: + cmd = ACTION_VIEW_LIST; + break; + case VIEWTYPE_DETAILS: + cmd = ACTION_VIEW_DETAILS; + break; + default: + cmd = (String) getValue(Action.NAME); + } + putValue(Action.ACTION_COMMAND_KEY, cmd); + } + + public void actionPerformed(final ActionEvent e) { + setViewType(viewType); + } + } + + /** + * This model allows for sorting JList + */ + @SuppressWarnings("serial") // JDK-implementation class + protected class SortableListModel extends AbstractListModel + implements TableModelListener, RowSorterListener { + + public SortableListModel() { + getDetailsTableModel().addTableModelListener(this); + getRowSorter().addRowSorterListener(this); + } + + public void tableChanged(final TableModelEvent e) { + fireContentsChanged(this, 0, getSize()); + } + + public int getSize() { + return getModel().getSize(); + } + + public Object getElementAt(final int index) { + // JList doesn't support RowSorter so far, so we put it into the list model + return getModel().getElementAt(getRowSorter().convertRowIndexToModel(index)); + } + + public void sorterChanged(final RowSorterEvent e) { + fireContentsChanged(this, 0, getSize()); + } + } + + @SuppressWarnings("serial") + // JDK-implementation class + class DetailsTableModel extends AbstractTableModel implements ListDataListener { + JFileChooser chooser; + BasicDirectoryModel directoryModel; + + ShellFolderColumnInfo[] columns; + int[] columnMap; + + DetailsTableModel(final JFileChooser fc) { + this.chooser = fc; + directoryModel = getModel(); + directoryModel.addListDataListener(this); + + updateColumnInfo(); + } + + void updateColumnInfo() { + File dir = chooser.getCurrentDirectory(); + if (dir != null && usesShellFolder(chooser)) { + try { + dir = ShellFolder.getShellFolder(dir); + } catch (FileNotFoundException e) { + // Leave dir without changing + } + } + + ShellFolderColumnInfo[] allColumns = ShellFolder.getFolderColumns(dir); + + ArrayList visibleColumns = new ArrayList<>(); + columnMap = new int[allColumns.length]; + for (int i = 0; i < allColumns.length; i++) { + ShellFolderColumnInfo column = allColumns[i]; + if (column.isVisible()) { + columnMap[visibleColumns.size()] = i; + visibleColumns.add(column); + } + } + + columns = new ShellFolderColumnInfo[visibleColumns.size()]; + visibleColumns.toArray(columns); + columnMap = Arrays.copyOf(columnMap, columns.length); + + List sortKeys = (rowSorter == null) ? null : rowSorter.getSortKeys(); + fireTableStructureChanged(); + restoreSortKeys(sortKeys); + } + + protected void restoreSortKeys(List sortKeys) { + if (sortKeys != null) { + // check if preserved sortKeys are valid for this folder + for (int i = 0; i < sortKeys.size(); i++) { + RowSorter.SortKey sortKey = sortKeys.get(i); + if (sortKey.getColumn() >= columns.length) { + sortKeys = null; + break; + } + } + if (sortKeys != null) { + rowSorter.setSortKeys(sortKeys); + } + } + } + + public int getRowCount() { + return directoryModel.getSize(); + } + + public int getColumnCount() { + return columns.length; + } + + public Object getValueAt(final int row, final int col) { + // Note: It is very important to avoid getting info on drives, as + // this will trigger "No disk in A:" and similar dialogs. + // + // Use (f.exists() && !chooser.getFileSystemView().isFileSystemRoot(f)) to + // determine if it is safe to call methods directly on f. + return getFileColumnValue((File) directoryModel.getElementAt(row), col); + } + + public boolean isCellEditable(final int row, final int column) { + File currentDirectory = getFileChooser().getCurrentDirectory(); + return (!readOnly && column == COLUMN_FILENAME + && canWrite(currentDirectory, getFileChooser())); + } + + public void setValueAt(final Object value, final int row, final int col) { + if (col == COLUMN_FILENAME) { + final JFileChooser chooser = getFileChooser(); + File f = (File) getValueAt(row, col); + if (f != null) { + String oldDisplayName = chooser.getName(f); + String oldFileName = f.getName(); + String newDisplayName = ((String) value).trim(); + String newFileName; + + if (!newDisplayName.equals(oldDisplayName)) { + newFileName = newDisplayName; + //Check if extension is hidden from user + int i1 = oldFileName.length(); + int i2 = oldDisplayName.length(); + if (i1 > i2 && oldFileName.charAt(i2) == '.') { + newFileName = newDisplayName + oldFileName.substring(i2); + } + + // rename + FileSystemView fsv = chooser.getFileSystemView(); + final File f2 = fsv.createFileObject(f.getParentFile(), newFileName); + if (f2.exists()) { + JOptionPane.showMessageDialog(chooser, MessageFormat.format(renameErrorFileExistsText, + oldFileName), renameErrorTitleText, + JOptionPane.ERROR_MESSAGE); + } else { + if (DarkFilePaneUIBridge.this.getModel().renameFile(f, f2)) { + if (fsv.isParent(chooser.getCurrentDirectory(), f2)) { + // The setSelectedFile method produces a new setValueAt invocation while the JTable + // is editing. Postpone file selection to be sure that edit mode of the JTable + // is completed + SwingUtilities.invokeLater(() -> { + if (chooser.isMultiSelectionEnabled()) { + chooser.setSelectedFiles(new File[]{f2}); + } else { + chooser.setSelectedFile(f2); + } + }); + } else { + // Could be because of delay in updating Desktop folder + // chooser.setSelectedFile(null); + } + } else { + JOptionPane.showMessageDialog(chooser, MessageFormat.format(renameErrorText, oldFileName), + renameErrorTitleText, JOptionPane.ERROR_MESSAGE); + } + } + } + } + } + } + + protected Object getFileColumnValue(final File f, final int col) { + if (col == COLUMN_SIZE) { + return f.isDirectory() ? null : f.length(); + } + return (col == COLUMN_FILENAME) + ? f // always return the file itself for the 1st column + : ShellFolder.getFolderColumnValue(f, columnMap[col]); + } + + public void intervalAdded(final ListDataEvent e) { + int i0 = e.getIndex0(); + int i1 = e.getIndex1(); + if (i0 == i1) { + File file = (File) getModel().getElementAt(i0); + if (file.equals(newFolderFile)) { + new DelayedSelectionUpdater(file); + newFolderFile = null; + } + } + + fireTableRowsInserted(e.getIndex0(), e.getIndex1()); + } + + public void intervalRemoved(final ListDataEvent e) { + fireTableRowsDeleted(e.getIndex0(), e.getIndex1()); + } + + public void contentsChanged(final ListDataEvent e) { + // Update the selection after the model has been updated + new DelayedSelectionUpdater(); + fireTableDataChanged(); + } + + public ShellFolderColumnInfo[] getColumns() { + return columns; + } + } + + protected class DetailsTableRowSorter extends TableRowSorter { + public DetailsTableRowSorter() { + SorterModelWrapper modelWrapper = new SorterModelWrapper(); + setModelWrapper(modelWrapper); + modelWrapper.getModel().addTableModelListener(e -> modelStructureChanged()); + } + + public void updateComparators(final ShellFolderColumnInfo[] columns) { + for (int i = 0; i < columns.length; i++) { + Comparator c = columns[i].getComparator(); + if (c != null) { + c = new DirectoriesFirstComparatorWrapper(i, c); + } + setComparator(i, c); + } + } + + @Override + public void sort() { + ShellFolder.invoke((Callable) () -> { + DetailsTableRowSorter.super.sort(); + return null; + }); + } + + public void modelStructureChanged() { + super.modelStructureChanged(); + updateComparators(detailsTableModel.getColumns()); + } + + protected class SorterModelWrapper extends ModelWrapper { + public TableModel getModel() { + return getDetailsTableModel(); + } + + public int getColumnCount() { + return getDetailsTableModel().getColumnCount(); + } + + public int getRowCount() { + return getDetailsTableModel().getRowCount(); + } + + public Object getValueAt(final int row, final int column) { + return DarkFilePaneUIBridge.this.getModel().getElementAt(row); + } + + public Integer getIdentifier(final int row) { + return row; + } + } + } + + /** + * This class sorts directories before files, comparing directory to + * directory and file to file using the wrapped comparator. + */ + protected class DirectoriesFirstComparatorWrapper implements Comparator { + protected Comparator comparator; + protected int column; + + @SuppressWarnings("unchecked") + public DirectoriesFirstComparatorWrapper(final int column, final Comparator comparator) { + this.column = column; + this.comparator = (Comparator) comparator; + } + + public int compare(final File f1, final File f2) { + if (f1 != null && f2 != null) { + boolean traversable1 = getFileChooser().isTraversable(f1); + boolean traversable2 = getFileChooser().isTraversable(f2); + // directories go first + if (traversable1 && !traversable2) { + return -1; + } + if (!traversable1 && traversable2) { + return 1; + } + } + if (detailsTableModel.getColumns()[column].isCompareByColumn()) { + return comparator.compare( + getDetailsTableModel().getFileColumnValue(f1, column), + getDetailsTableModel().getFileColumnValue(f2, column) + ); + } + // For this column we need to pass the file itself (not a + // column value) to the comparator + return comparator.compare(f1, f2); + } + } + + protected class DetailsTableCellEditor extends DarkTableCellEditor { + public DetailsTableCellEditor(final JTextField tf) { + super(tf); + tf.addFocusListener(editorFocusListener); + } + + public Component getTableCellEditorComponent(final JTable table, final Object value, + final boolean isSelected, final int row, final int column) { + Component comp = super.getTableCellEditorComponent(table, value, + isSelected, row, column); + if (value instanceof File) { + delegate.setValue(getFileChooser().getName((File) value)); + } + if (editorComponent instanceof JTextField) { + SwingUtilities.invokeLater(() -> { + ((JTextField) editorComponent).selectAll(); + editorComponent.requestFocus(); + }); + } + + return comp; + } + } + + @SuppressWarnings("serial") + // JDK-implementation class + class DetailsTableCellRenderer extends DarkTableCellRenderer { + JFileChooser chooser; + DateFormat df; + + DetailsTableCellRenderer(@NotNull final JFileChooser chooser) { + this.chooser = chooser; + df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, + chooser.getLocale()); + } + + public Component getTableCellRendererComponent(final JTable table, final Object value, + boolean isSelected, final boolean hasFocus, + final int row, final int column) { + + if ((table.convertColumnIndexToModel(column) != COLUMN_FILENAME || + (listViewWindowsStyle && !table.isFocusOwner())) && !fullRowSelection) { + isSelected = false; + } + + super.getTableCellRendererComponent(table, value, isSelected, + hasFocus, row, column); + + setIcon(null); + int modelColumn = table.convertColumnIndexToModel(column); + ShellFolderColumnInfo columnInfo = detailsTableModel.getColumns()[modelColumn]; + + Integer alignment = columnInfo.getAlignment(); + if (alignment == null) { + alignment = (value instanceof Number) + ? SwingConstants.RIGHT + : SwingConstants.LEADING; + } + + setHorizontalAlignment(alignment); + + // formatting cell text + // TODO: it's rather a temporary trick, to be revised + String text; + if (value == null) { + text = ""; + } else if (value instanceof File) { + File file = (File) value; + text = chooser.getName(file); + Icon icon = chooser.getIcon(file); + setIcon(icon); + + } else if (value instanceof Long) { + long len = ((Long) value) / 1024L; + if (listViewWindowsStyle) { + text = MessageFormat.format(kiloByteString, len + 1); + } else if (len < 1024L) { + text = MessageFormat.format(kiloByteString, (len == 0L) ? 1L : len); + } else { + len /= 1024L; + if (len < 1024L) { + text = MessageFormat.format(megaByteString, len); + } else { + len /= 1024L; + text = MessageFormat.format(gigaByteString, len); + } + } + + } else if (value instanceof Date) { + text = df.format((Date) value); + + } else { + text = value.toString(); + } + + setText(text); + + return this; + } + } + + protected class AlignableTableHeaderRenderer implements TableCellRenderer { + TableCellRenderer wrappedRenderer; + + public AlignableTableHeaderRenderer(final TableCellRenderer wrappedRenderer) { + this.wrappedRenderer = wrappedRenderer; + } + + public Component getTableCellRendererComponent( + final JTable table, final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + + Component c = wrappedRenderer.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column); + + int modelColumn = table.convertColumnIndexToModel(column); + ShellFolderColumnInfo columnInfo = detailsTableModel.getColumns()[modelColumn]; + + Integer alignment = columnInfo.getAlignment(); + if (alignment == null) { + alignment = SwingConstants.CENTER; + } + if (c instanceof JLabel) { + ((JLabel) c).setHorizontalAlignment(alignment); + } + + return c; + } + } + + protected class DelayedSelectionUpdater implements Runnable { + File editFile; + + DelayedSelectionUpdater() { + this(null); + } + + DelayedSelectionUpdater(final File editFile) { + this.editFile = editFile; + if (isShowing()) { + SwingUtilities.invokeLater(this); + } + } + + public void run() { + setFileSelected(); + if (editFile != null) { + editFileName(getRowSorter().convertRowIndexToView( + getModel().indexOf(editFile))); + editFile = null; + } + } + } + + class EditActionListener implements ActionListener { + public void actionPerformed(final ActionEvent e) { + applyEdit(); + } + } + + @SuppressWarnings("serial") // JDK-implementation class + protected class FileRenderer extends DarkListCellRenderer { + + public Component getListCellRendererComponent(@NotNull final JList list, final Object value, + final int index, boolean isSelected, + final boolean cellHasFocus) { + + if (listViewWindowsStyle && !list.isFocusOwner()) { + isSelected = false; + } + + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + File file = (File) value; + String fileName = getFileChooser().getName(file); + setText(fileName); + setFont(list.getFont()); + + Icon icon = getFileChooser().getIcon(file); + if (icon != null) { + setIcon(icon); + } else { + if (getFileChooser().getFileSystemView().isTraversable(file)) { + setText(fileName + File.separator); + } + } + + return this; + } + } + + protected class Handler implements MouseListener { + protected MouseListener doubleClickListener; + + @SuppressWarnings("deprecation") + public void mouseClicked(MouseEvent evt) { + JComponent source = (JComponent) evt.getSource(); + + int index; + if (source instanceof JList) { + index = SwingUtilities2.loc2IndexFileList(list, evt.getPoint()); + } else if (source instanceof JTable) { + JTable table = (JTable) source; + Point p = evt.getPoint(); + index = table.rowAtPoint(p); + + boolean pointOutsidePrefSize = + SwingUtilities2.pointOutsidePrefSize( + table, index, table.columnAtPoint(p), p); + + if (pointOutsidePrefSize && !fullRowSelection) { + return; + } + + // Translate point from table to list + if (index >= 0 && list != null && + listSelectionModel.isSelectedIndex(index)) { + + // Make a new event with the list as source, placing the + // click in the corresponding list cell. + Rectangle r = list.getCellBounds(index, index); + MouseEvent newEvent = new MouseEvent(list, evt.getID(), + evt.getWhen(), evt.getModifiers(), + r.x + 1, r.y + r.height / 2, + evt.getXOnScreen(), + evt.getYOnScreen(), + evt.getClickCount(), evt.isPopupTrigger(), + evt.getButton()); + AWTAccessor.MouseEventAccessor meAccessor = AWTAccessor.getMouseEventAccessor(); + meAccessor.setCausedByTouchEvent(newEvent, + meAccessor.isCausedByTouchEvent(evt)); + evt = newEvent; + } + } else { + return; + } + + if (index >= 0 && SwingUtilities.isLeftMouseButton(evt)) { + JFileChooser fc = getFileChooser(); + + // For single click, we handle editing file name + if (evt.getClickCount() == 1 && source instanceof JList) { + if ((!fc.isMultiSelectionEnabled() || fc.getSelectedFiles().length <= 1) + && listSelectionModel.isSelectedIndex(index) + && getEditIndex() == index && editFile == null) { + + editFileName(index); + } else { + setEditIndex(index); + } + } else if (evt.getClickCount() == 2) { + // on double click (open or drill down one directory) be + // sure to clear the edit index + resetEditIndex(); + } + } + + // Forward event to Basic + if (getDoubleClickListener() != null) { + getDoubleClickListener().mouseClicked(evt); + } + } + + public void mousePressed(final MouseEvent evt) { + if (evt.getSource() instanceof JList) { + // Forward event to Basic + if (getDoubleClickListener() != null) { + getDoubleClickListener().mousePressed(evt); + } + } + } + + public void mouseReleased(final MouseEvent evt) { + if (evt.getSource() instanceof JList) { + // Forward event to Basic + if (getDoubleClickListener() != null) { + getDoubleClickListener().mouseReleased(evt); + } + } + } + + public void mouseEntered(final MouseEvent evt) { + JComponent source = (JComponent) evt.getSource(); + if (source instanceof JTable) { + JTable table = (JTable) evt.getSource(); + + TransferHandler th1 = getFileChooser().getTransferHandler(); + TransferHandler th2 = table.getTransferHandler(); + if (th1 != th2) { + table.setTransferHandler(th1); + } + + boolean dragEnabled = getFileChooser().getDragEnabled(); + if (dragEnabled != table.getDragEnabled()) { + table.setDragEnabled(dragEnabled); + } + } else if (source instanceof JList) { + // Forward event to Basic + if (getDoubleClickListener() != null) { + getDoubleClickListener().mouseEntered(evt); + } + } + } + + public void mouseExited(final MouseEvent evt) { + if (evt.getSource() instanceof JList) { + // Forward event to Basic + if (getDoubleClickListener() != null) { + getDoubleClickListener().mouseExited(evt); + } + } + } + + protected MouseListener getDoubleClickListener() { + + // Lazy creation of Basic's listener + if (doubleClickListener == null && list != null) { + doubleClickListener = + fileChooserUIAccessor.createDoubleClickListener(list); + } + return doubleClickListener; + } + } +} diff --git a/src/main/java/com/weis/darklaf/ui/list/DarkListCellFocusBorder.java b/src/main/java/com/weis/darklaf/ui/list/DarkListCellFocusBorder.java new file mode 100644 index 00000000..2b0e22bf --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/list/DarkListCellFocusBorder.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.list; + +import com.weis.darklaf.ui.cell.DarkCellBorder; +import com.weis.darklaf.util.DarkUIUtil; + +import javax.swing.*; +import java.awt.*; + +public class DarkListCellFocusBorder extends DarkCellBorder { + + @Override + public void paintBorder(final Component c, final Graphics g, final int x, final int y, + final int width, final int height) { + super.paintBorder(c, g, x, y, width, height); + g.setColor(UIManager.getColor("List.focusBorderColor")); + DarkUIUtil.drawRect(g, 0, 0, width, height, 1); + } +} \ No newline at end of file diff --git a/src/main/java/com/weis/darklaf/ui/list/DarkListCellRenderer.java b/src/main/java/com/weis/darklaf/ui/list/DarkListCellRenderer.java new file mode 100644 index 00000000..3d7dcc66 --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/list/DarkListCellRenderer.java @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.list; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; + +public class DarkListCellRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(@NotNull final JList list, final Object value, + final int index, final boolean isSelected, + final boolean cellHasFocus) { + if (Boolean.TRUE.equals(list.getClientProperty("JList.isEditing"))) { + if (list.getSelectionModel().getLeadSelectionIndex() == index) { + return super.getListCellRendererComponent(list, value, index, false, false); + } + } + return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } +} diff --git a/src/main/java/com/weis/darklaf/ui/list/DarkListUI.java b/src/main/java/com/weis/darklaf/ui/list/DarkListUI.java index f2d6bf84..08adcd57 100644 --- a/src/main/java/com/weis/darklaf/ui/list/DarkListUI.java +++ b/src/main/java/com/weis/darklaf/ui/list/DarkListUI.java @@ -1,20 +1,119 @@ package com.weis.darklaf.ui.list; +import com.weis.darklaf.util.DarkUIUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.plaf.ComponentUI; -import javax.swing.plaf.basic.BasicListUI; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; /** * @author Jannis Weis */ -public class DarkListUI extends BasicListUI { +public class DarkListUI extends DarkListUIBridge { @NotNull @Contract("_ -> new") public static ComponentUI createUI(final JComponent list) { return new DarkListUI(); } + + protected void paintCell(final Graphics g, final int row, final Rectangle rowBounds, + final ListCellRenderer cellRenderer, final ListModel dataModel, + final ListSelectionModel selModel, final int leadIndex) { + Object value = dataModel.getElementAt(row); + boolean cellHasFocus = list.hasFocus() && (row == leadIndex); + boolean isSelected = selModel.isSelectedIndex(row); + + Component rendererComponent = + cellRenderer.getListCellRendererComponent(list, value, row, isSelected, cellHasFocus); + + int cx = rowBounds.x; + int cy = rowBounds.y; + int cw = rowBounds.width; + int ch = rowBounds.height; + + if (Boolean.TRUE.equals(list.getClientProperty("JList.shrinkWrap"))) { + // Shrink renderer to preferred size. This is mostly used on Windows + // where selection is only shown around the file name, instead of + // across the whole list cell. + int w = Math.min(cw, rendererComponent.getPreferredSize().width + 4); + if (!list.getComponentOrientation().isLeftToRight()) { + cx += (cw - w); + } + cw = w; + } + + rendererPane.paintComponent(g, rendererComponent, list, cx, cy, cw, ch, true); + } + + @Override + protected void installListeners() { + super.installListeners(); + list.addMouseListener(new MouseAdapter() { + + }); + } + + @Override + protected Handler getHandler() { + if (handler == null) { + handler = new DarkHandler(); + } + return handler; + } + + + protected class DarkHandler extends Handler { + + @Override + protected void adjustSelection(final MouseEvent e) { + int row = list.locationToIndex(e.getPoint()); + if (row < 0) { + // If shift is down in multi-select, we should do nothing. + // For single select or non-shift-click, clear the selection + if (isFileList && !Boolean.TRUE.equals(list.getClientProperty("JList.fullRowSelection")) + && e.getID() == MouseEvent.MOUSE_PRESSED && + (!e.isShiftDown() || list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)) { + list.clearSelection(); + } + } else { + int anchorIndex = adjustIndex(list.getAnchorSelectionIndex(), list); + boolean anchorSelected; + if (anchorIndex == -1) { + anchorIndex = 0; + anchorSelected = false; + } else { + anchorSelected = list.isSelectedIndex(anchorIndex); + } + + if (DarkUIUtil.isMenuShortcutKeyDown(e)) { + if (e.isShiftDown()) { + if (anchorSelected) { + list.addSelectionInterval(anchorIndex, row); + } else { + list.removeSelectionInterval(anchorIndex, row); + if (isFileList) { + list.addSelectionInterval(row, row); + list.getSelectionModel().setAnchorSelectionIndex(anchorIndex); + } + } + } else if (list.isSelectedIndex(row)) { + list.removeSelectionInterval(row, row); + } else { + list.addSelectionInterval(row, row); + } + } else if (e.isShiftDown()) { + list.setSelectionInterval(anchorIndex, row); + } else { + list.setSelectionInterval(row, row); + } + } + } + } + + } diff --git a/src/main/java/com/weis/darklaf/ui/list/DarkListUIBridge.java b/src/main/java/com/weis/darklaf/ui/list/DarkListUIBridge.java new file mode 100644 index 00000000..90c73d7e --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/list/DarkListUIBridge.java @@ -0,0 +1,2257 @@ +package com.weis.darklaf.ui.list; + +import com.weis.darklaf.util.DarkUIUtil; +import com.weis.darklaf.util.LazyActionMap; +import org.jdesktop.swingx.plaf.basic.core.BasicTransferable; +import org.jdesktop.swingx.plaf.basic.core.DragRecognitionSupport; +import sun.swing.DefaultLookup; +import sun.swing.SwingUtilities2; + +import javax.swing.*; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.MouseInputListener; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.UIResource; +import javax.swing.plaf.basic.BasicListUI; +import javax.swing.text.Position; +import java.awt.*; +import java.awt.datatransfer.Transferable; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; + +/** + * An extensible implementation of {@code ListUI}. + *

+ * {@code BasicListUI} instances cannot be shared between multiple + * lists. + * + * @author Hans Muller + * @author Philip Milne + * @author Shannon Hickey (drag and drop) + */ + +public class DarkListUIBridge extends BasicListUI { + + protected static final StringBuilder BASELINE_COMPONENT_KEY = + new StringBuilder("List.baselineComponent"); + /** + * The bit relates to model changed property. + */ + protected static final int modelChanged = 1 << 0; + /** + * The bit relates to selection model changed property. + */ + protected static final int selectionModelChanged = 1 << 1; + + // Listeners that this UI attaches to the JList + /** + * The bit relates to font changed property. + */ + protected static final int fontChanged = 1 << 2; + /** + * The bit relates to fixed cell width changed property. + */ + protected static final int fixedCellWidthChanged = 1 << 3; + /** + * The bit relates to fixed cell height changed property. + */ + protected static final int fixedCellHeightChanged = 1 << 4; + /** + * The bit relates to prototype cell value changed property. + */ + protected static final int prototypeCellValueChanged = 1 << 5; + /** + * The bit relates to cell renderer changed property. + */ + protected static final int cellRendererChanged = 1 << 6; + protected static final int layoutOrientationChanged = 1 << 7; + protected static final int heightChanged = 1 << 8; + protected static final int widthChanged = 1 << 9; + protected static final int componentOrientationChanged = 1 << 10; + protected static final int DROP_LINE_THICKNESS = 2; + /** + * Used by IncrementLeadSelectionAction. Indicates the action should + * change the lead, and not select it. + */ + protected static final int CHANGE_LEAD = 0; + /** + * Used by IncrementLeadSelectionAction. Indicates the action should + * change the selection and lead. + */ + protected static final int CHANGE_SELECTION = 1; + /** + * Used by IncrementLeadSelectionAction. Indicates the action should + * extend the selection from the anchor to the next index. + */ + protected static final int EXTEND_SELECTION = 2; + + // Following ivars are used if the list is laying out horizontally + protected static final TransferHandler defaultTransferHandler = new ListTransferHandler(); + /** + * The instance of {@code JList}. + */ + protected JList list = null; + /** + * The instance of {@code CellRendererPane}. + */ + protected CellRendererPane rendererPane; + /** + * {@code FocusListener} that attached to {@code JList}. + */ + protected FocusListener focusListener; + /** + * {@code MouseInputListener} that attached to {@code JList}. + */ + protected MouseInputListener mouseInputListener; + /** + * {@code ListSelectionListener} that attached to {@code JList}. + */ + protected ListSelectionListener listSelectionListener; + + /* The bits below define JList property changes that affect layout. + * When one of these properties changes we set a bit in + * updateLayoutStateNeeded. The change is dealt with lazily, see + * maybeUpdateLayoutState. Changes to the JLists model, e.g. the + * models length changed, are handled similarly, see DataListener. + */ + /** + * {@code ListDataListener} that attached to {@code JList}. + */ + protected ListDataListener listDataListener; + /** + * {@code PropertyChangeListener} that attached to {@code JList}. + */ + protected PropertyChangeListener propertyChangeListener; + protected Handler handler; + /** + * The array of cells' height + */ + protected int[] cellHeights = null; + /** + * The height of cell. + */ + protected int cellHeight = -1; + /** + * The width of cell. + */ + protected int cellWidth = -1; + /** + * The value represents changes to {@code JList} model. + */ + protected int updateLayoutStateNeeded = modelChanged; + /** + * Height of the list. When asked to paint, if the current size of + * the list differs, this will update the layout state. + */ + protected int listHeight; + /** + * Width of the list. When asked to paint, if the current size of + * the list differs, this will update the layout state. + */ + protected int listWidth; + /** + * The layout orientation of the list. + */ + protected int layoutOrientation; + /** + * Number of columns to create. + */ + protected int columnCount; + /** + * Preferred height to make the list, this is only used if the + * the list is layed out horizontally. + */ + protected int preferredHeight; + /** + * Number of rows per column. This is only used if the row height is + * fixed. + */ + protected int rowsPerColumn; + /** + * The time factor to treate the series of typed alphanumeric key + * as prefix for first letter navigation. + */ + protected long timeFactor = 1000L; + /** + * Local cache of JList's client property "List.isFileList" + */ + protected boolean isFileList = false; + /** + * Local cache of JList's component orientation property + */ + protected boolean isLeftToRight = true; + + /** + * Returns a new instance of {@code BasicListUI}. + * {@code BasicListUI} delegates are allocated one per {@code JList}. + * + * @param list a component + * @return a new {@code ListUI} implementation for the Windows look and feel. + */ + public static ComponentUI createUI(final JComponent list) { + return new BasicListUI(); + } + + protected static int adjustIndex(final int index, final JList list) { + return index < list.getModel().getSize() ? index : -1; + } + + /** + * Paint one List cell: compute the relevant state, get the "rubber stamp" + * cell renderer component, and then use the {@code CellRendererPane} to paint it. + * Subclasses may want to override this method rather than {@code paint()}. + * + * @param g an instance of {@code Graphics} + * @param row a row + * @param rowBounds a bounding rectangle to render to + * @param cellRenderer a list of {@code ListCellRenderer} + * @param dataModel a list model + * @param selModel a selection model + * @param leadIndex a lead index + * @see #paint + */ + protected void paintCell( + final Graphics g, + final int row, + final Rectangle rowBounds, + final ListCellRenderer cellRenderer, + final ListModel dataModel, + final ListSelectionModel selModel, + final int leadIndex) { + Object value = dataModel.getElementAt(row); + boolean cellHasFocus = list.hasFocus() && (row == leadIndex); + boolean isSelected = selModel.isSelectedIndex(row); + + Component rendererComponent = + cellRenderer.getListCellRendererComponent(list, value, row, isSelected, cellHasFocus); + + int cx = rowBounds.x; + int cy = rowBounds.y; + int cw = rowBounds.width; + int ch = rowBounds.height; + + if (isFileList) { + // Shrink renderer to preferred size. This is mostly used on Windows + // where selection is only shown around the file name, instead of + // across the whole list cell. + int w = Math.min(cw, rendererComponent.getPreferredSize().width + 4); + if (!isLeftToRight) { + cx += (cw - w); + } + cw = w; + } + + rendererPane.paintComponent(g, rendererComponent, list, cx, cy, cw, ch, true); + } + + /** + * Paint the rows that intersect the Graphics objects clipRect. This + * method calls paintCell as necessary. Subclasses + * may want to override these methods. + * + * @see #paintCell + */ + public void paint(final Graphics g, final JComponent c) { + Shape clip = g.getClip(); + paintImpl(g, c); + g.setClip(clip); + + paintDropLine(g); + } + + /** + * Returns the baseline. + * + * @throws NullPointerException {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + * @see javax.swing.JComponent#getBaseline(int, int) + * @since 1.6 + */ + public int getBaseline(final JComponent c, final int width, final int height) { + super.getBaseline(c, width, height); + int rowHeight = list.getFixedCellHeight(); + UIDefaults lafDefaults = UIManager.getLookAndFeelDefaults(); + Component renderer = (Component) lafDefaults.get( + BASELINE_COMPONENT_KEY); + if (renderer == null) { + @SuppressWarnings("unchecked") + ListCellRenderer lcr = (ListCellRenderer) UIManager.get( + "List.cellRenderer"); + + // fix for 6711072 some LAFs like Nimbus do not provide this + // UIManager key and we should not through a NPE here because of it + if (lcr == null) { + lcr = new DefaultListCellRenderer(); + } + renderer = lcr.getListCellRendererComponent( + list, "a", -1, false, false); + lafDefaults.put(BASELINE_COMPONENT_KEY, renderer); + } + renderer.setFont(list.getFont()); + // JList actually has much more complex behavior here. + // If rowHeight != -1 the rowHeight is either the max of all cell + // heights (layout orientation != VERTICAL), or is variable depending + // upon the cell. We assume a default size. + // We could theoretically query the real renderer, but that would + // not work for an empty model and the results may vary with + // the content. + if (rowHeight == -1) { + rowHeight = renderer.getPreferredSize().height; + } + return renderer.getBaseline(Integer.MAX_VALUE, rowHeight) + + list.getInsets().top; + } + + /** + * Returns an enum indicating how the baseline of the component + * changes as the size changes. + * + * @throws NullPointerException {@inheritDoc} + * @see javax.swing.JComponent#getBaseline(int, int) + * @since 1.6 + */ + public Component.BaselineResizeBehavior getBaselineResizeBehavior( + final JComponent c) { + super.getBaselineResizeBehavior(c); + return Component.BaselineResizeBehavior.CONSTANT_ASCENT; + } + + /** + * The preferredSize of the list depends upon the layout orientation. + * + * + * + * + * + * + * + * + * + * + * + *
Describes the preferred size for each layout orientation + *
Layout Orientation + * Preferred Size + *
JList.VERTICAL + * The preferredSize of the list is total height of the rows + * and the maximum width of the cells. If JList.fixedCellHeight + * is specified then the total height of the rows is just + * (cellVerticalMargins + fixedCellHeight) * model.getSize() where + * rowVerticalMargins is the space we allocate for drawing + * the yellow focus outline. Similarly if fixedCellWidth is + * specified then we just use that. + *
JList.VERTICAL_WRAP + * If the visible row count is greater than zero, the preferredHeight + * is the maximum cell height * visibleRowCount. If the visible row + * count is <= 0, the preferred height is either the current height + * of the list, or the maximum cell height, whichever is + * bigger. The preferred width is than the maximum cell width * + * number of columns needed. Where the number of columns needs is + * list.height / max cell height. Max cell height is either the fixed + * cell height, or is determined by iterating through all the cells + * to find the maximum height from the ListCellRenderer. + *
JList.HORIZONTAL_WRAP + * If the visible row count is greater than zero, the preferredHeight + * is the maximum cell height * adjustedRowCount. Where + * visibleRowCount is used to determine the number of columns. + * Because this lays out horizontally the number of rows is + * then determined from the column count. For example, lets say + * you have a model with 10 items and the visible row count is 8. + * The number of columns needed to display this is 2, but you no + * longer need 8 rows to display this, you only need 5, thus + * the adjustedRowCount is 5. + *

+ * If the visible row count is <= 0, the preferred height is dictated + * by the number of columns, which will be as many as can fit in the + * width of the {@code JList} (width / max cell width), with at least + * one column. The preferred height then becomes the model size / number + * of columns * maximum cell height. Max cell height is either the fixed + * cell height, or is determined by iterating through all the cells to + * find the maximum height from the ListCellRenderer. + *

+ *

+ * The above specifies the raw preferred width and height. The resulting + * preferred width is the above width + insets.left + insets.right and + * the resulting preferred height is the above height + insets.top + + * insets.bottom. Where the Insets are determined from + * list.getInsets(). + * + * @param c The JList component. + * @return The total size of the list. + */ + public Dimension getPreferredSize(final JComponent c) { + maybeUpdateLayoutState(); + + int lastRow = list.getModel().getSize() - 1; + if (lastRow < 0) { + return new Dimension(0, 0); + } + + Insets insets = list.getInsets(); + int width = cellWidth * columnCount + insets.left + insets.right; + int height; + + if (layoutOrientation != JList.VERTICAL) { + height = preferredHeight; + } else { + Rectangle bounds = getCellBounds(list, lastRow); + + if (bounds != null) { + height = bounds.y + bounds.height + insets.bottom; + } else { + height = 0; + } + } + return new Dimension(width, height); + } + + /** + * Selected the previous row and force it to be visible. + * + * @see JList#ensureIndexIsVisible + */ + protected void selectPreviousIndex() { + int s = list.getSelectedIndex(); + if (s > 0) { + s -= 1; + list.setSelectedIndex(s); + list.ensureIndexIsVisible(s); + } + } + + /** + * Selected the previous row and force it to be visible. + * + * @see JList#ensureIndexIsVisible + */ + protected void selectNextIndex() { + int s = list.getSelectedIndex(); + if ((s + 1) < list.getModel().getSize()) { + s += 1; + list.setSelectedIndex(s); + list.ensureIndexIsVisible(s); + } + } + + /** + * Registers the keyboard bindings on the JList that the + * BasicListUI is associated with. This method is called at + * installUI() time. + * + * @see #installUI + */ + protected void installKeyboardActions() { + InputMap inputMap = getInputMap(JComponent.WHEN_FOCUSED); + SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, inputMap); + LazyActionMap.installLazyActionMap(list, BasicListUI.class, "List.actionMap"); + } + + /** + * Unregisters keyboard actions installed from + * installKeyboardActions. + * This method is called at uninstallUI() time - subclassess should + * ensure that all of the keyboard actions registered at installUI + * time are removed here. + * + * @see #installUI + */ + protected void uninstallKeyboardActions() { + SwingUtilities.replaceUIActionMap(list, null); + SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, null); + } + + /** + * Creates and installs the listeners for the JList, its model, and its + * selectionModel. This method is called at installUI() time. + * + * @see #installUI + * @see #uninstallListeners + */ + protected void installListeners() { + TransferHandler th = list.getTransferHandler(); + if (th == null || th instanceof UIResource) { + list.setTransferHandler(defaultTransferHandler); + // default TransferHandler doesn't support drop + // so we don't want drop handling + if (list.getDropTarget() instanceof UIResource) { + list.setDropTarget(null); + } + } + + focusListener = createFocusListener(); + mouseInputListener = createMouseInputListener(); + propertyChangeListener = createPropertyChangeListener(); + listSelectionListener = createListSelectionListener(); + listDataListener = createListDataListener(); + + list.addFocusListener(focusListener); + list.addMouseListener(mouseInputListener); + list.addMouseMotionListener(mouseInputListener); + list.addPropertyChangeListener(propertyChangeListener); + list.addKeyListener(getHandler()); + + ListModel model = list.getModel(); + if (model != null) { + model.addListDataListener(listDataListener); + } + + ListSelectionModel selectionModel = list.getSelectionModel(); + if (selectionModel != null) { + selectionModel.addListSelectionListener(listSelectionListener); + } + } + + /** + * Removes the listeners from the JList, its model, and its + * selectionModel. All of the listener fields, are reset to + * null here. This method is called at uninstallUI() time, + * it should be kept in sync with installListeners. + * + * @see #uninstallUI + * @see #installListeners + */ + protected void uninstallListeners() { + list.removeFocusListener(focusListener); + list.removeMouseListener(mouseInputListener); + list.removeMouseMotionListener(mouseInputListener); + list.removePropertyChangeListener(propertyChangeListener); + list.removeKeyListener(getHandler()); + + ListModel model = list.getModel(); + if (model != null) { + model.removeListDataListener(listDataListener); + } + + ListSelectionModel selectionModel = list.getSelectionModel(); + if (selectionModel != null) { + selectionModel.removeListSelectionListener(listSelectionListener); + } + + focusListener = null; + mouseInputListener = null; + listSelectionListener = null; + listDataListener = null; + propertyChangeListener = null; + handler = null; + } + + /** + * Initializes list properties such as font, foreground, and background, + * and adds the CellRendererPane. The font, foreground, and background + * properties are only set if their current value is either null + * or a UIResource, other properties are set if the current + * value is null. + * + * @see #uninstallDefaults + * @see #installUI + * @see CellRendererPane + */ + protected void installDefaults() { + list.setLayout(null); + + LookAndFeel.installBorder(list, "List.border"); + + LookAndFeel.installColorsAndFont(list, "List.background", "List.foreground", "List.font"); + + LookAndFeel.installProperty(list, "opaque", Boolean.TRUE); + + if (list.getCellRenderer() == null) { + @SuppressWarnings("unchecked") + ListCellRenderer tmp = (ListCellRenderer) (UIManager.get("List.cellRenderer")); + list.setCellRenderer(tmp); + } + + Color sbg = list.getSelectionBackground(); + if (sbg == null || sbg instanceof UIResource) { + list.setSelectionBackground(UIManager.getColor("List.selectionBackground")); + } + + Color sfg = list.getSelectionForeground(); + if (sfg == null || sfg instanceof UIResource) { + list.setSelectionForeground(UIManager.getColor("List.selectionForeground")); + } + + Long l = (Long) UIManager.get("List.timeFactor"); + timeFactor = (l != null) ? l : 1000L; + + updateIsFileList(); + } + + /** + * Sets the list properties that have not been explicitly overridden to + * {@code null}. A property is considered overridden if its current value + * is not a {@code UIResource}. + * + * @see #installDefaults + * @see #uninstallUI + * @see CellRendererPane + */ + protected void uninstallDefaults() { + LookAndFeel.uninstallBorder(list); + if (list.getFont() instanceof UIResource) { + list.setFont(null); + } + if (list.getForeground() instanceof UIResource) { + list.setForeground(null); + } + if (list.getBackground() instanceof UIResource) { + list.setBackground(null); + } + if (list.getSelectionBackground() instanceof UIResource) { + list.setSelectionBackground(null); + } + if (list.getSelectionForeground() instanceof UIResource) { + list.setSelectionForeground(null); + } + if (list.getCellRenderer() instanceof UIResource) { + list.setCellRenderer(null); + } + if (list.getTransferHandler() instanceof UIResource) { + list.setTransferHandler(null); + } + } + + /** + * Initializes this.list by calling installDefaults(), + * installListeners(), and installKeyboardActions() + * in order. + * + * @see #installDefaults + * @see #installListeners + * @see #installKeyboardActions + */ + public void installUI(final JComponent c) { + @SuppressWarnings("unchecked") + JList tmp = (JList) c; + list = tmp; + + layoutOrientation = list.getLayoutOrientation(); + + rendererPane = new CellRendererPane(); + list.add(rendererPane); + + columnCount = 1; + + updateLayoutStateNeeded = modelChanged; + isLeftToRight = list.getComponentOrientation().isLeftToRight(); + + installDefaults(); + installListeners(); + installKeyboardActions(); + } + + protected void updateIsFileList() { + boolean b = Boolean.TRUE.equals(list.getClientProperty("List.isFileList")); + if (b != isFileList) { + isFileList = b; + Font oldFont = list.getFont(); + if (oldFont == null || oldFont instanceof UIResource) { + Font newFont = UIManager.getFont(b ? "FileChooser.listFont" : "List.font"); + if (newFont != null && newFont != oldFont) { + list.setFont(newFont); + } + } + } + } + + protected Handler getHandler() { + if (handler == null) { + handler = new Handler(); + } + return handler; + } + + InputMap getInputMap(final int condition) { + if (condition == JComponent.WHEN_FOCUSED) { + InputMap keyMap = (InputMap) DefaultLookup.get( + list, this, "List.focusInputMap"); + InputMap rtlKeyMap; + + if (isLeftToRight || + ((rtlKeyMap = (InputMap) DefaultLookup.get(list, this, + "List.focusInputMap.RightToLeft")) == null)) { + return keyMap; + } else { + rtlKeyMap.setParent(keyMap); + return rtlKeyMap; + } + } + return null; + } + + /** + * Uninitializes this.list by calling uninstallListeners(), + * uninstallKeyboardActions(), and uninstallDefaults() + * in order. Sets this.list to null. + * + * @see #uninstallListeners + * @see #uninstallKeyboardActions + * @see #uninstallDefaults + */ + public void uninstallUI(final JComponent c) { + uninstallListeners(); + uninstallDefaults(); + uninstallKeyboardActions(); + + cellWidth = cellHeight = -1; + cellHeights = null; + + listWidth = listHeight = -1; + + list.remove(rendererPane); + rendererPane = null; + list = null; + } + + /** + * {@inheritDoc} + * + * @throws NullPointerException {@inheritDoc} + */ + public int locationToIndex(final JList list, final Point location) { + maybeUpdateLayoutState(); + return convertLocationToModel(location.x, location.y); + } + + /** + * {@inheritDoc} + */ + public Point indexToLocation(final JList list, final int index) { + maybeUpdateLayoutState(); + Rectangle rect = getCellBounds(list, index, index); + + if (rect != null) { + return new Point(rect.x, rect.y); + } + return null; + } + + /** + * {@inheritDoc} + */ + public Rectangle getCellBounds(final JList list, final int index1, final int index2) { + maybeUpdateLayoutState(); + + int minIndex = Math.min(index1, index2); + int maxIndex = Math.max(index1, index2); + + if (minIndex >= list.getModel().getSize()) { + return null; + } + + Rectangle minBounds = getCellBounds(list, minIndex); + + if (minBounds == null) { + return null; + } + if (minIndex == maxIndex) { + return minBounds; + } + Rectangle maxBounds = getCellBounds(list, maxIndex); + + if (maxBounds != null) { + if (layoutOrientation == JList.HORIZONTAL_WRAP) { + int minRow = convertModelToRow(minIndex); + int maxRow = convertModelToRow(maxIndex); + + if (minRow != maxRow) { + minBounds.x = 0; + minBounds.width = list.getWidth(); + } + } else if (minBounds.x != maxBounds.x) { + // Different columns + minBounds.y = 0; + minBounds.height = list.getHeight(); + } + minBounds.add(maxBounds); + } + return minBounds; + } + + /** + * Returns the height of the specified row based on the current layout. + * + * @param row a row + * @return the specified row height or -1 if row isn't valid + * @see #convertYToRow + * @see #convertRowToY + * @see #updateLayoutState + */ + protected int getRowHeight(final int row) { + return getHeight(0, row); + } + + /** + * Convert the {@code JList} relative coordinate to the row that contains it, + * based on the current layout. If {@code y0} doesn't fall within any row, + * return -1. + * + * @param y0 a relative Y coordinate + * @return the row that contains y0, or -1 + * @see #getRowHeight + * @see #updateLayoutState + */ + protected int convertYToRow(final int y0) { + return convertLocationToRow(0, y0, false); + } + + /** + * Return the {@code JList} relative Y coordinate of the origin of the specified + * row or -1 if row isn't valid. + * + * @param row a row + * @return the Y coordinate of the origin of row, or -1 + * @see #getRowHeight + * @see #updateLayoutState + */ + protected int convertRowToY(final int row) { + if (row >= getRowCount(0) || row < 0) { + return -1; + } + Rectangle bounds = getCellBounds(list, row, row); + return bounds.y; + } + + /** + * If updateLayoutStateNeeded is non zero, call updateLayoutState() and reset + * updateLayoutStateNeeded. This method should be called by methods + * before doing any computation based on the geometry of the list. + * For example it's the first call in paint() and getPreferredSize(). + * + * @see #updateLayoutState + */ + protected void maybeUpdateLayoutState() { + if (updateLayoutStateNeeded != 0) { + updateLayoutState(); + updateLayoutStateNeeded = 0; + } + } + + /** + * Recompute the value of cellHeight or cellHeights based + * and cellWidth, based on the current font and the current + * values of fixedCellWidth, fixedCellHeight, and prototypeCellValue. + * + * @see #maybeUpdateLayoutState + */ + protected void updateLayoutState() { + /* If both JList fixedCellWidth and fixedCellHeight have been + * set, then initialize cellWidth and cellHeight, and set + * cellHeights to null. + */ + + int fixedCellHeight = list.getFixedCellHeight(); + int fixedCellWidth = list.getFixedCellWidth(); + + cellWidth = fixedCellWidth; + + if (fixedCellHeight != -1) { + cellHeight = fixedCellHeight; + cellHeights = null; + } else { + cellHeight = -1; + cellHeights = new int[list.getModel().getSize()]; + } + + /* If either of JList fixedCellWidth and fixedCellHeight haven't + * been set, then initialize cellWidth and cellHeights by + * scanning through the entire model. Note: if the renderer is + * null, we just set cellWidth and cellHeights[*] to zero, + * if they're not set already. + */ + + if ((fixedCellWidth == -1) || (fixedCellHeight == -1)) { + + ListModel dataModel = list.getModel(); + int dataModelSize = dataModel.getSize(); + ListCellRenderer renderer = list.getCellRenderer(); + + if (renderer != null) { + for (int index = 0; index < dataModelSize; index++) { + Object value = dataModel.getElementAt(index); + Component c = renderer.getListCellRendererComponent(list, value, index, false, false); + rendererPane.add(c); + Dimension cellSize = c.getPreferredSize(); + if (fixedCellWidth == -1) { + cellWidth = Math.max(cellSize.width, cellWidth); + } + if (fixedCellHeight == -1) { + cellHeights[index] = cellSize.height; + } + } + } else { + if (cellWidth == -1) { + cellWidth = 0; + } + if (cellHeights == null) { + cellHeights = new int[dataModelSize]; + } + for (int index = 0; index < dataModelSize; index++) { + cellHeights[index] = 0; + } + } + } + + columnCount = 1; + if (layoutOrientation != JList.VERTICAL) { + updateHorizontalLayoutState(fixedCellWidth, fixedCellHeight); + } + } + + /** + * Creates a delegate that implements {@code MouseInputListener}. + * The delegate is added to the corresponding {@code java.awt.Component} listener + * lists at {@code installUI()} time. Subclasses can override this method to return + * a custom {@code MouseInputListener}, e.g. + *
+     * class MyListUI extends BasicListUI {
+     *    protected MouseInputListener createMouseInputListener() {
+     *        return new MyMouseInputHandler();
+     *    }
+     *    public class MyMouseInputHandler extends MouseInputHandler {
+     *        public void mouseMoved(MouseEvent e) {
+     *            // do some extra work when the mouse moves
+     *            super.mouseMoved(e);
+     *        }
+     *    }
+     * }
+     * 
+ * + * @return an instance of {@code MouseInputListener} + * @see MouseInputHandler + * @see #installUI + */ + protected MouseInputListener createMouseInputListener() { + return getHandler(); + } + + /** + * Returns an instance of {@code FocusListener}. + * + * @return an instance of {@code FocusListener} + */ + protected FocusListener createFocusListener() { + return getHandler(); + } + + /** + * Creates an instance of {@code ListSelectionHandler} that's added to + * the {@code JLists} by selectionModel as needed. Subclasses can override + * this method to return a custom {@code ListSelectionListener}, e.g. + *
+     * class MyListUI extends BasicListUI {
+     *    protected ListSelectionListener createListSelectionListener() {
+     *        return new MySelectionListener();
+     *    }
+     *    public class MySelectionListener extends ListSelectionHandler {
+     *        public void valueChanged(ListSelectionEvent e) {
+     *            // do some extra work when the selection changes
+     *            super.valueChange(e);
+     *        }
+     *    }
+     * }
+     * 
+ * + * @return an instance of {@code ListSelectionHandler} + * @see ListSelectionHandler + * @see #installUI + */ + protected ListSelectionListener createListSelectionListener() { + return getHandler(); + } + + /** + * Creates an instance of {@code ListDataListener} that's added to + * the {@code JLists} by model as needed. Subclasses can override + * this method to return a custom {@code ListDataListener}, e.g. + *
+     * class MyListUI extends BasicListUI {
+     *    protected ListDataListener createListDataListener() {
+     *        return new MyListDataListener();
+     *    }
+     *    public class MyListDataListener extends ListDataHandler {
+     *        public void contentsChanged(ListDataEvent e) {
+     *            // do some extra work when the models contents change
+     *            super.contentsChange(e);
+     *        }
+     *    }
+     * }
+     * 
+ * + * @return an instance of {@code ListDataListener} + * @see ListDataListener + * @see JList#getModel + * @see #installUI + */ + protected ListDataListener createListDataListener() { + return getHandler(); + } + + /** + * Creates an instance of {@code PropertyChangeHandler} that's added to + * the {@code JList} by {@code installUI()}. Subclasses can override this method + * to return a custom {@code PropertyChangeListener}, e.g. + *
+     * class MyListUI extends BasicListUI {
+     *    protected PropertyChangeListener createPropertyChangeListener() {
+     *        return new MyPropertyChangeListener();
+     *    }
+     *    public class MyPropertyChangeListener extends PropertyChangeHandler {
+     *        public void propertyChange(PropertyChangeEvent e) {
+     *            if (e.getPropertyName().equals("model")) {
+     *                // do some extra work when the model changes
+     *            }
+     *            super.propertyChange(e);
+     *        }
+     *    }
+     * }
+     * 
+ * + * @return an instance of {@code PropertyChangeHandler} + * @see PropertyChangeListener + * @see #installUI + */ + protected PropertyChangeListener createPropertyChangeListener() { + return getHandler(); + } + + /** + * Returns the closest location to the model index of the passed in + * location. + */ + protected int convertLocationToModel(final int x, final int y) { + int row = convertLocationToRow(x, y, true); + int column = convertLocationToColumn(x, y); + + if (row >= 0 && column >= 0) { + return getModelIndex(column, row); + } + return -1; + } + + /** + * Returns the row at location x/y. + * + * @param closest If true and the location doesn't exactly match a + * particular location, this will return the closest row. + */ + protected int convertLocationToRow(final int x, final int y0, final boolean closest) { + int size = list.getModel().getSize(); + + if (size <= 0) { + return -1; + } + Insets insets = list.getInsets(); + if (cellHeights == null) { + int row = (cellHeight == 0) ? 0 : + ((y0 - insets.top) / cellHeight); + if (closest) { + if (row < 0) { + row = 0; + } else if (row >= size) { + row = size - 1; + } + } + return row; + } else if (size > cellHeights.length) { + return -1; + } else { + int y = insets.top; + int row = 0; + + if (closest && y0 < y) { + return 0; + } + int i; + for (i = 0; i < size; i++) { + if ((y0 >= y) && (y0 < y + cellHeights[i])) { + return row; + } + y += cellHeights[i]; + row += 1; + } + return i - 1; + } + } + + /** + * Returns the closest column to the passed in location. + */ + protected int convertLocationToColumn(final int x, final int y) { + if (cellWidth > 0) { + if (layoutOrientation == JList.VERTICAL) { + return 0; + } + Insets insets = list.getInsets(); + int col; + if (isLeftToRight) { + col = (x - insets.left) / cellWidth; + } else { + col = (list.getWidth() - x - insets.right - 1) / cellWidth; + } + if (col < 0) { + return 0; + } else if (col >= columnCount) { + return columnCount - 1; + } + return col; + } + return 0; + } + + /** + * Returns the model index for the specified display location. + * If columnxrow is beyond the length of the + * model, this will return the model size - 1. + */ + protected int getModelIndex(final int column, final int row) { + switch (layoutOrientation) { + case JList.VERTICAL_WRAP: + return Math.min(list.getModel().getSize() - 1, rowsPerColumn * + column + Math.min(row, rowsPerColumn - 1)); + case JList.HORIZONTAL_WRAP: + return Math.min(list.getModel().getSize() - 1, row * columnCount + + column); + default: + return row; + } + } + + /** + * Invoked when the list is layed out horizontally to determine how + * many columns to create. + *

+ * This updates the rowsPerColumn, columnCount, + * preferredHeight and potentially cellHeight + * instance variables. + */ + protected void updateHorizontalLayoutState(final int fixedCellWidth, + final int fixedCellHeight) { + int visRows = list.getVisibleRowCount(); + int dataModelSize = list.getModel().getSize(); + Insets insets = list.getInsets(); + + listHeight = list.getHeight(); + listWidth = list.getWidth(); + + if (dataModelSize == 0) { + rowsPerColumn = columnCount = 0; + preferredHeight = insets.top + insets.bottom; + return; + } + + int height; + + if (fixedCellHeight != -1) { + height = fixedCellHeight; + } else { + // Determine the max of the renderer heights. + int maxHeight = 0; + if (cellHeights.length > 0) { + maxHeight = cellHeights[cellHeights.length - 1]; + for (int counter = cellHeights.length - 2; + counter >= 0; counter--) { + maxHeight = Math.max(maxHeight, cellHeights[counter]); + } + } + height = cellHeight = maxHeight; + cellHeights = null; + } + // The number of rows is either determined by the visible row + // count, or by the height of the list. + rowsPerColumn = dataModelSize; + if (visRows > 0) { + rowsPerColumn = visRows; + columnCount = Math.max(1, dataModelSize / rowsPerColumn); + if (dataModelSize > 0 && dataModelSize > rowsPerColumn && + dataModelSize % rowsPerColumn != 0) { + columnCount++; + } + if (layoutOrientation == JList.HORIZONTAL_WRAP) { + // Because HORIZONTAL_WRAP flows differently, the + // rowsPerColumn needs to be adjusted. + rowsPerColumn = (dataModelSize / columnCount); + if (dataModelSize % columnCount > 0) { + rowsPerColumn++; + } + } + } else if (layoutOrientation == JList.VERTICAL_WRAP && height != 0) { + rowsPerColumn = Math.max(1, (listHeight - insets.top - + insets.bottom) / height); + columnCount = Math.max(1, dataModelSize / rowsPerColumn); + if (dataModelSize > 0 && dataModelSize > rowsPerColumn && + dataModelSize % rowsPerColumn != 0) { + columnCount++; + } + } else if (layoutOrientation == JList.HORIZONTAL_WRAP && cellWidth > 0 && + listWidth > 0) { + columnCount = Math.max(1, (listWidth - insets.left - + insets.right) / cellWidth); + rowsPerColumn = dataModelSize / columnCount; + if (dataModelSize % columnCount > 0) { + rowsPerColumn++; + } + } + preferredHeight = rowsPerColumn * cellHeight + insets.top + + insets.bottom; + } + + protected void paintImpl(final Graphics g, final JComponent c) { + switch (layoutOrientation) { + case JList.VERTICAL_WRAP: + if (list.getHeight() != listHeight) { + updateLayoutStateNeeded |= heightChanged; + redrawList(); + } + break; + case JList.HORIZONTAL_WRAP: + if (list.getWidth() != listWidth) { + updateLayoutStateNeeded |= widthChanged; + redrawList(); + } + break; + default: + break; + } + maybeUpdateLayoutState(); + + ListCellRenderer renderer = list.getCellRenderer(); + ListModel dataModel = list.getModel(); + ListSelectionModel selModel = list.getSelectionModel(); + int size; + + if ((renderer == null) || (size = dataModel.getSize()) == 0) { + return; + } + + // Determine how many columns we need to paint + Rectangle paintBounds = g.getClipBounds(); + + int startColumn, endColumn; + if (c.getComponentOrientation().isLeftToRight()) { + startColumn = convertLocationToColumn(paintBounds.x, + paintBounds.y); + endColumn = convertLocationToColumn(paintBounds.x + + paintBounds.width, + paintBounds.y); + } else { + startColumn = convertLocationToColumn(paintBounds.x + + paintBounds.width, + paintBounds.y); + endColumn = convertLocationToColumn(paintBounds.x, + paintBounds.y); + } + int maxY = paintBounds.y + paintBounds.height; + int leadIndex = adjustIndex(list.getLeadSelectionIndex(), list); + int rowIncrement = (layoutOrientation == JList.HORIZONTAL_WRAP) ? + columnCount : 1; + + + for (int colCounter = startColumn; colCounter <= endColumn; + colCounter++) { + // And then how many rows in this columnn + int row = convertLocationToRowInColumn(paintBounds.y, colCounter); + int rowCount = getRowCount(colCounter); + int index = getModelIndex(colCounter, row); + Rectangle rowBounds = getCellBounds(list, index, index); + + if (rowBounds == null) { + // Not valid, bail! + return; + } + while (row < rowCount && rowBounds.y < maxY && + index < size) { + rowBounds.height = getHeight(colCounter, row); + g.setClip(rowBounds.x, rowBounds.y, rowBounds.width, + rowBounds.height); + g.clipRect(paintBounds.x, paintBounds.y, paintBounds.width, + paintBounds.height); + paintCell(g, index, rowBounds, renderer, dataModel, selModel, + leadIndex); + rowBounds.y += rowBounds.height; + index += rowIncrement; + row++; + } + } + // Empty out the renderer pane, allowing renderers to be gc'ed. + rendererPane.removeAll(); + } + + protected void paintDropLine(final Graphics g) { + JList.DropLocation loc = list.getDropLocation(); + if (loc == null || !loc.isInsert()) { + return; + } + + Color c = DefaultLookup.getColor(list, this, "List.dropLineColor", null); + if (c != null) { + g.setColor(c); + Rectangle rect = getDropLineRect(loc); + g.fillRect(rect.x, rect.y, rect.width, rect.height); + } + } + + protected Rectangle getDropLineRect(final JList.DropLocation loc) { + int size = list.getModel().getSize(); + + if (size == 0) { + Insets insets = list.getInsets(); + if (layoutOrientation == JList.HORIZONTAL_WRAP) { + if (isLeftToRight) { + return new Rectangle(insets.left, insets.top, DROP_LINE_THICKNESS, 20); + } else { + return new Rectangle(list.getWidth() - DROP_LINE_THICKNESS - insets.right, + insets.top, DROP_LINE_THICKNESS, 20); + } + } else { + return new Rectangle(insets.left, insets.top, + list.getWidth() - insets.left - insets.right, + DROP_LINE_THICKNESS); + } + } + + Rectangle rect = null; + int index = loc.getIndex(); + boolean decr = false; + + if (layoutOrientation == JList.HORIZONTAL_WRAP) { + if (index == size) { + decr = true; + } else if (index != 0 && convertModelToRow(index) + != convertModelToRow(index - 1)) { + + Rectangle prev = getCellBounds(list, index - 1); + Rectangle me = getCellBounds(list, index); + Point p = loc.getDropPoint(); + + if (isLeftToRight) { + decr = Point2D.distance(prev.x + prev.width, + prev.y + (int) (prev.height / 2.0), + p.x, p.y) + < Point2D.distance(me.x, + me.y + (int) (me.height / 2.0), + p.x, p.y); + } else { + decr = Point2D.distance(prev.x, + prev.y + (int) (prev.height / 2.0), + p.x, p.y) + < Point2D.distance(me.x + me.width, + me.y + (int) (prev.height / 2.0), + p.x, p.y); + } + } + + if (decr) { + index--; + rect = getCellBounds(list, index); + if (isLeftToRight) { + rect.x += rect.width; + } else { + rect.x -= DROP_LINE_THICKNESS; + } + } else { + rect = getCellBounds(list, index); + if (!isLeftToRight) { + rect.x += rect.width - DROP_LINE_THICKNESS; + } + } + + if (rect.x >= list.getWidth()) { + rect.x = list.getWidth() - DROP_LINE_THICKNESS; + } else if (rect.x < 0) { + rect.x = 0; + } + + rect.width = DROP_LINE_THICKNESS; + } else if (layoutOrientation == JList.VERTICAL_WRAP) { + if (index == size) { + index--; + rect = getCellBounds(list, index); + rect.y += rect.height; + } else if (index != 0 && convertModelToColumn(index) + != convertModelToColumn(index - 1)) { + + Rectangle prev = getCellBounds(list, index - 1); + Rectangle me = getCellBounds(list, index); + Point p = loc.getDropPoint(); + if (Point2D.distance(prev.x + (int) (prev.width / 2.0), + prev.y + prev.height, + p.x, p.y) + < Point2D.distance(me.x + (int) (me.width / 2.0), + me.y, + p.x, p.y)) { + + index--; + rect = getCellBounds(list, index); + rect.y += rect.height; + } else { + rect = getCellBounds(list, index); + } + } else { + rect = getCellBounds(list, index); + } + + if (rect.y >= list.getHeight()) { + rect.y = list.getHeight() - DROP_LINE_THICKNESS; + } + + rect.height = DROP_LINE_THICKNESS; + } else { + if (index == size) { + index--; + rect = getCellBounds(list, index); + rect.y += rect.height; + } else { + rect = getCellBounds(list, index); + } + + if (rect.y >= list.getHeight()) { + rect.y = list.getHeight() - DROP_LINE_THICKNESS; + } + + rect.height = DROP_LINE_THICKNESS; + } + + return rect; + } + + /** + * Gets the bounds of the specified model index, returning the resulting + * bounds, or null if index is not valid. + */ + protected Rectangle getCellBounds(final JList list, final int index) { + maybeUpdateLayoutState(); + + int row = convertModelToRow(index); + int column = convertModelToColumn(index); + + if (row == -1 || column == -1) { + return null; + } + + Insets insets = list.getInsets(); + int x; + int w = cellWidth; + int y = insets.top; + int h; + switch (layoutOrientation) { + case JList.VERTICAL_WRAP: + case JList.HORIZONTAL_WRAP: + if (isLeftToRight) { + x = insets.left + column * cellWidth; + } else { + x = list.getWidth() - insets.right - (column + 1) * cellWidth; + } + y += cellHeight * row; + h = cellHeight; + break; + default: + x = insets.left; + if (cellHeights == null) { + y += (cellHeight * row); + } else if (row >= cellHeights.length) { + y = 0; + } else { + for (int i = 0; i < row; i++) { + y += cellHeights[i]; + } + } + w = list.getWidth() - (insets.left + insets.right); + h = getRowHeight(index); + break; + } + return new Rectangle(x, y, w, h); + } + + /** + * Returns the height of the cell at the passed in location. + */ + protected int getHeight(final int column, final int row) { + if (column < 0 || column > columnCount || row < 0) { + return -1; + } + if (layoutOrientation != JList.VERTICAL) { + return cellHeight; + } + if (row >= list.getModel().getSize()) { + return -1; + } + return (cellHeights == null) ? cellHeight : + ((row < cellHeights.length) ? cellHeights[row] : -1); + } + + /** + * Returns the closest row that starts at the specified y-location + * in the passed in column. + */ + protected int convertLocationToRowInColumn(final int y, final int column) { + int x = 0; + + if (layoutOrientation != JList.VERTICAL) { + if (isLeftToRight) { + x = column * cellWidth; + } else { + x = list.getWidth() - (column + 1) * cellWidth - list.getInsets().right; + } + } + return convertLocationToRow(x, y, true); + } + + /** + * Returns the number of rows in the given column. + */ + protected int getRowCount(final int column) { + if (column < 0 || column >= columnCount) { + return -1; + } + if (layoutOrientation == JList.VERTICAL || + (column == 0 && columnCount == 1)) { + return list.getModel().getSize(); + } + if (column >= columnCount) { + return -1; + } + if (layoutOrientation == JList.VERTICAL_WRAP) { + if (column < (columnCount - 1)) { + return rowsPerColumn; + } + return list.getModel().getSize() - (columnCount - 1) * + rowsPerColumn; + } + // JList.HORIZONTAL_WRAP + int diff = columnCount - (columnCount * rowsPerColumn - + list.getModel().getSize()); + + if (column >= diff) { + return Math.max(0, rowsPerColumn - 1); + } + return rowsPerColumn; + } + + /** + * Returns the row that the model index index will be + * displayed in.. + */ + protected int convertModelToRow(final int index) { + int size = list.getModel().getSize(); + + if ((index < 0) || (index >= size)) { + return -1; + } + + if (layoutOrientation != JList.VERTICAL && columnCount > 1 && + rowsPerColumn > 0) { + if (layoutOrientation == JList.VERTICAL_WRAP) { + return index % rowsPerColumn; + } + return index / columnCount; + } + return index; + } + + /** + * Returns the column that the model index index will be + * displayed in. + */ + protected int convertModelToColumn(final int index) { + int size = list.getModel().getSize(); + + if ((index < 0) || (index >= size)) { + return -1; + } + + if (layoutOrientation != JList.VERTICAL && rowsPerColumn > 0 && + columnCount > 1) { + if (layoutOrientation == JList.VERTICAL_WRAP) { + return index / rowsPerColumn; + } + return index % columnCount; + } + return 0; + } + + protected void redrawList() { + list.revalidate(); + list.repaint(); + } + + @SuppressWarnings("serial") // Superclass is a JDK-implementation class + static class ListTransferHandler extends TransferHandler implements UIResource { + + public int getSourceActions(final JComponent c) { + return COPY; + } + + /** + * Create a Transferable to use as the source for a data transfer. + * + * @param c The component holding the data to be transfered. This + * argument is provided to enable sharing of TransferHandlers by + * multiple components. + * @return The representation of the data to be transfered. + */ + @SuppressWarnings("deprecation") + protected Transferable createTransferable(final JComponent c) { + if (c instanceof JList) { + JList list = (JList) c; + Object[] values = list.getSelectedValues(); + + if (values == null || values.length == 0) { + return null; + } + + StringBuilder plainStr = new StringBuilder(); + StringBuilder htmlStr = new StringBuilder(); + + htmlStr.append("\n\n
    \n"); + + for (int i = 0; i < values.length; i++) { + Object obj = values[i]; + String val = ((obj == null) ? "" : obj.toString()); + plainStr.append(val).append('\n'); + htmlStr.append("
  • ").append(val).append('\n'); + } + + // remove the last newline + plainStr.deleteCharAt(plainStr.length() - 1); + htmlStr.append("
\n\n"); + + return new BasicTransferable(plainStr.toString(), htmlStr.toString()); + } + + return null; + } + + } + + /** + * Mouse input, and focus handling for JList. An instance of this + * class is added to the appropriate java.awt.Component lists + * at installUI() time. Note keyboard input is handled with JComponent + * KeyboardActions, see installKeyboardActions(). + *

+ * Warning: + * Serialized objects of this class will not be compatible with + * future Swing releases. The current serialization support is + * appropriate for short term storage or RMI between applications running + * the same version of Swing. As of 1.4, support for long term storage + * of all JavaBeans™ + * has been added to the java.beans package. + * Please see {@link java.beans.XMLEncoder}. + * + * @see #createMouseInputListener + * @see #installKeyboardActions + * @see #installUI + */ + @SuppressWarnings("serial") // Same-version serialization only + public class MouseInputHandler implements MouseInputListener { + public void mouseClicked(final MouseEvent e) { + getHandler().mouseClicked(e); + } + + public void mousePressed(final MouseEvent e) { + getHandler().mousePressed(e); + } + + public void mouseReleased(final MouseEvent e) { + getHandler().mouseReleased(e); + } + + public void mouseEntered(final MouseEvent e) { + getHandler().mouseEntered(e); + } + + public void mouseExited(final MouseEvent e) { + getHandler().mouseExited(e); + } + + public void mouseDragged(final MouseEvent e) { + getHandler().mouseDragged(e); + } + + public void mouseMoved(final MouseEvent e) { + getHandler().mouseMoved(e); + } + } + + /** + * This class should be treated as a "protected" inner class. + * Instantiate it only within subclasses of {@code BasicListUI}. + */ + public class FocusHandler implements FocusListener { + /** + * Repaints focused cells. + */ + protected void repaintCellFocus() { + getHandler().repaintCellFocus(); + } + + /* The focusGained() focusLost() methods run when the JList + * focus changes. + */ + + public void focusGained(final FocusEvent e) { + getHandler().focusGained(e); + } + + public void focusLost(final FocusEvent e) { + getHandler().focusLost(e); + } + } + + /** + * The ListSelectionListener that's added to the JLists selection + * model at installUI time, and whenever the JList.selectionModel property + * changes. When the selection changes we repaint the affected rows. + *

+ * Warning: + * Serialized objects of this class will not be compatible with + * future Swing releases. The current serialization support is + * appropriate for short term storage or RMI between applications running + * the same version of Swing. As of 1.4, support for long term storage + * of all JavaBeans™ + * has been added to the java.beans package. + * Please see {@link java.beans.XMLEncoder}. + * + * @see #createListSelectionListener + * @see #installUI + */ + @SuppressWarnings("serial") // Same-version serialization only + public class ListSelectionHandler implements ListSelectionListener { + public void valueChanged(final ListSelectionEvent e) { + getHandler().valueChanged(e); + } + } + + /** + * The {@code ListDataListener} that's added to the {@code JLists} model at + * {@code installUI time}, and whenever the JList.model property changes. + *

+ * Warning: + * Serialized objects of this class will not be compatible with + * future Swing releases. The current serialization support is + * appropriate for short term storage or RMI between applications running + * the same version of Swing. As of 1.4, support for long term storage + * of all JavaBeans™ + * has been added to the java.beans package. + * Please see {@link java.beans.XMLEncoder}. + * + * @see JList#getModel + * @see #maybeUpdateLayoutState + * @see #createListDataListener + * @see #installUI + */ + @SuppressWarnings("serial") // Same-version serialization only + public class ListDataHandler implements ListDataListener { + public void intervalAdded(final ListDataEvent e) { + getHandler().intervalAdded(e); + } + + + public void intervalRemoved(final ListDataEvent e) { + getHandler().intervalRemoved(e); + } + + + public void contentsChanged(final ListDataEvent e) { + getHandler().contentsChanged(e); + } + } + + /** + * The PropertyChangeListener that's added to the JList at + * installUI time. When the value of a JList property that + * affects layout changes, we set a bit in updateLayoutStateNeeded. + * If the JLists model changes we additionally remove our listeners + * from the old model. Likewise for the JList selectionModel. + *

+ * Warning: + * Serialized objects of this class will not be compatible with + * future Swing releases. The current serialization support is + * appropriate for short term storage or RMI between applications running + * the same version of Swing. As of 1.4, support for long term storage + * of all JavaBeans™ + * has been added to the java.beans package. + * Please see {@link java.beans.XMLEncoder}. + * + * @see #maybeUpdateLayoutState + * @see #createPropertyChangeListener + * @see #installUI + */ + @SuppressWarnings("serial") // Same-version serialization only + public class PropertyChangeHandler implements PropertyChangeListener { + public void propertyChange(final PropertyChangeEvent e) { + getHandler().propertyChange(e); + } + } + + protected class Handler implements FocusListener, KeyListener, + ListDataListener, ListSelectionListener, + MouseInputListener, PropertyChangeListener, + DragRecognitionSupport.BeforeDrag { + // + // KeyListener + // + protected String prefix = ""; + protected String typedString = ""; + protected long lastTime = 0L; + // Whether or not the mouse press (which is being considered as part + // of a drag sequence) also caused the selection change to be fully + // processed. + protected boolean dragPressDidSelection; + + /** + * Invoked when a key has been typed. + *

+ * Moves the keyboard focus to the first element whose prefix matches the + * sequence of alphanumeric keys pressed by the user with delay less + * than value of timeFactor property (or 1000 milliseconds + * if it is not defined). Subsequent same key presses move the keyboard + * focus to the next object that starts with the same letter until another + * key is pressed, then it is treated as the prefix with appropriate number + * of the same letters followed by first typed another letter. + */ + public void keyTyped(final KeyEvent e) { + JList src = (JList) e.getSource(); + ListModel model = src.getModel(); + + if (model.getSize() == 0 || e.isAltDown() || + DarkUIUtil.isMenuShortcutKeyDown(e) || + isNavigationKey(e)) { + // Nothing to select + return; + } + boolean startingFromSelection = true; + + char c = e.getKeyChar(); + + long time = e.getWhen(); + int startIndex = adjustIndex(src.getLeadSelectionIndex(), list); + if (time - lastTime < timeFactor) { + typedString += c; + if ((prefix.length() == 1) && (c == prefix.charAt(0))) { + // Subsequent same key presses move the keyboard focus to the next + // object that starts with the same letter. + startIndex++; + } else { + prefix = typedString; + } + } else { + startIndex++; + typedString = "" + c; + prefix = typedString; + } + lastTime = time; + + if (startIndex < 0 || startIndex >= model.getSize()) { + startingFromSelection = false; + startIndex = 0; + } + int index = src.getNextMatch(prefix, startIndex, + Position.Bias.Forward); + if (index >= 0) { + src.setSelectedIndex(index); + src.ensureIndexIsVisible(index); + } else if (startingFromSelection) { // wrap + index = src.getNextMatch(prefix, 0, + Position.Bias.Forward); + if (index >= 0) { + src.setSelectedIndex(index); + src.ensureIndexIsVisible(index); + } + } + } + + /** + * Invoked when a key has been pressed. + *

+ * Checks to see if the key event is a navigation key to prevent + * dispatching these keys for the first letter navigation. + */ + public void keyPressed(final KeyEvent e) { + if (isNavigationKey(e)) { + prefix = ""; + typedString = ""; + lastTime = 0L; + } + } + + /** + * Invoked when a key has been released. + * See the class description for {@link KeyEvent} for a definition of + * a key released event. + */ + public void keyReleased(final KeyEvent e) { + } + + /** + * Returns whether or not the supplied key event maps to a key that is used for + * navigation. This is used for optimizing key input by only passing non- + * navigation keys to the first letter navigation mechanism. + */ + protected boolean isNavigationKey(final KeyEvent event) { + InputMap inputMap = list.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + KeyStroke key = KeyStroke.getKeyStrokeForEvent(event); + + return inputMap != null && inputMap.get(key) != null; + } + + // + // PropertyChangeListener + // + public void propertyChange(final PropertyChangeEvent e) { + String propertyName = e.getPropertyName(); + + /* If the JList.model property changes, remove our listener, + * listDataListener from the old model and add it to the new one. + */ + if (Objects.equals(propertyName, "model")) { + ListModel oldModel = (ListModel) e.getOldValue(); + ListModel newModel = (ListModel) e.getNewValue(); + if (oldModel != null) { + oldModel.removeListDataListener(listDataListener); + } + if (newModel != null) { + newModel.addListDataListener(listDataListener); + } + updateLayoutStateNeeded |= modelChanged; + redrawList(); + } + + /* If the JList.selectionModel property changes, remove our listener, + * listSelectionListener from the old selectionModel and add it to the new one. + */ + else if (Objects.equals(propertyName, "selectionModel")) { + ListSelectionModel oldModel = (ListSelectionModel) e.getOldValue(); + ListSelectionModel newModel = (ListSelectionModel) e.getNewValue(); + if (oldModel != null) { + oldModel.removeListSelectionListener(listSelectionListener); + } + if (newModel != null) { + newModel.addListSelectionListener(listSelectionListener); + } + updateLayoutStateNeeded |= modelChanged; + redrawList(); + } else if (Objects.equals(propertyName, "cellRenderer")) { + updateLayoutStateNeeded |= cellRendererChanged; + redrawList(); + } else if (Objects.equals(propertyName, "font") + || SwingUtilities2.isScaleChanged(e)) { + updateLayoutStateNeeded |= fontChanged; + redrawList(); + } else if (Objects.equals(propertyName, "prototypeCellValue")) { + updateLayoutStateNeeded |= prototypeCellValueChanged; + redrawList(); + } else if (Objects.equals(propertyName, "fixedCellHeight")) { + updateLayoutStateNeeded |= fixedCellHeightChanged; + redrawList(); + } else if (Objects.equals(propertyName, "fixedCellWidth")) { + updateLayoutStateNeeded |= fixedCellWidthChanged; + redrawList(); + } else if (Objects.equals(propertyName, "selectionForeground")) { + list.repaint(); + } else if (Objects.equals(propertyName, "selectionBackground")) { + list.repaint(); + } else if ("layoutOrientation".equals(propertyName)) { + updateLayoutStateNeeded |= layoutOrientationChanged; + layoutOrientation = list.getLayoutOrientation(); + redrawList(); + } else if ("visibleRowCount".equals(propertyName)) { + if (layoutOrientation != JList.VERTICAL) { + updateLayoutStateNeeded |= layoutOrientationChanged; + redrawList(); + } + } else if ("componentOrientation".equals(propertyName)) { + isLeftToRight = list.getComponentOrientation().isLeftToRight(); + updateLayoutStateNeeded |= componentOrientationChanged; + redrawList(); + + InputMap inputMap = getInputMap(JComponent.WHEN_FOCUSED); + SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, + inputMap); + } else if ("List.isFileList".equals(propertyName)) { + updateIsFileList(); + redrawList(); + } else if ("dropLocation".equals(propertyName)) { + JList.DropLocation oldValue = (JList.DropLocation) e.getOldValue(); + repaintDropLocation(oldValue); + repaintDropLocation(list.getDropLocation()); + } + } + + protected void repaintDropLocation(final JList.DropLocation loc) { + if (loc == null) { + return; + } + + Rectangle r; + + if (loc.isInsert()) { + r = getDropLineRect(loc); + } else { + r = getCellBounds(list, loc.getIndex()); + } + + if (r != null) { + list.repaint(r); + } + } + + // + // ListDataListener + // + public void intervalAdded(final ListDataEvent e) { + updateLayoutStateNeeded = modelChanged; + + int minIndex = Math.min(e.getIndex0(), e.getIndex1()); + int maxIndex = Math.max(e.getIndex0(), e.getIndex1()); + + /* Sync the SelectionModel with the DataModel. + */ + + ListSelectionModel sm = list.getSelectionModel(); + if (sm != null) { + sm.insertIndexInterval(minIndex, maxIndex - minIndex + 1, true); + } + + /* Repaint the entire list, from the origin of + * the first added cell, to the bottom of the + * component. + */ + redrawList(); + } + + public void intervalRemoved(final ListDataEvent e) { + updateLayoutStateNeeded = modelChanged; + + /* Sync the SelectionModel with the DataModel. + */ + + ListSelectionModel sm = list.getSelectionModel(); + if (sm != null) { + sm.removeIndexInterval(e.getIndex0(), e.getIndex1()); + } + + /* Repaint the entire list, from the origin of + * the first removed cell, to the bottom of the + * component. + */ + + redrawList(); + } + + public void contentsChanged(final ListDataEvent e) { + updateLayoutStateNeeded = modelChanged; + redrawList(); + } + + // + // ListSelectionListener + // + public void valueChanged(final ListSelectionEvent e) { + maybeUpdateLayoutState(); + + int size = list.getModel().getSize(); + int firstIndex = Math.min(size - 1, Math.max(e.getFirstIndex(), 0)); + int lastIndex = Math.min(size - 1, Math.max(e.getLastIndex(), 0)); + + Rectangle bounds = getCellBounds(list, firstIndex, lastIndex); + + if (bounds != null) { + list.repaint(bounds.x, bounds.y, bounds.width, bounds.height); + } + } + + // + // MouseListener + // + public void mouseClicked(final MouseEvent e) { + } + + public void mousePressed(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, list)) { + return; + } + + boolean dragEnabled = list.getDragEnabled(); + boolean grabFocus = true; + + // different behavior if drag is enabled + if (dragEnabled) { + int row = SwingUtilities2.loc2IndexFileList(list, e.getPoint()); + // if we have a valid row and this is a drag initiating event + if (row != -1 && DragRecognitionSupport.mousePressed(e)) { + dragPressDidSelection = false; + + if (DarkUIUtil.isMenuShortcutKeyDown(e)) { + // do nothing for control - will be handled on release + // or when drag starts + return; + } else if (!e.isShiftDown() && list.isSelectedIndex(row)) { + // clicking on something that's already selected + // and need to make it the lead now + list.addSelectionInterval(row, row); + return; + } + + // could be a drag initiating event - don't grab focus + grabFocus = false; + + dragPressDidSelection = true; + } + } else { + // When drag is enabled mouse drags won't change the selection + // in the list, so we only set the isAdjusting flag when it's + // not enabled + list.setValueIsAdjusting(true); + } + + if (grabFocus) { + SwingUtilities2.adjustFocus(list); + } + + adjustSelection(e); + } + + protected void adjustSelection(final MouseEvent e) { + int row = SwingUtilities2.loc2IndexFileList(list, e.getPoint()); + if (row < 0) { + // If shift is down in multi-select, we should do nothing. + // For single select or non-shift-click, clear the selection + if (isFileList && e.getID() == MouseEvent.MOUSE_PRESSED && + (!e.isShiftDown() || + list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)) { + list.clearSelection(); + } + } else { + int anchorIndex = adjustIndex(list.getAnchorSelectionIndex(), list); + boolean anchorSelected; + if (anchorIndex == -1) { + anchorIndex = 0; + anchorSelected = false; + } else { + anchorSelected = list.isSelectedIndex(anchorIndex); + } + + if (DarkUIUtil.isMenuShortcutKeyDown(e)) { + if (e.isShiftDown()) { + if (anchorSelected) { + list.addSelectionInterval(anchorIndex, row); + } else { + list.removeSelectionInterval(anchorIndex, row); + if (isFileList) { + list.addSelectionInterval(row, row); + list.getSelectionModel().setAnchorSelectionIndex(anchorIndex); + } + } + } else if (list.isSelectedIndex(row)) { + list.removeSelectionInterval(row, row); + } else { + list.addSelectionInterval(row, row); + } + } else if (e.isShiftDown()) { + list.setSelectionInterval(anchorIndex, row); + } else { + list.setSelectionInterval(row, row); + } + } + } + + public void mouseReleased(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, list)) { + return; + } + + if (list.getDragEnabled()) { + MouseEvent me = DragRecognitionSupport.mouseReleased(e); + if (me != null) { + SwingUtilities2.adjustFocus(list); + if (!dragPressDidSelection) { + adjustSelection(me); + } + } + } else { + list.setValueIsAdjusting(false); + } + } + + public void mouseEntered(final MouseEvent e) { + } + + public void mouseExited(final MouseEvent e) { + } + + public void dragStarting(final MouseEvent me) { + if (DarkUIUtil.isMenuShortcutKeyDown(me)) { + int row = SwingUtilities2.loc2IndexFileList(list, me.getPoint()); + list.addSelectionInterval(row, row); + } + } + + public void mouseDragged(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, list)) { + return; + } + + if (list.getDragEnabled()) { + DragRecognitionSupport.mouseDragged(e, this); + return; + } + + if (e.isShiftDown() || DarkUIUtil.isMenuShortcutKeyDown(e)) { + return; + } + + int row = locationToIndex(list, e.getPoint()); + if (row != -1) { + // 4835633. Dragging onto a File should not select it. + if (isFileList) { + return; + } + Rectangle cellBounds = getCellBounds(list, row, row); + if (cellBounds != null) { + list.scrollRectToVisible(cellBounds); + list.setSelectionInterval(row, row); + } + } + } + + public void mouseMoved(final MouseEvent e) { + } + + public void focusGained(final FocusEvent e) { + repaintCellFocus(); + } + + /* The focusGained() focusLost() methods run when the JList + * focus changes. + */ + + // + // FocusListener + // + protected void repaintCellFocus() { + int leadIndex = adjustIndex(list.getLeadSelectionIndex(), list); + if (leadIndex != -1) { + Rectangle r = getCellBounds(list, leadIndex, leadIndex); + if (r != null) { + list.repaint(r.x, r.y, r.width, r.height); + } + } + } + + public void focusLost(final FocusEvent e) { + repaintCellFocus(); + } + } +} diff --git a/src/main/java/com/weis/darklaf/ui/menu/DarkMenuItemUIBase.java b/src/main/java/com/weis/darklaf/ui/menu/DarkMenuItemUIBase.java index 485b53ca..f26d51db 100644 --- a/src/main/java/com/weis/darklaf/ui/menu/DarkMenuItemUIBase.java +++ b/src/main/java/com/weis/darklaf/ui/menu/DarkMenuItemUIBase.java @@ -27,27 +27,15 @@ import com.weis.darklaf.util.DarkUIUtil; import com.weis.darklaf.util.LazyActionMap; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import sun.swing.MenuItemCheckIconFactory; import sun.swing.MenuItemLayoutHelper; import sun.swing.SwingUtilities2; import sun.swing.UIAction; import javax.swing.*; -import javax.swing.event.MenuDragMouseEvent; -import javax.swing.event.MenuDragMouseListener; -import javax.swing.event.MouseInputListener; -import javax.swing.plaf.ComponentInputMapUIResource; import javax.swing.plaf.ComponentUI; -import javax.swing.plaf.UIResource; -import javax.swing.plaf.basic.BasicHTML; import javax.swing.plaf.basic.BasicMenuItemUI; import java.awt.*; import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.MouseEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.Objects; /** * @author Konstantin Bulenkov @@ -55,8 +43,6 @@ import java.util.Objects; */ public class DarkMenuItemUIBase extends BasicMenuItemUI { - protected Handler handler; - @NotNull @Contract("_ -> new") public static ComponentUI createUI(final JComponent c) { @@ -256,103 +242,8 @@ public class DarkMenuItemUIBase extends BasicMenuItemUI { } } - /* - * Code from BasicMenuItemUI. - */ - - protected DarkMenuItemUIBase.Handler getHandler() { - if (handler == null) { - handler = new DarkMenuItemUIBase.Handler(); - } - return handler; - } - - protected void updateAcceleratorBinding() { - KeyStroke accelerator = menuItem.getAccelerator(); - InputMap windowInputMap = SwingUtilities.getUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW); - - if (windowInputMap != null) { - windowInputMap.clear(); - } - if (accelerator != null) { - if (windowInputMap == null) { - windowInputMap = createInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - SwingUtilities.replaceUIInputMap(menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW, windowInputMap); - } - windowInputMap.put(accelerator, "doClick"); - - int modifiers = accelerator.getModifiers(); - if (((modifiers & InputEvent.ALT_DOWN_MASK) != 0) && - ((modifiers & InputEvent.ALT_GRAPH_DOWN_MASK) != 0)) { - //When both ALT and ALT_GRAPH are set, add the ALT only - // modifier keystroke which is used for left ALT key. - // Unsetting the ALT_GRAPH will do that as ALT is already set - modifiers &= ~InputEvent.ALT_GRAPH_DOWN_MASK; - modifiers &= ~InputEvent.ALT_GRAPH_MASK; - KeyStroke keyStroke = KeyStroke.getKeyStroke(accelerator.getKeyCode(), - modifiers, accelerator.isOnKeyRelease()); - windowInputMap.put(keyStroke, "doClick"); - } else if (((modifiers & InputEvent.ALT_DOWN_MASK) != 0) && ( - (modifiers & InputEvent.ALT_GRAPH_DOWN_MASK) == 0)) { - //When only ALT modifier is set, add the ALT + ALT_GRAPH - // modifier keystroke which is used for right ALT key - modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK; - KeyStroke keyStroke = KeyStroke.getKeyStroke(accelerator.getKeyCode(), - modifiers, accelerator.isOnKeyRelease()); - windowInputMap.put(keyStroke, "doClick"); - } else if ((modifiers & InputEvent.ALT_GRAPH_DOWN_MASK) != 0) { - //When only ALT_GRAPH is set, remove the ALT_GRAPH only - // modifier and add the ALT and ALT+ALT_GRAPH modifiers - // keystroke which are used for left ALT key and right ALT - // respectively - modifiers &= ~InputEvent.ALT_GRAPH_DOWN_MASK; - modifiers &= ~InputEvent.ALT_GRAPH_MASK; - - modifiers |= InputEvent.ALT_DOWN_MASK; - KeyStroke keyStroke = KeyStroke.getKeyStroke(accelerator.getKeyCode(), - modifiers, accelerator.isOnKeyRelease()); - windowInputMap.put(keyStroke, "doClick"); - - //Add ALT+ALT_GRAPH modifier which is used for right ALT key - modifiers |= InputEvent.ALT_GRAPH_DOWN_MASK; - keyStroke = KeyStroke.getKeyStroke(accelerator.getKeyCode(), - modifiers, accelerator.isOnKeyRelease()); - windowInputMap.put(keyStroke, "doClick"); - } - } - } - - protected InputMap createInputMap(final int condition) { - if (condition == JComponent.WHEN_IN_FOCUSED_WINDOW) { - return new ComponentInputMapUIResource(menuItem); - } - return null; - } - - protected void updateCheckIcon() { - String prefix = getPropertyPrefix(); - - if (checkIcon == null || checkIcon instanceof UIResource) { - checkIcon = UIManager.getIcon(prefix + ".checkIcon"); - //In case of column layout, .checkIconFactory is defined for this UI, - //the icon is compatible with it and useCheckAndArrow() is true, - //then the icon is handled by the checkIcon. - boolean isColumnLayout = MenuItemLayoutHelper.isColumnLayout( - menuItem.getComponentOrientation().isLeftToRight(), menuItem); - if (isColumnLayout) { - MenuItemCheckIconFactory iconFactory = - (MenuItemCheckIconFactory) UIManager.get(prefix + ".checkIconFactory"); - if (iconFactory != null - && MenuItemLayoutHelper.useCheckAndArrow(menuItem) - && iconFactory.isCompatible(checkIcon, prefix)) { - checkIcon = iconFactory.getIcon(menuItem); - } - } - } - } - - private static class Actions extends UIAction { - private static final String CLICK = "doClick"; + protected static class Actions extends UIAction { + protected static final String CLICK = "doClick"; Actions(final String key) { super(key); @@ -364,129 +255,4 @@ public class DarkMenuItemUIBase extends BasicMenuItemUI { mi.doClick(); } } - - protected class Handler implements MenuDragMouseListener, MouseInputListener, PropertyChangeListener { - // - // MouseInputListener - // - public void mouseClicked(final MouseEvent e) { - } - - public void mousePressed(final MouseEvent e) { - } - - public void mouseReleased(final MouseEvent e) { - if (!menuItem.isEnabled()) { - return; - } - MenuSelectionManager manager = - MenuSelectionManager.defaultManager(); - Point p = e.getPoint(); - if (p.x >= 0 && p.x < menuItem.getWidth() && - p.y >= 0 && p.y < menuItem.getHeight()) { - doClick(manager); - } else { - manager.processMouseEvent(e); - } - } - - @SuppressWarnings("deprecation") - public void mouseEntered(@NotNull final MouseEvent e) { - MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - int modifiers = e.getModifiers(); - // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2 - if ((modifiers & (InputEvent.BUTTON1_MASK | - InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) != 0) { - MenuSelectionManager.defaultManager().processMouseEvent(e); - } else { - manager.setSelectedPath(getPath()); - } - } - - @SuppressWarnings("deprecation") - public void mouseExited(@NotNull final MouseEvent e) { - MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - - int modifiers = e.getModifiers(); - // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2 - if ((modifiers & (InputEvent.BUTTON1_MASK | - InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) != 0) { - MenuSelectionManager.defaultManager().processMouseEvent(e); - } else { - - MenuElement[] path = manager.getSelectedPath(); - if (path.length > 1 && path[path.length - 1] == menuItem) { - MenuElement[] newPath = new MenuElement[path.length - 1]; - int i, c; - for (i = 0, c = path.length - 1; i < c; i++) { newPath[i] = path[i]; } - manager.setSelectedPath(newPath); - } - } - } - - public void mouseDragged(final MouseEvent e) { - MenuSelectionManager.defaultManager().processMouseEvent(e); - } - - public void mouseMoved(final MouseEvent e) { - } - - // - // MenuDragListener - // - public void menuDragMouseEntered(@NotNull final MenuDragMouseEvent e) { - MenuSelectionManager manager = e.getMenuSelectionManager(); - MenuElement[] path = e.getPath(); - manager.setSelectedPath(path); - } - - public void menuDragMouseExited(final MenuDragMouseEvent e) { - } - - public void menuDragMouseDragged(@NotNull final MenuDragMouseEvent e) { - MenuSelectionManager manager = e.getMenuSelectionManager(); - MenuElement[] path = e.getPath(); - manager.setSelectedPath(path); - } - - public void menuDragMouseReleased(final MenuDragMouseEvent e) { - if (!menuItem.isEnabled()) { - return; - } - MenuSelectionManager manager = e.getMenuSelectionManager(); - e.getPath(); - Point p = e.getPoint(); - if (p.x >= 0 && p.x < menuItem.getWidth() && - p.y >= 0 && p.y < menuItem.getHeight()) { - doClick(manager); - } else { - manager.clearSelectedPath(); - } - } - - - // - // PropertyChangeListener - // - public void propertyChange(final PropertyChangeEvent e) { - String name = e.getPropertyName(); - - if (Objects.equals(name, "labelFor") || Objects.equals(name, "displayedMnemonic") || - Objects.equals(name, "accelerator")) { - updateAcceleratorBinding(); - } else if (Objects.equals(name, "text") || "font".equals(name) || "foreground".equals(name) - || SwingUtilities2.isScaleChanged(e)) { - // remove the old html view client property if one - // existed, and install a new one if the text installed - // into the JLabel is html source. - JMenuItem lbl = ((JMenuItem) e.getSource()); - String text = lbl.getText(); - BasicHTML.updateRenderer(lbl, text); - } else if (Objects.equals(name, "iconTextGap")) { - defaultTextIconGap = ((Number) e.getNewValue()).intValue(); - } else if (Objects.equals(name, "horizontalTextPosition")) { - updateCheckIcon(); - } - } - } } diff --git a/src/main/java/com/weis/darklaf/ui/menu/DarkMenuUI.java b/src/main/java/com/weis/darklaf/ui/menu/DarkMenuUI.java index 13e84472..f8a14d70 100644 --- a/src/main/java/com/weis/darklaf/ui/menu/DarkMenuUI.java +++ b/src/main/java/com/weis/darklaf/ui/menu/DarkMenuUI.java @@ -1,688 +1,245 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.menu; import com.weis.darklaf.util.DarkUIUtil; -import com.weis.darklaf.util.LazyActionMap; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import sun.swing.DefaultLookup; -import sun.swing.UIAction; +import sun.swing.MenuItemLayoutHelper; +import sun.swing.SwingUtilities2; import javax.swing.*; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.MenuDragMouseEvent; -import javax.swing.event.MenuDragMouseListener; -import javax.swing.event.MenuKeyEvent; -import javax.swing.event.MenuKeyListener; -import javax.swing.event.MenuListener; -import javax.swing.event.MouseInputListener; import javax.swing.plaf.ComponentUI; -import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicMenuUI; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; -/** - * Code taken from {@link BasicMenuUI} - */ -public class DarkMenuUI extends DarkMenuItemUIBase { +public class DarkMenuUI extends BasicMenuUI { - /* diagnostic aids -- should be false for production builds. */ - private static final boolean TRACE = false; // trace creates and disposes - private static final boolean VERBOSE = false; // show reuse hits/misses - private static final boolean DEBUG = false; // show bad params, misc. - private static boolean crossMenuMnemonic = true; - /** - * The instance of {@code ChangeListener}. - */ - protected ChangeListener changeListener; - /** - * The instance of {@code MenuListener}. - */ - protected MenuListener menuListener; - private int lastMnemonic = 0; - /** - * Uses as the parent of the windowInputMap when selected. - */ - private InputMap selectedWindowInputMap; + protected Icon arrowIconHover; - /** - * Constructs a new instance of {@code BasicMenuUI}. - * - * @param x a component - * @return a new instance of {@code BasicMenuUI} - */ - @NotNull - @Contract(value = "_ -> new", pure = true) public static ComponentUI createUI(final JComponent x) { return new DarkMenuUI(); } - protected static void loadActionMap(@NotNull final LazyActionMap map) { - loadActionMap(map); - map.put(new Actions(Actions.SELECT, null, true)); - } - - private static void appendPath(@NotNull final MenuElement[] path, final MenuElement elem) { - MenuElement[] newPath = new MenuElement[path.length + 1]; - System.arraycopy(path, 0, newPath, 0, path.length); - newPath[path.length] = elem; - MenuSelectionManager.defaultManager().setSelectedPath(newPath); - } - - @NotNull - protected static java.util.List getPopups() { - MenuSelectionManager msm = MenuSelectionManager.defaultManager(); - MenuElement[] p = msm.getSelectedPath(); - - java.util.List list = new ArrayList(p.length); - for (MenuElement element : p) { - if (element instanceof JPopupMenu) { - list.add((JPopupMenu) element); - } - } - return list; - } - - protected static JPopupMenu getLastPopup() { - MenuSelectionManager msm = MenuSelectionManager.defaultManager(); - MenuElement[] p = msm.getSelectedPath(); - JPopupMenu popup = null; - - for (int i = p.length - 1; popup == null && i >= 0; i--) { - if (p[i] instanceof JPopupMenu) { popup = (JPopupMenu) p[i]; } - } - return popup; - } - + @Override protected void installDefaults() { super.installDefaults(); - updateDefaultBackgroundColor(); - ((JMenu) menuItem).setDelay(200); - crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic"); - } - - protected String getPropertyPrefix() { - return "Menu"; - } - - protected void installListeners() { - super.installListeners(); - - if (changeListener == null) { changeListener = createChangeListener(menuItem); } - - if (changeListener != null) { menuItem.addChangeListener(changeListener); } - - if (menuListener == null) { menuListener = createMenuListener(menuItem); } - - if (menuListener != null) { ((JMenu) menuItem).addMenuListener(menuListener); } - } - - protected void installKeyboardActions() { - super.installKeyboardActions(); - updateMnemonicBinding(); - } - - @SuppressWarnings("deprecation") - void updateMnemonicBinding() { - int mnemonic = menuItem.getModel().getMnemonic(); - int[] shortcutKeys = (int[]) DefaultLookup.get(menuItem, this, - "Menu.shortcutKeys"); - if (shortcutKeys == null) { - shortcutKeys = new int[]{KeyEvent.ALT_MASK, - KeyEvent.ALT_MASK | KeyEvent.ALT_GRAPH_MASK}; - } - if (mnemonic == lastMnemonic) { - return; - } - InputMap windowInputMap = SwingUtilities.getUIInputMap( - menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW); - if (lastMnemonic != 0 && windowInputMap != null) { - for (int shortcutKey : shortcutKeys) { - windowInputMap.remove(KeyStroke.getKeyStroke - (lastMnemonic, shortcutKey, false)); - } - } - if (mnemonic != 0) { - if (windowInputMap == null) { - windowInputMap = createInputMap(JComponent. - WHEN_IN_FOCUSED_WINDOW); - SwingUtilities.replaceUIInputMap(menuItem, JComponent. - WHEN_IN_FOCUSED_WINDOW, windowInputMap); - } - for (int shortcutKey : shortcutKeys) { - windowInputMap.put(KeyStroke.getKeyStroke(mnemonic, - shortcutKey, false), "selectMenu"); - } - } - lastMnemonic = mnemonic; - } - - protected void uninstallDefaults() { - menuItem.setArmed(false); - menuItem.setSelected(false); - menuItem.resetKeyboardActions(); - super.uninstallDefaults(); + acceleratorFont = UIManager.getFont("MenuItem.font"); + acceleratorForeground = UIManager.getColor("MenuItem.foreground"); + acceleratorSelectionForeground = UIManager.getColor("MenuItem.selectionForeground"); + arrowIconHover = UIManager.getIcon("MenuItem.arrowHover.icon"); } - protected void uninstallListeners() { - super.uninstallListeners(); - - if (changeListener != null) { menuItem.removeChangeListener(changeListener); } - - if (menuListener != null) { ((JMenu) menuItem).removeMenuListener(menuListener); } - - changeListener = null; - menuListener = null; - handler = null; - } - - protected void uninstallKeyboardActions() { - super.uninstallKeyboardActions(); - lastMnemonic = 0; - } - - protected MouseInputListener createMouseInputListener(final JComponent c) { - return getHandler(); - } - - protected DarkMenuItemUIBase.Handler getHandler() { - if (handler == null) { - handler = new DarkMenuUI.Handler(); - } - return handler; - } - - protected MenuDragMouseListener createMenuDragMouseListener(final JComponent c) { - return getHandler(); - } - - protected MenuKeyListener createMenuKeyListener(final JComponent c) { - return (MenuKeyListener) getHandler(); - } - - protected PropertyChangeListener createPropertyChangeListener(final JComponent c) { - return getHandler(); - } - - public Dimension getMinimumSize(final JComponent c) { - return (((JMenu) menuItem).isTopLevelMenu()) ? - c.getPreferredSize() : null; + @Override + public void paint(final Graphics g, final JComponent c) { + paintMenuItem(g, c, checkIcon, getArrowIcon(), + selectionBackground, selectionForeground, + defaultTextIconGap); } - public Dimension getMaximumSize(final JComponent c) { - if (((JMenu) menuItem).isTopLevelMenu()) { - Dimension d = c.getPreferredSize(); - return new Dimension(d.width, Short.MAX_VALUE); - } - return null; - } - - /** - * Returns an instance of {@code ChangeListener}. - * - * @param c a component - * @return an instance of {@code ChangeListener} - */ - protected ChangeListener createChangeListener(final JComponent c) { - return null; - } - - /** - * Returns an instance of {@code MenuListener}. - * - * @param c a component - * @return an instance of {@code MenuListener} - */ - protected MenuListener createMenuListener(final JComponent c) { - return null; + protected Icon getArrowIcon() { + boolean hover = menuItem.getModel().isArmed() + || (menuItem instanceof JMenu && menuItem.getModel().isSelected()); + return hover ? arrowIconHover : arrowIcon; } - /* - * Set the background color depending on whether this is a toplevel menu - * in a menubar or a submenu of another menu. - */ - private void updateDefaultBackgroundColor() { - if (!UIManager.getBoolean("Menu.useMenuBarBackgroundForTopLevel")) { - return; - } - JMenu menu = (JMenu) menuItem; - if (menu.getBackground() instanceof UIResource) { - if (menu.isTopLevelMenu()) { - menu.setBackground(UIManager.getColor("MenuBar.background")); + protected void paintCheckIcon(final Graphics g, @NotNull final MenuItemLayoutHelper lh, + final MenuItemLayoutHelper.LayoutResult lr, + final Color holdc, final Color foreground) { + if (lh.getCheckIcon() != null) { + ButtonModel model = lh.getMenuItem().getModel(); + if (model.isArmed() || (lh.getMenuItem() instanceof JMenu + && model.isSelected())) { + g.setColor(foreground); } else { - menu.setBackground(UIManager.getColor(getPropertyPrefix() + ".background")); + g.setColor(holdc); + } + if (lh.useCheckAndArrow()) { + lh.getCheckIcon().paintIcon(lh.getMenuItem(), g, + lr.getCheckRect().x, lr.getCheckRect().y); } + g.setColor(holdc); } } - void installLazyActionMap() { - LazyActionMap.installLazyActionMap(menuItem, BasicMenuUI.class, - getPropertyPrefix() + ".actionMap"); - } - - /** - * Sets timer to the {@code menu}. - * - * @param menu an instance of {@code JMenu}. - */ - protected void setupPostTimer(@NotNull final JMenu menu) { - Timer timer = new Timer(menu.getDelay(), new Actions(Actions.SELECT, menu, false)); - timer.setRepeats(false); - timer.start(); - } - - private static class Actions extends UIAction { - private static final String SELECT = "selectMenu"; - - // NOTE: This will be null if the action is registered in the - // ActionMap. For the timer use it will be non-null. - private JMenu menu; - private boolean force = false; - - Actions(final String key, final JMenu menu, final boolean shouldForce) { - super(key); - this.menu = menu; - this.force = shouldForce; - } - - public void actionPerformed(final ActionEvent e) { - JMenu menu = getMenu(e); - if (!crossMenuMnemonic) { - JPopupMenu pm = getLastPopup(); - if (pm != null && pm != menu.getParent()) { - return; - } - } - - final MenuSelectionManager defaultManager = MenuSelectionManager.defaultManager(); - if (force) { - Container cnt = menu.getParent(); - if (cnt instanceof JMenuBar) { - MenuElement[] me; - MenuElement[] subElements; - - subElements = menu.getPopupMenu().getSubElements(); - if (subElements.length > 0) { - me = new MenuElement[4]; - me[0] = (MenuElement) cnt; - me[1] = menu; - me[2] = menu.getPopupMenu(); - me[3] = subElements[0]; - } else { - me = new MenuElement[3]; - me[0] = (MenuElement) cnt; - me[1] = menu; - me[2] = menu.getPopupMenu(); - } - defaultManager.setSelectedPath(me); + protected void paintIcon(final Graphics g, @NotNull final MenuItemLayoutHelper lh, + final MenuItemLayoutHelper.LayoutResult lr, final Color holdc) { + if (lh.getIcon() != null) { + Icon icon; + ButtonModel model = lh.getMenuItem().getModel(); + if (!model.isEnabled()) { + icon = lh.getMenuItem().getDisabledIcon(); + } else if (model.isPressed() && model.isArmed()) { + icon = lh.getMenuItem().getPressedIcon(); + if (icon == null) { + // Use default icon + icon = lh.getMenuItem().getIcon(); } } else { - MenuElement[] path = defaultManager.getSelectedPath(); - if (path.length > 0 && path[path.length - 1] == menu) { - appendPath(path, menu.getPopupMenu()); - } + icon = lh.getMenuItem().getIcon(); } - } - private JMenu getMenu(final ActionEvent e) { - if (e.getSource() instanceof JMenu) { - return (JMenu) e.getSource(); + if (icon != null) { + icon.paintIcon(lh.getMenuItem(), g, lr.getIconRect().x, lr.getIconRect().y); + g.setColor(holdc); } - return menu; - } - - @Override - public boolean accept(final Object c) { - if (c instanceof JMenu) { - return ((JMenu) c).isEnabled(); - } - return true; - } - } - - /** - * Instantiated and used by a menu item to handle the current menu selection - * from mouse events. A MouseInputHandler processes and forwards all mouse events - * to a shared instance of the MenuSelectionManager. - *

- * This class is protected so that it can be subclassed by other look and - * feels to implement their own mouse handling behavior. All overridden - * methods should call the parent methods so that the menu selection - * is correct. - * - * @see javax.swing.MenuSelectionManager - * @since 1.4 - */ - protected class MouseInputHandler implements MouseInputListener { - // NOTE: This class exists only for backward compatibility. All - // its functionality has been moved into Handler. If you need to add - // new functionality add it to the Handler, but make sure this - // class calls into the Handler. - - public void mouseClicked(final MouseEvent e) { - getHandler().mouseClicked(e); - } - - /** - * Invoked when the mouse has been clicked on the menu. This - * method clears or sets the selection path of the - * MenuSelectionManager. - * - * @param e the mouse event - */ - public void mousePressed(final MouseEvent e) { - getHandler().mousePressed(e); - } - - /** - * Invoked when the mouse has been released on the menu. Delegates the - * mouse event to the MenuSelectionManager. - * - * @param e the mouse event - */ - public void mouseReleased(final MouseEvent e) { - getHandler().mouseReleased(e); - } - - /** - * Invoked when the cursor enters the menu. This method sets the selected - * path for the MenuSelectionManager and handles the case - * in which a menu item is used to pop up an additional menu, as in a - * hierarchical menu system. - * - * @param e the mouse event; not used - */ - public void mouseEntered(final MouseEvent e) { - getHandler().mouseEntered(e); - } - - public void mouseExited(final MouseEvent e) { - getHandler().mouseExited(e); - } - - /** - * Invoked when a mouse button is pressed on the menu and then dragged. - * Delegates the mouse event to the MenuSelectionManager. - * - * @param e the mouse event - * @see java.awt.event.MouseMotionListener#mouseDragged - */ - public void mouseDragged(final MouseEvent e) { - getHandler().mouseDragged(e); - } - - public void mouseMoved(final MouseEvent e) { - getHandler().mouseMoved(e); } } - /** - * As of Java 2 platform 1.4, this previously undocumented class - * is now obsolete. KeyBindings are now managed by the popup menu. - */ - public class ChangeHandler implements ChangeListener { - /** - * The instance of {@code JMenu}. - */ - public JMenu menu; - - /** - * The instance of {@code BasicMenuUI}. - */ - public BasicMenuUI ui; - - /** - * {@code true} if an item of popup menu is selected. - */ - public boolean isSelected = false; - - /** - * The component that was focused. - */ - public Component wasFocused; - - /** - * Constructs a new instance of {@code ChangeHandler}. - * - * @param m an instance of {@code JMenu} - * @param ui an instance of {@code BasicMenuUI} - */ - public ChangeHandler(final JMenu m, final BasicMenuUI ui) { - menu = m; - this.ui = ui; - } - - public void stateChanged(final ChangeEvent e) { + protected void paintText(final Graphics g, @NotNull final MenuItemLayoutHelper lh, + final MenuItemLayoutHelper.LayoutResult lr) { + if (!lh.getText().isBlank()) { + if (lh.getHtmlView() != null) { + // Text is HTML + lh.getHtmlView().paint(g, lr.getTextRect()); + } else { + // Text isn't HTML + paintText(g, lh.getMenuItem(), lr.getTextRect(), lh.getText()); + } } } - private class Handler extends DarkMenuItemUIBase.Handler implements MenuKeyListener { - // - // MouseInputListener - // - public void mouseClicked(final MouseEvent e) { - } - - /** - * Invoked when the mouse has been clicked on the menu. This - * method clears or sets the selection path of the - * MenuSelectionManager. - * - * @param e the mouse event - */ - public void mousePressed(final MouseEvent e) { - JMenu menu = (JMenu) menuItem; - if (!menu.isEnabled()) { return; } - - MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - if (menu.isTopLevelMenu()) { - if (menu.isSelected() && menu.getPopupMenu().isShowing()) { - manager.clearSelectedPath(); + protected void paintAccText(final Graphics g, final MenuItemLayoutHelper lh, + final MenuItemLayoutHelper.LayoutResult lr) { + rightAlignAccText(lh, lr); + if (!lh.getAccText().isBlank()) { + ButtonModel model = lh.getMenuItem().getModel(); + g.setFont(lh.getAccFontMetrics().getFont()); + if (!model.isEnabled()) { + // *** paint the accText disabled + if (disabledForeground != null) { + g.setColor(disabledForeground); + SwingUtilities2.drawString(lh.getMenuItem(), g, + lh.getAccText(), lr.getAccRect().x, + lr.getAccRect().y + lh.getAccFontMetrics().getAscent()); } else { - Container cnt = menu.getParent(); - if (cnt instanceof JMenuBar) { - MenuElement[] me = new MenuElement[2]; - me[0] = (MenuElement) cnt; - me[1] = menu; - manager.setSelectedPath(me); - } + g.setColor(lh.getMenuItem().getBackground().brighter()); + SwingUtilities2.drawString(lh.getMenuItem(), g, + lh.getAccText(), lr.getAccRect().x, + lr.getAccRect().y + lh.getAccFontMetrics().getAscent()); + g.setColor(lh.getMenuItem().getBackground().darker()); + SwingUtilities2.drawString(lh.getMenuItem(), g, + lh.getAccText(), lr.getAccRect().x - 1, + lr.getAccRect().y + lh.getFontMetrics().getAscent() - 1); } - } - - MenuElement[] selectedPath = manager.getSelectedPath(); - if (selectedPath.length > 0 && selectedPath[selectedPath.length - 1] != menu.getPopupMenu()) { - if (menu.isTopLevelMenu() || menu.getDelay() == 0) { - appendPath(selectedPath, menu.getPopupMenu()); + } else { + // *** paint the accText normally + if (model.isArmed() + || (lh.getMenuItem() instanceof JMenu + && model.isSelected())) { + g.setColor(acceleratorSelectionForeground); } else { - setupPostTimer(menu); + g.setColor(acceleratorForeground); } + SwingUtilities2.drawString(lh.getMenuItem(), g, lh.getAccText(), + lr.getAccRect().x, lr.getAccRect().y + + lh.getAccFontMetrics().getAscent()); } } + } - /** - * Invoked when the mouse has been released on the menu. Delegates the - * mouse event to the MenuSelectionManager. - * - * @param e the mouse event - */ - public void mouseReleased(final MouseEvent e) { - JMenu menu = (JMenu) menuItem; - if (!menu.isEnabled()) { return; } - MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - manager.processMouseEvent(e); - if (!e.isConsumed()) { manager.clearSelectedPath(); } - } - - /** - * Invoked when the cursor enters the menu. This method sets the selected - * path for the MenuSelectionManager and handles the case - * in which a menu item is used to pop up an additional menu, as in a - * hierarchical menu system. - * - * @param e the mouse event; not used - */ - public void mouseEntered(final MouseEvent e) { - JMenu menu = (JMenu) menuItem; - // only disable the menu highlighting if it's disabled and the property isn't - // true. This allows disabled rollovers to work in WinL&F - if (!menu.isEnabled() && !UIManager.getBoolean("MenuItem.disabledAreNavigable")) { - return; + protected void paintArrowIcon(final Graphics g, @NotNull final MenuItemLayoutHelper lh, + final MenuItemLayoutHelper.LayoutResult lr, + final Color foreground) { + if (lh.getArrowIcon() != null) { + ButtonModel model = lh.getMenuItem().getModel(); + if (model.isArmed() || (lh.getMenuItem() instanceof JMenu + && model.isSelected())) { + g.setColor(foreground); } - - MenuSelectionManager manager = MenuSelectionManager.defaultManager(); - MenuElement[] selectedPath = manager.getSelectedPath(); - if (!menu.isTopLevelMenu()) { - if (!(selectedPath.length > 0 && selectedPath[selectedPath.length - 1] == menu.getPopupMenu())) { - if (menu.getDelay() == 0) { - appendPath(getPath(), menu.getPopupMenu()); - } else { - manager.setSelectedPath(getPath()); - setupPostTimer(menu); - } - } - } else { - if (selectedPath.length > 0 && selectedPath[0] == menu.getParent()) { - MenuElement[] newPath = new MenuElement[3]; - // A top level menu's parent is by definition - // a JMenuBar - newPath[0] = (MenuElement) menu.getParent(); - newPath[1] = menu; - if (getLastPopup() != null) { - newPath[2] = menu.getPopupMenu(); - } - manager.setSelectedPath(newPath); - } + if (lh.useCheckAndArrow()) { + lh.getArrowIcon().paintIcon(lh.getMenuItem(), g, + lr.getArrowRect().x, lr.getArrowRect().y); } } + } - public void mouseExited(final MouseEvent e) { - } - - /** - * Invoked when a mouse button is pressed on the menu and then dragged. - * Delegates the mouse event to the MenuSelectionManager. - * - * @param e the mouse event - * @see java.awt.event.MouseMotionListener#mouseDragged - */ - public void mouseDragged(final MouseEvent e) { - JMenu menu = (JMenu) menuItem; - if (!menu.isEnabled()) { return; } - MenuSelectionManager.defaultManager().processMouseEvent(e); + private static void rightAlignAccText(@NotNull final MenuItemLayoutHelper lh, + @NotNull final MenuItemLayoutHelper.LayoutResult lr) { + var accRect = lr.getAccRect(); + ButtonModel model = lh.getMenuItem().getModel(); + if (model.isEnabled()) { + accRect.x = lh.getViewRect().x + lh.getViewRect().width + - lh.getMenuItem().getIconTextGap() - lr.getAccRect().width; } + } - public void mouseMoved(final MouseEvent e) { - } + protected void paintMenuItem(@NotNull final Graphics g, final JComponent c, + final Icon checkIcon, final Icon arrowIcon, + final Color background, final Color foreground, + final int defaultTextIconGap) { + // Save original graphics font and color + Font holdf = g.getFont(); + Color holdc = g.getColor(); - // - // MenuDragHandler - // - public void menuDragMouseEntered(final MenuDragMouseEvent e) { - } + JMenuItem mi = (JMenuItem) c; + g.setFont(mi.getFont()); - public void menuDragMouseDragged(final MenuDragMouseEvent e) { - if (!menuItem.isEnabled()) return; + Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); + DarkUIUtil.applyInsets(viewRect, mi.getInsets()); - MenuSelectionManager manager = e.getMenuSelectionManager(); - MenuElement[] path = e.getPath(); + MenuItemLayoutHelper lh = new MenuItemLayoutHelper(mi, checkIcon, + arrowIcon, viewRect, defaultTextIconGap, acceleratorDelimiter, + mi.getComponentOrientation().isLeftToRight(), mi.getFont(), + acceleratorFont, MenuItemLayoutHelper.useCheckAndArrow(menuItem), + getPropertyPrefix()); + MenuItemLayoutHelper.LayoutResult lr = lh.layoutMenuItem(); - Point p = e.getPoint(); - if (p.x >= 0 && p.x < menuItem.getWidth() && - p.y >= 0 && p.y < menuItem.getHeight()) { - JMenu menu = (JMenu) menuItem; - MenuElement[] selectedPath = manager.getSelectedPath(); - if (!(selectedPath.length > 0 && - selectedPath[selectedPath.length - 1] == - menu.getPopupMenu())) { - if (menu.isTopLevelMenu() || - menu.getDelay() == 0 || - e.getID() == MouseEvent.MOUSE_DRAGGED) { - appendPath(path, menu.getPopupMenu()); - } else { - manager.setSelectedPath(path); - setupPostTimer(menu); - } - } - } else if (e.getID() == MouseEvent.MOUSE_RELEASED) { - Component comp = manager.componentForPoint(e.getComponent(), e.getPoint()); - if (comp == null) { manager.clearSelectedPath(); } - } - - } + paintBackground(g, mi, background); + paintCheckIcon(g, lh, lr, holdc, foreground); + paintIcon(g, lh, lr, holdc); + g.setColor(foreground); + paintText(g, lh, lr); + paintAccText(g, lh, lr); + paintArrowIcon(g, lh, lr, foreground); - public void menuDragMouseExited(final MenuDragMouseEvent e) { - } + // Restore original graphics font and color + g.setColor(holdc); + g.setFont(holdf); + } - public void menuDragMouseReleased(final MenuDragMouseEvent e) { - } + @Override + protected void paintBackground(@NotNull final Graphics g, @NotNull final JMenuItem menuItem, final Color bgColor) { + ButtonModel model = menuItem.getModel(); + Color oldColor = g.getColor(); + int menuWidth = menuItem.getWidth(); + int menuHeight = menuItem.getHeight() + 1; - // - // PropertyChangeListener - // - public void propertyChange(final PropertyChangeEvent e) { - if (Objects.equals(e.getPropertyName(), AbstractButton. - MNEMONIC_CHANGED_PROPERTY)) { - updateMnemonicBinding(); + boolean parentOpaque = menuItem.getParent().isOpaque(); + if (menuItem.isOpaque() && parentOpaque) { + if (model.isArmed() || (menuItem instanceof JMenu && model.isSelected())) { + g.setColor(bgColor); + g.fillRect(0, 0, menuWidth, menuHeight); } else { - if (e.getPropertyName().equals("ancestor")) { - updateDefaultBackgroundColor(); - } - super.propertyChange(e); - } - } - - // - // MenuKeyListener - // - - /** - * Open the Menu - */ - public void menuKeyTyped(final MenuKeyEvent e) { - if (!crossMenuMnemonic && getLastPopup() != null) { - // when crossMenuMnemonic is not set, we don't open a toplevel - // menu if another toplevel menu is already open - return; - } - - if (getPopups().size() != 0) { - //Fix 6939261: to return in case not on the main menu - //and has a pop-up. - //after return code will be handled in BasicPopupMenuUI.java - return; - } - - char key = Character.toLowerCase((char) menuItem.getMnemonic()); - MenuElement[] path = e.getPath(); - if (key == Character.toLowerCase(e.getKeyChar())) { - JPopupMenu popupMenu = ((JMenu) menuItem).getPopupMenu(); - ArrayList newList = new ArrayList<>(Arrays.asList(path)); - newList.add(popupMenu); - MenuElement[] subs = popupMenu.getSubElements(); - MenuElement sub = DarkUIUtil.findEnabledChild(subs, -1, true); - if (sub != null) { - newList.add(sub); - } - MenuSelectionManager manager = e.getMenuSelectionManager(); - MenuElement[] newPath = new MenuElement[0]; - newPath = newList.toArray(newPath); - manager.setSelectedPath(newPath); - e.consume(); - } - } - - public void menuKeyPressed(final MenuKeyEvent e) { - } - - public void menuKeyReleased(final MenuKeyEvent e) { + g.setColor(menuItem.getBackground()); + g.fillRect(0, 0, menuWidth, menuHeight); + } + g.setColor(oldColor); + } else if (model.isArmed() || (menuItem instanceof JMenu && + model.isSelected())) { + g.setColor(bgColor); + g.fillRect(0, 0, menuWidth, menuHeight); + g.setColor(oldColor); } } } diff --git a/src/main/java/com/weis/darklaf/ui/scrollpane/DarkScrollBarUI.java b/src/main/java/com/weis/darklaf/ui/scrollpane/DarkScrollBarUI.java index 885f89b7..2b48b3a0 100644 --- a/src/main/java/com/weis/darklaf/ui/scrollpane/DarkScrollBarUI.java +++ b/src/main/java/com/weis/darklaf/ui/scrollpane/DarkScrollBarUI.java @@ -28,7 +28,6 @@ import com.weis.darklaf.util.Animator; import com.weis.darklaf.util.DarkUIUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -71,9 +70,13 @@ public class DarkScrollBarUI extends BasicScrollBarUI { if (scrollbar.getOrientation() == VERTICAL && !e.isShiftDown() || scrollbar.getOrientation() == HORIZONTAL && e.isShiftDown()) { scrollbar.setValueIsAdjusting(true); + var sp = scrollbar.getClientProperty("JScrollBar.scrollPaneParent"); if (scrollbar.getParent() instanceof JScrollPane) { doScroll(scrollbar, ((JScrollPane) scrollbar.getParent()).getViewport(), e, scrollbar.getParent().getComponentOrientation().isLeftToRight()); + } else if (sp instanceof JScrollPane) { + doScroll(scrollbar, ((JScrollPane) sp).getViewport(), e, + scrollbar.getParent().getComponentOrientation().isLeftToRight()); } else { doScroll(scrollbar, null, e, scrollbar.getComponentOrientation().isLeftToRight()); } @@ -88,18 +91,15 @@ public class DarkScrollBarUI extends BasicScrollBarUI { private Animator thumbFadeinAnimator; private boolean mouseOverTrack = false; private boolean mouseOverThumb = false; - private final MouseMotionListener mouseMotionListener = new MouseMovementListener() { - @Override - public void mouseMoved(@Nullable final MouseEvent e) { - if (e == null) { - return; - } - boolean overThumb = isOverThumb(e.getPoint()); - if (overThumb != mouseOverThumb) { - mouseOverThumb = overThumb; - if (!scrollbar.getValueIsAdjusting()) { - resetThumbAnimator(); - } + private final MouseMotionListener mouseMotionListener = (MouseMovementListener) e -> { + if (e == null) { + return; + } + boolean overThumb = isOverThumb(e.getPoint()); + if (overThumb != mouseOverThumb) { + mouseOverThumb = overThumb; + if (!scrollbar.getValueIsAdjusting()) { + resetThumbAnimator(); } } }; diff --git a/src/main/java/com/weis/darklaf/ui/tabframe/DarkTabFrameTabLabelUI.java b/src/main/java/com/weis/darklaf/ui/tabframe/DarkTabFrameTabLabelUI.java index 1c2452ff..91155bc0 100644 --- a/src/main/java/com/weis/darklaf/ui/tabframe/DarkTabFrameTabLabelUI.java +++ b/src/main/java/com/weis/darklaf/ui/tabframe/DarkTabFrameTabLabelUI.java @@ -151,7 +151,6 @@ public class DarkTabFrameTabLabelUI extends DarkLabelUI implements PropertyChang if (tabFrame == null) return; int acc = tabComponent.getAccelerator(); if (acc < 0) return; - System.out.println("install"); tabFrame.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(UIManager.getString("TabFrame.acceleratorKeyCode") + " " + acc), "accelerator_" + acc); diff --git a/src/main/java/com/weis/darklaf/ui/table/BasicTableUIBridge.java b/src/main/java/com/weis/darklaf/ui/table/BasicTableUIBridge.java new file mode 100644 index 00000000..045832f6 --- /dev/null +++ b/src/main/java/com/weis/darklaf/ui/table/BasicTableUIBridge.java @@ -0,0 +1,2189 @@ +/* + * MIT License + * + * Copyright (c) 2019 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.weis.darklaf.ui.table; + +import com.weis.darklaf.util.DarkUIUtil; +import com.weis.darklaf.util.LazyActionMap; +import org.jdesktop.swingx.plaf.basic.core.BasicTransferable; +import org.jdesktop.swingx.plaf.basic.core.DragRecognitionSupport; +import sun.swing.DefaultLookup; +import sun.swing.SwingUtilities2; +import sun.swing.UIAction; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.MouseInputListener; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.TableHeaderUI; +import javax.swing.plaf.UIResource; +import javax.swing.plaf.basic.BasicTableUI; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import java.awt.*; +import java.awt.datatransfer.Transferable; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Enumeration; + +public class BasicTableUIBridge extends BasicTableUI { + protected static final StringBuilder BASELINE_COMPONENT_KEY = + new StringBuilder("Table.baselineComponent"); + +// +// Instance Variables +// + + // The JTable that is delegating the painting to this UI. + protected static final TransferHandler defaultTransferHandler = new TableTransferHandler(); + /** + * The instance of {@code JTable}. + */ + protected JTable table; + /** + * The instance of {@code CellRendererPane}. + */ + protected CellRendererPane rendererPane; + /** + * {@code KeyListener} that are attached to the {@code JTable}. + */ + protected KeyListener keyListener; + /** + * {@code FocusListener} that are attached to the {@code JTable}. + */ + protected FocusListener focusListener; + /** + * {@code MouseInputListener} that are attached to the {@code JTable}. + */ + protected MouseInputListener mouseInputListener; + protected Handler handler; + +// +// Helper class for keyboard actions +// + /** + * Local cache of Table's client property "Table.isFileList" + */ + protected boolean isFileList = false; + + +// +// The Table's Key listener +// + + /** + * Returns a new instance of {@code BasicTableUI}. + * + * @param c a component + * @return a new instance of {@code BasicTableUI} + */ + public static ComponentUI createUI(final JComponent c) { + return new BasicTableUI(); + } + +// +// The Table's focus listener +// + + protected static int getAdjustedLead(final JTable table, final boolean row) { + return row ? getAdjustedLead(table, row, table.getSelectionModel()) + : getAdjustedLead(table, row, table.getColumnModel().getSelectionModel()); + } + +// +// The Table's mouse and mouse motion listeners +// + + protected static int getAdjustedLead(final JTable table, + final boolean row, + final ListSelectionModel model) { + + int index = model.getLeadSelectionIndex(); + int compare = row ? table.getRowCount() : table.getColumnCount(); + return index < compare ? index : -1; + } + + /* + * Returns true if the given point is outside the preferredSize of the + * item at the given row of the table. (Column must be 0). + * Returns false if the "Table.isFileList" client property is not set. + */ + protected boolean pointOutsidePrefSize(final int row, final int column, final Point p) { + if (!isFileList) { + return false; + } + + return SwingUtilities2.pointOutsidePrefSize(table, row, column, p); + } + + protected static class Actions extends UIAction { + protected static final String CANCEL_EDITING = "cancel"; + protected static final String SELECT_ALL = "selectAll"; + protected static final String CLEAR_SELECTION = "clearSelection"; + protected static final String START_EDITING = "startEditing"; + + protected static final String NEXT_ROW = "selectNextRow"; + protected static final String NEXT_ROW_CELL = "selectNextRowCell"; + protected static final String NEXT_ROW_EXTEND_SELECTION = + "selectNextRowExtendSelection"; + protected static final String NEXT_ROW_CHANGE_LEAD = + "selectNextRowChangeLead"; + protected static final String PREVIOUS_ROW = "selectPreviousRow"; + protected static final String PREVIOUS_ROW_CELL = "selectPreviousRowCell"; + protected static final String PREVIOUS_ROW_EXTEND_SELECTION = + "selectPreviousRowExtendSelection"; + protected static final String PREVIOUS_ROW_CHANGE_LEAD = + "selectPreviousRowChangeLead"; + + protected static final String NEXT_COLUMN = "selectNextColumn"; + protected static final String NEXT_COLUMN_CELL = "selectNextColumnCell"; + protected static final String NEXT_COLUMN_EXTEND_SELECTION = + "selectNextColumnExtendSelection"; + protected static final String NEXT_COLUMN_CHANGE_LEAD = + "selectNextColumnChangeLead"; + protected static final String PREVIOUS_COLUMN = "selectPreviousColumn"; + protected static final String PREVIOUS_COLUMN_CELL = + "selectPreviousColumnCell"; + protected static final String PREVIOUS_COLUMN_EXTEND_SELECTION = + "selectPreviousColumnExtendSelection"; + protected static final String PREVIOUS_COLUMN_CHANGE_LEAD = + "selectPreviousColumnChangeLead"; + + protected static final String SCROLL_LEFT_CHANGE_SELECTION = + "scrollLeftChangeSelection"; + protected static final String SCROLL_LEFT_EXTEND_SELECTION = + "scrollLeftExtendSelection"; + protected static final String SCROLL_RIGHT_CHANGE_SELECTION = + "scrollRightChangeSelection"; + protected static final String SCROLL_RIGHT_EXTEND_SELECTION = + "scrollRightExtendSelection"; + + protected static final String SCROLL_UP_CHANGE_SELECTION = + "scrollUpChangeSelection"; + protected static final String SCROLL_UP_EXTEND_SELECTION = + "scrollUpExtendSelection"; + protected static final String SCROLL_DOWN_CHANGE_SELECTION = + "scrollDownChangeSelection"; + protected static final String SCROLL_DOWN_EXTEND_SELECTION = + "scrollDownExtendSelection"; + + protected static final String FIRST_COLUMN = + "selectFirstColumn"; + protected static final String FIRST_COLUMN_EXTEND_SELECTION = + "selectFirstColumnExtendSelection"; + protected static final String LAST_COLUMN = + "selectLastColumn"; + protected static final String LAST_COLUMN_EXTEND_SELECTION = + "selectLastColumnExtendSelection"; + + protected static final String FIRST_ROW = + "selectFirstRow"; + protected static final String FIRST_ROW_EXTEND_SELECTION = + "selectFirstRowExtendSelection"; + protected static final String LAST_ROW = + "selectLastRow"; + protected static final String LAST_ROW_EXTEND_SELECTION = + "selectLastRowExtendSelection"; + + // add the lead item to the selection without changing lead or anchor + protected static final String ADD_TO_SELECTION = "addToSelection"; + + // toggle the selected state of the lead item and move the anchor to it + protected static final String TOGGLE_AND_ANCHOR = "toggleAndAnchor"; + + // extend the selection to the lead item + protected static final String EXTEND_TO = "extendTo"; + + // move the anchor to the lead and ensure only that item is selected + protected static final String MOVE_SELECTION_TO = "moveSelectionTo"; + + // give focus to the JTableHeader, if one exists + protected static final String FOCUS_HEADER = "focusHeader"; + + protected int dx; + protected int dy; + protected boolean extend; + protected boolean inSelection; + + // horizontally, forwards always means right, + // regardless of component orientation + protected boolean forwards; + protected boolean vertically; + protected boolean toLimit; + + protected int leadRow; + protected int leadColumn; + + Actions(final String name) { + super(name); + } + + Actions(final String name, final boolean extend, final boolean forwards, + final boolean vertically, final boolean toLimit) { + this(name, 0, 0, extend, false); + this.forwards = forwards; + this.vertically = vertically; + this.toLimit = toLimit; + } + + Actions(final String name, int dx, int dy, final boolean extend, + final boolean inSelection) { + super(name); + + // Actions spcifying true for "inSelection" are + // fairly sensitive to bad parameter values. They require + // that one of dx and dy be 0 and the other be -1 or 1. + // Bogus parameter values could cause an infinite loop. + // To prevent any problems we massage the params here + // and complain if we get something we can't deal with. + if (inSelection) { + this.inSelection = true; + + // look at the sign of dx and dy only + dx = sign(dx); + dy = sign(dy); + + // make sure one is zero, but not both + assert (dx == 0 || dy == 0) && !(dx == 0 && dy == 0); + } + + this.dx = dx; + this.dy = dy; + this.extend = extend; + } + + protected static int sign(final int num) { + return Integer.compare(num, 0); + } + + public void actionPerformed(final ActionEvent e) { + String key = getName(); + JTable table = (JTable) e.getSource(); + + ListSelectionModel rsm = table.getSelectionModel(); + leadRow = getAdjustedLead(table, true, rsm); + + ListSelectionModel csm = table.getColumnModel().getSelectionModel(); + leadColumn = getAdjustedLead(table, false, csm); + + if (key.equals(SCROLL_LEFT_CHANGE_SELECTION) || // Paging Actions + key.equals(SCROLL_LEFT_EXTEND_SELECTION) || + key.equals(SCROLL_RIGHT_CHANGE_SELECTION) || + key.equals(SCROLL_RIGHT_EXTEND_SELECTION) || + key.equals(SCROLL_UP_CHANGE_SELECTION) || + key.equals(SCROLL_UP_EXTEND_SELECTION) || + key.equals(SCROLL_DOWN_CHANGE_SELECTION) || + key.equals(SCROLL_DOWN_EXTEND_SELECTION) || + key.equals(FIRST_COLUMN) || + key.equals(FIRST_COLUMN_EXTEND_SELECTION) || + key.equals(FIRST_ROW) || + key.equals(FIRST_ROW_EXTEND_SELECTION) || + key.equals(LAST_COLUMN) || + key.equals(LAST_COLUMN_EXTEND_SELECTION) || + key.equals(LAST_ROW) || + key.equals(LAST_ROW_EXTEND_SELECTION)) { + if (toLimit) { + if (vertically) { + int rowCount = table.getRowCount(); + this.dx = 0; + this.dy = forwards ? rowCount : -rowCount; + } else { + int colCount = table.getColumnCount(); + this.dx = forwards ? colCount : -colCount; + this.dy = 0; + } + } else { + if (!(SwingUtilities.getUnwrappedParent(table).getParent() instanceof + JScrollPane)) { + return; + } + + Dimension delta = table.getParent().getSize(); + + if (vertically) { + Rectangle r = table.getCellRect(leadRow, 0, true); + if (forwards) { + // scroll by at least one cell + r.y += Math.max(delta.height, r.height); + } else { + r.y -= delta.height; + } + + this.dx = 0; + int newRow = table.rowAtPoint(r.getLocation()); + if (newRow == -1 && forwards) { + newRow = table.getRowCount(); + } + this.dy = newRow - leadRow; + } else { + Rectangle r = table.getCellRect(0, leadColumn, true); + + if (forwards) { + // scroll by at least one cell + r.x += Math.max(delta.width, r.width); + } else { + r.x -= delta.width; + } + + int newColumn = table.columnAtPoint(r.getLocation()); + if (newColumn == -1) { + boolean ltr = table.getComponentOrientation().isLeftToRight(); + + newColumn = forwards ? (ltr ? table.getColumnCount() : 0) + : (ltr ? 0 : table.getColumnCount()); + + } + this.dx = newColumn - leadColumn; + this.dy = 0; + } + } + } + switch (key) { + case NEXT_ROW: + case NEXT_ROW_CELL: + case NEXT_ROW_EXTEND_SELECTION: + case NEXT_ROW_CHANGE_LEAD: + case NEXT_COLUMN: + case NEXT_COLUMN_CELL: + case NEXT_COLUMN_EXTEND_SELECTION: + case NEXT_COLUMN_CHANGE_LEAD: + case PREVIOUS_ROW: + case PREVIOUS_ROW_CELL: + case PREVIOUS_ROW_EXTEND_SELECTION: + case PREVIOUS_ROW_CHANGE_LEAD: + case PREVIOUS_COLUMN: + case PREVIOUS_COLUMN_CELL: + case PREVIOUS_COLUMN_EXTEND_SELECTION: + case PREVIOUS_COLUMN_CHANGE_LEAD: + case SCROLL_LEFT_CHANGE_SELECTION: + case SCROLL_LEFT_EXTEND_SELECTION: + case SCROLL_RIGHT_CHANGE_SELECTION: + case SCROLL_RIGHT_EXTEND_SELECTION: + case SCROLL_UP_CHANGE_SELECTION: + case SCROLL_UP_EXTEND_SELECTION: + case SCROLL_DOWN_CHANGE_SELECTION: + case SCROLL_DOWN_EXTEND_SELECTION: + case FIRST_COLUMN: + case FIRST_COLUMN_EXTEND_SELECTION: + case FIRST_ROW: + case FIRST_ROW_EXTEND_SELECTION: + case LAST_COLUMN: + case LAST_COLUMN_EXTEND_SELECTION: + case LAST_ROW: + case LAST_ROW_EXTEND_SELECTION: + + if (table.isEditing() && + !table.getCellEditor().stopCellEditing()) { + return; + } + + // Unfortunately, this strategy introduces bugs because + // of the asynchronous nature of requestFocus() call below. + // Introducing a delay with invokeLater() makes this work + // in the typical case though race conditions then allow + // focus to disappear altogether. The right solution appears + // to be to fix requestFocus() so that it queues a request + // for the focus regardless of who owns the focus at the + // time the call to requestFocus() is made. The optimisation + // to ignore the call to requestFocus() when the component + // already has focus may ligitimately be made as the + // request focus event is dequeued, not before. + + // boolean wasEditingWithFocus = table.isEditing() && + // table.getEditorComponent().isFocusOwner(); + + boolean changeLead = false; + if (key.equals(NEXT_ROW_CHANGE_LEAD) || key.equals(PREVIOUS_ROW_CHANGE_LEAD)) { + changeLead = (rsm.getSelectionMode() + == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } else if (key.equals(NEXT_COLUMN_CHANGE_LEAD) || key.equals(PREVIOUS_COLUMN_CHANGE_LEAD)) { + changeLead = (csm.getSelectionMode() + == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + + if (changeLead) { + moveWithinTableRange(table, dx, dy); + if (dy != 0) { + // casting should be safe since the action is only enabled + // for DefaultListSelectionModel + ((DefaultListSelectionModel) rsm).moveLeadSelectionIndex(leadRow); + if (getAdjustedLead(table, false, csm) == -1 + && table.getColumnCount() > 0) { + + ((DefaultListSelectionModel) csm).moveLeadSelectionIndex(0); + } + } else { + // casting should be safe since the action is only enabled + // for DefaultListSelectionModel + ((DefaultListSelectionModel) csm).moveLeadSelectionIndex(leadColumn); + if (getAdjustedLead(table, true, rsm) == -1 + && table.getRowCount() > 0) { + + ((DefaultListSelectionModel) rsm).moveLeadSelectionIndex(0); + } + } + + Rectangle cellRect = table.getCellRect(leadRow, leadColumn, false); + if (cellRect != null) { + table.scrollRectToVisible(cellRect); + } + } else if (!inSelection) { + moveWithinTableRange(table, dx, dy); + table.changeSelection(leadRow, leadColumn, false, extend); + } else { + if (table.getRowCount() <= 0 || table.getColumnCount() <= 0) { + // bail - don't try to move selection on an empty table + return; + } + + if (moveWithinSelectedRange(table, dx, dy, rsm, csm)) { + // this is the only way we have to set both the lead + // and the anchor without changing the selection + if (rsm.isSelectedIndex(leadRow)) { + rsm.addSelectionInterval(leadRow, leadRow); + } else { + rsm.removeSelectionInterval(leadRow, leadRow); + } + + if (csm.isSelectedIndex(leadColumn)) { + csm.addSelectionInterval(leadColumn, leadColumn); + } else { + csm.removeSelectionInterval(leadColumn, leadColumn); + } + + Rectangle cellRect = table.getCellRect(leadRow, leadColumn, false); + if (cellRect != null) { + table.scrollRectToVisible(cellRect); + } + } else { + table.changeSelection(leadRow, leadColumn, + false, false); + } + } + + /* + if (wasEditingWithFocus) { + table.editCellAt(leadRow, leadColumn); + final Component editorComp = table.getEditorComponent(); + if (editorComp != null) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + editorComp.requestFocus(); + } + }); + } + } + */ + break; + case CANCEL_EDITING: + table.removeEditor(); + break; + case SELECT_ALL: + table.selectAll(); + break; + case CLEAR_SELECTION: + table.clearSelection(); + break; + case START_EDITING: + if (!table.hasFocus()) { + CellEditor cellEditor = table.getCellEditor(); + if (cellEditor != null && !cellEditor.stopCellEditing()) { + return; + } + table.requestFocus(); + return; + } + table.editCellAt(leadRow, leadColumn, e); + Component editorComp = table.getEditorComponent(); + if (editorComp != null) { + editorComp.requestFocus(); + } + break; + case ADD_TO_SELECTION: + if (!table.isCellSelected(leadRow, leadColumn)) { + int oldAnchorRow = rsm.getAnchorSelectionIndex(); + int oldAnchorColumn = csm.getAnchorSelectionIndex(); + rsm.setValueIsAdjusting(true); + csm.setValueIsAdjusting(true); + table.changeSelection(leadRow, leadColumn, true, false); + rsm.setAnchorSelectionIndex(oldAnchorRow); + csm.setAnchorSelectionIndex(oldAnchorColumn); + rsm.setValueIsAdjusting(false); + csm.setValueIsAdjusting(false); + } + break; + case TOGGLE_AND_ANCHOR: + table.changeSelection(leadRow, leadColumn, true, false); + break; + case EXTEND_TO: + table.changeSelection(leadRow, leadColumn, false, true); + break; + case MOVE_SELECTION_TO: + table.changeSelection(leadRow, leadColumn, false, false); + break; + case FOCUS_HEADER: + JTableHeader th = table.getTableHeader(); + if (th != null) { + //Set the header's selected column to match the table. + int col = table.getSelectedColumn(); + if (col >= 0) { + TableHeaderUI thUI = th.getUI(); + if (thUI instanceof DarkTableHeaderUI) { + ((DarkTableHeaderUI) thUI).selectColumn(col); + } + } + + //Then give the header the focus. + th.requestFocusInWindow(); + } + break; + } + } + + protected void moveWithinTableRange(final JTable table, final int dx, final int dy) { + leadRow = clipToRange(leadRow + dy, 0, table.getRowCount()); + leadColumn = clipToRange(leadColumn + dx, 0, table.getColumnCount()); + } + + /** + * Called to move within the selected range of the given JTable. + * This method uses the table's notion of selection, which is + * important to allow the user to navigate between items visually + * selected on screen. This notion may or may not be the same as + * what could be determined by directly querying the selection models. + * It depends on certain table properties (such as whether or not + * row or column selection is allowed). When performing modifications, + * it is recommended that caution be taken in order to preserve + * the intent of this method, especially when deciding whether to + * query the selection models or interact with JTable directly. + */ + protected boolean moveWithinSelectedRange(final JTable table, final int dx, final int dy, + final ListSelectionModel rsm, final ListSelectionModel csm) { + + // Note: The Actions constructor ensures that only one of + // dx and dy is 0, and the other is either -1 or 1 + + // find out how many items the table is showing as selected + // and the range of items to navigate through + int totalCount; + int minX, maxX, minY, maxY; + + boolean rs = table.getRowSelectionAllowed(); + boolean cs = table.getColumnSelectionAllowed(); + + // both column and row selection + if (rs && cs) { + totalCount = table.getSelectedRowCount() * table.getSelectedColumnCount(); + minX = csm.getMinSelectionIndex(); + maxX = csm.getMaxSelectionIndex(); + minY = rsm.getMinSelectionIndex(); + maxY = rsm.getMaxSelectionIndex(); + // row selection only + } else if (rs) { + totalCount = table.getSelectedRowCount(); + minX = 0; + maxX = table.getColumnCount() - 1; + minY = rsm.getMinSelectionIndex(); + maxY = rsm.getMaxSelectionIndex(); + // column selection only + } else if (cs) { + totalCount = table.getSelectedColumnCount(); + minX = csm.getMinSelectionIndex(); + maxX = csm.getMaxSelectionIndex(); + minY = 0; + maxY = table.getRowCount() - 1; + // no selection allowed + } else { + totalCount = 0; + // A bogus assignment to stop javac from complaining + // about unitialized values. In this case, these + // won't even be used. + minX = maxX = minY = maxY = 0; + } + + // For some cases, there is no point in trying to stay within the + // selected area. Instead, move outside the selection, wrapping at + // the table boundaries. The cases are: + boolean stayInSelection; + + // - nothing selected + if (totalCount == 0 || + // - one item selected, and the lead is already selected + (totalCount == 1 && table.isCellSelected(leadRow, leadColumn))) { + + stayInSelection = false; + + maxX = table.getColumnCount() - 1; + maxY = table.getRowCount() - 1; + + // the mins are calculated like this in case the max is -1 + minX = Math.min(0, maxX); + minY = Math.min(0, maxY); + } else { + stayInSelection = true; + } + + // the algorithm below isn't prepared to deal with -1 lead/anchor + // so massage appropriately here first + if (dy == 1 && leadColumn == -1) { + leadColumn = minX; + leadRow = -1; + } else if (dx == 1 && leadRow == -1) { + leadRow = minY; + leadColumn = -1; + } else if (dy == -1 && leadColumn == -1) { + leadColumn = maxX; + leadRow = maxY + 1; + } else if (dx == -1 && leadRow == -1) { + leadRow = maxY; + leadColumn = maxX + 1; + } + + // In cases where the lead is not within the search range, + // we need to bring it within one cell for the search + // to work properly. Check these here. + leadRow = Math.min(Math.max(leadRow, minY - 1), maxY + 1); + leadColumn = Math.min(Math.max(leadColumn, minX - 1), maxX + 1); + + // find the next position, possibly looping until it is selected + do { + calcNextPos(dx, minX, maxX, dy, minY, maxY); + } while (stayInSelection && !table.isCellSelected(leadRow, leadColumn)); + + return stayInSelection; + } + + protected static int clipToRange(final int i, final int a, final int b) { + return Math.min(Math.max(i, a), b - 1); + } + + /** + * Find the next lead row and column based on the given + * dx/dy and max/min values. + */ + protected void calcNextPos(final int dx, final int minX, final int maxX, + final int dy, final int minY, final int maxY) { + + if (dx != 0) { + leadColumn += dx; + if (leadColumn > maxX) { + leadColumn = minX; + leadRow++; + if (leadRow > maxY) { + leadRow = minY; + } + } else if (leadColumn < minX) { + leadColumn = maxX; + leadRow--; + if (leadRow < minY) { + leadRow = maxY; + } + } + } else { + leadRow += dy; + if (leadRow > maxY) { + leadRow = minY; + leadColumn++; + if (leadColumn > maxX) { + leadColumn = minX; + } + } else if (leadRow < minY) { + leadRow = maxY; + leadColumn--; + if (leadColumn < minX) { + leadColumn = maxX; + } + } + } + } + + @Override + public boolean accept(final Object sender) { + String key = getName(); + + if (sender instanceof JTable && + Boolean.TRUE.equals(((JTable) sender).getClientProperty("Table.isFileList"))) { + if (key.equals(NEXT_COLUMN) || + key.equals(NEXT_COLUMN_CELL) || + key.equals(NEXT_COLUMN_EXTEND_SELECTION) || + key.equals(NEXT_COLUMN_CHANGE_LEAD) || + key.equals(PREVIOUS_COLUMN) || + key.equals(PREVIOUS_COLUMN_CELL) || + key.equals(PREVIOUS_COLUMN_EXTEND_SELECTION) || + key.equals(PREVIOUS_COLUMN_CHANGE_LEAD) || + key.equals(SCROLL_LEFT_CHANGE_SELECTION) || + key.equals(SCROLL_LEFT_EXTEND_SELECTION) || + key.equals(SCROLL_RIGHT_CHANGE_SELECTION) || + key.equals(SCROLL_RIGHT_EXTEND_SELECTION) || + key.equals(FIRST_COLUMN) || + key.equals(FIRST_COLUMN_EXTEND_SELECTION) || + key.equals(LAST_COLUMN) || + key.equals(LAST_COLUMN_EXTEND_SELECTION) || + key.equals(NEXT_ROW_CELL) || + key.equals(PREVIOUS_ROW_CELL)) { + + return false; + } + } + + if (key.equals(CANCEL_EDITING) && sender instanceof JTable) { + return ((JTable) sender).isEditing(); + } else if (key.equals(NEXT_ROW_CHANGE_LEAD) || + key.equals(PREVIOUS_ROW_CHANGE_LEAD)) { + // discontinuous selection actions are only enabled for + // DefaultListSelectionModel + return sender != null && + ((JTable) sender).getSelectionModel() + instanceof DefaultListSelectionModel; + } else if (key.equals(NEXT_COLUMN_CHANGE_LEAD) || + key.equals(PREVIOUS_COLUMN_CHANGE_LEAD)) { + // discontinuous selection actions are only enabled for + // DefaultListSelectionModel + return sender != null && + ((JTable) sender).getColumnModel().getSelectionModel() + instanceof DefaultListSelectionModel; + } else if (key.equals(ADD_TO_SELECTION) && sender instanceof JTable) { + // This action is typically bound to SPACE. + // If the table is already in an editing mode, SPACE should + // simply enter a space character into the table, and not + // select a cell. Likewise, if the lead cell is already selected + // then hitting SPACE should just enter a space character + // into the cell and begin editing. In both of these cases + // this action will be disabled. + JTable table = (JTable) sender; + int leadRow = getAdjustedLead(table, true); + int leadCol = getAdjustedLead(table, false); + return !(table.isEditing() || table.isCellSelected(leadRow, leadCol)); + } else if (key.equals(FOCUS_HEADER) && sender instanceof JTable) { + JTable table = (JTable) sender; + return table.getTableHeader() != null; + } + + return true; + } + } + +// +// Factory methods for the Listeners +// + + @SuppressWarnings("serial") // JDK-implementation class + static class TableTransferHandler extends TransferHandler implements UIResource { + + public int getSourceActions(final JComponent c) { + return COPY; + } + + /** + * Create a Transferable to use as the source for a data transfer. + * + * @param c The component holding the data to be transfered. This + * argument is provided to enable sharing of TransferHandlers by + * multiple components. + * @return The representation of the data to be transfered. + */ + protected Transferable createTransferable(final JComponent c) { + if (c instanceof JTable) { + JTable table = (JTable) c; + int[] rows; + int[] cols; + + if (!table.getRowSelectionAllowed() && !table.getColumnSelectionAllowed()) { + return null; + } + + if (!table.getRowSelectionAllowed()) { + int rowCount = table.getRowCount(); + + rows = new int[rowCount]; + for (int counter = 0; counter < rowCount; counter++) { + rows[counter] = counter; + } + } else { + rows = table.getSelectedRows(); + } + + if (!table.getColumnSelectionAllowed()) { + int colCount = table.getColumnCount(); + + cols = new int[colCount]; + for (int counter = 0; counter < colCount; counter++) { + cols[counter] = counter; + } + } else { + cols = table.getSelectedColumns(); + } + + if (rows == null || cols == null || rows.length == 0 || cols.length == 0) { + return null; + } + + StringBuilder plainStr = new StringBuilder(); + StringBuilder htmlStr = new StringBuilder(); + + htmlStr.append("\n\n\n"); + + for (int value : rows) { + htmlStr.append("\n"); + for (int i : cols) { + Object obj = table.getValueAt(value, i); + String val = ((obj == null) ? "" : obj.toString()); + plainStr.append(val).append('\t'); + htmlStr.append(" \n"); + } + // we want a newline at the end of each line and not a tab + plainStr.deleteCharAt(plainStr.length() - 1).append('\n'); + htmlStr.append("\n"); + } + + // remove the last newline + plainStr.deleteCharAt(plainStr.length() - 1); + htmlStr.append("
").append(val).append("
\n\n"); + + return new BasicTransferable(plainStr.toString(), htmlStr.toString()); + } + + return null; + } + + } + + protected Handler getHandler() { + if (handler == null) { + handler = new Handler(); + } + return handler; + } + + /** + * This class should be treated as a "protected" inner class. + * Instantiate it only within subclasses of {@code BasicTableUI}. + *

As of Java 2 platform v1.3 this class is no longer used. + * Instead JTable + * overrides processKeyBinding to dispatch the event to + * the current TableCellEditor. + */ + public class KeyHandler implements KeyListener { + public void keyTyped(final KeyEvent e) { + getHandler().keyTyped(e); + } + + // NOTE: This class exists only for backward compatibility. All + // its functionality has been moved into Handler. If you need to add + // new functionality add it to the Handler, but make sure this + // class calls into the Handler. + public void keyPressed(final KeyEvent e) { + getHandler().keyPressed(e); + } + + public void keyReleased(final KeyEvent e) { + getHandler().keyReleased(e); + } + } + + /** + * Creates the key listener for handling keyboard navigation in the {@code JTable}. + * + * @return the key listener for handling keyboard navigation in the {@code JTable} + */ + protected KeyListener createKeyListener() { + return null; + } + + /** + * This class should be treated as a "protected" inner class. + * Instantiate it only within subclasses of {@code BasicTableUI}. + */ + public class FocusHandler implements FocusListener { + // NOTE: This class exists only for backward compatibility. All + // its functionality has been moved into Handler. If you need to add + // new functionality add it to the Handler, but make sure this + // class calls into the Handler. + public void focusGained(final FocusEvent e) { + getHandler().focusGained(e); + } + + public void focusLost(final FocusEvent e) { + getHandler().focusLost(e); + } + } + + /** + * Creates the focus listener for handling keyboard navigation in the {@code JTable}. + * + * @return the focus listener for handling keyboard navigation in the {@code JTable} + */ + protected FocusListener createFocusListener() { + return getHandler(); + } + + /** + * This class should be treated as a "protected" inner class. + * Instantiate it only within subclasses of BasicTableUI. + */ + public class MouseInputHandler implements MouseInputListener { + // NOTE: This class exists only for backward compatibility. All + // its functionality has been moved into Handler. If you need to add + // new functionality add it to the Handler, but make sure this + // class calls into the Handler. + public void mouseClicked(final MouseEvent e) { + getHandler().mouseClicked(e); + } + + public void mousePressed(final MouseEvent e) { + getHandler().mousePressed(e); + } + + public void mouseReleased(final MouseEvent e) { + getHandler().mouseReleased(e); + } + + public void mouseEntered(final MouseEvent e) { + getHandler().mouseEntered(e); + } + + public void mouseExited(final MouseEvent e) { + getHandler().mouseExited(e); + } + + public void mouseDragged(final MouseEvent e) { + getHandler().mouseDragged(e); + } + + public void mouseMoved(final MouseEvent e) { + getHandler().mouseMoved(e); + } + } + + /** + * Creates the mouse listener for the {@code JTable}. + * + * @return the mouse listener for the {@code JTable} + */ + protected MouseInputListener createMouseInputListener() { + return getHandler(); + } + +// +// The installation/uninstall procedures and support +// + + protected class Handler implements FocusListener, MouseInputListener, + PropertyChangeListener, ListSelectionListener, ActionListener, + DragRecognitionSupport.BeforeDrag { + + // Component receiving mouse events during editing. + // May not be editorComponent. + protected Component dispatchComponent; + // The row and column where the press occurred and the + // press event itself + protected int pressedRow; + protected int pressedCol; + protected MouseEvent pressedEvent; + // Whether or not the mouse press (which is being considered as part + // of a drag sequence) also caused the selection change to be fully + // processed. + protected boolean dragPressDidSelection; + // Set to true when a drag gesture has been fully recognized and DnD + // begins. Use this to ignore further mouse events which could be + // delivered if DnD is cancelled (via ESCAPE for example) + protected boolean dragStarted; + + + // MouseInputListener + // Whether or not we should start the editing timer on release + protected boolean shouldStartTimer; + // To cache the return value of pointOutsidePrefSize since we use + // it multiple times. + protected boolean outsidePrefSize; + // Used to delay the start of editing. + protected Timer timer = null; + + public void focusGained(final FocusEvent e) { + repaintLeadCell(); + } + + // FocusListener + protected void repaintLeadCell() { + int lr = getAdjustedLead(table, true); + int lc = getAdjustedLead(table, false); + + if (lr < 0 || lc < 0) { + return; + } + + Rectangle dirtyRect = table.getCellRect(lr, lc, false); + table.repaint(dirtyRect); + } + + public void focusLost(final FocusEvent e) { + repaintLeadCell(); + } + + // KeyListener + public void keyPressed(final KeyEvent e) { + } + + public void keyReleased(final KeyEvent e) { + } + + @SuppressWarnings("deprecation") + public void keyTyped(final KeyEvent e) { + KeyStroke keyStroke = KeyStroke.getKeyStroke(e.getKeyChar(), + e.getModifiers()); + + // We register all actions using ANCESTOR_OF_FOCUSED_COMPONENT + // which means that we might perform the appropriate action + // in the table and then forward it to the editor if the editor + // had focus. Make sure this doesn't happen by checking our + // InputMaps. + InputMap map = table.getInputMap(JComponent.WHEN_FOCUSED); + if (map != null && map.get(keyStroke) != null) { + return; + } + map = table.getInputMap(JComponent. + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + if (map != null && map.get(keyStroke) != null) { + return; + } + + keyStroke = KeyStroke.getKeyStrokeForEvent(e); + + // The AWT seems to generate an unconsumed \r event when + // ENTER (\n) is pressed. + if (e.getKeyChar() == '\r') { + return; + } + + int leadRow = getAdjustedLead(table, true); + int leadColumn = getAdjustedLead(table, false); + if (leadRow != -1 && leadColumn != -1 && !table.isEditing()) { + if (!table.editCellAt(leadRow, leadColumn)) { + return; + } + } + + // Forwarding events this way seems to put the component + // in a state where it believes it has focus. In reality + // the table retains focus - though it is difficult for + // a user to tell, since the caret is visible and flashing. + + // Calling table.requestFocus() here, to get the focus back to + // the table, seems to have no effect. + + Component editorComp = table.getEditorComponent(); + if (table.isEditing() && editorComp != null) { + if (editorComp instanceof JComponent) { + JComponent component = (JComponent) editorComp; + map = component.getInputMap(JComponent.WHEN_FOCUSED); + Object binding = (map != null) ? map.get(keyStroke) : null; + if (binding == null) { + map = component.getInputMap(JComponent. + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + binding = (map != null) ? map.get(keyStroke) : null; + } + if (binding != null) { + ActionMap am = component.getActionMap(); + Action action = (am != null) ? am.get(binding) : null; + if (action != null && SwingUtilities.notifyAction(action, keyStroke, e, component, + e.getModifiers())) { + e.consume(); + } + } + } + } + } + + public void mouseClicked(final MouseEvent e) { + } + + public void mousePressed(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, table)) { + return; + } + + if (table.isEditing() && !table.getCellEditor().stopCellEditing()) { + Component editorComponent = table.getEditorComponent(); + if (editorComponent != null && !editorComponent.hasFocus()) { + SwingUtilities2.compositeRequestFocus(editorComponent); + } + return; + } + + Point p = e.getPoint(); + pressedRow = table.rowAtPoint(p); + pressedCol = table.columnAtPoint(p); + outsidePrefSize = pointOutsidePrefSize(pressedRow, pressedCol, p); + + if (isFileList) { + shouldStartTimer = + table.isCellSelected(pressedRow, pressedCol) && + !e.isShiftDown() && + !DarkUIUtil.isMenuShortcutKeyDown(e) && + !outsidePrefSize; + } + + if (table.getDragEnabled()) { + mousePressedDND(e); + } else { + SwingUtilities2.adjustFocus(table); + if (!isFileList) { + setValueIsAdjusting(true); + } + adjustSelection(e); + } + } + + public void mouseReleased(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, table)) { + return; + } + + if (table.getDragEnabled()) { + mouseReleasedDND(e); + } else { + if (isFileList) { + maybeStartTimer(); + } + } + + pressedEvent = null; + repostEvent(e); + dispatchComponent = null; + setValueIsAdjusting(false); + } + + protected void mouseReleasedDND(final MouseEvent e) { + MouseEvent me = DragRecognitionSupport.mouseReleased(e); + if (me != null) { + SwingUtilities2.adjustFocus(table); + if (!dragPressDidSelection) { + adjustSelection(me); + } + } + + if (!dragStarted) { + if (isFileList) { + maybeStartTimer(); + return; + } + + Point p = e.getPoint(); + + if (pressedEvent != null && + table.rowAtPoint(p) == pressedRow && + table.columnAtPoint(p) == pressedCol && + table.editCellAt(pressedRow, pressedCol, pressedEvent)) { + + setDispatchComponent(pressedEvent); + repostEvent(pressedEvent); + + // This may appear completely odd, but must be done for backward + // compatibility reasons. Developers have been known to rely on + // a call to shouldSelectCell after editing has begun. + CellEditor ce = table.getCellEditor(); + if (ce != null) { + ce.shouldSelectCell(pressedEvent); + } + } + } + } + + protected void maybeStartTimer() { + if (!shouldStartTimer) { + return; + } + + if (timer == null) { + int delay = UIManager.getInt("FileChooser.editDelay"); + if (delay == 0) delay = 1200; + timer = new Timer(delay, this); + timer.setRepeats(false); + } + + timer.start(); + } + + protected boolean repostEvent(final MouseEvent e) { + // Check for isEditing() in case another event has + // caused the editor to be removed. See bug #4306499. + if (dispatchComponent == null || !table.isEditing()) { + return false; + } + MouseEvent e2 = SwingUtilities.convertMouseEvent(table, e, + dispatchComponent); + dispatchComponent.dispatchEvent(e2); + return true; + } + + protected void setValueIsAdjusting(final boolean flag) { + table.getSelectionModel().setValueIsAdjusting(flag); + table.getColumnModel().getSelectionModel(). + setValueIsAdjusting(flag); + } + + protected void adjustSelection(final MouseEvent e) { + // Fix for 4835633 + if (outsidePrefSize) { + // If shift is down in multi-select, we should just return. + // For single select or non-shift-click, clear the selection + if (e.getID() == MouseEvent.MOUSE_PRESSED && + (!e.isShiftDown() || + table.getSelectionModel().getSelectionMode() == + ListSelectionModel.SINGLE_SELECTION)) { + table.clearSelection(); + TableCellEditor tce = table.getCellEditor(); + if (tce != null) { + tce.stopCellEditing(); + } + } + return; + } + // The autoscroller can generate drag events outside the + // table's range. + if ((pressedCol == -1) || (pressedRow == -1)) { + return; + } + + boolean dragEnabled = table.getDragEnabled(); + + if (!dragEnabled && !isFileList && table.editCellAt(pressedRow, pressedCol, e)) { + setDispatchComponent(e); + repostEvent(e); + } + + CellEditor editor = table.getCellEditor(); + if (dragEnabled || editor == null || editor.shouldSelectCell(e)) { + table.changeSelection(pressedRow, pressedCol, + DarkUIUtil.isMenuShortcutKeyDown(e), + e.isShiftDown()); + } + } + + protected void setDispatchComponent(final MouseEvent e) { + Component editorComponent = table.getEditorComponent(); + Point p = e.getPoint(); + Point p2 = SwingUtilities.convertPoint(table, p, editorComponent); + dispatchComponent = + SwingUtilities.getDeepestComponentAt(editorComponent, + p2.x, p2.y); + SwingUtilities2.setSkipClickCount(dispatchComponent, + e.getClickCount() - 1); + } + + public void mouseEntered(final MouseEvent e) { + } + + public void mouseExited(final MouseEvent e) { + } + + protected boolean canStartDrag() { + if (pressedRow == -1 || pressedCol == -1) { + return false; + } + + if (isFileList) { + return !outsidePrefSize; + } + + // if this is a single selection table + if ((table.getSelectionModel().getSelectionMode() == + ListSelectionModel.SINGLE_SELECTION) && + (table.getColumnModel().getSelectionModel().getSelectionMode() == + ListSelectionModel.SINGLE_SELECTION)) { + + return true; + } + + return table.isCellSelected(pressedRow, pressedCol); + } + + protected void mousePressedDND(final MouseEvent e) { + pressedEvent = e; + boolean grabFocus = true; + dragStarted = false; + + if (canStartDrag() && DragRecognitionSupport.mousePressed(e)) { + + dragPressDidSelection = false; + + if (DarkUIUtil.isMenuShortcutKeyDown(e) && isFileList) { + // do nothing for control - will be handled on release + // or when drag starts + return; + } else if (!e.isShiftDown() && table.isCellSelected(pressedRow, pressedCol)) { + // clicking on something that's already selected + // and need to make it the lead now + table.getSelectionModel().addSelectionInterval(pressedRow, + pressedRow); + table.getColumnModel().getSelectionModel(). + addSelectionInterval(pressedCol, pressedCol); + + return; + } + + dragPressDidSelection = true; + + // could be a drag initiating event - don't grab focus + grabFocus = false; + } else if (!isFileList) { + // When drag can't happen, mouse drags might change the selection in the table + // so we want the isAdjusting flag to be set + setValueIsAdjusting(true); + } + + if (grabFocus) { + SwingUtilities2.adjustFocus(table); + } + + adjustSelection(e); + } + + public void valueChanged(final ListSelectionEvent e) { + if (timer != null) { + timer.stop(); + timer = null; + } + } + + public void actionPerformed(final ActionEvent ae) { + table.editCellAt(pressedRow, pressedCol, null); + Component editorComponent = table.getEditorComponent(); + if (editorComponent != null && !editorComponent.hasFocus()) { + SwingUtilities2.compositeRequestFocus(editorComponent); + } + } + + public void dragStarting(final MouseEvent me) { + dragStarted = true; + + if (DarkUIUtil.isMenuShortcutKeyDown(me) && isFileList) { + table.getSelectionModel().addSelectionInterval(pressedRow, pressedRow); + table.getColumnModel().getSelectionModel(). + addSelectionInterval(pressedCol, pressedCol); + } + + pressedEvent = null; + } + + public void mouseDragged(final MouseEvent e) { + if (SwingUtilities2.shouldIgnore(e, table)) { + return; + } + + if (table.getDragEnabled() && + (DragRecognitionSupport.mouseDragged(e, this) || dragStarted)) { + + return; + } + + repostEvent(e); + + // Check isFileList: + // Until we support drag-selection, dragging should not change + // the selection (act like single-select). + if (isFileList || table.isEditing()) { + return; + } + + Point p = e.getPoint(); + int row = table.rowAtPoint(p); + int column = table.columnAtPoint(p); + // The autoscroller can generate drag events outside the + // table's range. + if ((column == -1) || (row == -1)) { + return; + } + + table.changeSelection(row, column, DarkUIUtil.isMenuShortcutKeyDown(e), true); + } + + public void mouseMoved(final MouseEvent e) { + } + + // PropertyChangeListener + public void propertyChange(final PropertyChangeEvent event) { + String changeName = event.getPropertyName(); + + if ("componentOrientation".equals(changeName)) { + InputMap inputMap = getInputMap( + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + SwingUtilities.replaceUIInputMap(table, + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, + inputMap); + + JTableHeader header = table.getTableHeader(); + if (header != null) { + header.setComponentOrientation( + (ComponentOrientation) event.getNewValue()); + } + } else if ("dropLocation".equals(changeName)) { + JTable.DropLocation oldValue = (JTable.DropLocation) event.getOldValue(); + repaintDropLocation(oldValue); + repaintDropLocation(table.getDropLocation()); + } else if ("Table.isFileList".equals(changeName)) { + isFileList = Boolean.TRUE.equals(table.getClientProperty("Table.isFileList")); + table.revalidate(); + table.repaint(); + if (isFileList) { + table.getSelectionModel().addListSelectionListener(getHandler()); + } else { + table.getSelectionModel().removeListSelectionListener(getHandler()); + timer = null; + } + } else if ("selectionModel".equals(changeName)) { + if (isFileList) { + ListSelectionModel old = (ListSelectionModel) event.getOldValue(); + old.removeListSelectionListener(getHandler()); + table.getSelectionModel().addListSelectionListener(getHandler()); + } + } + } + + protected void repaintDropLocation(final JTable.DropLocation loc) { + if (loc == null) { + return; + } + + if (!loc.isInsertRow() && !loc.isInsertColumn()) { + Rectangle rect = table.getCellRect(loc.getRow(), loc.getColumn(), false); + if (rect != null) { + table.repaint(rect); + } + return; + } + + if (loc.isInsertRow()) { + Rectangle rect = extendRect(getHDropLineRect(loc), true); + if (rect != null) { + table.repaint(rect); + } + } + + if (loc.isInsertColumn()) { + Rectangle rect = extendRect(getVDropLineRect(loc), false); + if (rect != null) { + table.repaint(rect); + } + } + } + } + +// Installation + + public void installUI(final JComponent c) { + table = (JTable) c; + + rendererPane = new CellRendererPane(); + table.add(rendererPane); + installDefaults(); + installDefaults2(); + installListeners(); + installKeyboardActions(); + } + + /** + * Initialize JTable properties, e.g. font, foreground, and background. + * The font, foreground, and background properties are only set if their + * current value is either null or a UIResource, other properties are set + * if the current value is null. + * + * @see #installUI + */ + protected void installDefaults() { + LookAndFeel.installColorsAndFont(table, "Table.background", + "Table.foreground", "Table.font"); + // JTable's original row height is 16. To correctly display the + // contents on Linux we should have set it to 18, Windows 19 and + // Solaris 20. As these values vary so much it's too hard to + // be backward compatable and try to update the row height, we're + // therefor NOT going to adjust the row height based on font. If the + // developer changes the font, it's there responsability to update + // the row height. + + LookAndFeel.installProperty(table, "opaque", Boolean.TRUE); + + Color sbg = table.getSelectionBackground(); + if (sbg == null || sbg instanceof UIResource) { + sbg = UIManager.getColor("Table.selectionBackground"); + table.setSelectionBackground(sbg != null ? sbg : UIManager.getColor("textHighlight")); + } + + Color sfg = table.getSelectionForeground(); + if (sfg == null || sfg instanceof UIResource) { + sfg = UIManager.getColor("Table.selectionForeground"); + table.setSelectionForeground(sfg != null ? sfg : UIManager.getColor("textHighlightText")); + } + + Color gridColor = table.getGridColor(); + if (gridColor == null || gridColor instanceof UIResource) { + gridColor = UIManager.getColor("Table.gridColor"); + table.setGridColor(gridColor != null ? gridColor : Color.GRAY); + } + + // install the scrollpane border + Container parent = SwingUtilities.getUnwrappedParent(table); // should be viewport + if (parent != null) { + parent = parent.getParent(); // should be the scrollpane + if (parent instanceof JScrollPane) { + LookAndFeel.installBorder((JScrollPane) parent, "Table.scrollPaneBorder"); + } + } + + isFileList = Boolean.TRUE.equals(table.getClientProperty("Table.isFileList")); + } + + protected void installDefaults2() { + TransferHandler th = table.getTransferHandler(); + if (th == null || th instanceof UIResource) { + table.setTransferHandler(defaultTransferHandler); + // default TransferHandler doesn't support drop + // so we don't want drop handling + if (table.getDropTarget() instanceof UIResource) { + table.setDropTarget(null); + } + } + } + + /** + * Attaches listeners to the JTable. + */ + protected void installListeners() { + focusListener = createFocusListener(); + keyListener = createKeyListener(); + mouseInputListener = createMouseInputListener(); + + table.addFocusListener(focusListener); + table.addKeyListener(keyListener); + table.addMouseListener(mouseInputListener); + table.addMouseMotionListener(mouseInputListener); + table.addPropertyChangeListener(getHandler()); + if (isFileList) { + table.getSelectionModel().addListSelectionListener(getHandler()); + } + } + + /** + * Register all keyboard actions on the JTable. + */ + protected void installKeyboardActions() { + LazyActionMap.installLazyActionMap(table, BasicTableUI.class, "Table.actionMap"); + + InputMap inputMap = getInputMap(JComponent. + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + SwingUtilities.replaceUIInputMap(table, + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, + inputMap); + } + + InputMap getInputMap(final int condition) { + if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { + InputMap keyMap = + (InputMap) DefaultLookup.get(table, this, + "Table.ancestorInputMap"); + InputMap rtlKeyMap; + + if (table.getComponentOrientation().isLeftToRight() || + ((rtlKeyMap = (InputMap) DefaultLookup.get(table, this, + "Table.ancestorInputMap.RightToLeft")) == null)) { + return keyMap; + } else { + rtlKeyMap.setParent(keyMap); + return rtlKeyMap; + } + } + return null; + } + + // Uninstallation + public void uninstallUI(final JComponent c) { + uninstallDefaults(); + uninstallListeners(); + uninstallKeyboardActions(); + + table.remove(rendererPane); + rendererPane = null; + table = null; + } + + /** + * Uninstalls default properties. + */ + protected void uninstallDefaults() { + if (table.getTransferHandler() instanceof UIResource) { + table.setTransferHandler(null); + } + } + + /** + * Unregisters listeners. + */ + protected void uninstallListeners() { + table.removeFocusListener(focusListener); + table.removeKeyListener(keyListener); + table.removeMouseListener(mouseInputListener); + table.removeMouseMotionListener(mouseInputListener); + table.removePropertyChangeListener(getHandler()); + if (isFileList) { + table.getSelectionModel().removeListSelectionListener(getHandler()); + } + + focusListener = null; + keyListener = null; + mouseInputListener = null; + handler = null; + } + + /** + * Unregisters keyboard actions. + */ + protected void uninstallKeyboardActions() { + SwingUtilities.replaceUIInputMap(table, JComponent. + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null); + SwingUtilities.replaceUIActionMap(table, null); + } + + /** + * Returns the baseline. + * + * @throws NullPointerException {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + * @see javax.swing.JComponent#getBaseline(int, int) + * @since 1.6 + */ + public int getBaseline(final JComponent c, final int width, final int height) { + super.getBaseline(c, width, height); + UIDefaults lafDefaults = UIManager.getLookAndFeelDefaults(); + Component renderer = (Component) lafDefaults.get( + BASELINE_COMPONENT_KEY); + if (renderer == null) { + DefaultTableCellRenderer tcr = new DefaultTableCellRenderer(); + renderer = tcr.getTableCellRendererComponent( + table, "a", false, false, -1, -1); + lafDefaults.put(BASELINE_COMPONENT_KEY, renderer); + } + renderer.setFont(table.getFont()); + int rowMargin = table.getRowMargin(); + return renderer.getBaseline(Integer.MAX_VALUE, table.getRowHeight() - + rowMargin) + rowMargin / 2; + } + + /** + * Returns an enum indicating how the baseline of the component + * changes as the size changes. + * + * @throws NullPointerException {@inheritDoc} + * @see javax.swing.JComponent#getBaseline(int, int) + * @since 1.6 + */ + public Component.BaselineResizeBehavior getBaselineResizeBehavior( + final JComponent c) { + super.getBaselineResizeBehavior(c); + return Component.BaselineResizeBehavior.CONSTANT_ASCENT; + } + +// +// Size Methods +// + + protected Dimension createTableSize(final long width) { + int height = 0; + int rowCount = table.getRowCount(); + if (rowCount > 0 && table.getColumnCount() > 0) { + Rectangle r = table.getCellRect(rowCount - 1, 0, true); + height = r.y + r.height; + } + // Width is always positive. The call to abs() is a workaround for + // a bug in the 1.1.6 JIT on Windows. + long tmp = Math.abs(width); + if (tmp > Integer.MAX_VALUE) { + tmp = Integer.MAX_VALUE; + } + return new Dimension((int) tmp, height); + } + + /** + * Return the minimum size of the table. The minimum height is the + * row height times the number of rows. + * The minimum width is the sum of the minimum widths of each column. + */ + public Dimension getMinimumSize(final JComponent c) { + long width = 0; + Enumeration enumeration = table.getColumnModel().getColumns(); + while (enumeration.hasMoreElements()) { + TableColumn aColumn = enumeration.nextElement(); + width = width + aColumn.getMinWidth(); + } + return createTableSize(width); + } + + /** + * Return the preferred size of the table. The preferred height is the + * row height times the number of rows. + * The preferred width is the sum of the preferred widths of each column. + */ + public Dimension getPreferredSize(final JComponent c) { + long width = 0; + Enumeration enumeration = table.getColumnModel().getColumns(); + while (enumeration.hasMoreElements()) { + TableColumn aColumn = enumeration.nextElement(); + width = width + aColumn.getPreferredWidth(); + } + return createTableSize(width); + } + + /** + * Return the maximum size of the table. The maximum height is the + * row heighttimes the number of rows. + * The maximum width is the sum of the maximum widths of each column. + */ + public Dimension getMaximumSize(final JComponent c) { + long width = 0; + Enumeration enumeration = table.getColumnModel().getColumns(); + while (enumeration.hasMoreElements()) { + TableColumn aColumn = enumeration.nextElement(); + width = width + aColumn.getMaxWidth(); + } + return createTableSize(width); + } + +// +// Paint methods and support +// + + /** + * Paint a representation of the table instance + * that was set in installUI(). + */ + public void paint(final Graphics g, final JComponent c) { + Rectangle clip = g.getClipBounds(); + + Rectangle bounds = table.getBounds(); + // account for the fact that the graphics has already been translated + // into the table's bounds + bounds.x = bounds.y = 0; + + if (table.getRowCount() <= 0 || table.getColumnCount() <= 0 || + // this check prevents us from painting the entire table + // when the clip doesn't intersect our bounds at all + !bounds.intersects(clip)) { + + paintDropLines(g); + return; + } + + boolean ltr = table.getComponentOrientation().isLeftToRight(); + Point upperLeft, lowerRight; + // compute the visible part of table which needs to be painted + Rectangle visibleBounds = clip.intersection(bounds); + upperLeft = visibleBounds.getLocation(); + lowerRight = new Point(visibleBounds.x + visibleBounds.width - 1, + visibleBounds.y + visibleBounds.height - 1); + + int rMin = table.rowAtPoint(upperLeft); + int rMax = table.rowAtPoint(lowerRight); + // This should never happen (as long as our bounds intersect the clip, + // which is why we bail above if that is the case). + if (rMin == -1) { + rMin = 0; + } + // If the table does not have enough rows to fill the view we'll get -1. + // (We could also get -1 if our bounds don't intersect the clip, + // which is why we bail above if that is the case). + // Replace this with the index of the last row. + if (rMax == -1) { + rMax = table.getRowCount() - 1; + } + + // For FIT_WIDTH, all columns should be printed irrespective of + // how many columns are visible. So, we used clip which is already set to + // total col width instead of visible region + // Since JTable.PrintMode is not accessible + // from here, we aet "Table.printMode" in TablePrintable#print and + // access from here. + Object printMode = table.getClientProperty("Table.printMode"); + if ((printMode == JTable.PrintMode.FIT_WIDTH)) { + upperLeft = clip.getLocation(); + lowerRight = new Point(clip.x + clip.width - 1, + clip.y + clip.height - 1); + } + int cMin = table.columnAtPoint(ltr ? upperLeft : lowerRight); + int cMax = table.columnAtPoint(ltr ? lowerRight : upperLeft); + // This should never happen. + if (cMin == -1) { + cMin = 0; + } + // If the table does not have enough columns to fill the view we'll get -1. + // Replace this with the index of the last column. + if (cMax == -1) { + cMax = table.getColumnCount() - 1; + } + + Container comp = SwingUtilities.getUnwrappedParent(table); + if (comp != null) { + comp = comp.getParent(); + } + + if (comp != null && !(comp instanceof JViewport) && !(comp instanceof JScrollPane)) { + // We did rMax-1 to paint the same number of rows that are drawn on console + // otherwise 1 extra row is printed per page than that are displayed + // when there is no scrollPane and we do printing of table + // but not when rmax is already pointing to index of last row + // and if there is any selected rows + if (rMax != (table.getRowCount() - 1) && + (table.getSelectedRow() == -1)) { + // Do not decrement rMax if rMax becomes + // less than or equal to rMin + // else cells will not be painted + if (rMax - rMin > 1) { + rMax = rMax - 1; + } + } + } + + // Paint the grid. + paintGrid(g, rMin, rMax, cMin, cMax); + + // Paint the cells. + paintCells(g, rMin, rMax, cMin, cMax); + + paintDropLines(g); + } + + protected void paintDropLines(final Graphics g) { + JTable.DropLocation loc = table.getDropLocation(); + if (loc == null) { + return; + } + + Color color = UIManager.getColor("Table.dropLineColor"); + Color shortColor = UIManager.getColor("Table.dropLineShortColor"); + if (color == null && shortColor == null) { + return; + } + + Rectangle rect; + + rect = getHDropLineRect(loc); + if (rect != null) { + int x = rect.x; + int w = rect.width; + if (color != null) { + extendRect(rect, true); + g.setColor(color); + g.fillRect(rect.x, rect.y, rect.width, rect.height); + } + if (!loc.isInsertColumn() && shortColor != null) { + g.setColor(shortColor); + g.fillRect(x, rect.y, w, rect.height); + } + } + + rect = getVDropLineRect(loc); + if (rect != null) { + int y = rect.y; + int h = rect.height; + if (color != null) { + extendRect(rect, false); + g.setColor(color); + g.fillRect(rect.x, rect.y, rect.width, rect.height); + } + if (!loc.isInsertRow() && shortColor != null) { + g.setColor(shortColor); + g.fillRect(rect.x, y, rect.width, h); + } + } + } + + protected Rectangle getHDropLineRect(final JTable.DropLocation loc) { + if (!loc.isInsertRow()) { + return null; + } + + int row = loc.getRow(); + int col = loc.getColumn(); + if (col >= table.getColumnCount()) { + col--; + } + + Rectangle rect = table.getCellRect(row, col, true); + + if (row >= table.getRowCount()) { + row--; + Rectangle prevRect = table.getCellRect(row, col, true); + rect.y = prevRect.y + prevRect.height; + } + + if (rect.y == 0) { + rect.y = -1; + } else { + rect.y -= 2; + } + + rect.height = 3; + + return rect; + } + + protected Rectangle getVDropLineRect(final JTable.DropLocation loc) { + if (!loc.isInsertColumn()) { + return null; + } + + boolean ltr = table.getComponentOrientation().isLeftToRight(); + int col = loc.getColumn(); + Rectangle rect = table.getCellRect(loc.getRow(), col, true); + + if (col >= table.getColumnCount()) { + col--; + rect = table.getCellRect(loc.getRow(), col, true); + if (ltr) { + rect.x = rect.x + rect.width; + } + } else if (!ltr) { + rect.x = rect.x + rect.width; + } + + if (rect.x == 0) { + rect.x = -1; + } else { + rect.x -= 2; + } + + rect.width = 3; + + return rect; + } + + protected Rectangle extendRect(final Rectangle rect, final boolean horizontal) { + if (rect == null) { + return rect; + } + + if (horizontal) { + rect.x = 0; + rect.width = table.getWidth(); + } else { + rect.y = 0; + + if (table.getRowCount() != 0) { + Rectangle lastRect = table.getCellRect(table.getRowCount() - 1, 0, true); + rect.height = lastRect.y + lastRect.height; + } else { + rect.height = table.getHeight(); + } + } + + return rect; + } + + /* + * Paints the grid lines within aRect, using the grid + * color set with setGridColor. Paints vertical lines + * if getShowVerticalLines() returns true and paints + * horizontal lines if getShowHorizontalLines() + * returns true. + */ + protected void paintGrid(final Graphics g, final int rMin, final int rMax, final int cMin, final int cMax) { + g.setColor(table.getGridColor()); + + Rectangle minCell = table.getCellRect(rMin, cMin, true); + Rectangle maxCell = table.getCellRect(rMax, cMax, true); + Rectangle damagedArea = minCell.union(maxCell); + + if (table.getShowHorizontalLines()) { + int tableWidth = damagedArea.x + damagedArea.width; + int y = damagedArea.y; + for (int row = rMin; row <= rMax; row++) { + y += table.getRowHeight(row); + SwingUtilities2.drawHLine(g, damagedArea.x, tableWidth - 1, y - 1); + } + } + if (table.getShowVerticalLines()) { + TableColumnModel cm = table.getColumnModel(); + int tableHeight = damagedArea.y + damagedArea.height; + int x; + if (table.getComponentOrientation().isLeftToRight()) { + x = damagedArea.x; + for (int column = cMin; column <= cMax; column++) { + int w = cm.getColumn(column).getWidth(); + x += w; + SwingUtilities2.drawVLine(g, x - 1, 0, tableHeight - 1); + } + } else { + x = damagedArea.x; + for (int column = cMax; column >= cMin; column--) { + int w = cm.getColumn(column).getWidth(); + x += w; + SwingUtilities2.drawVLine(g, x - 1, 0, tableHeight - 1); + } + } + } + } + + protected int viewIndexForColumn(final TableColumn aColumn) { + TableColumnModel cm = table.getColumnModel(); + for (int column = 0; column < cm.getColumnCount(); column++) { + if (cm.getColumn(column) == aColumn) { + return column; + } + } + return -1; + } + + protected void paintCells(final Graphics g, final int rMin, final int rMax, final int cMin, final int cMax) { + JTableHeader header = table.getTableHeader(); + TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn(); + + TableColumnModel cm = table.getColumnModel(); + int columnMargin = cm.getColumnMargin(); + + Rectangle cellRect; + TableColumn aColumn; + int columnWidth; + if (table.getComponentOrientation().isLeftToRight()) { + for (int row = rMin; row <= rMax; row++) { + cellRect = table.getCellRect(row, cMin, false); + for (int column = cMin; column <= cMax; column++) { + aColumn = cm.getColumn(column); + columnWidth = aColumn.getWidth(); + cellRect.width = columnWidth - columnMargin; + if (aColumn != draggedColumn) { + paintCell(g, cellRect, row, column); + } + cellRect.x += columnWidth; + } + } + } else { + for (int row = rMin; row <= rMax; row++) { + cellRect = table.getCellRect(row, cMin, false); + aColumn = cm.getColumn(cMin); + if (aColumn != draggedColumn) { + columnWidth = aColumn.getWidth(); + cellRect.width = columnWidth - columnMargin; + paintCell(g, cellRect, row, cMin); + } + for (int column = cMin + 1; column <= cMax; column++) { + aColumn = cm.getColumn(column); + columnWidth = aColumn.getWidth(); + cellRect.width = columnWidth - columnMargin; + cellRect.x -= columnWidth; + if (aColumn != draggedColumn) { + paintCell(g, cellRect, row, column); + } + } + } + } + + // Paint the dragged column if we are dragging. + if (draggedColumn != null) { + paintDraggedArea(g, rMin, rMax, draggedColumn, header.getDraggedDistance()); + } + + // Remove any renderers that may be left in the rendererPane. + rendererPane.removeAll(); + } + + protected void paintDraggedArea(final Graphics g, final int rMin, final int rMax, final TableColumn draggedColumn, final int distance) { + int draggedColumnIndex = viewIndexForColumn(draggedColumn); + + Rectangle minCell = table.getCellRect(rMin, draggedColumnIndex, true); + Rectangle maxCell = table.getCellRect(rMax, draggedColumnIndex, true); + + Rectangle vacatedColumnRect = minCell.union(maxCell); + + // Paint a gray well in place of the moving column. + g.setColor(table.getParent().getBackground()); + g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, + vacatedColumnRect.width, vacatedColumnRect.height); + + // Move to the where the cell has been dragged. + vacatedColumnRect.x += distance; + + // Fill the background. + g.setColor(table.getBackground()); + g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, + vacatedColumnRect.width, vacatedColumnRect.height); + + // Paint the vertical grid lines if necessary. + if (table.getShowVerticalLines()) { + g.setColor(table.getGridColor()); + int x1 = vacatedColumnRect.x; + int y1 = vacatedColumnRect.y; + int x2 = x1 + vacatedColumnRect.width - 1; + int y2 = y1 + vacatedColumnRect.height - 1; + // Left + g.drawLine(x1 - 1, y1, x1 - 1, y2); + // Right + g.drawLine(x2, y1, x2, y2); + } + + for (int row = rMin; row <= rMax; row++) { + // Render the cell value + Rectangle r = table.getCellRect(row, draggedColumnIndex, false); + r.x += distance; + paintCell(g, r, row, draggedColumnIndex); + + // Paint the (lower) horizontal grid line if necessary. + if (table.getShowHorizontalLines()) { + g.setColor(table.getGridColor()); + Rectangle rcr = table.getCellRect(row, draggedColumnIndex, true); + rcr.x += distance; + int x1 = rcr.x; + int y1 = rcr.y; + int x2 = x1 + rcr.width - 1; + int y2 = y1 + rcr.height - 1; + g.drawLine(x1, y2, x2, y2); + } + } + } + + protected void paintCell(final Graphics g, final Rectangle cellRect, final int row, final int column) { + if (table.isEditing() && table.getEditingRow() == row && + table.getEditingColumn() == column) { + Component component = table.getEditorComponent(); + component.setBounds(cellRect); + component.validate(); + } else { + TableCellRenderer renderer = table.getCellRenderer(row, column); + Component component = table.prepareRenderer(renderer, row, column); + rendererPane.paintComponent(g, component, table, cellRect.x, cellRect.y, + cellRect.width, cellRect.height, true); + } + } + + +} diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellEditor.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellEditor.java index 2618c96f..b53eaebb 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellEditor.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellEditor.java @@ -23,6 +23,7 @@ import java.util.EventObject; public class DarkTableCellEditor extends DefaultCellEditor { private static final JCheckBox dummyCheckBox = new JCheckBox(); + private static final IconWrapper iconWrapper = new IconWrapper(); private final DarkTableCellEditorToggleButton checkBoxEditor = new DarkTableCellEditorToggleButton(this, new DarkTableCellEditorToggleButton.CellCheckBox()); @@ -39,6 +40,7 @@ public class DarkTableCellEditor extends DefaultCellEditor { public DarkTableCellEditor(final JTextField textField) { super(textField); textField.setBorder(new TextFieldTableCellEditorBorder()); + textField.putClientProperty("JTextField.isCellEditor", Boolean.TRUE); setClickCountToStart(2); } @@ -148,7 +150,8 @@ public class DarkTableCellEditor extends DefaultCellEditor { } @Override - public boolean isCellEditable(@NotNull final EventObject anEvent) { + public boolean isCellEditable(final EventObject anEvent) { + if (anEvent == null) return super.isCellEditable(anEvent); var table = ((JTable) anEvent.getSource()); if (DarkTableCellRenderer.isBooleanRenderingEnabled(table) && anEvent instanceof MouseEvent) { var p = ((MouseEvent) anEvent).getPoint(); @@ -191,6 +194,8 @@ public class DarkTableCellEditor extends DefaultCellEditor { delegate.setValue(value); + var comp = editorComponent; + if (editorComponent instanceof JComboBox) { ((JComboBox) editorComponent).removeAllItems(); ((JComboBox) editorComponent).addItem(value); @@ -207,19 +212,30 @@ public class DarkTableCellEditor extends DefaultCellEditor { } } + var rendererComp = table.getCellRenderer(row, column) + .getTableCellRendererComponent(table, value, isSelected, false, row, column); + if (rendererComp instanceof JLabel) { + var icon = ((JLabel) rendererComp).getIcon(); + if (icon != null) { + comp = iconWrapper; + iconWrapper.init(editorComponent, icon, rendererComp.getComponentOrientation().isLeftToRight()); + iconWrapper.setIconGap(((JLabel) rendererComp).getIconTextGap() - 1); + } + } + boolean alternativeRow = UIManager.getBoolean("Table.alternateRowColor"); Color alternativeRowColor = UIManager.getColor("Table.alternateRowBackground"); Color normalColor = UIManager.getColor("Table.background"); if (alternativeRow) { if (!isSelected) { if (row % 2 == 1) { - editorComponent.setBackground(alternativeRowColor); + comp.setBackground(alternativeRowColor); } else { - editorComponent.setBackground(normalColor); + comp.setBackground(normalColor); } } } - return editorComponent; + return comp; } @Contract("null, _ -> false") @@ -234,4 +250,58 @@ public class DarkTableCellEditor extends DefaultCellEditor { } return checkBoxEditor; } + + protected static class IconWrapper extends JPanel { + + private final JLabel label; + private JComponent c; + private int iconGap; + + protected IconWrapper() { + setLayout(null); + label = new JLabel(); + label.setIconTextGap(0); + add(label); + } + + protected void setIconGap(final int iconGap) { + this.iconGap = iconGap; + } + + protected void init(@NotNull final JComponent component, final Icon icon, final boolean ltr) { + setComponentOrientation(ltr ? ComponentOrientation.LEFT_TO_RIGHT : ComponentOrientation.RIGHT_TO_LEFT); + if (c != null) { + remove(c); + } + add(component); + this.c = component; + label.setIcon(icon); + } + + @Override + public void doLayout() { + if (c == null) return; + int w = getWidth(); + int h = getHeight(); + var b = c.getBorder(); + var ins = new Insets(0, 0, 0, 0); + var labelSize = label.getPreferredSize(); + int gap = getIconCompGap(); + if (b != null) { + ins = b.getBorderInsets(c); + } + if (getComponentOrientation().isLeftToRight()) { + label.setBounds(ins.left + gap, 0, labelSize.width + 1, h); + c.setBounds(ins.left + labelSize.width + 2 * gap - 1, 0, + w - ins.left - labelSize.width - 2 * gap + 1, h); + } else { + c.setBounds(0, 0, w - ins.right - labelSize.width - gap - 1, h); + label.setBounds(w - ins.right - labelSize.width - gap - 1, 0, labelSize.width + 1, h); + } + } + + public int getIconCompGap() { + return iconGap; + } + } } diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellFocusBorder.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellFocusBorder.java index 0498be6c..cb06091f 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellFocusBorder.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellFocusBorder.java @@ -15,7 +15,35 @@ public class DarkTableCellFocusBorder extends DarkCellBorder { public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) { super.paintBorder(c, g, x, y, width, height); - g.setColor(UIManager.getColor("Table.focusBorderColor")); - DarkUIUtil.drawRect(g, 0, 0, width, height, 1); + if (isRowFocusBorder(c)) { + g.setColor(UIManager.getColor("Table.focusRowBorderColor")); + ((Graphics2D) g).scale(0.5, 0.5); + g.drawRect(0, 0, 2 * width, 1); + g.drawRect(0, 2 * height - 2, 2 * width, 1); + if (forcePaintLeft(c)) { + g.drawRect(0, 0, 1, 2 * height); + } + if (forcePaintRight(c)) { + g.drawRect(2 * width - 2, 0, 1, 2 * height); + } + } else { + g.setColor(UIManager.getColor("Table.focusBorderColor")); + DarkUIUtil.drawRect(g, 0, 0, width, height, 1); + } + } + + protected static boolean isRowFocusBorder(final Component c) { + return c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty("JTable.rowFocusBorder")); + } + + protected static boolean forcePaintLeft(final Component c) { + return c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty("JTable.forcePaintLeft")); + } + + protected static boolean forcePaintRight(final Component c) { + return c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty("JTable.forcePaintRight")); } } diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellRenderer.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellRenderer.java index 1812557a..c88e6d8f 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableCellRenderer.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableCellRenderer.java @@ -1,11 +1,14 @@ package com.weis.darklaf.ui.table; import com.weis.darklaf.ui.cell.DarkCellRendererToggleButton; +import com.weis.darklaf.util.DarkUIUtil; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; import java.awt.*; /** @@ -34,6 +37,27 @@ public class DarkTableCellRenderer extends DefaultTableCellRenderer { this.setVerticalAlignment(SwingConstants.CENTER); setHorizontalAlignment(table.getComponentOrientation().isLeftToRight() ? LEFT : RIGHT); + if (DarkTableCellFocusBorder.isRowFocusBorder(table) + && table.getSelectionModel().getLeadSelectionIndex() == row + && !table.isEditing() + && DarkUIUtil.hasFocus(table)) { + component.setBorder(UIManager.getBorder("Table.focusSelectedCellHighlightBorder")); + component.putClientProperty("JTable.rowFocusBorder", true); + JTableHeader header = table.getTableHeader(); + TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn(); + boolean forceLeft = false; + boolean forceRight = false; + if (draggedColumn != null) { + int index = DarkTableUI.viewIndexForColumn(draggedColumn, table); + forceLeft = column == index + 1 || column == index; + forceRight = column == index - 1 || column == index; + } + component.putClientProperty("JTable.forcePaintRight", forceRight); + component.putClientProperty("JTable.forcePaintLeft", forceLeft); + } else { + component.putClientProperty("JTable.rowFocusBorder", false); + } + boolean alternativeRow = UIManager.getBoolean("Table.alternateRowColor"); Color alternativeRowColor = UIManager.getColor("Table.alternateRowBackground"); Color normalColor = UIManager.getColor("Table.background"); diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableHeaderUI.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableHeaderUI.java index 0cadeb91..88ab697c 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableHeaderUI.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableHeaderUI.java @@ -164,6 +164,9 @@ public class DarkTableHeaderUI extends DarkTableHeaderUIBridge { if (draggedColumnIndex != cMax) { g.fillRect(draggedCellRect.x + draggedCellRect.width - 1, draggedCellRect.y, 1, draggedCellRect.height); + } else { + g.fillRect(draggedCellRect.x + draggedCellRect.width, draggedCellRect.y, + 1, draggedCellRect.height); } if (draggedColumnIndex == cMin) { g.fillRect(draggedCellRect.x, draggedCellRect.y, 1, draggedCellRect.height); @@ -188,6 +191,9 @@ public class DarkTableHeaderUI extends DarkTableHeaderUIBridge { if (draggedColumnIndex != cMax) { g.fillRect(draggedCellRect.x + draggedCellRect.width - 1, draggedCellRect.y, 1, draggedCellRect.height); + } else { + g.fillRect(draggedCellRect.x + draggedCellRect.width, draggedCellRect.y, + 1, draggedCellRect.height); } } else { if (draggedColumnIndex != cMin) { diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableUI.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableUI.java index 5f88f4e9..9db169ee 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableUI.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableUI.java @@ -1,5 +1,6 @@ package com.weis.darklaf.ui.table; +import com.weis.darklaf.util.DarkUIUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import sun.swing.SwingUtilities2; @@ -11,9 +12,12 @@ import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.awt.*; +import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.awt.event.MouseEvent; import java.beans.PropertyChangeListener; +import java.util.function.Supplier; /** * @author Jannis Weis @@ -52,6 +56,22 @@ public class DarkTableUI extends DarkTableUIBridge { } else if ("showVerticalLines".equals(key)) { var b = (boolean) e.getNewValue(); table.getColumnModel().setColumnMargin(b ? 1 : 0); + } else if ("ancestor".equals(key)) { + var oldVal = e.getOldValue(); + var newVal = e.getNewValue(); + if (oldVal instanceof Component) { + var oldUnwrapped = SwingUtilities.getUnwrappedParent((Component) oldVal); + if ((oldUnwrapped instanceof JScrollPane) + && ((JComponent) oldUnwrapped).getBorder() instanceof UIResource) { + LookAndFeel.uninstallBorder((JComponent) oldUnwrapped); + } + } + if (newVal instanceof Component) { + var newUnwrapped = SwingUtilities.getUnwrappedParent((Component) newVal); + if ((newUnwrapped instanceof JScrollPane)) { + LookAndFeel.installBorder((JComponent) newUnwrapped, "Table.scrollPaneBorder"); + } + } } }; @@ -61,53 +81,6 @@ public class DarkTableUI extends DarkTableUIBridge { return new DarkTableUI(); } - @Override - protected void installDefaults() { - super.installDefaults(); - table.setRowHeight(ROW_HEIGHT); - table.setDefaultEditor(Object.class, new DarkTableCellEditor()); - table.putClientProperty("JTable.renderBooleanAsCheckBox", - UIManager.getBoolean("Table.renderBooleanAsCheckBox")); - table.putClientProperty("JTable.booleanRenderType", UIManager.getString("Table.booleanRenderType")); - setupRendererComponents(table); - } - - protected static void setupRendererComponents(@NotNull final JTable table) { - var cellRenderer = new DarkTableCellRenderer(); - var cellEditor = new DarkTableCellEditor(); - var colorRendererEditor = new DarkColorTableCellRendererEditor(); - - table.setDefaultRenderer(Object.class, cellRenderer); - table.setDefaultRenderer(String.class, cellRenderer); - table.setDefaultRenderer(Integer.class, cellRenderer); - table.setDefaultRenderer(Double.class, cellRenderer); - table.setDefaultRenderer(Float.class, cellRenderer); - table.setDefaultRenderer(Boolean.class, cellRenderer); - table.setDefaultRenderer(Color.class, colorRendererEditor); - - table.setDefaultEditor(Object.class, cellEditor); - table.setDefaultEditor(String.class, cellEditor); - table.setDefaultEditor(Integer.class, cellEditor); - table.setDefaultEditor(Double.class, cellEditor); - table.setDefaultEditor(Float.class, cellEditor); - table.setDefaultEditor(Boolean.class, cellEditor); - table.setDefaultEditor(Color.class, colorRendererEditor); - } - - @Override - protected void installListeners() { - super.installListeners(); - table.addFocusListener(focusListener); - table.addPropertyChangeListener(propertyChangeListener); - } - - @Override - protected void uninstallListeners() { - super.uninstallListeners(); - table.removeFocusListener(focusListener); - table.removePropertyChangeListener(propertyChangeListener); - } - @Override protected void paintGrid(@NotNull final Graphics g, final int rMin, final int rMax, final int cMin, final int cMax) { @@ -140,7 +113,7 @@ public class DarkTableUI extends DarkTableUIBridge { boolean scrollVisible = scrollBarVisible(); if (table.getShowVerticalLines()) { TableColumnModel cm = table.getColumnModel(); - int tableHeight = damagedArea.y + damagedArea.height; + int tableHeight = getPreferredSize(table).height; int x; boolean ltr = table.getComponentOrientation().isLeftToRight(); if (ltr) { @@ -169,16 +142,129 @@ public class DarkTableUI extends DarkTableUIBridge { } } } - if (!table.getShowHorizontalLines() && table.getRowCount() != 0 && !scrollVisible) { - g.setColor(getBorderColor()); - var clip = g.getClipBounds(); - clip.height += 1; - g.setClip(clip); - int y = table.getHeight(); - g.fillRect(0, y, table.getWidth(), 1); + } + + + protected static void setupRendererComponents(@NotNull final JTable table) { + var cellRenderer = new DarkTableCellRenderer(); + var cellEditor = new DarkTableCellEditor(); + var colorRendererEditor = new DarkColorTableCellRendererEditor(); + + table.setDefaultRenderer(Object.class, cellRenderer); + table.setDefaultRenderer(String.class, cellRenderer); + table.setDefaultRenderer(Integer.class, cellRenderer); + table.setDefaultRenderer(Double.class, cellRenderer); + table.setDefaultRenderer(Float.class, cellRenderer); + table.setDefaultRenderer(Boolean.class, cellRenderer); + table.setDefaultRenderer(Color.class, colorRendererEditor); + + table.setDefaultEditor(Object.class, cellEditor); + table.setDefaultEditor(String.class, cellEditor); + table.setDefaultEditor(Integer.class, cellEditor); + table.setDefaultEditor(Double.class, cellEditor); + table.setDefaultEditor(Float.class, cellEditor); + table.setDefaultEditor(Boolean.class, cellEditor); + table.setDefaultEditor(Color.class, colorRendererEditor); + } + + @Override + protected void paintDraggedArea(@NotNull final Graphics g, final int rMin, final int rMax, + final int cMin, final int cMax, + final TableColumn draggedColumn, final int distance) { + int draggedColumnIndex = viewIndexForColumn(draggedColumn); + + Rectangle minCell = table.getCellRect(rMin, draggedColumnIndex, true); + Rectangle maxCell = table.getCellRect(rMax, draggedColumnIndex, true); + + Rectangle vacatedColumnRect = minCell.union(maxCell); + + int dist = adjustDistance(distance, vacatedColumnRect, table); + + // Paint a gray well in place of the moving column. + g.setColor(table.getParent().getBackground()); + g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, + vacatedColumnRect.width - 1, vacatedColumnRect.height); + + + // Move to the where the cell has been dragged. + vacatedColumnRect.x += dist; + + boolean scrollVisible = scrollBarVisible(); + boolean drawBottomBorder = !table.getShowHorizontalLines() && !scrollVisible && table.getShowVerticalLines(); + boolean ltr = table.getComponentOrientation().isLeftToRight(); + + // Fill the background. + g.setColor(table.getBackground()); + g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, + vacatedColumnRect.width, vacatedColumnRect.height); + + + // Paint the vertical grid lines if necessary. + if (table.getShowVerticalLines()) { + g.setColor(table.getGridColor()); + int x1 = vacatedColumnRect.x; + int y1 = vacatedColumnRect.y; + int x2 = x1 + vacatedColumnRect.width - 1; + int y2 = y1 + vacatedColumnRect.height - 1; + + boolean onLeftEdge = ltr ? draggedColumnIndex == cMin : draggedColumnIndex == cMax; + boolean onRightEdge = ltr ? draggedColumnIndex == cMax : draggedColumnIndex == cMin; + if (scrollBarVisible()) { + if (isScrollPaneRtl()) { + onLeftEdge = false; + } else { + onRightEdge = false; + } + } + // Left + if (dist != 0 || !onLeftEdge) { + if (draggedColumnIndex == cMin && scrollBarVisible() && isScrollPaneRtl()) x1++; + g.fillRect(x1 - 1, y1, 1, y2 - y1); + } + // Right + if (dist != 0 || !onRightEdge) { + g.fillRect(x2, y1, 1, y2 - y1); + } + } + + for (int row = rMin; row <= rMax; row++) { + // Render the cell value + Rectangle r = table.getCellRect(row, draggedColumnIndex, false); + r.x += dist; + paintCell(g, r, row, draggedColumnIndex); + + // Paint the (lower) horizontal grid line if necessary. + if (table.getShowHorizontalLines() || (!scrollVisible && row == rMax)) { + g.setColor(table.getGridColor()); + Rectangle rcr = table.getCellRect(row, draggedColumnIndex, true); + rcr.x += dist; + int x1 = rcr.x - 1; + int y1 = rcr.y; + int x2 = x1 + rcr.width + 1; + int y2 = y1 + rcr.height - 1; + g.fillRect(x1, y2, x2 - x1, 1); + } } } + @Override + protected void installListeners() { + super.installListeners(); + table.addFocusListener(focusListener); + table.addPropertyChangeListener(propertyChangeListener); + } + + @Override + protected void uninstallListeners() { + super.uninstallListeners(); + table.removeFocusListener(focusListener); + table.removePropertyChangeListener(propertyChangeListener); + } + + protected boolean pointOutsidePrefSize(final int row, final int column, final Point p) { + return false; + } + protected boolean isScrollPaneRtl() { if (!isInScrollPane()) return false; Container comp = SwingUtilities.getUnwrappedParent(table).getParent(); @@ -300,91 +386,94 @@ public class DarkTableUI extends DarkTableUIBridge { } @Override - protected void paintDraggedArea(@NotNull final Graphics g, final int rMin, final int rMax, - final int cMin, final int cMax, - final TableColumn draggedColumn, final int distance) { - int draggedColumnIndex = viewIndexForColumn(draggedColumn); - - Rectangle minCell = table.getCellRect(rMin, draggedColumnIndex, true); - Rectangle maxCell = table.getCellRect(rMax, draggedColumnIndex, true); - - Rectangle vacatedColumnRect = minCell.union(maxCell); - - int dist = adjustDistance(distance, vacatedColumnRect, table); - - // Paint a gray well in place of the moving column. - g.setColor(table.getParent().getBackground()); - g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, - vacatedColumnRect.width - 1, vacatedColumnRect.height); - - - // Move to the where the cell has been dragged. - vacatedColumnRect.x += dist; + protected Handler getHandler() { + if (handler == null) { + handler = new DarkHandler(); + } + return handler; + } - boolean scrollVisible = scrollBarVisible(); - boolean drawBottomBorder = !table.getShowHorizontalLines() && !scrollVisible; - boolean ltr = table.getComponentOrientation().isLeftToRight(); + @Override + protected void installDefaults() { + super.installDefaults(); + int rowHeight = UIManager.getInt("Table.rowHeight"); + if (rowHeight > 0) { + table.setRowHeight(ROW_HEIGHT); + } + table.setDefaultEditor(Object.class, new DarkTableCellEditor()); + table.putClientProperty("JTable.renderBooleanAsCheckBox", + UIManager.getBoolean("Table.renderBooleanAsCheckBox")); + table.putClientProperty("JTable.booleanRenderType", UIManager.getString("Table.booleanRenderType")); + setupRendererComponents(table); + } - // Fill the background. - g.setColor(table.getBackground()); - g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, - vacatedColumnRect.width, vacatedColumnRect.height); + @Override + public Dimension getPreferredSize(final JComponent c) { + var prefSize = super.getPreferredSize(c); + if (!isInScrollPane()) { + return prefSize; + } else { + var dim = SwingUtilities.getUnwrappedParent(table).getSize(); + if (dim.width < prefSize.width || dim.height < prefSize.height) { + return prefSize; + } else { + return dim; + } + } + } + protected class DarkHandler extends Handler { - // Paint the vertical grid lines if necessary. - if (table.getShowVerticalLines()) { - g.setColor(table.getGridColor()); - int x1 = vacatedColumnRect.x; - int y1 = vacatedColumnRect.y; - int x2 = x1 + vacatedColumnRect.width - 1; - int y2 = y1 + vacatedColumnRect.height - 1; + protected int lastIndex = -1; - boolean onLeftEdge = ltr ? draggedColumnIndex == cMin : draggedColumnIndex == cMax; - boolean onRightEdge = ltr ? draggedColumnIndex == cMax : draggedColumnIndex == cMin; - if (scrollBarVisible()) { - if (isScrollPaneRtl()) { - onLeftEdge = false; + @Override + public void mouseClicked(final MouseEvent e) { + super.mouseClicked(e); + if (isFileList) { + int row = table.rowAtPoint(e.getPoint()); + JFileChooser fc = getFileChooser(); + if (row < 0 || fc == null) return; + int column = getFileNameColumnIndex(); + boolean isSelected = table.getSelectionModel().getLeadSelectionIndex() == row + && table.getColumnModel().getSelectionModel().getLeadSelectionIndex() == column; + if ((!fc.isMultiSelectionEnabled() || fc.getSelectedFiles().length <= 1) + && isSelected && lastIndex == row + && DarkUIUtil.isOverText(e, row, column, table)) { + startEditing(row, column); } else { - onRightEdge = false; + lastIndex = row; } } - // Left - if (dist != 0 || !onLeftEdge) { - if (draggedColumnIndex == cMin && scrollBarVisible() && isScrollPaneRtl()) x1++; - g.fillRect(x1 - 1, y1, 1, y2 - y1); - } - // Right - if (dist != 0 || !onRightEdge) { - g.fillRect(x2, y1, 1, y2 - y1); + } + + protected JFileChooser getFileChooser() { + var obj = table.getClientProperty("JTable.fileChooserParent"); + if (obj instanceof Supplier) { + var supplied = ((Supplier) obj).get(); + return supplied instanceof JFileChooser ? (JFileChooser) supplied : null; } + return null; } - for (int row = rMin; row <= rMax; row++) { - // Render the cell value - Rectangle r = table.getCellRect(row, draggedColumnIndex, false); - r.x += dist; - paintCell(g, r, row, draggedColumnIndex); + protected Integer getFileNameColumnIndex() { + var obj = table.getClientProperty("JTable.fileNameColumnIndex"); + return obj instanceof Integer ? (Integer) obj : 0; + } - // Paint the (lower) horizontal grid line if necessary. - if (table.getShowHorizontalLines() || (!scrollVisible && row == rMax)) { - g.setColor(table.getGridColor()); - if (drawBottomBorder) { - g.setColor(getBorderColor()); - } - Rectangle rcr = table.getCellRect(row, draggedColumnIndex, true); - rcr.x += dist; - int x1 = rcr.x - 1; - int y1 = rcr.y; - int x2 = x1 + rcr.width + 1; - int y2 = y1 + rcr.height - 1; - g.fillRect(x1, y2, x2 - x1, 1); + protected void startEditing(final int row, final int column) { + table.editCellAt(row, column, null); + Component editorComponent = table.getEditorComponent(); + if (editorComponent != null && !editorComponent.hasFocus()) { + SwingUtilities2.compositeRequestFocus(editorComponent); } } - if (drawBottomBorder) { - var rect = table.getCellRect(rMax, draggedColumnIndex, true); - int y = rect.y + rect.height - 1; - g.setColor(getBorderColor()); - g.fillRect(rect.x, y, rect.width, 1); + + @Override + protected void maybeStartTimer() { + } + + @Override + public void actionPerformed(final ActionEvent ae) { } } } diff --git a/src/main/java/com/weis/darklaf/ui/table/DarkTableUIBridge.java b/src/main/java/com/weis/darklaf/ui/table/DarkTableUIBridge.java index e52824b3..d9245cf4 100644 --- a/src/main/java/com/weis/darklaf/ui/table/DarkTableUIBridge.java +++ b/src/main/java/com/weis/darklaf/ui/table/DarkTableUIBridge.java @@ -27,7 +27,6 @@ import org.jetbrains.annotations.NotNull; import sun.swing.SwingUtilities2; import javax.swing.*; -import javax.swing.plaf.basic.BasicTableUI; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; @@ -37,21 +36,7 @@ import java.awt.*; /** * @author Jannis Weis */ -public class DarkTableUIBridge extends BasicTableUI { - - protected static int getAdjustedLead(final JTable table, final boolean row) { - return row ? getAdjustedLead(table, true, table.getSelectionModel()) - : getAdjustedLead(table, false, table.getColumnModel().getSelectionModel()); - } - - protected static int getAdjustedLead(final JTable table, - final boolean row, - final ListSelectionModel model) { - - int index = model.getLeadSelectionIndex(); - int compare = row ? table.getRowCount() : table.getColumnCount(); - return index < compare ? index : -1; - } +public class DarkTableUIBridge extends BasicTableUIBridge { /** * Paint a representation of the table instance @@ -446,6 +431,10 @@ public class DarkTableUIBridge extends BasicTableUI { } protected int viewIndexForColumn(final TableColumn aColumn) { + return viewIndexForColumn(aColumn, table); + } + + public static int viewIndexForColumn(final TableColumn aColumn, final JTable table) { TableColumnModel cm = table.getColumnModel(); for (int column = 0; column < cm.getColumnCount(); column++) { if (cm.getColumn(column) == aColumn) { diff --git a/src/main/java/com/weis/darklaf/ui/table/TextFieldTableCellEditorBorder.java b/src/main/java/com/weis/darklaf/ui/table/TextFieldTableCellEditorBorder.java index 53c89bf2..8a50dc0a 100644 --- a/src/main/java/com/weis/darklaf/ui/table/TextFieldTableCellEditorBorder.java +++ b/src/main/java/com/weis/darklaf/ui/table/TextFieldTableCellEditorBorder.java @@ -1,6 +1,8 @@ package com.weis.darklaf.ui.table; import com.weis.darklaf.ui.text.DarkTextFieldUI; +import com.weis.darklaf.util.DarkUIUtil; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -12,12 +14,11 @@ import java.awt.*; public class TextFieldTableCellEditorBorder extends DarkTableCellBorder { @Override - public void paintBorder(final Component c, @NotNull final Graphics g, final int x, final int y, + public void paintBorder(@NotNull final Component c, @NotNull final Graphics g, final int x, final int y, final int width, final int height) { - g.setColor(DarkTextFieldUI.getBorderColor(c)); - var parent = c.getParent(); - if (parent instanceof JTable) { - var table = ((JTable) parent); + g.setColor(DarkTextFieldUI.getBorderColor(false, false, true, true)); + var table = DarkUIUtil.getParentOfType(JTable.class, c); + if (table != null) { if (!table.getShowHorizontalLines()) { g.fillRect(0, 0, width, 1); g.fillRect(0, height - 1, width, 1); @@ -25,7 +26,53 @@ public class TextFieldTableCellEditorBorder extends DarkTableCellBorder { if (!table.getShowVerticalLines()) { g.fillRect(0, 0, 1, height); g.fillRect(width - 1, 0, 1, height); + } else if (isInWrapper(c)) { + if (c.getParent().getComponentOrientation().isLeftToRight()) { + g.fillRect(0, 0, 1, height); + } else { + g.fillRect(width - 1, 0, 1, height); + } } + } else { + DarkUIUtil.drawRect(g, x, y, width, height, 1); } } + + protected static boolean isInWrapper(@NotNull final Component c) { + return c.getParent() instanceof DarkTableCellEditor.IconWrapper; + } + + @Override + public Insets getBorderInsets(final Component c) { + var ins = super.getBorderInsets(); + if (isInWrapper(c)) { + if (parentLTR(c)) { + ins.left -= ((DarkTableCellEditor.IconWrapper) c.getParent()).getIconCompGap(); + } else { + ins.right -= ((DarkTableCellEditor.IconWrapper) c.getParent()).getIconCompGap(); + } + } else if (isListEditor(c)) { + var renderer = ((JList) c.getParent()).getCellRenderer(); + if (renderer instanceof JLabel) { + if (parentLTR(c)) { + ins.left -= ((JLabel) renderer).getIconTextGap() - 1; + } else { + ins.right -= ((JLabel) renderer).getIconTextGap() - 1; + } + } + } + return ins; + } + + protected static boolean parentLTR(@NotNull final Component c) { + return c.getParent().getComponentOrientation().isLeftToRight(); + } + + @Contract("null -> false") + protected static boolean isListEditor(final Component c) { + return c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty("JTextField.listCellEditor")) + && c.getParent() instanceof JList; + } + } diff --git a/src/main/java/com/weis/darklaf/ui/text/DarkTextBorder.java b/src/main/java/com/weis/darklaf/ui/text/DarkTextBorder.java index a2463303..81c30316 100644 --- a/src/main/java/com/weis/darklaf/ui/text/DarkTextBorder.java +++ b/src/main/java/com/weis/darklaf/ui/text/DarkTextBorder.java @@ -23,6 +23,7 @@ */ package com.weis.darklaf.ui.text; +import com.weis.darklaf.ui.table.TextFieldTableCellEditorBorder; import com.weis.darklaf.util.DarkUIUtil; import com.weis.darklaf.util.GraphicsContext; import com.weis.darklaf.util.GraphicsUtil; @@ -38,11 +39,18 @@ import java.awt.*; */ public class DarkTextBorder implements Border, UIResource { + private static final Border editorBorder = new TextFieldTableCellEditorBorder(); + public final static int BORDER_SIZE = 3; public final static int PADDING = 4; public void paintBorder(final Component c, final Graphics g2, final int x, final int y, final int width, final int height) { + if (isCellEditor(c)) { + editorBorder.paintBorder(c, g2, x, y, width, height); + return; + } + Graphics2D g = (Graphics2D) g2; g.translate(x, y); GraphicsContext config = GraphicsUtil.setupStrokePainting(g); @@ -76,8 +84,16 @@ public class DarkTextBorder implements Border, UIResource { config.restore(); } + protected static boolean isCellEditor(final Component c) { + return c instanceof JComponent + && Boolean.TRUE.equals(((JComponent) c).getClientProperty("JTextField.cellEditor")); + } + @Override public Insets getBorderInsets(final Component c) { + if (isCellEditor(c)) { + return editorBorder.getBorderInsets(c); + } Insets insets = new Insets(BORDER_SIZE + PADDING, BORDER_SIZE + PADDING, BORDER_SIZE + PADDING, BORDER_SIZE + PADDING); if (DarkTextFieldUI.isSearchField(c)) { diff --git a/src/main/java/com/weis/darklaf/ui/text/DarkTextFieldUI.java b/src/main/java/com/weis/darklaf/ui/text/DarkTextFieldUI.java index 0706134e..6d2886d2 100644 --- a/src/main/java/com/weis/darklaf/ui/text/DarkTextFieldUI.java +++ b/src/main/java/com/weis/darklaf/ui/text/DarkTextFieldUI.java @@ -95,6 +95,11 @@ public class DarkTextFieldUI extends DarkTextFieldUIBridge implements PropertyCh boolean editable = !(c instanceof JTextComponent) || ((JTextComponent) c).isEditable(); boolean focus = DarkUIUtil.hasFocus(c); boolean error = hasError(c); + return getBorderColor(focus, error, editable, c.isEnabled()); + } + + public static Color getBorderColor(@NotNull final boolean focus, final boolean error, + final boolean editable, final boolean enabled) { if (focus) { if (error) { return UIManager.getColor("TextField.border.focusError"); @@ -104,7 +109,7 @@ public class DarkTextFieldUI extends DarkTextFieldUIBridge implements PropertyCh } else if (error) { return UIManager.getColor("TextField.border.error"); } - return c.isEnabled() && editable + return enabled && editable ? UIManager.getColor("TextField.border.enabled") : UIManager.getColor("TextField.border.disabled"); } diff --git a/src/main/java/com/weis/darklaf/ui/tooltip/DarkTooltipBorder.java b/src/main/java/com/weis/darklaf/ui/tooltip/DarkTooltipBorder.java index 85cdc6bd..8d5a452a 100644 --- a/src/main/java/com/weis/darklaf/ui/tooltip/DarkTooltipBorder.java +++ b/src/main/java/com/weis/darklaf/ui/tooltip/DarkTooltipBorder.java @@ -82,6 +82,8 @@ public class DarkTooltipBorder implements Border, UIResource { si.right = 0; } else if (align == Alignment.WEST) { si.left = 0; + } else if (align == Alignment.NORTH_EAST || align == Alignment.NORTH || align == Alignment.NORTH_WEST) { + si.bottom = 0; } } diff --git a/src/main/java/com/weis/darklaf/util/DarkUIUtil.java b/src/main/java/com/weis/darklaf/util/DarkUIUtil.java index f852d56d..6c35d947 100644 --- a/src/main/java/com/weis/darklaf/util/DarkUIUtil.java +++ b/src/main/java/com/weis/darklaf/util/DarkUIUtil.java @@ -37,6 +37,8 @@ import javax.swing.table.TableCellRenderer; import javax.swing.tree.TreeCellRenderer; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; import java.awt.geom.RoundRectangle2D; @@ -166,13 +168,7 @@ public final class DarkUIUtil { return new Color(redPart, greenPart, bluePart); } - public static void drawRect(final Graphics g, final int x, final int y, final int width, final int height, - final int thickness) { - g.fillRect(x, y, width, thickness); - g.fillRect(x, y, thickness, height); - g.fillRect(x + width - thickness, y, thickness, height); - g.fillRect(x, y + height - thickness, width, thickness); - } + private static final Rectangle iconRect = new Rectangle(); public static void applyInsets(final Rectangle rect, final Insets insets) { if (insets != null && rect != null) { @@ -316,6 +312,59 @@ public final class DarkUIUtil { return null; } + private static final Rectangle textRect = new Rectangle(); + + public static void drawRect(@NotNull final Graphics g, final int x, final int y, + final int width, final int height, final int thickness) { + g.fillRect(x, y, width, thickness); + g.fillRect(x, y + thickness, thickness, height - 2 * thickness); + g.fillRect(x + width - thickness, y + thickness, thickness, height - 2 * thickness); + g.fillRect(x, y + height - thickness, width, thickness); + } + + public static boolean isOverText(@NotNull final MouseEvent e, final int index, final JList list) { + var bounds = list.getCellBounds(index, index); + if (!bounds.contains(e.getPoint())) return false; + //noinspection unchecked + var cellRenderer = ((ListCellRenderer) list.getCellRenderer()) + .getListCellRendererComponent(list, list.getModel().getElementAt(index), + index, false, false); + if (cellRenderer instanceof JLabel) { + return isOverText((JLabel) cellRenderer, bounds, e.getPoint()); + } else { + return true; + } + } + + public static boolean isOverText(final JLabel label, final Rectangle bounds, final Point p) { + textRect.setBounds(0, 0, 0, 0); + iconRect.setBounds(0, 0, 0, 0); + SwingUtilities.layoutCompoundLabel(label, label.getFontMetrics(label.getFont()), label.getText(), + label.getIcon(), label.getVerticalAlignment(), + label.getHorizontalAlignment(), + label.getVerticalTextPosition(), label.getHorizontalTextPosition(), + bounds, iconRect, textRect, label.getIconTextGap()); + return textRect.contains(p); + } + + public static boolean isOverText(@NotNull final MouseEvent e, final int row, final int column, + final JTable table) { + var bounds = table.getCellRect(row, column, false); + if (!bounds.contains(e.getPoint())) return false; + var cellRenderer = table.getCellRenderer(row, column).getTableCellRendererComponent( + table, table.getValueAt(row, column), false, false, row, column); + if (cellRenderer instanceof JLabel) { + return isOverText((JLabel) cellRenderer, bounds, e.getPoint()); + } else { + return true; + } + } + + public static boolean isMenuShortcutKeyDown(final InputEvent event) { + return (event.getModifiersEx() & + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()) != 0; + } + public enum Outline { error { @Override diff --git a/src/main/resources/com/weis/darklaf/icons/dark/files/drive.svg b/src/main/resources/com/weis/darklaf/icons/dark/files/drive.svg new file mode 100644 index 00000000..8f2c4455 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/files/drive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/menu/down.svg b/src/main/resources/com/weis/darklaf/icons/dark/menu/down.svg new file mode 100644 index 00000000..40cfbf37 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/menu/down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/menu/save.svg b/src/main/resources/com/weis/darklaf/icons/dark/menu/save.svg new file mode 100644 index 00000000..20ed7c88 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/menu/save.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/menu/up.svg b/src/main/resources/com/weis/darklaf/icons/dark/menu/up.svg new file mode 100644 index 00000000..d1682d2e --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/menu/up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownHover.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownHover.svg new file mode 100644 index 00000000..1ad4f5e9 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownHover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownSelected.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownSelected.svg new file mode 100644 index 00000000..dcb01140 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftHover.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftHover.svg new file mode 100644 index 00000000..202f9025 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftHover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftSelected.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftSelected.svg new file mode 100644 index 00000000..36a33e0a --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightHover.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightHover.svg new file mode 100644 index 00000000..ee411c48 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightHover.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightSelected.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightSelected.svg new file mode 100644 index 00000000..9cb57a3c --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightSelected.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpHover.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpHover.svg new file mode 100644 index 00000000..006b43aa --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpHover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpSelected.svg b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpSelected.svg new file mode 100644 index 00000000..0b6368f5 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/files/drive.svg b/src/main/resources/com/weis/darklaf/icons/light/files/drive.svg new file mode 100644 index 00000000..8a776df5 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/files/drive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/files/folder.svg b/src/main/resources/com/weis/darklaf/icons/light/files/folder.svg index f2b64f37..c346241d 100644 --- a/src/main/resources/com/weis/darklaf/icons/light/files/folder.svg +++ b/src/main/resources/com/weis/darklaf/icons/light/files/folder.svg @@ -1,7 +1,3 @@ - - - - diff --git a/src/main/resources/com/weis/darklaf/icons/light/menu/down.svg b/src/main/resources/com/weis/darklaf/icons/light/menu/down.svg new file mode 100644 index 00000000..826a5a6b --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/menu/down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/menu/save.svg b/src/main/resources/com/weis/darklaf/icons/light/menu/save.svg new file mode 100644 index 00000000..c1bee39e --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/menu/save.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/menu/up.svg b/src/main/resources/com/weis/darklaf/icons/light/menu/up.svg new file mode 100644 index 00000000..268c4cc5 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/menu/up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownHover.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownHover.svg new file mode 100644 index 00000000..c5e2808e --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownHover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownSelected.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownSelected.svg new file mode 100644 index 00000000..856b4a7d --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftHover.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftHover.svg new file mode 100644 index 00000000..1c45d361 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftHover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftSelected.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftSelected.svg new file mode 100644 index 00000000..bcbbad2c --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightHover.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightHover.svg new file mode 100644 index 00000000..2d4cdeb4 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightHover.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightSelected.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightSelected.svg new file mode 100644 index 00000000..b84a1050 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightSelected.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpHover.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpHover.svg new file mode 100644 index 00000000..5380b781 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpHover.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpSelected.svg b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpSelected.svg new file mode 100644 index 00000000..e011b0b8 --- /dev/null +++ b/src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/com/weis/darklaf/properties/platform/windows.properties b/src/main/resources/com/weis/darklaf/properties/platform/windows.properties index 4dff0aa5..9dba80dc 100644 --- a/src/main/resources/com/weis/darklaf/properties/platform/windows.properties +++ b/src/main/resources/com/weis/darklaf/properties/platform/windows.properties @@ -62,3 +62,5 @@ ToolBar.borderColor = %border #MenuBar MenuBar.border = com.weis.darklaf.ui.menu.DarkMenuBarBorder MenuBar.borderColor = %border + +FileChooser.listViewWindowsStyle = true \ No newline at end of file diff --git a/src/main/resources/com/weis/darklaf/properties/ui/comboBox.properties b/src/main/resources/com/weis/darklaf/properties/ui/comboBox.properties index 1bfc7af2..c8bc85d4 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/comboBox.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/comboBox.properties @@ -29,3 +29,4 @@ ComboBox.focusBorderColor = %glowFocusLine ComboBox.inactiveBackground = %widgetFillInactive ComboBox.activeBackground = %background ComboBox.disabledForeground = %textForegroundInactive +ComboBox.disabledBackground = %widgetFillInactive \ No newline at end of file diff --git a/src/main/resources/com/weis/darklaf/properties/ui/fileChooser.properties b/src/main/resources/com/weis/darklaf/properties/ui/fileChooser.properties index 2c6be708..7f728689 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/fileChooser.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/fileChooser.properties @@ -21,12 +21,28 @@ #SOFTWARE. # # suppress inspection "UnusedProperty" for whole file -FileChooser.newFolderIcon = files/newFolder.svg[aware] -FileChooser.upFolderIcon = files/upFolder.svg[aware] -FileChooser.homeFolderIcon = files/homeFolder.svg[aware] +FileChooserUI = com.weis.darklaf.ui.filechooser.DarkFileChooserUI +FileChooser.newFolderIcon = files/newFolder.svg[aware] +FileChooser.upFolderIcon = files/upFolder.svg[aware] +FileChooser.homeFolderIcon = files/homeFolder.svg[aware] +FileChooser.listViewBorder = com.weis.darklaf.ui.filechooser.DarkFileChooserListViewBorder -FileView.fileIcon = files/general.svg[aware] -FileView.directoryIcon = files/folder.svg[aware] -FileChooser.detailsViewIcon = menu/listFiles.svg[aware] -FileChooser.listViewIcon = menu/groupBy.svg[aware] -FileView.computerIcon = files/desktop.svg[aware] \ No newline at end of file +FileChooser.detailsViewIcon = menu/listFiles.svg[aware] +FileChooser.listViewIcon = menu/groupBy.svg[aware] + +FileChooser.borderColor = %borderSecondary +FileChooser.rowHeight = 20 +FileChooser.minEditDelay = 200 +FileChooser.maxEditDelay = 600 +FileView.fullRowSelection = true +FileChooser.listViewWindowsStyle = false +FileChooser.fileSizeKiloBytes = {0} kb +FileChooser.fileSizeMegaBytes = {0} mb +FileChooser.fileSizeGigaBytes = {0} gb +FileChooser.readOnly = false + +FileView.fileIcon = files/general.svg[aware] +FileView.directoryIcon = files/folder.svg[aware] +FileView.computerIcon = files/desktop.svg[aware] +FileView.floppyDriveIcon = menu/save.svg[aware] +FileView.hardDriveIcon = files/drive.svg[aware] \ No newline at end of file diff --git a/src/main/resources/com/weis/darklaf/properties/ui/list.properties b/src/main/resources/com/weis/darklaf/properties/ui/list.properties index 3c08d425..e4b06a37 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/list.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/list.properties @@ -22,9 +22,12 @@ # # suppress inspection "UnusedProperty" for whole file ListUI = com.weis.darklaf.ui.list.DarkListUI +List.cellRenderer = com.weis.darklaf.ui.list.DarkListCellRenderer List.border = null List.background = %background -List.focusSelectedCellHighlightBorder = com.weis.darklaf.ui.list.DarkListCellBorder +List.focusSelectedCellHighlightBorder = com.weis.darklaf.ui.list.DarkListCellFocusBorder List.focusCellHighlightBorder = com.weis.darklaf.ui.list.DarkListCellBorder List.cellNoFocusBorder = com.weis.darklaf.ui.list.DarkListCellBorder List.dropLineColor = %dropForeground +List.selectionBackground = %highlightFillFocus +List.focusBorderColor = %borderFocus diff --git a/src/main/resources/com/weis/darklaf/properties/ui/menu.properties b/src/main/resources/com/weis/darklaf/properties/ui/menu.properties index e05b2795..33da1f89 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/menu.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/menu.properties @@ -27,6 +27,9 @@ Menu.border = com.weis.darklaf.ui.menu.DarkMenuItemBorde Menu.selectionBackground = %highlightFillFocus Menu.acceleratorForeground = %acceleratorForeground Menu.acceleratorSelectionForeground = %acceleratorForeground +Menu.submenuPopupOffsetX = -4 +Menu.submenuPopupOffsetY = -2 #Icons -Menu.arrowIcon = navigation/arrowRight.svg[aware] \ No newline at end of file +Menu.arrowIcon = navigation/arrowRight.svg[aware] +MenuItem.arrowHover.icon = navigation/arrowRightHover.svg[aware] \ No newline at end of file diff --git a/src/main/resources/com/weis/darklaf/properties/ui/table.properties b/src/main/resources/com/weis/darklaf/properties/ui/table.properties index dbbc9051..ec02fe0e 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/table.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/table.properties @@ -37,10 +37,13 @@ Table.cellEditorBorder = com.weis.darklaf.ui.table.DarkTableCell Table.scrollPaneBorder = com.weis.darklaf.ui.table.DarkTableBorder Table.background = %background Table.focusBorderColor = %borderFocus +Table.focusRowBorderColor = %borderFocus Table.gridColor = %gridLine Table.dropLineColor = %dropForeground Table.dropLineShortColor = %dropForeground Table.focusSelectionBackground = %highlightFillFocus +Table.focusCellBackground = %background +Table.focusCellForeground = %textForeground Table.selectionNoFocusBackground = %highlightFill Table.selectionBackground = %highlightFillFocus @@ -49,7 +52,8 @@ Table.alternateRowBackground = %backgroundAlternative Table.renderBooleanAsCheckBox = true Table.booleanRenderType = checkBox +Table.rowHeight = 22 #Icons -Table.ascendingSortIcon = menu/upDown.svg[aware] -Table.descendingSortIcon = menu/upDown.svg[aware] +Table.ascendingSortIcon = menu/up.svg[aware](8,16) +Table.descendingSortIcon = menu/down.svg[aware](8,16) diff --git a/src/main/resources/com/weis/darklaf/properties/ui/toggleButton.properties b/src/main/resources/com/weis/darklaf/properties/ui/toggleButton.properties index d6471d7f..b7ec0b1f 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/toggleButton.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/toggleButton.properties @@ -27,4 +27,7 @@ ToggleButton.sliderBorderColor = %widgetBorder ToggleButton.disabledSliderBorderColor = %widgetBorderInactive ToggleButton.focusedSliderBorderColor = %glowFocusLine ToggleButton.sliderColor = %controlFill -ToggleButton.disabledSliderColor = %controlFillDisabled \ No newline at end of file +ToggleButton.disabledSliderColor = %controlFillDisabled + +ToggleButton.inactiveFillColor = %widgetFill +ToggleButton.activeFillColor = %backgroundHoverSecondary \ No newline at end of file diff --git a/src/main/resources/com/weis/darklaf/properties/ui/tree.properties b/src/main/resources/com/weis/darklaf/properties/ui/tree.properties index 97799120..b979d091 100644 --- a/src/main/resources/com/weis/darklaf/properties/ui/tree.properties +++ b/src/main/resources/com/weis/darklaf/properties/ui/tree.properties @@ -52,12 +52,12 @@ Tree.expandedIcon = navigation/arrowDown.svg[aware] Tree.closedIcon = files/folder.svg[aware] Tree.openIcon = files/folder.svg[aware] Tree.leafIcon = files/general.svg[aware] -Tree.collapsed.selected.focused.icon = navigation/arrowRight.svg[aware] +Tree.collapsed.selected.focused.icon = navigation/arrowRightSelected.svg[aware] Tree.collapsed.selected.unfocused.icon = navigation/arrowRight.svg[aware] Tree.collapsed.unselected.focused.icon = navigation/arrowRight.svg[aware] Tree.collapsed.unselected.unfocused.icon = navigation/arrowRight.svg[aware] -Tree.expanded.selected.focused.icon = navigation/arrowDown.svg[aware] +Tree.expanded.selected.focused.icon = navigation/arrowDownSelected.svg[aware] Tree.expanded.selected.unfocused.icon = navigation/arrowDown.svg[aware] Tree.expanded.unselected.focused.icon = navigation/arrowDown.svg[aware] Tree.expanded.unselected.unfocused.icon = navigation/arrowDown.svg[aware] \ No newline at end of file diff --git a/src/main/resources/library/x64/jniplatform.dll b/src/main/resources/library/x64/jniplatform.dll index 1be11206..622a1b86 100644 Binary files a/src/main/resources/library/x64/jniplatform.dll and b/src/main/resources/library/x64/jniplatform.dll differ diff --git a/src/main/resources/library/x86/jniplatform.dll b/src/main/resources/library/x86/jniplatform.dll index 3d0f9351..2448068e 100644 Binary files a/src/main/resources/library/x86/jniplatform.dll and b/src/main/resources/library/x86/jniplatform.dll differ diff --git a/src/test/java/FileChooserDemo.java b/src/test/java/FileChooserDemo.java index 5589463f..88825da4 100644 --- a/src/test/java/FileChooserDemo.java +++ b/src/test/java/FileChooserDemo.java @@ -1,5 +1,4 @@ import com.weis.darklaf.LafManager; -import com.weis.darklaf.theme.Theme; import javax.swing.*; @@ -9,11 +8,11 @@ public final class FileChooserDemo { SwingUtilities.invokeLater(() -> { LafManager.install(); var chooser = new JFileChooser(System.getProperty("user.home")); + chooser.setMultiSelectionEnabled(true); var frame = new JFrame(); - frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - frame.setSize(100, 100); - frame.setVisible(true); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); + frame.setVisible(true); chooser.showOpenDialog(frame); }); } diff --git a/src/test/java/ToolTipDemo.java b/src/test/java/ToolTipDemo.java index 1f543492..5ebcfaa9 100644 --- a/src/test/java/ToolTipDemo.java +++ b/src/test/java/ToolTipDemo.java @@ -1,4 +1,5 @@ import com.weis.darklaf.LafManager; +import com.weis.darklaf.components.alignment.Alignment; import com.weis.darklaf.components.tooltip.ToolTipContext; import javax.swing.*; @@ -13,7 +14,8 @@ public class ToolTipDemo { JFrame f = new JFrame(); var p = new JPanel(); p.add(new JButton("Button with very very long text") { - private final ToolTipContext context = new ToolTipContext(this); + private final ToolTipContext context = new ToolTipContext(this).setAlignment(Alignment.CENTER) + .setCenterAlignment(Alignment.SOUTH); { setToolTipText("ToolTip \n multiline \n third line's a charm"); diff --git a/src/test/java/UIDemo.java b/src/test/java/UIDemo.java index a528ae97..5576bf6f 100644 --- a/src/test/java/UIDemo.java +++ b/src/test/java/UIDemo.java @@ -391,7 +391,13 @@ public final class UIDemo { var menuBar = new JMenuBar(); var menu = new JMenu("test"); - menu.add(new JMenuItem("item")); + menu.add(new JMenu("submenu") {{ + add(new JMenuItem("item1")); + add(new JMenuItem("item2")); + add(new JMenuItem("item3")); + add(new JMenuItem("item4")); + add(new JMenuItem("item5")); + }}); menu.addSeparator(); menu.add(new JRadioButtonMenuItem("radioButton")); menu.add(new JCheckBoxMenuItem("checkBox")); diff --git a/src/test/java/UIManagerDefaults.java b/src/test/java/UIManagerDefaults.java index dfcad0b8..dad37e15 100644 --- a/src/test/java/UIManagerDefaults.java +++ b/src/test/java/UIManagerDefaults.java @@ -5,6 +5,7 @@ import com.weis.darklaf.DarkLafInfo; import com.weis.darklaf.LafManager; +import com.weis.darklaf.components.OverlayScrollPane; import com.weis.darklaf.ui.cell.DarkCellRendererToggleButton; import com.weis.darklaf.ui.table.DarkColorTableCellRendererEditor; import org.jetbrains.annotations.Contract; @@ -211,7 +212,7 @@ public class UIManagerDefaults implements ItemListener { d.height = 350; table.setPreferredScrollableViewportSize(d); - return new JScrollPane(table); + return new OverlayScrollPane(table); } /*