From 86215082cb59fc0634ee620330c966ce3d462a70 Mon Sep 17 00:00:00 2001 From: weisj Date: Mon, 14 Oct 2019 19:27:39 +0200 Subject: [PATCH] Fixed an issue where the cursor wasn't correctly displayed in OverlayScrollPanes. Added convenience class for tooltip alignment. Fixed an issue where clicking on a JToggleButton would not be recognized. Fixed an issue where JToggleButtons were too wide. Fixed an issue where the arrowIcon wouldn't be displayed in menus. Added ui for fileChooser. Fixed issue where the tooltip border was too small. --- build.gradle | 3 +- .../darklaf/components/OverlayScrollPane.java | 99 +- .../weis/darklaf/components/ShadowButton.java | 46 + .../components/TooltipAwareButton.java | 68 + .../components/TooltipAwareToggleButton.java | 68 + .../darklaf/ui/button/DarkToggleButtonUI.java | 77 +- .../ui/checkbox/DarkCheckBoxMenuItemUI.java | 2 +- .../DarkFileChooserListViewBorder.java | 41 + .../ui/filechooser/DarkFileChooserUI.java | 294 +++ .../filechooser/DarkFileChooserUIBridge.java | 1390 ++++++++++ .../darklaf/ui/filechooser/DarkFilePane.java | 434 ++++ .../ui/filechooser/DarkFilePaneUIBridge.java | 2060 +++++++++++++++ .../ui/list/DarkListCellFocusBorder.java | 41 + .../darklaf/ui/list/DarkListCellRenderer.java | 44 + .../com/weis/darklaf/ui/list/DarkListUI.java | 103 +- .../darklaf/ui/list/DarkListUIBridge.java | 2257 +++++++++++++++++ .../darklaf/ui/menu/DarkMenuItemUIBase.java | 238 +- .../com/weis/darklaf/ui/menu/DarkMenuUI.java | 805 ++---- .../ui/scrollpane/DarkScrollBarUI.java | 26 +- .../ui/tabframe/DarkTabFrameTabLabelUI.java | 1 - .../darklaf/ui/table/BasicTableUIBridge.java | 2189 ++++++++++++++++ .../darklaf/ui/table/DarkTableCellEditor.java | 78 +- .../ui/table/DarkTableCellFocusBorder.java | 32 +- .../ui/table/DarkTableCellRenderer.java | 24 + .../darklaf/ui/table/DarkTableHeaderUI.java | 6 + .../weis/darklaf/ui/table/DarkTableUI.java | 341 ++- .../darklaf/ui/table/DarkTableUIBridge.java | 21 +- .../table/TextFieldTableCellEditorBorder.java | 57 +- .../weis/darklaf/ui/text/DarkTextBorder.java | 16 + .../weis/darklaf/ui/text/DarkTextFieldUI.java | 7 +- .../darklaf/ui/tooltip/DarkTooltipBorder.java | 2 + .../com/weis/darklaf/util/DarkUIUtil.java | 63 +- .../weis/darklaf/icons/dark/files/drive.svg | 9 + .../com/weis/darklaf/icons/dark/menu/down.svg | 6 + .../com/weis/darklaf/icons/dark/menu/save.svg | 4 + .../com/weis/darklaf/icons/dark/menu/up.svg | 5 + .../icons/dark/navigation/arrowDownHover.svg | 3 + .../dark/navigation/arrowDownSelected.svg | 3 + .../icons/dark/navigation/arrowLeftHover.svg | 3 + .../dark/navigation/arrowLeftSelected.svg | 3 + .../icons/dark/navigation/arrowRightHover.svg | 4 + .../dark/navigation/arrowRightSelected.svg | 4 + .../icons/dark/navigation/arrowUpHover.svg | 4 + .../icons/dark/navigation/arrowUpSelected.svg | 3 + .../weis/darklaf/icons/light/files/drive.svg | 9 + .../weis/darklaf/icons/light/files/folder.svg | 4 - .../weis/darklaf/icons/light/menu/down.svg | 6 + .../weis/darklaf/icons/light/menu/save.svg | 4 + .../com/weis/darklaf/icons/light/menu/up.svg | 5 + .../icons/light/navigation/arrowDownHover.svg | 3 + .../light/navigation/arrowDownSelected.svg | 3 + .../icons/light/navigation/arrowLeftHover.svg | 3 + .../light/navigation/arrowLeftSelected.svg | 3 + .../light/navigation/arrowRightHover.svg | 4 + .../light/navigation/arrowRightSelected.svg | 4 + .../icons/light/navigation/arrowUpHover.svg | 3 + .../light/navigation/arrowUpSelected.svg | 3 + .../properties/platform/windows.properties | 2 + .../darklaf/properties/ui/comboBox.properties | 1 + .../properties/ui/fileChooser.properties | 32 +- .../darklaf/properties/ui/list.properties | 5 +- .../darklaf/properties/ui/menu.properties | 5 +- .../darklaf/properties/ui/table.properties | 8 +- .../properties/ui/toggleButton.properties | 5 +- .../darklaf/properties/ui/tree.properties | 4 +- .../resources/library/x64/jniplatform.dll | Bin 2691649 -> 2691649 bytes .../resources/library/x86/jniplatform.dll | Bin 1683306 -> 1683306 bytes src/test/java/FileChooserDemo.java | 7 +- src/test/java/ToolTipDemo.java | 4 +- src/test/java/UIDemo.java | 8 +- src/test/java/UIManagerDefaults.java | 3 +- 71 files changed, 9975 insertions(+), 1147 deletions(-) create mode 100644 src/main/java/com/weis/darklaf/components/ShadowButton.java create mode 100644 src/main/java/com/weis/darklaf/components/TooltipAwareButton.java create mode 100644 src/main/java/com/weis/darklaf/components/TooltipAwareToggleButton.java create mode 100644 src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserListViewBorder.java create mode 100644 src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUI.java create mode 100644 src/main/java/com/weis/darklaf/ui/filechooser/DarkFileChooserUIBridge.java create mode 100644 src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePane.java create mode 100644 src/main/java/com/weis/darklaf/ui/filechooser/DarkFilePaneUIBridge.java create mode 100644 src/main/java/com/weis/darklaf/ui/list/DarkListCellFocusBorder.java create mode 100644 src/main/java/com/weis/darklaf/ui/list/DarkListCellRenderer.java create mode 100644 src/main/java/com/weis/darklaf/ui/list/DarkListUIBridge.java create mode 100644 src/main/java/com/weis/darklaf/ui/table/BasicTableUIBridge.java create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/files/drive.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/menu/down.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/menu/save.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/menu/up.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowDownSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowLeftSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowRightSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/dark/navigation/arrowUpSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/files/drive.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/menu/down.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/menu/save.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/menu/up.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowDownSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowLeftSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowRightSelected.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpHover.svg create mode 100644 src/main/resources/com/weis/darklaf/icons/light/navigation/arrowUpSelected.svg 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 1be1120678c7cdc0b0ecdfc8541d61e5ca230ff2..622a1b86bbff8865cf72384ecef428853c6d44b1 100644 GIT binary patch delta 178 zcmWm5xeqy@*AHa4^p*WiV(EDEQT%zC%` zD}P?yZ~7qUfG+5P6|f4{Km*pn2G|5!U>od!U9k72A1D8v80$-41`^0nMlzO(Ol2l> aS;$gWvX+f(WhZ+%$WcymmhfY2bV+}qx^JtPIabpUFcF*y4H!Un(SG2 E8(G0BrT_o{ 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); } /*