From ef21efecf52ea948171e29266c76ab283c1c27e6 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 1 Nov 2022 10:12:49 +0100 Subject: [PATCH] Tree: - Fixed missing tree lines (if enabled) for wide-selected rows. (issue #598) - Fixed scaling of tree lines and fixed alignment to expand/collapse arrows. - Removed support for dashed tree lines. `Tree.lineTypeDashed` is now ignored. --- CHANGELOG.md | 4 + .../com/formdev/flatlaf/ui/FlatTreeUI.java | 142 +++++++++++++++++- .../flatlaf/testing/FlatComponents2Test.java | 34 ++++- .../flatlaf/testing/FlatComponents2Test.jfd | 27 +++- 4 files changed, 196 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e68bff0..6bad05270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ FlatLaf Change Log - FileChooser: Fixed layout of (optional) accessory component and fixed too large right margin. (issue #604; regression since implementing PR #522 in FlatLaf 2.3) +- Tree: + - Fixed missing tree lines (if enabled) for wide-selected rows. (issue #598) + - Fixed scaling of tree lines and fixed alignment to expand/collapse arrows. + - Removed support for dashed tree lines. `Tree.lineTypeDashed` is now ignored. ## 2.6 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java index b9c441bb0..44fbafb44 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java @@ -25,7 +25,11 @@ import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; import java.util.Map; import javax.swing.CellRendererPane; import javax.swing.Icon; @@ -152,6 +156,7 @@ public class FlatTreeUI // only used via styling (not in UI defaults, but has likewise client properties) /** @since 2 */ @Styleable protected boolean paintSelection = true; + private boolean paintLines; private Color defaultCellNonSelectionBackground; private Color defaultSelectionBackground; private Color defaultSelectionForeground; @@ -185,6 +190,7 @@ protected void installDefaults() { wideSelection = UIManager.getBoolean( "Tree.wideSelection" ); showCellFocusIndicator = UIManager.getBoolean( "Tree.showCellFocusIndicator" ); + paintLines = UIManager.getBoolean( "Tree.paintLines" ); defaultCellNonSelectionBackground = UIManager.getColor( "Tree.textBackground" ); defaultSelectionBackground = selectionBackground; defaultSelectionForeground = selectionForeground; @@ -381,6 +387,125 @@ public Object getStyleableValue( JComponent c, String key ) { return FlatStylingSupport.getAnnotatedStyleableValue( this, key ); } + @Override + public void paint( Graphics g, JComponent c ) { + if( treeState == null ) + return; + + // use clip bounds to limit painting to needed rows + Rectangle clipBounds = g.getClipBounds(); + TreePath firstPath = getClosestPathForLocation( tree, 0, clipBounds.y ); + Enumeration visiblePaths = treeState.getVisiblePathsFrom( firstPath ); + + if( visiblePaths != null ) { + Insets insets = tree.getInsets(); + + HashSet verticalLinePaths = paintLines ? new HashSet<>() : null; + ArrayList paintLinesLater = paintLines ? new ArrayList<>() : null; + ArrayList paintExpandControlsLater = paintLines ? new ArrayList<>() : null; + + // add parents for later painting of vertical lines + if( paintLines ) { + for( TreePath path = firstPath.getParentPath(); path != null; path = path.getParentPath() ) + verticalLinePaths.add( path ); + } + + Rectangle boundsBuffer = new Rectangle(); + boolean rootVisible = isRootVisible(); + int row = treeState.getRowForPath( firstPath ); + boolean leftToRight = tree.getComponentOrientation().isLeftToRight(); + int treeWidth = tree.getWidth(); + + // iterate over visible rows and paint rows, expand control and lines + while( visiblePaths.hasMoreElements() ) { + TreePath path = visiblePaths.nextElement(); + if( path == null ) + break; + + // compute path bounds + Rectangle bounds = treeState.getBounds( path, boundsBuffer ); + if( bounds == null ) + break; + + // add tree insets to path bounds + if( leftToRight ) + bounds.x += insets.left; + else + bounds.x = treeWidth - insets.right - (bounds.x + bounds.width); + bounds.y += insets.top; + + boolean isLeaf = treeModel.isLeaf( path.getLastPathComponent() ); + boolean isExpanded = isLeaf ? false : treeState.getExpandedState( path ); + boolean hasBeenExpanded = isLeaf ? false : tree.hasBeenExpanded( path ); + + // paint row (including selection) + paintRow( g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); + + // collect lines for later painting + if( paintLines ) { + TreePath parentPath = path.getParentPath(); + + // add parent for later painting of vertical lines + if( parentPath != null ) + verticalLinePaths.add( parentPath ); + + // paint horizontal line later (for using rendering hints) + if( parentPath != null || (rootVisible && row == 0) ) { + Rectangle bounds2 = new Rectangle( bounds ); + int row2 = row; + paintLinesLater.add( () -> { + paintHorizontalPartOfLeg( g, clipBounds, insets, bounds2, path, row2, isExpanded, hasBeenExpanded, isLeaf ); + } ); + } + } + + // paint expand control + if( shouldPaintExpandControl( path, row, isExpanded, hasBeenExpanded, isLeaf ) ) { + if( paintLines ) { + // need to paint after painting lines + Rectangle bounds2 = new Rectangle( bounds ); + int row2 = row; + paintExpandControlsLater.add( () -> { + paintExpandControl( g, clipBounds, insets, bounds2, path, row2, isExpanded, hasBeenExpanded, isLeaf ); + } ); + } else + paintExpandControl( g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); + } + + if( bounds.y + bounds.height >= clipBounds.y + clipBounds.height ) + break; + + row++; + } + + if( paintLines ) { + // enable antialiasing for line painting + Object[] oldRenderingHints = FlatUIUtils.setRenderingHints( g ); + + // paint horizontal lines + for( Runnable r : paintLinesLater ) + r.run(); + + // paint vertical lines + g.setColor( Color.green ); + for( TreePath path : verticalLinePaths ) + paintVerticalPartOfLeg( g, clipBounds, insets, path ); + + // restore rendering hints + if( oldRenderingHints != null ) + FlatUIUtils.resetRenderingHints( g, oldRenderingHints ); + + // paint expand controls + for( Runnable r : paintExpandControlsLater ) + r.run(); + } + } + + paintDropLine( g ); + + rendererPane.removeAll(); + } + /** * Similar to super.paintRow(), but supports wide selection and uses * inactive selection background/foreground if tree is not focused. @@ -544,13 +669,6 @@ private void paintWideSelection( Graphics g, Rectangle clipBounds, Insets insets FlatUIUtils.paintSelection( (Graphics2D) g, 0, bounds.y, tree.getWidth(), bounds.height, UIScale.scale( selectionInsets ), arcTop, arcTop, arcBottom, arcBottom, 0 ); - - // paint expand/collapse icon - // (was already painted before, but painted over with wide selection) - if( shouldPaintExpandControl( path, row, isExpanded, hasBeenExpanded, isLeaf ) ) { - paintExpandControl( g, clipBounds, insets, bounds, - path, row, isExpanded, hasBeenExpanded, isLeaf ); - } } private void paintCellBackground( Graphics g, Component rendererComponent, Rectangle bounds, @@ -596,6 +714,16 @@ private boolean useUnitedRoundedSelection() { (selectionInsets == null || (selectionInsets.top == 0 && selectionInsets.bottom == 0)); } + @Override + protected void paintVerticalLine( Graphics g, JComponent c, int x, int top, int bottom ) { + ((Graphics2D)g).fill( new Rectangle2D.Float( x, top, UIScale.scale( 1f ), bottom - top ) ); + } + + @Override + protected void paintHorizontalLine( Graphics g, JComponent c, int y, int left, int right ) { + ((Graphics2D)g).fill( new Rectangle2D.Float( left, y, right - left, UIScale.scale( 1f ) ) ); + } + /** * Checks whether dropping on a row. * See DefaultTreeCellRenderer.getTreeCellRendererComponent(). diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java index 6379bb3de..b9eaf22e3 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java @@ -473,6 +473,22 @@ private void treePaintSelectionChanged() { tree.putClientProperty( FlatClientProperties.TREE_PAINT_SELECTION, paintSelection ); } + private void treePaintLinesChanged() { + boolean paintLines = treePaintLinesCheckBox.isSelected(); + UIManager.put( "Tree.paintLines", paintLines ? true : null ); + for( JTree tree : allTrees ) + tree.updateUI(); + + treeRedLinesCheckBox.setEnabled( paintLines ); + } + + private void treeRedLinesChanged() { + boolean redLines = treeRedLinesCheckBox.isSelected(); + UIManager.put( "Tree.hash", redLines ? Color.red : null ); + for( JTree tree : allTrees ) + tree.updateUI(); + } + private void treeEditableChanged() { boolean editable = treeEditableCheckBox.isSelected(); for( JTree tree : allTrees ) @@ -575,6 +591,8 @@ private void initComponents() { treeRendererComboBox = new JComboBox<>(); treeWideSelectionCheckBox = new JCheckBox(); treePaintSelectionCheckBox = new JCheckBox(); + treePaintLinesCheckBox = new JCheckBox(); + treeRedLinesCheckBox = new JCheckBox(); treeEditableCheckBox = new JCheckBox(); JPanel tableOptionsPanel = new JPanel(); JLabel autoResizeModeLabel = new JLabel(); @@ -917,6 +935,7 @@ public void mouseClicked(MouseEvent e) { "[]" + "[]0" + "[]0" + + "[]0" + "[]")); //---- treeRendererLabel ---- @@ -946,10 +965,21 @@ public void mouseClicked(MouseEvent e) { treePaintSelectionCheckBox.addActionListener(e -> treePaintSelectionChanged()); treeOptionsPanel.add(treePaintSelectionCheckBox, "cell 0 2"); + //---- treePaintLinesCheckBox ---- + treePaintLinesCheckBox.setText("paint lines"); + treePaintLinesCheckBox.addActionListener(e -> treePaintLinesChanged()); + treeOptionsPanel.add(treePaintLinesCheckBox, "cell 0 3"); + + //---- treeRedLinesCheckBox ---- + treeRedLinesCheckBox.setText("red lines"); + treeRedLinesCheckBox.setEnabled(false); + treeRedLinesCheckBox.addActionListener(e -> treeRedLinesChanged()); + treeOptionsPanel.add(treeRedLinesCheckBox, "cell 0 3"); + //---- treeEditableCheckBox ---- treeEditableCheckBox.setText("editable"); treeEditableCheckBox.addActionListener(e -> treeEditableChanged()); - treeOptionsPanel.add(treeEditableCheckBox, "cell 0 3"); + treeOptionsPanel.add(treeEditableCheckBox, "cell 0 4"); } add(treeOptionsPanel, "cell 0 4 4 1"); @@ -1083,6 +1113,8 @@ public void mouseClicked(MouseEvent e) { private JComboBox treeRendererComboBox; private JCheckBox treeWideSelectionCheckBox; private JCheckBox treePaintSelectionCheckBox; + private JCheckBox treePaintLinesCheckBox; + private JCheckBox treeRedLinesCheckBox; private JCheckBox treeEditableCheckBox; private JComboBox autoResizeModeField; private JComboBox sortIconPositionComboBox; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd index 2f7dff158..f90ea4d00 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.5.0.404" Java: "17.0.2" encoding: "UTF-8" +JFDML JFormDesigner: "8.0.0.0.194" Java: "17.0.2" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -395,7 +395,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "hidemode 3" "$columnConstraints": "[left]" - "$rowConstraints": "[][]0[]0[]" + "$rowConstraints": "[][]0[]0[]0[]" } ) { name: "treeOptionsPanel" "border": new javax.swing.border.TitledBorder( "JTree Control" ) @@ -445,6 +445,27 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "treePaintLinesCheckBox" + "text": "paint lines" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "treePaintLinesChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "treeRedLinesCheckBox" + "text": "red lines" + "enabled": false + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "treeRedLinesChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "treeEditableCheckBox" "text": "editable" @@ -453,7 +474,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "treeEditableChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4 4 1"