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}