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}