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    }
400
401    // allow private classes to fire property change events as the RGP
402    protected void firePropertyChangeAsRGP(String propertyName, Object oldValue, Object newValue) {
403        if (propertyName.equals(SELECTED_ROSTER_GROUP)) {
404            selectedRosterGroup = (String) newValue;
405        }
406        this.firePropertyChange(propertyName, oldValue, newValue);
407    }
408
409    class MenuActionListener implements ActionListener {
410
411        @Override
412        public void actionPerformed(ActionEvent e) {
413            TreePath g = new TreePath(_model.getPathToRoot(_groups));
414            WindowInterface wi = (WindowInterface) scrollPane.getTopLevelAncestor();
415            if (g.isDescendant(_tree.getSelectionPath())) {
416                if (e.getActionCommand().equals("export")) {
417                    new FullBackupExportAction(ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("Exportddd"), wi).actionPerformed(e);
418                } else if (e.getActionCommand().equals("import")) {
419                    new FullBackupImportAction(ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("Importddd"), wi).actionPerformed(e);
420                } else if (e.getActionCommand().equals("rename")) {
421                    new RenameRosterGroupAction("Rename", wi).actionPerformed(e);
422                } else if (e.getActionCommand().equals("duplicate")) {
423                    new CopyRosterGroupAction("Duplicate", wi).actionPerformed(e);
424                } else if (e.getActionCommand().equals("delete")) {
425                    new DeleteRosterGroupAction("Delete", wi).actionPerformed(e);
426                } else if (e.getActionCommand().equals("newWindow") && newWindowMenuItemAction != null) {
427                    newWindowMenuItemAction.actionPerformed(e);
428                } else {
429                    JmriJOptionPane.showMessageDialog((JComponent) e.getSource(),
430                            ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("NotImplemented"),
431                            ResourceBundle.getBundle("jmri.jmrit.roster.JmritRosterBundle").getString("NotImplemented"),
432                            JmriJOptionPane.ERROR_MESSAGE);
433                }
434            }
435        }
436    }
437
438    class MouseAdapter extends java.awt.event.MouseAdapter {
439
440        @Override
441        public void mousePressed(MouseEvent e) {
442            if (SwingUtilities.isLeftMouseButton(e)) {
443                JTree t = (JTree) e.getSource();
444                int closestRow = t.getClosestRowForLocation(e.getX(), e.getY());
445                Rectangle closestRowBounds = t.getRowBounds(closestRow);
446                if (e.getY() >= closestRowBounds.getY()
447                        && e.getY() < closestRowBounds.getY() + closestRowBounds.getHeight()) {
448                    // test the click is after start of row renderer
449                    //if (e.getX() > closestRowBounds.getX()
450                    //        && closestRow < t.getRowCount()) {
451                    t.setSelectionRow(closestRow);
452                    // setting selection to -1 removes the selection
453                    //} else {
454                    //    t.setSelectionRow(-1);
455                }
456            } else if (e.isPopupTrigger()) {
457                showMenu(e);
458            }
459        }
460
461        @Override
462        public void mouseReleased(MouseEvent e) {
463            if (e.isPopupTrigger()) {
464                showMenu(e);
465            }
466        }
467
468        public void showMenu(MouseEvent e) {
469            JTree t = (JTree) e.getSource();
470            int closestRow = t.getClosestRowForLocation(e.getX(), e.getY());
471            Rectangle closestRowBounds = t.getRowBounds(closestRow);
472            if (e.getY() >= closestRowBounds.getY()
473                    && e.getY() < closestRowBounds.getY() + closestRowBounds.getHeight()) {
474                t.setSelectionRow(closestRow);
475                TreePath g = new TreePath(_model.getPathToRoot(_groups));
476                if (t.getSelectionPath().getLastPathComponent().toString().equals(Roster.ALLENTRIES)) {
477                    allEntriesMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
478                } else if (g.isDescendant(t.getSelectionPath()) && !t.getSelectionPath().isDescendant(g)) {
479                    groupsMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
480                }
481            }
482        }
483    }
484
485    class PropertyChangeListener implements java.beans.PropertyChangeListener {
486
487        @Override
488        public void propertyChange(java.beans.PropertyChangeEvent e) {
489            //log.debug(e.getPropertyName()); // seems a bit too much to keep active!
490            if ((e.getPropertyName().equals("RosterGroupRemoved"))
491                    || (e.getPropertyName().equals("RosterGroupAdded"))
492                    || (e.getPropertyName().equals("RosterGroupRenamed"))) {
493                setRosterGroups(_groups);
494                _model.reload(_groups);
495                setSelectionToGroup(selectedRosterGroup);
496                log.debug("Refreshed Roster Groups pane"); // test for panel redraw after duplication
497            }
498        }
499    }
500
501    class TransferHandler extends javax.swing.TransferHandler {
502
503        @Override
504        public boolean canImport(JComponent c, DataFlavor[] transferFlavors) {
505            for (DataFlavor flavor : transferFlavors) {
506                if (RosterEntrySelection.rosterEntryFlavor.equals(flavor)) {
507                    return true;
508                }
509            }
510            return false;
511        }
512
513        @Override
514        public boolean importData(JComponent c, Transferable t) {
515            if (canImport(c, t.getTransferDataFlavors()) && (c instanceof JTree)) {
516                // getDropLocation is null unless dropping on an existing path
517                return importData(c, t, ((JTree) c).getDropLocation().getPath());
518            }
519            return false;
520        }
521
522        public boolean importData(JComponent c, Transferable t, TreePath p) {
523            if (p != null) {
524                TreePath g = new TreePath(_model.getPathToRoot(_groups));
525                // drag onto existing user defined group, but not onto current selection
526                if (g.isDescendant(p) && !p.isDescendant(g)
527                    && ( (c instanceof JTree) && !p.isDescendant(((JTree)c).getSelectionPath()))
528                    ) {
529                    try {
530                        ArrayList<RosterEntry> REs = RosterEntrySelection.getRosterEntries(t);
531                        for (RosterEntry re : REs) {
532                            re.putAttribute(Roster.getRosterGroupProperty(p.getLastPathComponent().toString()), "yes");
533                            re.updateFile();
534                        }
535                        Roster.getDefault().writeRoster();
536                        setSelectedRosterGroup(p.getLastPathComponent().toString());
537                    } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException | RuntimeException e) {
538                        log.warn("Exception dragging RosterEntries onto RosterGroups", e);
539                    }
540                }
541            } else {
542                try {
543                    JmriAbstractAction a = new CreateRosterGroupAction("Create From Selection", scrollPane.getTopLevelAncestor());
544                    a.setParameter("RosterEntries", RosterEntrySelection.getRosterEntries(t));
545                    a.actionPerformed(null);
546                } catch (java.awt.datatransfer.UnsupportedFlavorException | java.io.IOException | RuntimeException e) {
547                    log.warn("Exception creating RosterGroups from selection", e);
548                }
549            }
550            return false;
551        }
552    }
553
554    static public class TreeCellRenderer extends DefaultTreeCellRenderer {
555
556    }
557
558    public class TreeSelectionListener implements javax.swing.event.TreeSelectionListener {
559
560        @Override
561        public void valueChanged(TreeSelectionEvent e) {
562            TreePath g = new TreePath(_model.getPathToRoot(_groups));
563            String oldGroup = selectedRosterGroup;
564            if (e.getNewLeadSelectionPath() == null) {
565                // if there are no Roster Groups set selection to "All Entries"
566                if (Roster.getDefault().getRosterGroupList().isEmpty()) {
567                    _tree.setSelectionPath(new TreePath(_model.getPathToRoot(_groups.getFirstChild())));
568                }
569            } else if (e.getNewLeadSelectionPath().isDescendant(g)) {
570                // reject user attempts to select the "Roster Groups" header
571                _tree.setSelectionPath(e.getOldLeadSelectionPath());
572            } else if (g.isDescendant(e.getNewLeadSelectionPath())) {
573                selectedRosterGroup = _tree.getSelectionPath().getLastPathComponent().toString();
574                if (Roster.ALLENTRIES.equals(selectedRosterGroup)) {
575                    selectedRosterGroup = null;
576                }
577            } else {
578                selectedRosterGroup = null;
579            }
580            firePropertyChangeAsRGP(SELECTED_ROSTER_GROUP, oldGroup, selectedRosterGroup);
581        }
582    }
583
584    public class TreeWillExpandListener implements javax.swing.event.TreeWillExpandListener {
585
586        @Override
587        public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException {
588            log.debug("Selected rows {}", _tree.getSelectionRows());
589        }
590
591        @Override
592        public void treeWillCollapse(TreeExpansionEvent e) throws ExpandVetoException {
593            if (e.getPath().getLastPathComponent().toString().equals("Roster Groups")) {
594                throw new ExpandVetoException(e);
595            }
596        }
597    }
598
599    static public class TreeUI extends BasicTreeUI {
600
601        @Override
602        public void paint(Graphics g, JComponent c) {
603            // TODO use c.getVisibleRect to trim painting to minimum rectangle.
604            // paint the background for the tree.
605            g.setColor(UIManager.getColor("Tree.textBackground"));
606            g.fillRect(0, 0, c.getWidth(), c.getHeight());
607
608            // TODO use c.getVisibleRect to trim painting to minimum rectangle.
609            // paint the background for the selected entry, if there is one.
610            int selectedRow = getSelectionModel().getLeadSelectionRow();
611            if (selectedRow >= 0 && tree.isVisible(tree.getPathForRow(selectedRow))) {
612
613                Rectangle bounds = tree.getRowBounds(selectedRow);
614
615                Graphics2D selectionBackgroundGraphics = (Graphics2D) g.create();
616                selectionBackgroundGraphics.translate(0, bounds.y);
617                selectionBackgroundGraphics.setColor(UIManager.getColor("Tree.selectionBackground"));
618                selectionBackgroundGraphics.fillRect(0, 0, c.getWidth(), bounds.height);
619                selectionBackgroundGraphics.dispose();
620            }
621
622            super.paint(g, c);
623        }
624
625        @Override
626        protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
627            // do nothing - don't paint horizontal lines.
628        }
629
630        @Override
631        protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets,
632                TreePath path) {
633            // do nothing - don't paint vertical lines.
634        }
635    }
636
637    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterGroupsPanel.class);
638}