/* * This file is part of WebLookAndFeel library. * * WebLookAndFeel library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * WebLookAndFeel library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with WebLookAndFeel library. If not, see . */ package com.alee.laf.tree; import com.alee.extended.tree.WebCheckBoxTree; import com.alee.global.StyleConstants; import com.alee.laf.WebLookAndFeel; import com.alee.utils.*; import com.alee.utils.ninepatch.NinePatchIcon; import javax.swing.*; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicTreeUI; import javax.swing.tree.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; /** * Custom UI for JTree component. * * @author Mikle Garin */ public class WebTreeUI extends BasicTreeUI { /** * Expand and collapse control icons. */ public static ImageIcon EXPAND_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/expand.png" ) ); public static ImageIcon COLLAPSE_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/collapse.png" ) ); public static ImageIcon DISABLED_EXPAND_ICON = ImageUtils.createDisabledCopy ( EXPAND_ICON ); public static ImageIcon DISABLED_COLLAPSE_ICON = ImageUtils.createDisabledCopy ( COLLAPSE_ICON ); /** * Default node icons. */ public static ImageIcon ROOT_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/root.png" ) ); public static ImageIcon CLOSED_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/closed.png" ) ); public static ImageIcon OPEN_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/open.png" ) ); public static ImageIcon LEAF_ICON = new ImageIcon ( WebTreeUI.class.getResource ( "icons/leaf.png" ) ); /** * Default drop line gradient fractions. */ protected static final float[] fractions = { 0, 0.25f, 0.75f, 1f }; /** * Style settings. */ protected boolean autoExpandSelectedNode = WebTreeStyle.autoExpandSelectedPath; protected boolean highlightRolloverNode = WebTreeStyle.highlightRolloverNode; protected boolean paintLines = WebTreeStyle.paintLines; protected Color linesColor = WebTreeStyle.linesColor; protected TreeSelectionStyle selectionStyle = WebTreeStyle.selectionStyle; protected int selectionRound = WebTreeStyle.selectionRound; protected int selectionShadeWidth = WebTreeStyle.selectionShadeWidth; protected boolean selectorEnabled = WebTreeStyle.selectorEnabled; protected Color selectorColor = WebTreeStyle.selectorColor; protected Color selectorBorderColor = WebTreeStyle.selectorBorderColor; protected int selectorRound = WebTreeStyle.selectorRound; protected BasicStroke selectorStroke = WebTreeStyle.selectorStroke; protected int dropCellShadeWidth = WebTreeStyle.dropCellShadeWidth; /** * Tree listeners. */ protected PropertyChangeListener orientationChangeListener; protected PropertyChangeListener dropLocationChangeListener; protected TreeSelectionListener treeSelectionListener; protected TreeExpansionListener treeExpansionListener; protected MouseAdapter mouseAdapter; /** * Runtime variables. */ protected int rolloverRow = -1; protected List initialSelection = new ArrayList (); protected Point selectionStart = null; protected Point selectionEnd = null; protected boolean leftToRight = true; protected TreePath draggablePath = null; /** * Returns an instance of the WebTreeUI for the specified component. * This tricky method is used by UIManager to create component UIs when needed. * * @param c component that will use UI instance * @return instance of the WebTreeUI */ @SuppressWarnings ("UnusedParameters") public static ComponentUI createUI ( final JComponent c ) { return new WebTreeUI (); } /** * Installs UI in the specified component. * * @param c component for this UI */ @Override public void installUI ( final JComponent c ) { super.installUI ( c ); this.leftToRight = tree.getComponentOrientation ().isLeftToRight (); // Overwrite indent in case WebLookAndFeel is not installed as L&F if ( !WebLookAndFeel.isInstalled () ) { setRightChildIndent ( WebTreeStyle.nodeLineIndent ); setLeftChildIndent ( WebTreeStyle.nodeLineIndent ); } // Allow each cell to choose its own preferred height tree.setRowHeight ( -1 ); // Use a moderate amount of visible rows by default // BasicTreeUI uses 20 rows by default which is too much tree.setVisibleRowCount ( 10 ); // Orientation change listener orientationChangeListener = new PropertyChangeListener () { @Override public void propertyChange ( final PropertyChangeEvent evt ) { leftToRight = tree.getComponentOrientation ().isLeftToRight (); } }; tree.addPropertyChangeListener ( WebLookAndFeel.ORIENTATION_PROPERTY, orientationChangeListener ); SwingUtils.setOrientation ( tree ); // Drop location change listener dropLocationChangeListener = new PropertyChangeListener () { @Override public void propertyChange ( final PropertyChangeEvent evt ) { // Repainting previous drop location final JTree.DropLocation oldLocation = ( JTree.DropLocation ) evt.getOldValue (); if ( oldLocation != null ) { tree.repaint ( getNodeDropLocationBounds ( oldLocation.getPath () ) ); } // Repainting current drop location final JTree.DropLocation newLocation = ( JTree.DropLocation ) evt.getNewValue (); if ( newLocation != null ) { tree.repaint ( getNodeDropLocationBounds ( newLocation.getPath () ) ); } } }; tree.addPropertyChangeListener ( WebLookAndFeel.DROP_LOCATION, dropLocationChangeListener ); // Selection listener treeSelectionListener = new TreeSelectionListener () { @Override public void valueChanged ( final TreeSelectionEvent e ) { // Optimized selection repaint repaintSelection (); // Tree expansion on selection if ( autoExpandSelectedNode && tree.getSelectionCount () > 0 ) { tree.expandPath ( tree.getSelectionPath () ); } } }; tree.addTreeSelectionListener ( treeSelectionListener ); // Expansion listener treeExpansionListener = new TreeExpansionListener () { @Override public void treeExpanded ( final TreeExpansionEvent event ) { repaintSelection (); } @Override public void treeCollapsed ( final TreeExpansionEvent event ) { repaintSelection (); } }; tree.addTreeExpansionListener ( treeExpansionListener ); // Mouse events adapter mouseAdapter = new MouseAdapter () { @Override public void mousePressed ( final MouseEvent e ) { // Only left mouse button events if ( SwingUtilities.isLeftMouseButton ( e ) ) { // Check that mouse did not hit actuall tree cell if ( getRowForPoint ( e.getPoint (), false ) == -1 ) { if ( isSelectorAvailable () ) { // todo Start DnD on selected row // Avoiding selection start when pressed on tree expand handle final TreePath path = getClosestPathForLocation ( tree, e.getX (), e.getY () ); if ( path == null || !isLocationInExpandControl ( path, e.getX (), e.getY () ) && !isLocationInCheckBoxControl ( path, e.getX (), e.getY () ) ) { // Avoid starting multiselection if row is selected and drag is possible final int rowForPath = getRowForPath ( tree, path ); if ( isDragAvailable () && getRowBounds ( rowForPath ).contains ( e.getX (), e.getY () ) && tree.isRowSelected ( rowForPath ) ) { // Marking row to be dragged draggablePath = path; } else { // Selection selectionStart = e.getPoint (); selectionEnd = selectionStart; // Initial tree selection initialSelection = getSelectionRowsList (); // Updating selection validateSelection ( e ); // Repainting selection on the tree repaintSelector (); } } } else if ( isFullLineSelection () ) { // todo Start DnD on selected row // Avoiding selection start when pressed on tree expand handle final TreePath path = getClosestPathForLocation ( tree, e.getX (), e.getY () ); if ( path != null && !isLocationInExpandControl ( path, e.getX (), e.getY () ) && !isLocationInCheckBoxControl ( path, e.getX (), e.getY () ) ) { // Single row selection if ( tree.getSelectionModel ().getSelectionMode () == TreeSelectionModel.SINGLE_TREE_SELECTION ) { tree.setSelectionRow ( getRowForPoint ( e.getPoint (), true ) ); } // Marking row to be dragged final int rowForPath = getRowForPath ( tree, path ); if ( isDragAvailable () && getRowBounds ( rowForPath ).contains ( e.getX (), e.getY () ) && tree.isRowSelected ( rowForPath ) ) { draggablePath = path; } } } } } } @Override public void mouseDragged ( final MouseEvent e ) { if ( draggablePath != null ) { final TransferHandler transferHandler = tree.getTransferHandler (); transferHandler.exportAsDrag ( tree, e, transferHandler.getSourceActions ( tree ) ); draggablePath = null; } if ( isSelectorAvailable () && selectionStart != null ) { // Selection selectionEnd = e.getPoint (); // Updating selection validateSelection ( e ); // Repainting selection on the tree repaintSelector (); if ( !tree.getVisibleRect ().contains ( e.getPoint () ) ) { tree.scrollRectToVisible ( new Rectangle ( e.getPoint (), new Dimension ( 0, 0 ) ) ); } } } @Override public void mouseReleased ( final MouseEvent e ) { if ( draggablePath != null ) { draggablePath = null; } if ( isSelectorAvailable () && selectionStart != null ) { // Saving selection rect to repaint // Rectangle fr = GeometryUtils.getContainingRect ( selectionStart, selectionEnd ); // Selection selectionStart = null; selectionEnd = null; // Repainting selection on the tree repaintSelector ( /*fr*/ ); } } private void validateSelection ( final MouseEvent e ) { // todo Possibly optimize selection? - modify it instead of overwriting each time // Selection rect final Rectangle selection = GeometryUtils.getContainingRect ( selectionStart, selectionEnd ); // Compute new selection final List newSelection = new ArrayList (); if ( SwingUtils.isShift ( e ) ) { for ( int row = 0; row < tree.getRowCount (); row++ ) { if ( getRowBounds ( row ).intersects ( selection ) && !initialSelection.contains ( row ) ) { newSelection.add ( row ); } } for ( final int row : initialSelection ) { newSelection.add ( row ); } } else if ( SwingUtils.isCtrl ( e ) ) { final List excludedRows = new ArrayList (); for ( int row = 0; row < tree.getRowCount (); row++ ) { if ( getRowBounds ( row ).intersects ( selection ) ) { if ( initialSelection.contains ( row ) ) { excludedRows.add ( row ); } else { newSelection.add ( row ); } } } for ( final int row : initialSelection ) { if ( !excludedRows.contains ( row ) ) { newSelection.add ( row ); } } } else { for ( int row = 0; row < tree.getRowCount (); row++ ) { if ( getRowBounds ( row ).intersects ( selection ) ) { newSelection.add ( row ); } } } // Change selection if it is not the same as before if ( !CollectionUtils.areEqual ( getSelectionRowsList (), newSelection ) ) { if ( newSelection.size () > 0 ) { tree.setSelectionRows ( CollectionUtils.toArray ( newSelection ) ); } else { tree.clearSelection (); } } } private List getSelectionRowsList () { final List selection = new ArrayList (); final int[] selectionRows = tree.getSelectionRows (); if ( selectionRows != null ) { for ( final int row : selectionRows ) { selection.add ( row ); } } return selection; } private void repaintSelector () { // // Calculating selector pervious and current rects // final Rectangle sb1 = GeometryUtils.getContainingRect ( selectionStart, selectionPrevEnd ); // final Rectangle sb2 = GeometryUtils.getContainingRect ( selectionStart, selectionEnd ); // // // Repainting final rect // repaintSelector ( GeometryUtils.getContainingRect ( sb1, sb2 ) ); // Replaced with full repaint due to strange tree lines painting bug tree.repaint ( tree.getVisibleRect () ); } // private void repaintSelector ( Rectangle fr ) // { // // // Expanding width and height to fully cover the selector // // fr.x -= 1; // // fr.y -= 1; // // fr.width += 2; // // fr.height += 2; // // // // // Repainting selector area // // tree.repaint ( fr ); // // // Replaced with full repaint due to strange tree lines painting bug // tree.repaint (); // } @Override public void mouseEntered ( final MouseEvent e ) { updateMouseover ( e ); } @Override public void mouseExited ( final MouseEvent e ) { clearMouseover (); } @Override public void mouseMoved ( final MouseEvent e ) { updateMouseover ( e ); } private void updateMouseover ( final MouseEvent e ) { if ( tree.isEnabled () && highlightRolloverNode ) { final int index = getRowForPoint ( e.getPoint () ); if ( rolloverRow != index ) { final int oldRollover = rolloverRow; rolloverRow = index; updateRow ( index ); updateRow ( oldRollover ); } } else { clearMouseover (); } } private void clearMouseover () { final int oldRollover = rolloverRow; rolloverRow = -1; updateRow ( oldRollover ); } private void updateRow ( final int row ) { if ( row != -1 ) { final Rectangle rowBounds = getFullRowBounds ( row ); if ( rowBounds != null ) { tree.repaint ( rowBounds ); } } } }; tree.addMouseListener ( mouseAdapter ); tree.addMouseMotionListener ( mouseAdapter ); } /** * Uninstalls UI from the specified component. * * @param c component with this UI */ @Override public void uninstallUI ( final JComponent c ) { tree.removePropertyChangeListener ( WebLookAndFeel.ORIENTATION_PROPERTY, orientationChangeListener ); tree.removePropertyChangeListener ( WebLookAndFeel.DROP_LOCATION, dropLocationChangeListener ); tree.removeTreeSelectionListener ( treeSelectionListener ); tree.removeTreeExpansionListener ( treeExpansionListener ); tree.removeMouseListener ( mouseAdapter ); tree.removeMouseMotionListener ( mouseAdapter ); super.uninstallUI ( c ); } /** * Returns whether tree nodes drag available or not. * * @return true if tree nodes drag available, false otherwise */ protected boolean isDragAvailable () { return tree != null && tree.isEnabled () && tree.getDragEnabled () && tree.getTransferHandler () != null && tree.getTransferHandler ().getSourceActions ( tree ) > 0; } /** * Returns whether tree should expand nodes on selection or not. * * @return true if tree should expand nodes on selection, false otherwise */ public boolean isAutoExpandSelectedNode () { return autoExpandSelectedNode; } /** * Sets whether tree should expand nodes on selection or not. * * @param autoExpandSelectedNode whether tree should expand nodes on selection or not */ public void setAutoExpandSelectedNode ( final boolean autoExpandSelectedNode ) { this.autoExpandSelectedNode = autoExpandSelectedNode; } /** * Returns whether tree should highlight rollover node or not. * * @return true if tree should highlight rollover, false otherwise */ public boolean isHighlightRolloverNode () { return highlightRolloverNode; } /** * Sets whether tree should highlight rollover node or not. * * @param highlight whether tree should highlight rollover node or not */ public void setHighlightRolloverNode ( final boolean highlight ) { this.highlightRolloverNode = highlight; } /** * Returns whether tree should paint structure lines or not. * * @return true if tree should paint structure lines, false otherwise */ public boolean isPaintLines () { return paintLines; } /** * Sets whether tree should paint structure lines or not. * * @param paint whether tree should paint structure lines or not */ public void setPaintLines ( final boolean paint ) { this.paintLines = paint; } /** * Returns tree structure lines color. * * @return tree structure lines color */ public Color getLinesColor () { return linesColor; } /** * Sets tree structure lines color. * * @param color tree structure lines color */ public void setLinesColor ( final Color color ) { this.linesColor = color; } /** * Returns tree selection style. * * @return tree selection style */ public TreeSelectionStyle getSelectionStyle () { return selectionStyle; } /** * Sets tree selection style. * * @param style tree selection style */ public void setSelectionStyle ( final TreeSelectionStyle style ) { this.selectionStyle = style; } /** * Returns tree selection rounding. * * @return tree selection rounding */ public int getSelectionRound () { return selectionRound; } /** * Sets tree selection rounding. * * @param round tree selection rounding */ public void setSelectionRound ( final int round ) { this.selectionRound = round; } /** * Returns tree selection shade width. * * @return tree selection shade width */ public int getSelectionShadeWidth () { return selectionShadeWidth; } /** * Sets tree selection shade width. * * @param shadeWidth tree selection shade width */ public void setSelectionShadeWidth ( final int shadeWidth ) { this.selectionShadeWidth = shadeWidth; } /** * Returns whether selector is enabled or not. * * @return true if selector is enabled, false otherwise */ public boolean isSelectorEnabled () { return selectorEnabled; } /** * Sets whether selector is enabled or not. * * @param enabled whether selector is enabled or not */ public void setSelectorEnabled ( final boolean enabled ) { this.selectorEnabled = enabled; } /** * Returns selector color. * * @return selector color */ public Color getSelectorColor () { return selectorColor; } /** * Sets selector color. * * @param color selector color */ public void setSelectorColor ( final Color color ) { this.selectorColor = color; } /** * Returns selector border color. * * @return selector border color */ public Color getSelectorBorderColor () { return selectorBorderColor; } /** * Sets selector border color. * * @param color selector border color */ public void setSelectorBorderColor ( final Color color ) { this.selectorBorderColor = color; } /** * Returns selector rounding. * * @return selector rounding */ public int getSelectorRound () { return selectorRound; } /** * Sets selector rounding. * * @param round selector rounding */ public void setSelectorRound ( final int round ) { this.selectorRound = round; } /** * Returns selector border stroke. * * @return selector border stroke */ public BasicStroke getSelectorStroke () { return selectorStroke; } /** * Sets selector border stroke. * * @param stroke selector border stroke */ public void setSelectorStroke ( final BasicStroke stroke ) { this.selectorStroke = stroke; } /** * Returns drop cell highlight shade width. * * @return drop cell highlight shade width */ public int getDropCellShadeWidth () { return dropCellShadeWidth; } /** * Sets drop cell highlight shade width. * * @param dropCellShadeWidth new drop cell highlight shade width */ public void setDropCellShadeWidth ( final int dropCellShadeWidth ) { this.dropCellShadeWidth = dropCellShadeWidth; } /** * Returns whether selector is available for current tree or not. * * @return true if selector is available for current tree, false otherwise */ public boolean isSelectorAvailable () { return isSelectorEnabled () && tree != null && tree.isEnabled () && tree.getSelectionModel ().getSelectionMode () != TreeSelectionModel.SINGLE_TREE_SELECTION; } /** * Returns whether tree selection style points that the whole line is a single cell or not. * * @return true if tree selection style points that the whole line is a single cell, false otherwise */ public boolean isFullLineSelection () { return selectionStyle == TreeSelectionStyle.line; } /** * Returns row index for specified point on the tree. * This method takes selection style into account. * * @param point point on the tree * @return row index for specified point on the tree */ public int getRowForPoint ( final Point point ) { return getRowForPoint ( point, isFullLineSelection () ); } /** * Returns row index for specified point on the tree. * * @param point point on the tree * @param countFullRow whether take the whole row into account or just node renderer rect * @return row index for specified point on the tree */ public int getRowForPoint ( final Point point, final boolean countFullRow ) { if ( tree != null ) { // todo Optimize for ( int row = 0; row < tree.getRowCount (); row++ ) { final Rectangle bounds = getRowBounds ( row, countFullRow ); if ( bounds.contains ( point ) ) { return row; } } } return -1; } /** * Returns row bounds by its index. * This method takes selection style into account. * * @param row row index * @return row bounds by its index */ public Rectangle getRowBounds ( final int row ) { return getRowBounds ( row, isFullLineSelection () ); } /** * Returns row bounds by its index. * * @param row row index * @param countFullRow whether take the whole row into account or just node renderer rect * @return row bounds by its index */ public Rectangle getRowBounds ( final int row, final boolean countFullRow ) { return countFullRow ? getFullRowBounds ( row ) : tree.getRowBounds ( row ); } /** * Returns full row bounds by its index. * * @param row row index * @return full row bounds by its index */ public Rectangle getFullRowBounds ( final int row ) { final Rectangle b = tree.getRowBounds ( row ); if ( b != null ) { final Insets insets = tree.getInsets (); b.x = insets.left; b.width = tree.getWidth () - insets.left - insets.right; } return b; } /** * Returns default tree cell editor. * * @return default tree cell editor */ @Override protected TreeCellEditor createDefaultCellEditor () { return new WebTreeCellEditor (); } /** * Returns default tree cell renderer. * * @return default tree cell renderer */ @Override protected TreeCellRenderer createDefaultCellRenderer () { return new WebTreeCellRenderer (); } /** * Selects tree path for the specified event. * * @param path tree path to select * @param e event to process */ @Override protected void selectPathForEvent ( final TreePath path, final MouseEvent e ) { if ( !isLocationInCheckBoxControl ( path, e.getX (), e.getY () ) ) { super.selectPathForEvent ( path, e ); } } /** * Returns whether location is in the checkbox tree checkbox control or not. * * @param path tree path * @param x location X coordinate * @param y location Y coordinate * @return true if location is in the checkbox tree checkbox control, false otherwise */ protected boolean isLocationInCheckBoxControl ( final TreePath path, final int x, final int y ) { if ( tree instanceof WebCheckBoxTree ) { final WebCheckBoxTree checkBoxTree = ( WebCheckBoxTree ) tree; if ( checkBoxTree.isCheckingByUserEnabled () ) { final DefaultMutableTreeNode node = ( DefaultMutableTreeNode ) path.getLastPathComponent (); if ( checkBoxTree.isCheckBoxVisible ( node ) && checkBoxTree.isCheckBoxEnabled ( node ) ) { final Rectangle checkBoxBounds = checkBoxTree.getCheckBoxBounds ( path ); return checkBoxBounds != null && checkBoxBounds.contains ( x, y ); } else { return false; } } else { return false; } } else { return false; } } /** * Returns tree structure lines color. * * @return tree structure lines color */ @Override protected Color getHashColor () { return linesColor; } /** * Returns tree structure expanded node icon. * * @return tree structure expanded node icon */ @Override public Icon getExpandedIcon () { return tree.isEnabled () ? COLLAPSE_ICON : DISABLED_COLLAPSE_ICON; } /** * Returns tree structure collapsed node icon. * * @return tree structure collapsed node icon */ @Override public Icon getCollapsedIcon () { return tree.isEnabled () ? EXPAND_ICON : DISABLED_EXPAND_ICON; } /** * Paints centered icon. * * @param c component * @param g graphics * @param icon icon * @param x x coordinate * @param y y coordinate */ @Override protected void drawCentered ( final Component c, final Graphics g, final Icon icon, final int x, final int y ) { //x-icon.getIconWidth ()/2-2 icon.paintIcon ( c, g, findCenteredX ( x, icon.getIconWidth () ), y - icon.getIconHeight () / 2 ); } /** * Returns centered x coordinate for the icon. * * @param x x coordinate * @param iconWidth icon width * @return centered x coordinate */ protected int findCenteredX ( final int x, final int iconWidth ) { return leftToRight ? ( x - 2 - ( int ) Math.ceil ( iconWidth / 2.0 ) ) : ( x - 1 - ( int ) Math.floor ( iconWidth / 2.0 ) ); } /** * Repaints all rectangles containing tree selections. * This method is optimized to repaint only those area which are actually has selection in them. */ protected void repaintSelection () { if ( tree.getSelectionCount () > 0 ) { for ( final Rectangle rect : getSelectionRects () ) { tree.repaint ( rect ); } } } /** * Returns list of tree selections bounds. * This method takes selection style into account. * * @return list of tree selections bounds */ protected List getSelectionRects () { final List selections = new ArrayList (); // Checking that selection exists final int[] rows = tree.getSelectionRows (); if ( rows == null ) { return selections; } // Sorting selected rows Arrays.sort ( rows ); // Calculating selection rects final Insets insets = tree.getInsets (); Rectangle maxRect = null; int lastRow = -1; for ( final int row : rows ) { if ( selectionStyle.equals ( TreeSelectionStyle.single ) ) { // Required bounds selections.add ( tree.getRowBounds ( row ) ); } else { if ( lastRow != -1 && lastRow + 1 != row ) { // Save determined group selections.add ( maxRect ); // Reset counting maxRect = null; lastRow = -1; } if ( lastRow == -1 || lastRow + 1 == row ) { // Required bounds final Rectangle b = tree.getRowBounds ( row ); if ( isFullLineSelection () ) { b.x = insets.left; b.width = tree.getWidth () - insets.left - insets.right; } // Increase rect maxRect = lastRow == -1 ? b : GeometryUtils.getContainingRect ( maxRect, b ); // Remember last row lastRow = row; } } } if ( maxRect != null ) { selections.add ( maxRect ); } return selections; } /** * {@inheritDoc} */ @Override protected int getHorizontalLegBuffer () { return -2; } /** * Paints tree. * * @param g graphics * @param c component */ @Override public void paint ( final Graphics g, final JComponent c ) { // Initial variables validation if ( tree != c ) { throw new InternalError ( "incorrect component" ); } if ( treeState == null ) { return; } final Graphics2D g2d = ( Graphics2D ) g; // Cells selection paintSelection ( g2d ); // Rollover cell paintRolloverNodeHighlight ( g2d ); // Painting tree paintTree ( g2d ); // Drop cell paintDropLocation ( g2d ); // Multiselector paintMultiselector ( g2d ); } /** * Paints special WebLaF tree nodes selection. * It is rendered separately from nodes allowing you to simplify your tree cell renderer component. * * @param g2d graphics context */ protected void paintSelection ( final Graphics2D g2d ) { if ( tree.getSelectionCount () > 0 ) { // Draw final selections final List selections = getSelectionRects (); for ( final Rectangle rect : selections ) { LafUtils.drawCustomWebBorder ( g2d, tree, new RoundRectangle2D.Double ( rect.x + selectionShadeWidth, rect.y + selectionShadeWidth, rect.width - selectionShadeWidth * 2 - 1, rect.height - selectionShadeWidth * 2 - 1, selectionRound * 2, selectionRound * 2 ), StyleConstants.shadeColor, selectionShadeWidth, true, true ); } } } /** * Paints rollover node highlight. * * @param g2d graphics context */ protected void paintRolloverNodeHighlight ( final Graphics2D g2d ) { if ( tree.isEnabled () && highlightRolloverNode && rolloverRow != -1 && !tree.isRowSelected ( rolloverRow ) ) { final Rectangle rect = isFullLineSelection () ? getFullRowBounds ( rolloverRow ) : tree.getRowBounds ( rolloverRow ); if ( rect != null ) { rect.x += selectionShadeWidth; rect.y += selectionShadeWidth; rect.width -= selectionShadeWidth * 2 + 1; rect.height -= selectionShadeWidth * 2 + 1; final Composite old = GraphicsUtils.setupAlphaComposite ( g2d, 0.35f ); LafUtils.drawCustomWebBorder ( g2d, tree, new RoundRectangle2D.Double ( rect.x, rect.y, rect.width, rect.height, selectionRound * 2, selectionRound * 2 ), StyleConstants.shadeColor, selectionShadeWidth, true, true ); GraphicsUtils.restoreComposite ( g2d, old ); } } } /** * Paints all base tree elements. * This is almost the same method as in BasicTreeUI but it doesn't paint drop line. * * @param g2d graphics context */ protected void paintTree ( final Graphics2D g2d ) { final Rectangle paintBounds = g2d.getClipBounds (); final Insets insets = tree.getInsets (); final TreePath initialPath = getClosestPathForLocation ( tree, 0, paintBounds.y ); final Enumeration paintingEnumerator = treeState.getVisiblePathsFrom ( initialPath ); final int endY = paintBounds.y + paintBounds.height; int row = treeState.getRowForPath ( initialPath ); drawingCache.clear (); if ( initialPath != null && paintingEnumerator != null ) { TreePath parentPath = initialPath; // Draw the lines, knobs, and rows // Find each parent and have them draw a line to their last child parentPath = parentPath.getParentPath (); while ( parentPath != null ) { paintVerticalPartOfLeg ( g2d, paintBounds, insets, parentPath ); drawingCache.put ( parentPath, Boolean.TRUE ); parentPath = parentPath.getParentPath (); } // Information for the node being rendered. final Rectangle boundsBuffer = new Rectangle (); final boolean rootVisible = isRootVisible (); boolean isExpanded; boolean hasBeenExpanded; boolean isLeaf; Rectangle bounds; TreePath path; boolean done = false; while ( !done && paintingEnumerator.hasMoreElements () ) { path = ( TreePath ) paintingEnumerator.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 ) { // This will only happen if the model changes out // from under us (usually in another thread). // Swing isn't multithreaded, but I'll put this // check in anyway. return; } // See if the vertical line to the parent has been drawn. parentPath = path.getParentPath (); if ( parentPath != null ) { if ( drawingCache.get ( parentPath ) == null ) { paintVerticalPartOfLeg ( g2d, paintBounds, insets, parentPath ); drawingCache.put ( parentPath, Boolean.TRUE ); } paintHorizontalPartOfLeg ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); } else if ( rootVisible && row == 0 ) { paintHorizontalPartOfLeg ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); } if ( shouldPaintExpandControl ( path, row, isExpanded, hasBeenExpanded, isLeaf ) ) { paintExpandControl ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); } paintRow ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf ); if ( ( bounds.y + bounds.height ) >= endY ) { done = true; } } else { done = true; } row++; } } // Empty out the renderer pane, allowing renderers to be gc'ed. rendererPane.removeAll (); } /** * Paints drop location if it is available. * * @param g2d graphics context */ protected void paintDropLocation ( final Graphics2D g2d ) { final JTree.DropLocation dropLocation = tree.getDropLocation (); if ( dropLocation != null ) { final TreePath dropPath = dropLocation.getPath (); if ( isDropLine ( dropLocation ) ) { // Painting drop line (between nodes) final Color background = tree.getBackground (); final Color dropLineColor = UIManager.getColor ( "Tree.dropLineColor" ); final Color[] colors = { background, dropLineColor, dropLineColor, background }; final Rectangle rect = getDropLineRect ( dropLocation ); g2d.setPaint ( new LinearGradientPaint ( rect.x, rect.y, rect.x + rect.width, rect.y, fractions, colors ) ); g2d.fillRect ( rect.x, rect.y, rect.width, 1 ); } else { // Painting drop bounds (onto node) final Rectangle bounds = getNodeDropLocationBounds ( dropPath ); final NinePatchIcon dropShade = NinePatchUtils.getShadeIcon ( dropCellShadeWidth, selectionRound, 1f ); dropShade.paintIcon ( g2d, bounds ); } } } /** * Returns node drop location drawing bounds. * * @param dropPath node path * @return node drop location drawing bounds */ protected Rectangle getNodeDropLocationBounds ( final TreePath dropPath ) { final Rectangle bounds = tree.getPathBounds ( dropPath ); bounds.x -= dropCellShadeWidth; bounds.y -= dropCellShadeWidth; bounds.width += dropCellShadeWidth * 2; bounds.height += dropCellShadeWidth * 2; return bounds; } /** * Paints custom WebLaF multiselector. * * @param g2d graphics context */ protected void paintMultiselector ( final Graphics2D g2d ) { if ( isSelectorAvailable () && selectionStart != null && selectionEnd != null ) { final Object aa = GraphicsUtils.setupAntialias ( g2d ); final Stroke os = GraphicsUtils.setupStroke ( g2d, selectorStroke ); final Rectangle sb = GeometryUtils.getContainingRect ( selectionStart, selectionEnd ); final Rectangle fsb = sb.intersection ( SwingUtils.size ( tree ) ); fsb.width -= 1; fsb.height -= 1; g2d.setPaint ( selectorColor ); g2d.fill ( getSelectionShape ( fsb, true ) ); g2d.setPaint ( selectorBorderColor ); g2d.draw ( getSelectionShape ( fsb, false ) ); GraphicsUtils.restoreStroke ( g2d, os ); GraphicsUtils.restoreAntialias ( g2d, aa ); } } /** * Returns whether the specified drop location should be displayed as line or not. * * @param loc drop location * @return true if the specified drop location should be displayed as line, false otherwise */ protected boolean isDropLine ( final JTree.DropLocation loc ) { return loc != null && loc.getPath () != null && loc.getChildIndex () != -1; } /** * Returns drop line rectangle. * * @param loc drop location * @return drop line rectangle */ protected Rectangle getDropLineRect ( final JTree.DropLocation loc ) { final Rectangle rect; final TreePath path = loc.getPath (); final int index = loc.getChildIndex (); final Insets insets = tree.getInsets (); if ( tree.getRowCount () == 0 ) { rect = new Rectangle ( insets.left, insets.top, tree.getWidth () - insets.left - insets.right, 0 ); } else { final TreeModel model = getModel (); final Object root = model.getRoot (); if ( path.getLastPathComponent () == root && index >= model.getChildCount ( root ) ) { rect = tree.getRowBounds ( tree.getRowCount () - 1 ); rect.y = rect.y + rect.height; final Rectangle xRect; if ( !tree.isRootVisible () ) { xRect = tree.getRowBounds ( 0 ); } else if ( model.getChildCount ( root ) == 0 ) { xRect = tree.getRowBounds ( 0 ); xRect.x += totalChildIndent; xRect.width -= totalChildIndent + totalChildIndent; } else { final TreePath lastChildPath = path.pathByAddingChild ( model.getChild ( root, model.getChildCount ( root ) - 1 ) ); xRect = tree.getPathBounds ( lastChildPath ); } rect.x = xRect.x; rect.width = xRect.width; } else { rect = tree.getPathBounds ( path.pathByAddingChild ( model.getChild ( path.getLastPathComponent (), index ) ) ); } } if ( rect.y != 0 ) { rect.y--; } if ( !leftToRight ) { rect.x = rect.x + rect.width - 80; } rect.width = 80; rect.height = 2; return rect; } /** * Returns path bounds used for painting. * * @param path tree path * @param insets tree insets * @param bounds bounds buffer * @return path bounds */ protected Rectangle getPathBounds ( final TreePath path, final Insets insets, Rectangle bounds ) { bounds = treeState.getBounds ( path, bounds ); if ( bounds != null ) { if ( leftToRight ) { bounds.x += insets.left; } else { bounds.x = tree.getWidth () - ( bounds.x + bounds.width ) - insets.right; } bounds.y += insets.top; } return bounds; } /** * Returns selection shape for specified selection bounds. * * @param sb selection bounds * @param fill should fill the whole cell * @return selection shape for specified selection bounds */ protected Shape getSelectionShape ( final Rectangle sb, final boolean fill ) { final int shear = fill ? 1 : 0; if ( selectorRound > 0 ) { return new RoundRectangle2D.Double ( sb.x + shear, sb.y + shear, sb.width - shear, sb.height - shear, selectorRound * 2, selectorRound * 2 ); } else { return new Rectangle2D.Double ( sb.x + shear, sb.y + shear, sb.width - shear, sb.height - shear ); } } }