001package jmri.jmrit.roster.swing;
002
003import java.awt.BorderLayout;
004import java.awt.Graphics;
005import java.awt.Graphics2D;
006import java.awt.GridLayout;
007import java.awt.Insets;
008import java.awt.Rectangle;
009import java.awt.datatransfer.DataFlavor;
010import java.awt.datatransfer.Transferable;
011import java.awt.event.ActionEvent;
012import java.awt.event.ActionListener;
013import java.awt.event.ItemEvent;
014import java.awt.event.ItemListener;
015import java.awt.event.MouseEvent;
016import java.util.ArrayList;
017import java.util.ResourceBundle;
018
019import javax.swing.DropMode;
020import javax.swing.ImageIcon;
021import javax.swing.JComponent;
022import javax.swing.JMenuItem;
023import javax.swing.JPanel;
024import javax.swing.JPopupMenu;
025import javax.swing.JScrollPane;
026import javax.swing.JSeparator;
027import javax.swing.JToggleButton;
028import javax.swing.JToolBar;
029import javax.swing.JTree;
030import javax.swing.ScrollPaneConstants;
031import javax.swing.SwingUtilities;
032import javax.swing.UIManager;
033import javax.swing.event.PopupMenuEvent;
034import javax.swing.event.PopupMenuListener;
035import javax.swing.event.TreeExpansionEvent;
036import javax.swing.event.TreeSelectionEvent;
037import javax.swing.plaf.basic.BasicTreeUI;
038import javax.swing.tree.DefaultMutableTreeNode;
039import javax.swing.tree.DefaultTreeCellRenderer;
040import javax.swing.tree.DefaultTreeModel;
041import javax.swing.tree.DefaultTreeSelectionModel;
042import javax.swing.tree.ExpandVetoException;
043import javax.swing.tree.TreeNode;
044import javax.swing.tree.TreePath;
045import javax.swing.tree.TreeSelectionModel;
046
047import jmri.jmrit.roster.FullBackupExportAction;
048import jmri.jmrit.roster.FullBackupImportAction;
049import jmri.jmrit.roster.Roster;
050import jmri.jmrit.roster.RosterEntry;
051import jmri.jmrit.roster.rostergroup.RosterGroupSelector;
052import jmri.util.FileUtil;
053import jmri.util.IterableEnumeration;
054import jmri.util.datatransfer.RosterEntrySelection;
055import jmri.util.swing.JmriAbstractAction;
056import jmri.util.swing.JmriJOptionPane;
057import jmri.util.swing.WindowInterface;
058
059/**
060 * A JPanel that lists Roster Groups
061 * <p>
062 * This panel contains a fairly self-contained display of Roster Groups that
063 * allows roster groups to be fully manipulated through context menus.
064 *
065 * @author Randall Wood Copyright (C) 2011
066 * @see jmri.jmrit.roster.Roster
067 */
068public class RosterGroupsPanel extends JPanel implements RosterGroupSelector {
069
070    private static int GROUPS_MENU = 1;
071    private static int ALL_ENTRIES_MENU = 2;
072    private JScrollPane scrollPane;
073    private JTree _tree;
074    private DefaultTreeModel _model;
075    private DefaultMutableTreeNode _root;
076    private DefaultMutableTreeNode _groups;
077    //private DefaultMutableTreeNode _consists;
078    private TreeSelectionListener _TSL;
079    private String selectedRosterGroup = "";
080    private JPopupMenu groupsMenu;
081    private JPopupMenu allEntriesMenu;
082    private JmriAbstractAction newWindowMenuItemAction = null;
083
084    /**
085     * Create a RosterGroupsPanel with default settings
086     */
087    public RosterGroupsPanel() {
088        this(null);
089    }
090
091    /**
092     * Create a RosterGroupTreePane with the defaultRosterGroup selected.
093     *
094     * @param defaultRosterGroup the name of the default selection
095     */
096    public RosterGroupsPanel(String defaultRosterGroup) {
097        this.scrollPane = new JScrollPane(getTree());
098        this.scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
099        setGroupsMenu(defaultMenu(GROUPS_MENU));
100        setAllEntriesMenu(defaultMenu(ALL_ENTRIES_MENU));
101        setLayout(new BorderLayout());
102        add(scrollPane, BorderLayout.CENTER);
103        add(getButtons(), BorderLayout.SOUTH);
104        setSelectedRosterGroup(defaultRosterGroup);
105    }
106
107    /**
108     * Get the selected roster group.
109     *
110     * @return The selected roster group
111     */
112    @Override
113    public String getSelectedRosterGroup() {
114        return selectedRosterGroup;
115    }
116
117    /**
118     * Set the selected roster group.
119     * <p>
120     * If the group is <code>null</code>, the selected roster group is set to
121     * "All Entries".
122     *
123     * @param group The name of the group to set the selection to.
124     */
125    public final void setSelectedRosterGroup(String group) {
126        if (group == null ? selectedRosterGroup != null : !group.equals(selectedRosterGroup)) {
127            String oldGroup = selectedRosterGroup;
128            selectedRosterGroup = group;
129            setSelectionToGroup(group);
130            firePropertyChange(SELECTED_ROSTER_GROUP, oldGroup, group);
131        }
132    }
133
134    /**
135     * Is the selected roster group user or system defined.
136     *
137     * @return flag indicating current selection is a user defined roster group.
138     */
139    public boolean isSelectionUserDefinedRosterGroup() {
140        return (selectedRosterGroup != null && !selectedRosterGroup.equals(Roster.ALLENTRIES));
141    }
142
143    /**
144     * Set the context menu for Roster Groups
145     *
146     * @param menu The new menu for Roster Groups.
147     */
148    public final void setGroupsMenu(JPopupMenu menu) {
149        this.groupsMenu = menu;
150    }
151
152    /**
153     * Get the context menu for Roster Groups
154     *
155     * @return The current groups menu.
156     */
157    public JPopupMenu getGroupsMenu() {
158        return this.groupsMenu;
159    }
160
161    /**
162     * Set the context menu for the "All Entries" roster group
163     *
164     * @param menu The new menu for All Entries.
165     */
166    public final void setAllEntriesMenu(JPopupMenu menu) {
167        this.allEntriesMenu = menu;
168    }
169
170    /**
171     * Get the context menu for "All Entries"
172     *
173     * @return The menu for All Entries.
174     */
175    public JPopupMenu getAllEntriesMenu() {
176        return this.allEntriesMenu;
177    }
178
179    /**
180     * Set an action that the menu item "Open in New Window" will trigger.
181     * <p>
182     * Set a {@link JmriAbstractAction} that the "Open in New Window" menu item
183     * will trigger. <code>null</code> will remove the "Open in New Window" menu
184     * item from context menus. The "Open in New Window" menu item will be added
185     * or removed from the menu as appropriate.
186     * <p>
187     * If the action you pass has access to the RosterGroupPanel, it may call
188     * RosterGroupPanel.getSelectedRosterGroup to determine which group to open
189     * in the new window, otherwise it must accept a String defining the group
190     * in JmriAbstractAction.setParameter(String, String).
191     *
192     * @param action  An action that can work on the current selection
193     */
194    public void setNewWindowMenuAction(JmriAbstractAction action) {
195        if (action != null) {
196            if (newWindowMenuItemAction == null) {
197                MenuActionListener ml = new MenuActionListener();
198                JMenuItem mi = new JMenuItem(ResourceBundle.getBundle("jmri.jmrit.Bundle").getString("MenuOpenInNewWindow"));
199                mi.addActionListener(ml);
200                mi.setActionCommand("newWindow");
201                groupsMenu.insert(mi, 0);
202                groupsMenu.insert(new JSeparator(), 1);
203                // create the menu item twice because a menu item can only
204                // be attached to a single menu
205                mi = new JMenuItem(ResourceBundle.getBundle("jmri.jmrit.Bundle").getString("MenuOpenInNewWindow"));
206                mi.addActionListener(ml);
207                mi.setActionCommand("newWindow");
208                allEntriesMenu.insert(mi, 0);
209                allEntriesMenu.insert(new JSeparator(), 1);
210            }
211            newWindowMenuItemAction = action;
212        } else if (newWindowMenuItemAction != null) {
213            groupsMenu.remove(0);
214            groupsMenu.remove(0);
215            allEntriesMenu.remove(0);
216            allEntriesMenu.remove(0);
217            newWindowMenuItemAction = null;
218        }
219        groupsMenu.revalidate();
220        allEntriesMenu.revalidate();
221    }
222
223    /**
224     * The action triggered by the "Open in New Window" menu item.
225     *
226     * @return A JmriAbstractAction or null
227     */
228    public JmriAbstractAction getNewWindowMenuAction() {
229        return newWindowMenuItemAction;
230    }
231
232    private void setSelectionToGroup(String group) {
233        _tree.removeTreeSelectionListener(_TSL);
234        if (group == null || group.equals(Roster.ALLENTRIES) || group.equals("")) {
235            _tree.setSelectionPath(new TreePath(_model.getPathToRoot(_groups.getFirstChild())));
236        } else {
237            for (TreeNode n : new IterableEnumeration<TreeNode>(_groups.children())) {
238                if (n.toString().equals(group)) {
239                    _tree.setSelectionPath(new TreePath(_model.getPathToRoot(n)));
240                }
241            }
242        }
243        _tree.addTreeSelectionListener(_TSL);
244    }
245
246    private JToolBar getButtons() {
247        JToolBar controls = new JToolBar();
248        controls.setLayout(new GridLayout(1, 0, 0, 0));
249        controls.setFloatable(false);
250        final JToggleButton addGroupBtn = new JToggleButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/gui3/Add.png")), false);
251        final JToggleButton actGroupBtn = new JToggleButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/gui3/Action.png")), false);
252        addGroupBtn.addActionListener(new ActionListener() {
253
254            @Override
255            public void actionPerformed(ActionEvent e) {
256                new CreateRosterGroupAction("", scrollPane.getTopLevelAncestor()).actionPerformed(e);
257                addGroupBtn.setSelected(false);
258            }
259        });
260        actGroupBtn.addItemListener(new ItemListener() {
261
262            @Override
263            public void itemStateChanged(ItemEvent ie) {
264                if (ie.getStateChange() == ItemEvent.SELECTED) {
265                    TreePath g = new TreePath(_model.getPathToRoot(_groups));
266                    if (_tree.getSelectionPath() != null) {
267                        if (_tree.getSelectionPath().getLastPathComponent().toString().equals(Roster.ALLENTRIES)) {
268                            allEntriesMenu.show((JComponent) ie.getSource(), actGroupBtn.getX() - actGroupBtn.getWidth(), actGroupBtn.getY() - allEntriesMenu.getPreferredSize().height);
269                        } else if (g.isDescendant(_tree.getSelectionPath()) && !_tree.getSelectionPath().isDescendant(g)) {
270                            groupsMenu.show((JComponent) ie.getSource(), actGroupBtn.getX() - actGroupBtn.getWidth(), actGroupBtn.getY() - groupsMenu.getPreferredSize().height);
271                        }
272                    }
273                }
274            }
275        });
276        PopupMenuListener PML = new PopupMenuListener() {
277
278            @Override
279            public void popupMenuWillBecomeVisible(PopupMenuEvent pme) {
280                // do nothing
281            }
282
283            @Override
284            public void popupMenuWillBecomeInvisible(PopupMenuEvent pme) {
285                actGroupBtn.setSelected(false);
286            }
287
288            @Override
289            public void popupMenuCanceled(PopupMenuEvent pme) {
290                actGroupBtn.setSelected(false);
291            }
292        };
293        allEntriesMenu.addPopupMenuListener(PML);
294        groupsMenu.addPopupMenuListener(PML);
295        controls.add(addGroupBtn);
296        controls.add(actGroupBtn);
297        return controls;
298    }
299
300    /**
301     * Get a JScrollPane containing the JTree that does not display horizontal
302     * scrollbars.
303     *
304     * @return The internal JScrollPane
305     */
306    public JScrollPane getScrollPane() {
307        return this.scrollPane;
308    }
309
310    /**
311     * Get the JTree containing the roster groups.
312     *
313     * @return The internal JTree
314     */
315    public final JTree getTree() {
316        if (_tree == null) {
317            TreeSelectionModel sm = new DefaultTreeSelectionModel();
318            DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
319            sm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
320            renderer.setLeafIcon(null);
321            renderer.setClosedIcon(null);
322            renderer.setOpenIcon(null);
323            _tree = new JTree(getModel());
324            _tree.setUI(new TreeUI());
325            _tree.putClientProperty("JTree.lineStyle", "None");
326            _tree.setRootVisible(false);
327            _tree.expandRow(0);
328            _tree.setSelectionModel(sm);
329            _tree.setCellRenderer(renderer);
330            _tree.addTreeWillExpandListener(new TreeWillExpandListener());
331            _TSL = new TreeSelectionListener();
332            _tree.addTreeSelectionListener(_TSL);
333            _tree.setDragEnabled(true);
334            _tree.setDropMode(DropMode.ON);
335            _tree.setTransferHandler(new TransferHandler());
336            _tree.addMouseListener(new MouseAdapter());
337            setSelectionToGroup(selectedRosterGroup);
338            Roster.getDefault().addPropertyChangeListener(new PropertyChangeListener());
339        }
340        return _tree;
341    }
342
343    private DefaultTreeModel getModel() {
344        if (_model == null) {
345            _model = new DefaultTreeModel(getRoot());
346        }
347        return _model;
348    }
349
350    private DefaultMutableTreeNode getRoot() {
351        if (_root == null) {
352            _root = new DefaultMutableTreeNode();
353            _groups = new DefaultMutableTreeNode(Bundle.getMessage("MenuRosterGroups")); // "Roster Groups"
354            _root.add(_groups);
355            setRosterGroups(_groups);
356            // once consists can be displayed in the DP3 table, add them here
357            //_consists = new DefaultMutableTreeNode("Consists");
358            //setConsists(_consists);
359            //_root.add(_consists);
360        }
361        return _root;
362    }
363
364    private JPopupMenu defaultMenu(int menu) {
365        JPopupMenu pm = new JPopupMenu();
366        MenuActionListener ml = new MenuActionListener();
367        JMenuItem mi = new JMenuItem(Bundle.getMessage("Exportddd"));
368        mi.addActionListener(ml);
369        mi.setActionCommand("export");
370        pm.add(mi);
371        mi = new JMenuItem(Bundle.getMessage("Importddd"));
372        mi.addActionListener(ml);
373        mi.setActionCommand("import");
374        pm.add(mi);
375        if (menu == GROUPS_MENU) {
376            pm.addSeparator();
377            mi = new JMenuItem(Bundle.getMessage("Renameddd")); // key is in jmri.NamedBeanBundle
378            mi.addActionListener(ml);
379            mi.setActionCommand("rename");
380            pm.add(mi);
381            mi = new JMenuItem(Bundle.getMessage("Duplicateddd"));
382            mi.addActionListener(ml);
383            mi.setActionCommand("duplicate");
384            pm.add(mi);
385            mi = new JMenuItem(Bundle.getMessage("ButtonDelete"));
386            mi.addActionListener(ml);
387            mi.setActionCommand("delete");
388            pm.add(mi);
389        }
390        return pm;
391    }
392
393    private void setRosterGroups(DefaultMutableTreeNode root) {
394        root.removeAllChildren();
395        root.add(new DefaultMutableTreeNode(Roster.ALLENTRIES));
396        for (String g : Roster.getDefault().getRosterGroupList()) {
397            root.add(new DefaultMutableTreeNode(g));
398        }
399        root.add(new DefaultMutableTreeNode(Roster.NOGROUP));
400    }
401
402    // allow private classes to fire property change events as the RGP
403    protected void firePropertyChangeAsRGP(String propertyName, Object oldValue, Object newValue) {
404        if (propertyName.equals(SELECTED_ROSTER_GROUP)) {
405            selectedRosterGroup = (String) newValue;
406        }
407        this.firePropertyChange(propertyName, oldValue, newValue);
408    }
409
410    class MenuActionListener implements ActionListener {
411
412        @Override
413        public void actionPerformed(ActionEvent e) {
414            TreePath g = new TreePath(_model.getPathToRoot(_groups));
415            WindowInterface wi = (WindowInterface) scrollPane.getTopLevelAncestor();
416            if (g.isDescendant(_tree.getSelectionPath())) {
417                if (e.getActionCommand().equals("export")) {
418                    new FullBackupExportAction(ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("Exportddd"), wi).actionPerformed(e);
419                } else if (e.getActionCommand().equals("import")) {
420                    new FullBackupImportAction(ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("Importddd"), wi).actionPerformed(e);
421                } else if (e.getActionCommand().equals("rename")) {
422                    new RenameRosterGroupAction("Rename", wi).actionPerformed(e);
423                } else if (e.getActionCommand().equals("duplicate")) {
424                    new CopyRosterGroupAction("Duplicate", wi).actionPerformed(e);
425                } else if (e.getActionCommand().equals("delete")) {
426                    new DeleteRosterGroupAction("Delete", wi).actionPerformed(e);
427                } else if (e.getActionCommand().equals("newWindow") && newWindowMenuItemAction != null) {
428                    newWindowMenuItemAction.actionPerformed(e);
429                } else {
430                    JmriJOptionPane.showMessageDialog((JComponent) e.getSource(),
431                            ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("NotImplemented"),
432                            ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("NotImplemented"),
433                            JmriJOptionPane.ERROR_MESSAGE);
434                }
435            }
436        }
437    }
438
439    class MouseAdapter extends java.awt.event.MouseAdapter {
440
441        @Override
442        public void mousePressed(MouseEvent e) {
443            if (SwingUtilities.isLeftMouseButton(e)) {
444                JTree t = (JTree) e.getSource();
445                int closestRow = t.getClosestRowForLocation(e.getX(), e.getY());
446                Rectangle closestRowBounds = t.getRowBounds(closestRow);
447                if (e.getY() >= closestRowBounds.getY()
448                        && e.getY() < closestRowBounds.getY() + closestRowBounds.getHeight()) {
449                    // test the click is after start of row renderer
450                    //if (e.getX() > closestRowBounds.getX()
451                    //        && closestRow < t.getRowCount()) {
452                    t.setSelectionRow(closestRow);
453                    // setting selection to -1 removes the selection
454                    //} else {
455                    //    t.setSelectionRow(-1);
456                }
457            } else if (e.isPopupTrigger()) {
458                showMenu(e);
459            }
460        }
461
462        @Override
463        public void mouseReleased(MouseEvent e) {
464            if (e.isPopupTrigger()) {
465                showMenu(e);
466            }
467        }
468
469        public void showMenu(MouseEvent e) {
470            JTree t = (JTree) e.getSource();
471            int closestRow = t.getClosestRowForLocation(e.getX(), e.getY());
472            Rectangle closestRowBounds = t.getRowBounds(closestRow);
473            if (e.getY() >= closestRowBounds.getY()
474                    && e.getY() < closestRowBounds.getY() + closestRowBounds.getHeight()) {
475                t.setSelectionRow(closestRow);
476                TreePath g = new TreePath(_model.getPathToRoot(_groups));
477                if (t.getSelectionPath().getLastPathComponent().toString().equals(Roster.ALLENTRIES)) {
478                    allEntriesMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
479                } else if (g.isDescendant(t.getSelectionPath()) && !t.getSelectionPath().isDescendant(g)) {
480                    groupsMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
481                }
482            }
483        }
484    }
485
486    class PropertyChangeListener implements java.beans.PropertyChangeListener {
487
488        @Override
489        public void propertyChange(java.beans.PropertyChangeEvent e) {
490            //log.debug(e.getPropertyName()); // seems a bit too much to keep active!
491            if ((e.getPropertyName().equals("RosterGroupRemoved"))
492                    || (e.getPropertyName().equals("RosterGroupAdded"))
493                    || (e.getPropertyName().equals("RosterGroupRenamed"))) {
494                setRosterGroups(_groups);
495                _model.reload(_groups);
496                setSelectionToGroup(selectedRosterGroup);
497                log.debug("Refreshed Roster Groups pane"); // test for panel redraw after duplication
498            }
499        }
500    }
501
502    class TransferHandler extends javax.swing.TransferHandler {
503
504        @Override
505        public boolean canImport(JComponent c, DataFlavor[] transferFlavors) {
506            for (DataFlavor flavor : transferFlavors) {
507                if (RosterEntrySelection.rosterEntryFlavor.equals(flavor)) {
508                    return true;
509                }
510            }
511            return false;
512        }
513
514        @Override
515        public boolean importData(JComponent c, Transferable t) {
516            if (canImport(c, t.getTransferDataFlavors()) && (c instanceof JTree)) {
517                // getDropLocation is null unless dropping on an existing path
518                return importData(c, t, ((JTree) c).getDropLocation().getPath());
519            }
520            return false;
521        }
522
523        public boolean importData(JComponent c, Transferable t, TreePath p) {
524            if (p != null) {
525                TreePath g = new TreePath(_model.getPathToRoot(_groups));
526                // drag onto existing user defined group, but not onto current selection
527                if (g.isDescendant(p) && !p.isDescendant(g)
528                    && ( (c instanceof JTree) && !p.isDescendant(((JTree)c).getSelectionPath()))
529                    ) {
530                    try {
531                        ArrayList<RosterEntry> REs = RosterEntrySelection.getRosterEntries(t);
532                        for (RosterEntry re : REs) {
533                            re.putAttribute(Roster.getRosterGroupProperty(p.getLastPathComponent().toString()), "yes");
534                            re.updateFile();
535                        }
536                        Roster.getDefault().writeRoster();
537                        setSelectedRosterGroup(p.getLastPathComponent().toString());
538                    } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException | RuntimeException e) {
539                        log.warn("Exception dragging RosterEntries onto RosterGroups", e);
540                    }
541                }
542            } else {
543                try {
544                    JmriAbstractAction a = new CreateRosterGroupAction("Create From Selection", scrollPane.getTopLevelAncestor());
545                    a.setParameter("RosterEntries", RosterEntrySelection.getRosterEntries(t));
546                    a.actionPerformed(null);
547                } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException | RuntimeException e) {
548                    log.warn("Exception creating RosterGroups from selection", e);
549                }
550            }
551            return false;
552        }
553    }
554
555    static public class TreeCellRenderer extends DefaultTreeCellRenderer {
556
557    }
558
559    public class TreeSelectionListener implements javax.swing.event.TreeSelectionListener {
560
561        @Override
562        public void valueChanged(TreeSelectionEvent e) {
563            TreePath g = new TreePath(_model.getPathToRoot(_groups));
564            String oldGroup = selectedRosterGroup;
565            if (e.getNewLeadSelectionPath() == null) {
566                // if there are no Roster Groups set selection to "All Entries"
567                if (Roster.getDefault().getRosterGroupList().isEmpty()) {
568                    _tree.setSelectionPath(new TreePath(_model.getPathToRoot(_groups.getFirstChild())));
569                }
570            } else if (e.getNewLeadSelectionPath().isDescendant(g)) {
571                // reject user attempts to select the "Roster Groups" header
572                _tree.setSelectionPath(e.getOldLeadSelectionPath());
573            } else if (g.isDescendant(e.getNewLeadSelectionPath())) {
574                selectedRosterGroup = _tree.getSelectionPath().getLastPathComponent().toString();
575                if (Roster.ALLENTRIES.equals(selectedRosterGroup)) {
576                    selectedRosterGroup = null;
577                }
578            } else {
579                selectedRosterGroup = null;
580            }
581            firePropertyChangeAsRGP(SELECTED_ROSTER_GROUP, oldGroup, selectedRosterGroup);
582        }
583    }
584
585    public class TreeWillExpandListener implements javax.swing.event.TreeWillExpandListener {
586
587        @Override
588        public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException {
589            log.debug("Selected rows {}", _tree.getSelectionRows());
590        }
591
592        @Override
593        public void treeWillCollapse(TreeExpansionEvent e) throws ExpandVetoException {
594            if (e.getPath().getLastPathComponent().toString().equals("Roster Groups")) {
595                throw new ExpandVetoException(e);
596            }
597        }
598    }
599
600    static public class TreeUI extends BasicTreeUI {
601
602        @Override
603        public void paint(Graphics g, JComponent c) {
604            // TODO use c.getVisibleRect to trim painting to minimum rectangle.
605            // paint the background for the tree.
606            g.setColor(UIManager.getColor("Tree.textBackground"));
607            g.fillRect(0, 0, c.getWidth(), c.getHeight());
608
609            // TODO use c.getVisibleRect to trim painting to minimum rectangle.
610            // paint the background for the selected entry, if there is one.
611            int selectedRow = getSelectionModel().getLeadSelectionRow();
612            if (selectedRow >= 0 && tree.isVisible(tree.getPathForRow(selectedRow))) {
613
614                Rectangle bounds = tree.getRowBounds(selectedRow);
615
616                Graphics2D selectionBackgroundGraphics = (Graphics2D) g.create();
617                selectionBackgroundGraphics.translate(0, bounds.y);
618                selectionBackgroundGraphics.setColor(UIManager.getColor("Tree.selectionBackground"));
619                selectionBackgroundGraphics.fillRect(0, 0, c.getWidth(), bounds.height);
620                selectionBackgroundGraphics.dispose();
621            }
622
623            super.paint(g, c);
624        }
625
626        @Override
627        protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
628            // do nothing - don't paint horizontal lines.
629        }
630
631        @Override
632        protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets,
633                TreePath path) {
634            // do nothing - don't paint vertical lines.
635        }
636    }
637
638    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterGroupsPanel.class);
639}