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