001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 */ 017package org.opends.guitools.controlpanel.ui.components; 018 019import static com.forgerock.opendj.util.OperatingSystem.isMacOS; 020 021import java.awt.Graphics; 022import java.awt.Insets; 023import java.awt.Rectangle; 024import java.awt.event.InputEvent; 025import java.awt.event.MouseAdapter; 026import java.awt.event.MouseEvent; 027import java.awt.event.MouseListener; 028import java.util.ArrayList; 029import java.util.HashSet; 030import java.util.Set; 031 032import javax.swing.JPopupMenu; 033import javax.swing.JTree; 034import javax.swing.tree.TreePath; 035 036import org.opends.guitools.controlpanel.ui.renderer.TreeCellRenderer; 037 038/** 039 * The tree that is used in different places in the Control Panel (schema 040 * browser, index browser or the LDAP entries browser). It renders in a 041 * different manner than the default tree (selection takes the whole width 042 * of the tree, in a similar manner as happens with trees in Mac OS). 043 */ 044public class CustomTree extends JTree 045{ 046 private static final long serialVersionUID = -8351107707374485555L; 047 private Set<MouseListener> mouseListeners; 048 private JPopupMenu popupMenu; 049 private final int MAX_ICON_HEIGHT = 18; 050 051 /** Internal enumeration used to translate mouse events. */ 052 private enum NewEventType 053 { 054 MOUSE_PRESSED, MOUSE_CLICKED, MOUSE_RELEASED 055 } 056 057 @Override 058 public void paintComponent(Graphics g) 059 { 060 int[] selectedRows = getSelectionRows(); 061 if (selectedRows == null) 062 { 063 selectedRows = new int[] {}; 064 } 065 Insets insets = getInsets(); 066 int w = getWidth( ) - insets.left - insets.right; 067 int h = getHeight( ) - insets.top - insets.bottom; 068 int x = insets.left; 069 int y = insets.top; 070 int nRows = getRowCount(); 071 for ( int i = 0; i < nRows; i++) 072 { 073 int rowHeight = getRowBounds( i ).height; 074 if (isRowSelected(selectedRows, i)) 075 { 076 g.setColor(TreeCellRenderer.selectionBackground); 077 } 078 else 079 { 080 g.setColor(TreeCellRenderer.nonselectionBackground); 081 } 082 g.fillRect( x, y, w, rowHeight ); 083 y += rowHeight; 084 } 085 final int remainder = insets.top + h - y; 086 if ( remainder > 0 ) 087 { 088 g.setColor(TreeCellRenderer.nonselectionBackground); 089 g.fillRect(x, y, w, remainder); 090 } 091 092 boolean isOpaque = isOpaque(); 093 setOpaque(false); 094 super.paintComponent(g); 095 setOpaque(isOpaque); 096 } 097 098 private boolean isRowSelected(int[] selectedRows, int i) 099 { 100 for (int selectedRow : selectedRows) 101 { 102 if (selectedRow == i) 103 { 104 return true; 105 } 106 } 107 return false; 108 } 109 110 /** 111 * Sets a popup menu that will be displayed when the user clicks on the tree. 112 * @param popMenu the popup menu. 113 */ 114 public void setPopupMenu(JPopupMenu popMenu) 115 { 116 this.popupMenu = popMenu; 117 } 118 119 /** Default constructor. */ 120 public CustomTree() 121 { 122 putClientProperty("JTree.lineStyle", "Angled"); 123 // This mouse listener is used so that when the user clicks on a row, 124 // the items are selected (is not required to click directly on the label). 125 // This code tries to have a similar behavior as in Mac OS). 126 MouseListener mouseListener = new MouseAdapter() 127 { 128 private boolean ignoreEvents; 129 @Override 130 public void mousePressed(MouseEvent ev) 131 { 132 if (ignoreEvents) 133 { 134 return; 135 } 136 MouseEvent newEvent = getTranslatedEvent(ev); 137 138 if (isMacOS() && ev.isPopupTrigger() && 139 ev.getButton() != MouseEvent.BUTTON1) 140 { 141 MouseEvent baseEvent = ev; 142 if (newEvent != null) 143 { 144 baseEvent = newEvent; 145 } 146 int mods = baseEvent.getModifiersEx(); 147 mods &= InputEvent.ALT_DOWN_MASK | InputEvent.META_DOWN_MASK | 148 InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK; 149 mods |= InputEvent.BUTTON1_DOWN_MASK; 150 final MouseEvent macEvent = new MouseEvent( 151 baseEvent.getComponent(), 152 baseEvent.getID(), 153 System.currentTimeMillis(), 154 mods, 155 baseEvent.getX(), 156 baseEvent.getY(), 157 baseEvent.getClickCount(), 158 false, 159 MouseEvent.BUTTON1); 160 // This is done to select the node when the user does a right 161 // click on Mac OS. 162 notifyNewEvent(macEvent, NewEventType.MOUSE_PRESSED); 163 } 164 165 if (ev.isPopupTrigger() 166 && popupMenu != null 167 && (getPathForLocation(ev.getPoint().x, ev.getPoint().y) != null 168 || newEvent != null)) 169 { 170 popupMenu.show(ev.getComponent(), ev.getX(), ev.getY()); 171 } 172 if (newEvent != null) 173 { 174 notifyNewEvent(newEvent, NewEventType.MOUSE_PRESSED); 175 } 176 } 177 178 @Override 179 public void mouseReleased(MouseEvent ev) 180 { 181 if (ignoreEvents) 182 { 183 return; 184 } 185 MouseEvent newEvent = getTranslatedEvent(ev); 186 if (ev.isPopupTrigger() 187 && popupMenu != null 188 && !popupMenu.isVisible() 189 && (getPathForLocation(ev.getPoint().x, ev.getPoint().y) != null 190 || newEvent != null)) 191 { 192 popupMenu.show(ev.getComponent(), ev.getX(), ev.getY()); 193 } 194 195 if (newEvent != null) 196 { 197 notifyNewEvent(newEvent, NewEventType.MOUSE_RELEASED); 198 } 199 } 200 201 @Override 202 public void mouseClicked(MouseEvent ev) 203 { 204 if (ignoreEvents) 205 { 206 return; 207 } 208 MouseEvent newEvent = getTranslatedEvent(ev); 209 if (newEvent != null) 210 { 211 notifyNewEvent(newEvent, NewEventType.MOUSE_CLICKED); 212 } 213 } 214 215 private void notifyNewEvent(MouseEvent newEvent, NewEventType type) 216 { 217 ignoreEvents = true; 218 // New ArrayList to avoid concurrent modifications (the listeners 219 // could be unregistering themselves). 220 for (MouseListener mouseListener : 221 new ArrayList<MouseListener>(mouseListeners)) 222 { 223 if (mouseListener != this) 224 { 225 switch (type) 226 { 227 case MOUSE_RELEASED: 228 mouseListener.mouseReleased(newEvent); 229 break; 230 case MOUSE_CLICKED: 231 mouseListener.mouseClicked(newEvent); 232 break; 233 default: 234 mouseListener.mousePressed(newEvent); 235 } 236 } 237 } 238 ignoreEvents = false; 239 } 240 241 private MouseEvent getTranslatedEvent(MouseEvent ev) 242 { 243 MouseEvent newEvent = null; 244 int x = ev.getPoint().x; 245 int y = ev.getPoint().y; 246 if (getPathForLocation(x, y) == null) 247 { 248 TreePath path = getWidePathForLocation(x, y); 249 if (path != null) 250 { 251 Rectangle r = getPathBounds(path); 252 if (r != null) 253 { 254 int newX = r.x + r.width / 2; 255 int newY = r.y + r.height / 2; 256 // Simulate an event 257 newEvent = new MouseEvent( 258 ev.getComponent(), 259 ev.getID(), 260 ev.getWhen(), 261 ev.getModifiersEx(), 262 newX, 263 newY, 264 ev.getClickCount(), 265 ev.isPopupTrigger(), 266 ev.getButton()); 267 } 268 } 269 } 270 return newEvent; 271 } 272 }; 273 addMouseListener(mouseListener); 274 if (getRowHeight() <= MAX_ICON_HEIGHT) 275 { 276 setRowHeight(MAX_ICON_HEIGHT + 1); 277 } 278 } 279 280 @Override 281 public void addMouseListener(MouseListener mouseListener) 282 { 283 super.addMouseListener(mouseListener); 284 if (mouseListeners == null) 285 { 286 mouseListeners = new HashSet<>(); 287 } 288 mouseListeners.add(mouseListener); 289 } 290 291 @Override 292 public void removeMouseListener(MouseListener mouseListener) 293 { 294 super.removeMouseListener(mouseListener); 295 mouseListeners.remove(mouseListener); 296 } 297 298 private TreePath getWidePathForLocation(int x, int y) 299 { 300 TreePath path = null; 301 TreePath closestPath = getClosestPathForLocation(x, y); 302 if (closestPath != null) 303 { 304 Rectangle pathBounds = getPathBounds(closestPath); 305 if (pathBounds != null && 306 x >= pathBounds.x && x < getX() + getWidth() && 307 y >= pathBounds.y && y < pathBounds.y + pathBounds.height) 308 { 309 path = closestPath; 310 } 311 } 312 return path; 313 } 314}