001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import java.awt.Component;
005import java.awt.Point;
006import java.awt.event.FocusAdapter;
007import java.awt.event.FocusEvent;
008import java.awt.event.MouseAdapter;
009import java.awt.event.MouseEvent;
010
011import javax.swing.JList;
012import javax.swing.JPopupMenu;
013import javax.swing.JTable;
014import javax.swing.JTree;
015import javax.swing.SwingUtilities;
016import javax.swing.tree.TreePath;
017
018import org.openstreetmap.josm.tools.Logging;
019
020/**
021 * Utility class that helps to display popup menus on mouse events.
022 * @since 2688
023 */
024public class PopupMenuLauncher extends MouseAdapter {
025    protected JPopupMenu menu;
026    private final boolean checkEnabled;
027
028    /**
029     * Creates a new {@link PopupMenuLauncher} with no defined menu.
030     * It is then needed to override the {@link #launch} method.
031     * @see #launch(MouseEvent)
032     */
033    public PopupMenuLauncher() {
034        this(null);
035    }
036
037    /**
038     * Creates a new {@link PopupMenuLauncher} with the given menu.
039     * @param menu The popup menu to display
040     */
041    public PopupMenuLauncher(JPopupMenu menu) {
042        this(menu, false);
043    }
044
045    /**
046     * Creates a new {@link PopupMenuLauncher} with the given menu.
047     * @param menu The popup menu to display
048     * @param checkEnabled if {@code true}, the popup menu will only be displayed if the component triggering the mouse event is enabled
049     * @since 5886
050     */
051    public PopupMenuLauncher(JPopupMenu menu, boolean checkEnabled) {
052        this.menu = menu;
053        this.checkEnabled = checkEnabled;
054    }
055
056    @Override
057    public void mousePressed(MouseEvent e) {
058        processEvent(e);
059    }
060
061    @Override
062    public void mouseReleased(MouseEvent e) {
063        processEvent(e);
064    }
065
066    private void processEvent(MouseEvent e) {
067        if (e.isPopupTrigger() && (!checkEnabled || e.getComponent().isEnabled())) {
068            launch(e);
069        }
070    }
071
072    /**
073     * Launches the popup menu according to the given mouse event.
074     * This method needs to be overridden if the default constructor has been called.
075     * @param evt A mouse event
076     */
077    public void launch(final MouseEvent evt) {
078        if (evt != null) {
079            final Component component = evt.getComponent();
080            if (checkSelection(component, evt.getPoint())) {
081                checkFocusAndShowMenu(component, evt);
082            }
083        }
084    }
085
086    protected boolean checkSelection(Component component, Point p) {
087        if (component instanceof JList) {
088            return checkListSelection((JList<?>) component, p) > -1;
089        } else if (component instanceof JTable) {
090            return checkTableSelection((JTable) component, p) > -1;
091        } else if (component instanceof JTree) {
092            return checkTreeSelection((JTree) component, p) != null;
093        }
094        return true;
095    }
096
097    protected void checkFocusAndShowMenu(final Component component, final MouseEvent evt) {
098        if (component != null && component.isFocusable() && !component.hasFocus() && component.requestFocusInWindow()) {
099            component.addFocusListener(new FocusAdapter() {
100                @Override
101                public void focusGained(FocusEvent e) {
102                    showMenu(evt);
103                    component.removeFocusListener(this);
104                }
105            });
106        } else {
107            showMenu(evt);
108        }
109    }
110
111    protected void showMenu(MouseEvent evt) {
112        if (menu != null && evt != null) {
113            Component component = evt.getComponent();
114            if (component.isShowing()) {
115                menu.show(component, evt.getX(), evt.getY());
116            }
117        } else {
118            Logging.warn("Unable to display menu {0} - {1}", menu, evt);
119        }
120    }
121
122    protected int checkListSelection(JList<?> list, Point p) {
123        int idx = list.locationToIndex(p);
124        if (idx >= 0 && idx < list.getModel().getSize() && list.getSelectedIndices().length < 2 && !list.isSelectedIndex(idx)) {
125            list.setSelectedIndex(idx);
126        }
127        return idx;
128    }
129
130    protected int checkTableSelection(JTable table, Point p) {
131        int row = table.rowAtPoint(p);
132        if (row >= 0 && row < table.getRowCount() && table.getSelectedRowCount() < 2 && table.getSelectedRow() != row) {
133            table.getSelectionModel().setSelectionInterval(row, row);
134        }
135        return row;
136    }
137
138    protected TreePath checkTreeSelection(JTree tree, Point p) {
139        TreePath path = tree.getPathForLocation(p.x, p.y);
140        if (path != null && tree.getSelectionCount() < 2 && !tree.isPathSelected(path)) {
141            tree.setSelectionPath(path);
142        }
143        return path;
144    }
145
146    protected static boolean isDoubleClick(MouseEvent e) {
147        return e != null && SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2;
148    }
149
150    /**
151     * @return the popup menu if defined, {@code null} otherwise.
152     * @since 5884
153     */
154    public final JPopupMenu getMenu() {
155        return menu;
156    }
157
158    /**
159     * Empties the internal undo manager, if any.
160     * @since 14977
161     */
162    public void discardAllUndoableEdits() {
163        if (menu instanceof TextContextualPopupMenu) {
164            ((TextContextualPopupMenu) menu).discardAllUndoableEdits();
165        }
166    }
167}