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}