From 9854161a5738dd888de6eb440f9e576f8531e8b5 Mon Sep 17 00:00:00 2001 From: weisj Date: Wed, 24 Jun 2020 00:45:53 +0200 Subject: [PATCH] Show hidden part of tree cell on mouse over using popup. --- .../components/border/MutableLineBorder.java | 2 +- .../weisj/darklaf/graphics/PaintUtil.java | 13 +- .../darklaf/ui/cell/hint/CellContainer.java | 40 ++ .../ui/cell/hint/CellHintPopupListener.java | 376 ++++++++++++++++++ .../ui/cell/hint/IndexedCellContainer.java | 56 +++ .../weisj/darklaf/ui/table/DarkTableUI.java | 16 +- .../darklaf/ui/table/TableCellContainer.java | 95 +++++ .../weisj/darklaf/ui/tree/DarkTreeUI.java | 35 +- .../darklaf/ui/tree/TreeCellContainer.java | 94 +++++ .../github/weisj/darklaf/util/DarkUIUtil.java | 30 +- .../darklaf/properties/ui/cell.properties | 2 + core/src/test/java/ui/tree/TreeDemo.java | 26 +- 12 files changed, 748 insertions(+), 37 deletions(-) create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellContainer.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellHintPopupListener.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/IndexedCellContainer.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/table/TableCellContainer.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/ui/tree/TreeCellContainer.java diff --git a/core/src/main/java/com/github/weisj/darklaf/components/border/MutableLineBorder.java b/core/src/main/java/com/github/weisj/darklaf/components/border/MutableLineBorder.java index 5c96f2fc..c19089dc 100644 --- a/core/src/main/java/com/github/weisj/darklaf/components/border/MutableLineBorder.java +++ b/core/src/main/java/com/github/weisj/darklaf/components/border/MutableLineBorder.java @@ -66,7 +66,7 @@ public class MutableLineBorder extends AbstractBorder { return true; } - protected Color getColor() { + public Color getColor() { return color; } diff --git a/core/src/main/java/com/github/weisj/darklaf/graphics/PaintUtil.java b/core/src/main/java/com/github/weisj/darklaf/graphics/PaintUtil.java index 292babe5..9c2768a8 100644 --- a/core/src/main/java/com/github/weisj/darklaf/graphics/PaintUtil.java +++ b/core/src/main/java/com/github/weisj/darklaf/graphics/PaintUtil.java @@ -30,9 +30,7 @@ import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; -import javax.swing.*; - -import com.github.weisj.darklaf.util.*; +import com.github.weisj.darklaf.util.Scale; public class PaintUtil { @@ -245,6 +243,15 @@ public class PaintUtil { g.fillRect(x, y + height - thickness, width, thickness); } + public static void drawRect(final Graphics g, final int x, final int y, + final int width, final int height, final Insets lineWidths) { + g.fillRect(x, y, width, lineWidths.top); + g.fillRect(x, y + lineWidths.top, lineWidths.left, height - lineWidths.top - lineWidths.bottom); + g.fillRect(x + width - lineWidths.right, y + lineWidths.top, lineWidths.right, + height - lineWidths.left - lineWidths.right); + g.fillRect(x, y + height - lineWidths.bottom, width, lineWidths.bottom); + } + public static void fillRect(final Graphics g, final Rectangle r) { fillRect(g, r.x, r.y, r.width, r.height); } diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellContainer.java b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellContainer.java new file mode 100644 index 00000000..df002db7 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellContainer.java @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.cell.hint; + +import java.awt.*; + +import javax.swing.*; + +public interface CellContainer { + + T getComponent(); + + boolean isEditing(); + + default Rectangle getAllocation() { + return getComponent().getVisibleRect(); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellHintPopupListener.java b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellHintPopupListener.java new file mode 100644 index 00000000..1d8a476c --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/CellHintPopupListener.java @@ -0,0 +1,376 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.cell.hint; + +import java.awt.*; +import java.awt.event.MouseEvent; +import java.util.Objects; + +import javax.swing.*; +import javax.swing.event.MouseInputAdapter; + +import com.github.weisj.darklaf.graphics.PaintUtil; +import com.github.weisj.darklaf.ui.DarkPopupFactory; +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class CellHintPopupListener extends MouseInputAdapter { + + private final IndexedCellContainer cellContainer; + private final PopupComponent popupComponent; + private I lastIndex; + private Popup popup; + + private boolean respectCellHeight = false; + private boolean respectCellWidth = true; + + public CellHintPopupListener(final IndexedCellContainer cellContainer) { + this.cellContainer = cellContainer; + this.popupComponent = new PopupComponent(this); + } + + public void setRespectCellHeight(final boolean respectCellHeight) { + this.respectCellHeight = respectCellHeight; + } + + public void setRespectCellWidth(final boolean respectCellWidth) { + this.respectCellWidth = respectCellWidth; + } + + public boolean isRespectCellHeight() { + return respectCellHeight; + } + + public boolean isRespectCellWidth() { + return respectCellWidth; + } + + public void install() { + JComponent comp = cellContainer.getComponent(); + comp.addMouseListener(this); + comp.addMouseMotionListener(this); + } + + public void uninstall() { + JComponent comp = cellContainer.getComponent(); + comp.removeMouseListener(this); + comp.removeMouseMotionListener(this); + } + + @Override + public void mouseMoved(final MouseEvent e) { + final Point p = e.getPoint(); + final I index = cellContainer.getCellPosition(p); + updatePopup(index, p); + } + + private void updatePopup(final I index, final Point p) { + if (cellContainer.getComponent() == null || index == null) return; + final boolean isEditing = cellContainer.isEditingCell(index); + final Rectangle allocation = cellContainer.getAllocation(); + final Rectangle cellBounds = cellContainer.getCellBoundsAt(index, isEditing); + final Rectangle visibleBounds = allocation.intersection(cellBounds); + if (visibleBounds.contains(p)) { + final Component comp = cellContainer.getEffectiveCellRendererComponent(index, isEditing); + final Dimension prefSize = isEditing + ? comp.getBounds().getSize() + : comp.getPreferredSize(); + if (!(visibleBounds.width >= prefSize.width && visibleBounds.height >= prefSize.height)) { + Point popupLocation = cellContainer.getComponent().getLocationOnScreen(); + Rectangle rect = calculatePopupBounds(cellBounds, visibleBounds, !isEditing); + if (!visibleBounds.contains(rect)) { + cellBounds.x = rect.x - cellBounds.x; + cellBounds.y = rect.y - cellBounds.y; + rect.x += popupLocation.x; + rect.y += popupLocation.y; + enter(index, rect, cellBounds); + return; + } else { + lastIndex = index; + } + } + } else { + lastIndex = null; + } + leave(); + } + + private Rectangle calculatePopupBounds(final Rectangle cellBounds, final Rectangle visibleBounds, + final boolean addBorder) { + Rectangle rect = new Rectangle(visibleBounds); + boolean ltr = cellContainer.getComponent().getComponentOrientation().isLeftToRight(); + popupComponent.setBorderInsets(1, 1, 1, 1); + + if (isRespectCellWidth()) { + if (cellBounds.x >= visibleBounds.x + && cellBounds.x + cellBounds.width <= visibleBounds.x + visibleBounds.width) { + rect.x = cellBounds.x; + rect.width = cellBounds.width; + } else { + int upperDiff = Math.max(cellBounds.x + cellBounds.width - visibleBounds.x - visibleBounds.width, + 0); + int lowerDiff = Math.max(visibleBounds.x - cellBounds.x, 0); + if (ltr && upperDiff > 0) lowerDiff = 0; + if (!ltr && lowerDiff > 0) upperDiff = 0; + if (upperDiff >= lowerDiff) { + rect.x = visibleBounds.x + visibleBounds.width; + rect.width = upperDiff; + popupComponent.setLeft(0); + } else { + rect.x = cellBounds.x; + rect.width = lowerDiff; + popupComponent.setRight(0); + } + } + } + if (isRespectCellHeight()) { + if ((cellBounds.y >= visibleBounds.y + && cellBounds.y + cellBounds.height <= visibleBounds.y + visibleBounds.height)) { + rect.y = cellBounds.y; + rect.height = cellBounds.height; + } else { + int upperDiff = Math.max(cellBounds.y + cellBounds.height - visibleBounds.y - visibleBounds.height, + 0); + int lowerDiff = Math.max(visibleBounds.y - cellBounds.y, 0); + if (upperDiff > 0) lowerDiff = 0; + if (upperDiff >= lowerDiff) { + rect.y = visibleBounds.y + visibleBounds.height; + rect.height = upperDiff; + popupComponent.setBottom(0); + } else { + rect.y = cellBounds.y; + rect.height = lowerDiff; + popupComponent.setTop(0); + } + } + } + if (!addBorder) { + popupComponent.setBorderInsets(0, 0, 0, 0); + } else { + rect.x -= popupComponent.getLeft(); + rect.y -= popupComponent.getTop(); + rect.width += popupComponent.getLeft() + popupComponent.getRight(); + rect.height += popupComponent.getTop() + popupComponent.getBottom(); + } + return rect; + } + + @Override + public void mouseExited(final MouseEvent e) { + if (isOverEditor(e.getPoint())) { + if (popup == null) { + /* + * If mouse is over editor and no popup is currently visible + * check if we need to show the popup. + */ + mouseMoved(e); + } + return; + } + leave(); + } + + private boolean isOverEditor(final Point p) { + return cellContainer.isEditing() + && cellContainer.getCellEditorComponent(lastIndex).getBounds().contains(p); + } + + public void repaint() { + if (!cellContainer.getComponent().isShowing()) return; + if (popup != null) popupComponent.repaint(); + if (lastIndex != null) { + Point p = MouseInfo.getPointerInfo().getLocation(); + SwingUtilities.convertPointFromScreen(p, cellContainer.getComponent()); + updatePopup(lastIndex, p); + } + } + + private void enter(final I index, final Rectangle bounds, final Rectangle rendererBounds) { + if (index != null) { + lastIndex = index; + popupComponent.setPreferredSize(bounds.getSize()); + popupComponent.setRendererBounds(rendererBounds); + if (popup != null) { + Point p = popupComponent.isShowing() ? popupComponent.getLocationOnScreen() : null; + if (p == null + || p.x != bounds.x || p.y != bounds.y + || popupComponent.getWidth() != bounds.width + || popupComponent.getHeight() != bounds.height) { + movePopup(bounds); + } + } + if (popup == null) { + popup = PopupFactory.getSharedInstance() + .getPopup(cellContainer.getComponent(), popupComponent, bounds.x, bounds.y); + popup.show(); + if (DarkPopupFactory.getPopupType(popup) == DarkPopupFactory.PopupType.HEAVY_WEIGHT) { + // Ensure heavy weight popup is at desired location. + movePopup(bounds); + } + } + } + } + + private void movePopup(final Rectangle bounds) { + DarkPopupFactory.PopupType popupType = DarkPopupFactory.getPopupType(popup); + Window w = DarkUIUtil.getWindow(popupComponent); + if (popupType == DarkPopupFactory.PopupType.HEAVY_WEIGHT) { + moveHeavyWeightPopup(bounds, w); + } else if (w instanceof RootPaneContainer) { + moveMediumLightWeightPopup(bounds, w); + } + popupComponent.repaint(); + } + + private void moveHeavyWeightPopup(final Rectangle bounds, final Window popupWindow) { + GraphicsConfiguration gc = popupWindow.getGraphicsConfiguration(); + GraphicsConfiguration componentGc = cellContainer.getComponent().getGraphicsConfiguration(); + if (!Objects.equals(componentGc, gc)) { + popup.hide(); + popup = null; + } else { + popupWindow.setBounds(bounds); + } + } + + private void moveMediumLightWeightPopup(final Rectangle bounds, final Window parentWindow) { + JLayeredPane layeredPane = ((RootPaneContainer) parentWindow).getLayeredPane(); + Component comp = DarkUIUtil.getParentBeforeMatching(popupComponent.getParent(), + c -> c == layeredPane); + Rectangle windowBounds = parentWindow.getBounds(); + if (windowBounds.contains(bounds.x, bounds.y) + && windowBounds.contains(bounds.x + bounds.width, bounds.y + bounds.height)) { + Point windowPos = parentWindow.getLocationOnScreen(); + bounds.x -= windowPos.x; + bounds.y -= windowPos.y; + comp.setBounds(bounds); + Component c = popupComponent; + while (c != null && comp != c) { + c.setBounds(0, 0, bounds.width, bounds.height); + c = c.getParent(); + } + } else { + /* + * Popup was moved outside the window. Request heavy weight popup. + */ + popup.hide(); + popup = null; + } + } + + private void leave() { + if (popup != null) { + popup.hide(); + popup = null; + } + } + + private Component getRenderer() { + return cellContainer.getEffectiveCellRendererComponent(lastIndex, cellContainer.isEditingCell(lastIndex)); + } + + private Color getBackground(final Component renderer) { + return cellContainer.getBackgroundAt(lastIndex, renderer); + } + + private static class PopupComponent extends JComponent { + + private final CellHintPopupListener cellHintPopupListener; + private final Insets borderInsets = new Insets(0, 0, 0, 0); + private final Color borderColor; + private Rectangle rendererBounds; + + private PopupComponent(final CellHintPopupListener cellHintPopupListener) { + this.cellHintPopupListener = cellHintPopupListener; + this.borderColor = UIManager.getColor("CellHintPopup.borderColor"); + putClientProperty(DarkPopupFactory.KEY_NO_DECORATION, true); + putClientProperty(DarkPopupFactory.KEY_OPAQUE, true); + } + + public void setTop(final int top) { + borderInsets.top = top; + } + + public void setBottom(final int bottom) { + borderInsets.bottom = bottom; + } + + public void setLeft(final int left) { + borderInsets.left = left; + } + + public void setRight(final int right) { + borderInsets.right = right; + } + + public int getTop() { + return borderInsets.top; + } + + public int getBottom() { + return borderInsets.bottom; + } + + public int getLeft() { + return borderInsets.left; + } + + public int getRight() { + return borderInsets.right; + } + + public void setBorderInsets(final int top, final int left, final int bottom, final int right) { + borderInsets.set(top, left, bottom, right); + } + + public void setRendererBounds(final Rectangle rendererBounds) { + this.rendererBounds = rendererBounds; + } + + @Override + public void paint(final Graphics g) { + Component renderer = cellHintPopupListener.getRenderer(); + if (rendererBounds != null && renderer != null) { + Color bg = cellHintPopupListener.getBackground(renderer); + g.setColor(bg); + g.fillRect(0, 0, getWidth(), getHeight()); + g.translate(-rendererBounds.x, -rendererBounds.y); + + // If the renderer is an editor we need to restore the bounds. + Rectangle bounds = renderer.getBounds(); + renderer.setBounds(rendererBounds); + renderer.paint(g); + renderer.setBounds(bounds); + + g.translate(rendererBounds.x, rendererBounds.y); + } + g.setColor(getBorderColor()); + PaintUtil.drawRect(g, 0, 0, getWidth(), getHeight(), borderInsets); + } + + public Color getBorderColor() { + return borderColor; + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/IndexedCellContainer.java b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/IndexedCellContainer.java new file mode 100644 index 00000000..8db75f3b --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/cell/hint/IndexedCellContainer.java @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.cell.hint; + +import java.awt.*; + +import javax.swing.*; + +public interface IndexedCellContainer extends CellContainer { + + default Rectangle getCellBoundsAt(final I position) { + return getCellBoundsAt(position, false); + } + + Rectangle getCellBoundsAt(final I position, final boolean isEditing); + + I getCellPosition(final Point p); + + Color getBackgroundAt(final I position, final Component renderer); + + default Component getEffectiveCellRendererComponent(final I position, final boolean isEditing) { + if (isEditing) { + return getCellEditorComponent(position); + } else { + return getCellRendererComponent(position); + } + } + + boolean isEditingCell(final I position); + + Component getCellRendererComponent(final I position); + + Component getCellEditorComponent(final I position); +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/table/DarkTableUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/table/DarkTableUI.java index dd9c9688..1ca3ef4d 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/table/DarkTableUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/table/DarkTableUI.java @@ -91,12 +91,6 @@ public class DarkTableUI extends DarkTableUIBridge implements TableConstants { LookAndFeel.uninstallBorder((JComponent) oldUnwrapped); } - @Override - protected void uninstallListeners() { - super.uninstallListeners(); - handler = null; - } - @Override protected void installDefaults() { super.installDefaults(); @@ -116,6 +110,12 @@ public class DarkTableUI extends DarkTableUIBridge implements TableConstants { table.setSurrendersFocusOnKeystroke(true); } + @Override + protected void uninstallListeners() { + super.uninstallListeners(); + handler = null; + } + protected CellRendererPane createCellRendererPane() { return new DarkCellRendererPane(); } @@ -417,7 +417,7 @@ public class DarkTableUI extends DarkTableUIBridge implements TableConstants { } } if (isEditorCell) { - Component component = getCellEditor(); + Component component = getCellEditorComponent(); component.setBounds(r); component.validate(); } else { @@ -437,7 +437,7 @@ public class DarkTableUI extends DarkTableUIBridge implements TableConstants { return rendererDelegate; } - protected Component getCellEditor() { + public Component getCellEditorComponent() { Component c = table.getEditorComponent(); if (!(table.getCellEditor() instanceof DarkTableCellEditorDelegate)) { int row = table.getEditingRow(); diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/table/TableCellContainer.java b/core/src/main/java/com/github/weisj/darklaf/ui/table/TableCellContainer.java new file mode 100644 index 00000000..0df0c8b8 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/table/TableCellContainer.java @@ -0,0 +1,95 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.table; + +import java.awt.*; + +import javax.swing.*; + +import com.github.weisj.darklaf.ui.cell.hint.IndexedCellContainer; +import com.github.weisj.darklaf.util.Pair; + +public class TableCellContainer implements IndexedCellContainer> { + + private final JTable table; + private final DarkTableUI ui; + + public TableCellContainer(final JTable table, final DarkTableUI ui) { + this.table = table; + this.ui = ui; + } + + @Override + public Rectangle getCellBoundsAt(final Pair position, final boolean isEditing) { + return isEditing + ? ui.getCellEditorComponent().getBounds() + : table.getCellRect(position.getFirst(), position.getSecond(), false); + } + + @Override + public Pair getCellPosition(final Point p) { + return new Pair<>(table.rowAtPoint(p), table.columnAtPoint(p)); + } + + @Override + public JTable getComponent() { + return table; + } + + @Override + public boolean isEditing() { + return table.isEditing(); + } + + @Override + public Color getBackgroundAt(final Pair position, final Component renderer) { + return renderer.getBackground(); + } + + @Override + public boolean isEditingCell(final Pair position) { + return isEditing() + && position != null + && table.getEditingColumn() == position.getSecond() + && table.getEditingRow() == position.getFirst(); + } + + @Override + public Component getCellRendererComponent(final Pair position) { + if (position == null) return null; + int row = position.getFirst(); + int column = position.getSecond(); + boolean isSelected = table.isCellSelected(row, column); + Object value = table.getValueAt(row, column); + boolean focus = table.hasFocus(); + return ui.getCellRenderer(row, column) + .getTableCellRendererComponent(table, value, isSelected, focus, row, column); + } + + @Override + public Component getCellEditorComponent(final Pair position) { + return ui.getCellEditorComponent(); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java index f607ec10..bbbc4d95 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java @@ -39,6 +39,7 @@ import javax.swing.tree.*; import com.github.weisj.darklaf.ui.cell.CellConstants; import com.github.weisj.darklaf.ui.cell.CellUtil; import com.github.weisj.darklaf.ui.cell.DarkCellRendererPane; +import com.github.weisj.darklaf.ui.cell.hint.CellHintPopupListener; import com.github.weisj.darklaf.util.DarkUIUtil; import com.github.weisj.darklaf.util.PropertyUtil; import com.github.weisj.darklaf.util.SystemInfo; @@ -117,6 +118,8 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C protected Icon collapsed; private boolean oldRepaintAllRowValue; + protected CellHintPopupListener popupListener; + protected DarkTreeCellRendererDelegate rendererDelegate; public static ComponentUI createUI(final JComponent c) { @@ -176,10 +179,16 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C @Override protected void installListeners() { super.installListeners(); + popupListener = createPopupMouseListener(); + popupListener.install(); tree.addPropertyChangeListener(this); tree.addMouseListener(selectionListener); } + protected CellHintPopupListener createPopupMouseListener() { + return new CellHintPopupListener<>(new TreeCellContainer(tree, this)); + } + @Override protected void installKeyboardActions() { super.installKeyboardActions(); @@ -368,6 +377,8 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C @Override protected void uninstallListeners() { super.uninstallListeners(); + popupListener.uninstall(); + popupListener = null; tree.removeMouseListener(selectionListener); tree.removePropertyChangeListener(this); } @@ -425,20 +436,12 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C while (backgroundEnumerator != null && backgroundEnumerator.hasMoreElements()) { path = (TreePath) backgroundEnumerator.nextElement(); if (path != null) { - isLeaf = treeModel.isLeaf(path.getLastPathComponent()); - if (isLeaf) { - isExpanded = hasBeenExpanded = false; - } else { - isExpanded = treeState.getExpandedState(path); - hasBeenExpanded = tree.hasBeenExpanded(path); - } bounds = getPathBounds(path, insets, boundsBuffer); if (bounds == null) return; bounds.x = xOffset; bounds.width = containerWidth; if (paintBounds.intersects(bounds)) { - paintRowBackground(g, paintBounds, bounds, path, - tree.getRowForPath(path)); + paintRowBackground(g, paintBounds, bounds, path, tree.getRowForPath(path)); } } } @@ -508,6 +511,14 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C return super.getCellRenderer(); } + public Component getEditingComponent() { + return editingComponent; + } + + public int getEditingRow() { + return editingRow; + } + protected Rectangle getPathBounds(final TreePath path, final Insets insets, Rectangle bounds) { bounds = treeState.getBounds(path, bounds); if (bounds != null) { @@ -537,6 +548,12 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C } } + @Override + public void update(final Graphics g, final JComponent c) { + popupListener.repaint(); + super.update(g, c); + } + @Override protected void updateRenderer() { super.updateRenderer(); diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/tree/TreeCellContainer.java b/core/src/main/java/com/github/weisj/darklaf/ui/tree/TreeCellContainer.java new file mode 100644 index 00000000..79985126 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/tree/TreeCellContainer.java @@ -0,0 +1,94 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.tree; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; + +import com.github.weisj.darklaf.ui.cell.CellUtil; +import com.github.weisj.darklaf.ui.cell.hint.IndexedCellContainer; + +public class TreeCellContainer implements IndexedCellContainer { + + private final JTree tree; + private final DarkTreeUI ui; + + public TreeCellContainer(final JTree tree, final DarkTreeUI ui) { + this.tree = tree; + this.ui = ui; + } + + @Override + public Rectangle getCellBoundsAt(final Integer position, final boolean isEditing) { + return isEditing ? ui.getEditingComponent().getBounds() : tree.getRowBounds(position); + } + + @Override + public Integer getCellPosition(final Point p) { + return tree.getClosestRowForLocation(p.x, p.y); + } + + @Override + public JTree getComponent() { + return tree; + } + + @Override + public Color getBackgroundAt(final Integer position, final Component renderer) { + if (position == null) return null; + return CellUtil.getTreeBackground(tree, tree.isRowSelected(position), position); + } + + @Override + public boolean isEditingCell(final Integer row) { + return isEditing() && row != null && ui.getEditingRow() == row; + } + + @Override + public Component getCellRendererComponent(final Integer row) { + if (row == null) return null; + TreeCellRenderer renderer = ui.getCellRenderer(); + TreePath path = tree.getPathForRow(row); + boolean isExpanded = tree.isExpanded(row); + boolean isLeaf = tree.getModel().isLeaf(path.getLastPathComponent()); + int leadIndex = tree.getLeadSelectionRow(); + return renderer.getTreeCellRendererComponent(tree, path.getLastPathComponent(), + tree.isRowSelected(row), isExpanded, isLeaf, row, + (leadIndex == row)); + } + + @Override + public Component getCellEditorComponent(final Integer position) { + return ui.getEditingComponent(); + } + + @Override + public boolean isEditing() { + return tree.isEditing(); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/util/DarkUIUtil.java b/core/src/main/java/com/github/weisj/darklaf/util/DarkUIUtil.java index a41d83ca..0ff87338 100644 --- a/core/src/main/java/com/github/weisj/darklaf/util/DarkUIUtil.java +++ b/core/src/main/java/com/github/weisj/darklaf/util/DarkUIUtil.java @@ -189,14 +189,20 @@ public final class DarkUIUtil { return SwingUtilities.getWindowAncestor(owner) == w; } - public static Container getUnwrappedParent(final Container comp) { + public static Container getUnwrappedParent(final Component comp) { if (comp == null) return null; return SwingUtilities.getUnwrappedParent(comp); } - public static Container getUnwrappedParent(final Component comp) { - if (comp == null) return null; - return SwingUtilities.getUnwrappedParent(comp); + public static Component unwrapComponent(final Component component) { + if (component == null) return null; + if (!(component.getParent() instanceof JLayer) + && !(component.getParent() instanceof JViewport)) return component; + Container parent = component.getParent(); + while (parent instanceof JLayer || parent instanceof JViewport) { + parent = parent.getParent(); + } + return parent; } public static int getFocusAcceleratorKeyMask() { @@ -480,11 +486,23 @@ public final class DarkUIUtil { } public static Container getParentMatching(final Container parent, final Predicate test) { - Container p; - for (p = parent; p != null && !test.test(p); p = p.getParent()) {} + Container p = parent; + while (p != null && !test.test(p)) { + p = p.getParent(); + } return p; } + public static Container getParentBeforeMatching(final Container parent, final Predicate test) { + Container p = parent; + Container prev = null; + while (p != null && !test.test(p)) { + prev = p; + p = prev.getParent(); + } + return prev; + } + public static Container getOpaqueParent(final Container parent) { return getParentMatching(parent, Container::isOpaque); } diff --git a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/cell.properties b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/cell.properties index b7cd88e0..382dd26f 100644 --- a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/cell.properties +++ b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/cell.properties @@ -47,3 +47,5 @@ Cell.inactiveBackgroundSelected = %highlightFill Cell.inactiveBackgroundNoFocus = %backgroundContainer Cell.inactiveBackgroundNoFocusAlternative = %backgroundAlternative Cell.inactiveBackgroundSelectedNoFocus = %highlightFill + +CellHintPopup.borderColor = %borderSecondary diff --git a/core/src/test/java/ui/tree/TreeDemo.java b/core/src/test/java/ui/tree/TreeDemo.java index d3a4dd2e..68f011bd 100644 --- a/core/src/test/java/ui/tree/TreeDemo.java +++ b/core/src/test/java/ui/tree/TreeDemo.java @@ -49,11 +49,11 @@ public class TreeDemo implements ComponentDemo { @Override public JComponent createComponent() { DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root"); - DefaultMutableTreeNode parent1 = new DefaultMutableTreeNode("Node A"); - DefaultMutableTreeNode child = new DefaultMutableTreeNode("Leaf A"); + DefaultMutableTreeNode parent1 = new DefaultMutableTreeNode("Very very very very very long node A"); + DefaultMutableTreeNode child = new DefaultMutableTreeNode("A loooooooooooooooooooooooooooooooooooooong leaf A"); DefaultMutableTreeNode child1 = new SelectableTreeNode("Leaf B (boolean)", true); DefaultMutableTreeNode parent2 = new DefaultMutableTreeNode("Node B"); - DefaultMutableTreeNode child2 = new DefaultMutableTreeNode("Leaf C"); + DefaultMutableTreeNode child2 = new DefaultMutableTreeNode("Leaf that is unnecessary verbose and ridiculously long C"); parent1.add(child); parent1.add(child1); @@ -77,7 +77,11 @@ public class TreeDemo implements ComponentDemo { return component; } }); - DemoPanel panel = new DemoPanel(new OverlayScrollPane(tree), new BorderLayout(), 0); + JSplitPane splitPane = new JSplitPane(); + splitPane.setLeftComponent(new OverlayScrollPane(tree)); + splitPane.setRightComponent(new JPanel()); + SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5)); + DemoPanel panel = new DemoPanel(splitPane, new BorderLayout(), 0); JPanel controlPanel = panel.addControls(); controlPanel.setLayout(new MigLayout("fillx, wrap 2", "[][grow]")); controlPanel.add(new JCheckBox(PropertyKey.ENABLED) { @@ -117,6 +121,8 @@ public class TreeDemo implements ComponentDemo { addActionListener(e -> tree.putClientProperty(DarkTreeUI.KEY_RENDER_BOOLEAN_AS_CHECKBOX, isSelected())); } }, "span"); + + controlPanel = panel.addControls(); controlPanel.add(new JLabel(DarkTreeUI.KEY_BOOLEAN_RENDER_TYPE + ":", JLabel.RIGHT)); controlPanel.add(new JComboBox() { { @@ -126,14 +132,14 @@ public class TreeDemo implements ComponentDemo { addItemListener(e -> tree.putClientProperty(DarkTreeUI.KEY_BOOLEAN_RENDER_TYPE, e.getItem())); } }); - controlPanel.add(new JLabel("JTree.lineStyle:", JLabel.RIGHT)); + controlPanel.add(new JLabel(DarkTreeUI.KEY_LINE_STYLE + ":", JLabel.RIGHT)); controlPanel.add(new JComboBox() { { - addItem("Dashed"); - addItem("None"); - addItem("Line"); - setSelectedItem("Line"); - addItemListener(e -> tree.putClientProperty("JTree.lineStyle", e.getItem())); + addItem(DarkTreeUI.STYLE_LINE); + addItem(DarkTreeUI.STYLE_DASHED); + addItem(DarkTreeUI.STYLE_NONE); + setSelectedItem(DarkTreeUI.STYLE_LINE); + addItemListener(e -> tree.putClientProperty(DarkTreeUI.KEY_LINE_STYLE, e.getItem())); } }); tree.setLargeModel(true);