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