001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.AWTEvent;
007import java.awt.BorderLayout;
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.Graphics;
012import java.awt.GraphicsEnvironment;
013import java.awt.GridBagLayout;
014import java.awt.GridLayout;
015import java.awt.Rectangle;
016import java.awt.Toolkit;
017import java.awt.event.AWTEventListener;
018import java.awt.event.ActionEvent;
019import java.awt.event.ComponentAdapter;
020import java.awt.event.ComponentEvent;
021import java.awt.event.MouseEvent;
022import java.awt.event.WindowAdapter;
023import java.awt.event.WindowEvent;
024import java.beans.PropertyChangeEvent;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.LinkedList;
029import java.util.List;
030
031import javax.swing.AbstractAction;
032import javax.swing.BorderFactory;
033import javax.swing.ButtonGroup;
034import javax.swing.ImageIcon;
035import javax.swing.JButton;
036import javax.swing.JCheckBoxMenuItem;
037import javax.swing.JComponent;
038import javax.swing.JDialog;
039import javax.swing.JLabel;
040import javax.swing.JMenu;
041import javax.swing.JPanel;
042import javax.swing.JPopupMenu;
043import javax.swing.JRadioButtonMenuItem;
044import javax.swing.JScrollPane;
045import javax.swing.JToggleButton;
046import javax.swing.Scrollable;
047import javax.swing.SwingUtilities;
048
049import org.openstreetmap.josm.actions.JosmAction;
050import org.openstreetmap.josm.data.preferences.BooleanProperty;
051import org.openstreetmap.josm.data.preferences.ParametrizedEnumProperty;
052import org.openstreetmap.josm.gui.MainApplication;
053import org.openstreetmap.josm.gui.MainMenu;
054import org.openstreetmap.josm.gui.ShowHideButtonListener;
055import org.openstreetmap.josm.gui.SideButton;
056import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
057import org.openstreetmap.josm.gui.help.HelpUtil;
058import org.openstreetmap.josm.gui.help.Helpful;
059import org.openstreetmap.josm.gui.preferences.PreferenceDialog;
060import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
061import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
062import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
063import org.openstreetmap.josm.gui.util.GuiHelper;
064import org.openstreetmap.josm.gui.util.WindowGeometry;
065import org.openstreetmap.josm.gui.util.WindowGeometry.WindowGeometryException;
066import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
067import org.openstreetmap.josm.spi.preferences.Config;
068import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
069import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
070import org.openstreetmap.josm.tools.Destroyable;
071import org.openstreetmap.josm.tools.GBC;
072import org.openstreetmap.josm.tools.ImageProvider;
073import org.openstreetmap.josm.tools.Logging;
074import org.openstreetmap.josm.tools.Shortcut;
075
076/**
077 * This class is a toggle dialog that can be turned on and off.
078 * @since 8
079 */
080public class ToggleDialog extends JPanel implements ShowHideButtonListener, Helpful, AWTEventListener, Destroyable, PreferenceChangedListener {
081
082    /**
083     * The button-hiding strategy in toggler dialogs.
084     */
085    public enum ButtonHidingType {
086        /** Buttons are always shown (default) **/
087        ALWAYS_SHOWN,
088        /** Buttons are always hidden **/
089        ALWAYS_HIDDEN,
090        /** Buttons are dynamically hidden, i.e. only shown when mouse cursor is in dialog */
091        DYNAMIC
092    }
093
094    /**
095     * Property to enable dynamic buttons globally.
096     * @since 6752
097     */
098    public static final BooleanProperty PROP_DYNAMIC_BUTTONS = new BooleanProperty("dialog.dynamic.buttons", false);
099
100    private final transient ParametrizedEnumProperty<ButtonHidingType> propButtonHiding =
101            new ParametrizedEnumProperty<ToggleDialog.ButtonHidingType>(ButtonHidingType.class, ButtonHidingType.DYNAMIC) {
102        @Override
103        protected String getKey(String... params) {
104            return preferencePrefix + ".buttonhiding";
105        }
106
107        @Override
108        protected ButtonHidingType parse(String s) {
109            try {
110                return super.parse(s);
111            } catch (IllegalArgumentException e) {
112                // Legacy settings
113                Logging.trace(e);
114                return Boolean.parseBoolean(s) ? ButtonHidingType.DYNAMIC : ButtonHidingType.ALWAYS_SHOWN;
115            }
116        }
117    };
118
119    /** The action to toggle this dialog */
120    protected final ToggleDialogAction toggleAction;
121    protected String preferencePrefix;
122    protected final String name;
123
124    /** DialogsPanel that manages all ToggleDialogs */
125    protected DialogsPanel dialogsPanel;
126
127    protected TitleBar titleBar;
128
129    /**
130     * Indicates whether the dialog is showing or not.
131     */
132    protected boolean isShowing;
133
134    /**
135     * If isShowing is true, indicates whether the dialog is docked or not, e. g.
136     * shown as part of the main window or as a separate dialog window.
137     */
138    protected boolean isDocked;
139
140    /**
141     * If isShowing and isDocked are true, indicates whether the dialog is
142     * currently minimized or not.
143     */
144    protected boolean isCollapsed;
145
146    /**
147     * Indicates whether dynamic button hiding is active or not.
148     */
149    protected ButtonHidingType buttonHiding;
150
151    /** the preferred height if the toggle dialog is expanded */
152    private int preferredHeight;
153
154    /** the JDialog displaying the toggle dialog as undocked dialog */
155    protected JDialog detachedDialog;
156
157    protected JToggleButton button;
158    private JPanel buttonsPanel;
159    private final transient List<javax.swing.Action> buttonActions = new ArrayList<>();
160
161    /** holds the menu entry in the windows menu. Required to properly
162     * toggle the checkbox on show/hide
163     */
164    protected JCheckBoxMenuItem windowMenuItem;
165
166    private final JRadioButtonMenuItem alwaysShown = new JRadioButtonMenuItem(new AbstractAction(tr("Always shown")) {
167        @Override
168        public void actionPerformed(ActionEvent e) {
169            setIsButtonHiding(ButtonHidingType.ALWAYS_SHOWN);
170        }
171    });
172
173    private final JRadioButtonMenuItem dynamic = new JRadioButtonMenuItem(new AbstractAction(tr("Dynamic")) {
174        @Override
175        public void actionPerformed(ActionEvent e) {
176            setIsButtonHiding(ButtonHidingType.DYNAMIC);
177        }
178    });
179
180    private final JRadioButtonMenuItem alwaysHidden = new JRadioButtonMenuItem(new AbstractAction(tr("Always hidden")) {
181        @Override
182        public void actionPerformed(ActionEvent e) {
183            setIsButtonHiding(ButtonHidingType.ALWAYS_HIDDEN);
184        }
185    });
186
187    /**
188     * The linked preferences class (optional). If set, accessible from the title bar with a dedicated button
189     */
190    protected Class<? extends PreferenceSetting> preferenceClass;
191
192    /**
193     * Constructor
194     *
195     * @param name  the name of the dialog
196     * @param iconName the name of the icon to be displayed
197     * @param tooltip  the tool tip
198     * @param shortcut  the shortcut
199     * @param preferredHeight the preferred height for the dialog
200     */
201    public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight) {
202        this(name, iconName, tooltip, shortcut, preferredHeight, false);
203    }
204
205    /**
206     * Constructor
207
208     * @param name  the name of the dialog
209     * @param iconName the name of the icon to be displayed
210     * @param tooltip  the tool tip
211     * @param shortcut  the shortcut
212     * @param preferredHeight the preferred height for the dialog
213     * @param defShow if the dialog should be shown by default, if there is no preference
214     */
215    public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow) {
216        this(name, iconName, tooltip, shortcut, preferredHeight, defShow, null);
217    }
218
219    /**
220     * Constructor
221     *
222     * @param name  the name of the dialog
223     * @param iconName the name of the icon to be displayed
224     * @param tooltip  the tool tip
225     * @param shortcut  the shortcut
226     * @param preferredHeight the preferred height for the dialog
227     * @param defShow if the dialog should be shown by default, if there is no preference
228     * @param prefClass the preferences settings class, or null if not applicable
229     */
230    public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow,
231            Class<? extends PreferenceSetting> prefClass) {
232        this(name, iconName, tooltip, shortcut, preferredHeight, defShow, prefClass, false);
233    }
234
235    /**
236     * Constructor
237     *
238     * @param name  the name of the dialog
239     * @param iconName the name of the icon to be displayed
240     * @param tooltip  the tool tip
241     * @param shortcut  the shortcut
242     * @param preferredHeight the preferred height for the dialog
243     * @param defShow if the dialog should be shown by default, if there is no preference
244     * @param prefClass the preferences settings class, or null if not applicable
245     * @param isExpert {@code true} if this dialog should only be displayed in expert mode
246     * @since 15650
247     */
248    public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow,
249            Class<? extends PreferenceSetting> prefClass, boolean isExpert) {
250        super(new BorderLayout());
251        this.preferencePrefix = iconName;
252        this.name = name;
253        this.preferenceClass = prefClass;
254
255        /** Use the full width of the parent element */
256        setPreferredSize(new Dimension(0, preferredHeight));
257        /** Override any minimum sizes of child elements so the user can resize freely */
258        setMinimumSize(new Dimension(0, 0));
259        this.preferredHeight = Config.getPref().getInt(preferencePrefix+".preferredHeight", preferredHeight);
260        toggleAction = new ToggleDialogAction(name, "dialogs/"+iconName, tooltip, shortcut, helpTopic());
261
262        isShowing = Config.getPref().getBoolean(preferencePrefix+".visible", defShow);
263        isDocked = Config.getPref().getBoolean(preferencePrefix+".docked", true);
264        isCollapsed = Config.getPref().getBoolean(preferencePrefix+".minimized", false);
265        buttonHiding = propButtonHiding.get();
266
267        /** show the minimize button */
268        titleBar = new TitleBar(name, iconName);
269        add(titleBar, BorderLayout.NORTH);
270
271        setBorder(BorderFactory.createEtchedBorder());
272
273        MainApplication.redirectToMainContentPane(this);
274        Config.getPref().addPreferenceChangeListener(this);
275
276        registerInWindowMenu(isExpert);
277    }
278
279    /**
280     * Registers this dialog in the window menu. Called in the constructor.
281     * @param isExpert {@code true} if this dialog should only be displayed in expert mode
282     * @since 15650
283     */
284    protected void registerInWindowMenu(boolean isExpert) {
285        windowMenuItem = MainMenu.addWithCheckbox(MainApplication.getMenu().windowMenu,
286                (JosmAction) getToggleAction(),
287                MainMenu.WINDOW_MENU_GROUP.TOGGLE_DIALOG, isExpert, true);
288    }
289
290    /**
291     * The action to toggle the visibility state of this toggle dialog.
292     *
293     * Emits {@link PropertyChangeEvent}s for the property <code>selected</code>:
294     * <ul>
295     *   <li>true, if the dialog is currently visible</li>
296     *   <li>false, if the dialog is currently invisible</li>
297     * </ul>
298     *
299     */
300    public final class ToggleDialogAction extends JosmAction {
301
302        private ToggleDialogAction(String name, String iconName, String tooltip, Shortcut shortcut, String helpId) {
303            super(name, iconName, tooltip, shortcut, false, false);
304            setHelpId(helpId);
305        }
306
307        @Override
308        public void actionPerformed(ActionEvent e) {
309            toggleButtonHook();
310            if (getValue("toolbarbutton") instanceof JButton) {
311                ((JButton) getValue("toolbarbutton")).setSelected(!isShowing);
312            }
313            if (isShowing) {
314                hideDialog();
315                if (dialogsPanel != null) {
316                    dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
317                }
318                hideNotify();
319            } else {
320                showDialog();
321                if (isDocked && isCollapsed) {
322                    expand();
323                }
324                if (isDocked && dialogsPanel != null) {
325                    dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
326                }
327                showNotify();
328            }
329        }
330
331        @Override
332        public String toString() {
333            return "ToggleDialogAction [" + ToggleDialog.this + ']';
334        }
335    }
336
337    /**
338     * Shows the dialog
339     */
340    public void showDialog() {
341        setIsShowing(true);
342        if (!isDocked) {
343            detach();
344        } else {
345            dock();
346            this.setVisible(true);
347        }
348        // toggling the selected value in order to enforce PropertyChangeEvents
349        setIsShowing(true);
350        if (windowMenuItem != null) {
351            windowMenuItem.setState(true);
352        }
353        toggleAction.putValue("selected", Boolean.FALSE);
354        toggleAction.putValue("selected", Boolean.TRUE);
355    }
356
357    /**
358     * Changes the state of the dialog such that the user can see the content.
359     * (takes care of the panel reconstruction)
360     */
361    public void unfurlDialog() {
362        if (isDialogInDefaultView())
363            return;
364        if (isDialogInCollapsedView()) {
365            expand();
366            dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
367        } else if (!isDialogShowing()) {
368            showDialog();
369            if (isDocked && isCollapsed) {
370                expand();
371            }
372            if (isDocked) {
373                dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, this);
374            }
375            showNotify();
376        }
377    }
378
379    @Override
380    public void buttonHidden() {
381        if ((Boolean) toggleAction.getValue("selected")) {
382            toggleAction.actionPerformed(null);
383        }
384    }
385
386    @Override
387    public void buttonShown() {
388        unfurlDialog();
389    }
390
391    /**
392     * Hides the dialog
393     */
394    public void hideDialog() {
395        closeDetachedDialog();
396        this.setVisible(false);
397        if (windowMenuItem != null) {
398            windowMenuItem.setState(false);
399        }
400        setIsShowing(false);
401        toggleAction.putValue("selected", Boolean.FALSE);
402    }
403
404    /**
405     * Displays the toggle dialog in the toggle dialog view on the right
406     * of the main map window.
407     *
408     */
409    protected void dock() {
410        detachedDialog = null;
411        titleBar.setVisible(true);
412        setIsDocked(true);
413    }
414
415    /**
416     * Display the dialog in a detached window.
417     *
418     */
419    protected void detach() {
420        setContentVisible(true);
421        this.setVisible(true);
422        titleBar.setVisible(false);
423        if (!GraphicsEnvironment.isHeadless()) {
424            detachedDialog = new DetachedDialog();
425            detachedDialog.setVisible(true);
426        }
427        setIsShowing(true);
428        setIsDocked(false);
429    }
430
431    /**
432     * Collapses the toggle dialog to the title bar only
433     *
434     */
435    public void collapse() {
436        if (isDialogInDefaultView()) {
437            setContentVisible(false);
438            setIsCollapsed(true);
439            setPreferredSize(new Dimension(0, 20));
440            setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));
441            setMinimumSize(new Dimension(Integer.MAX_VALUE, 20));
442            titleBar.lblMinimized.setIcon(ImageProvider.get("misc", "minimized"));
443        } else
444            throw new IllegalStateException();
445    }
446
447    /**
448     * Expands the toggle dialog
449     */
450    protected void expand() {
451        if (isDialogInCollapsedView()) {
452            setContentVisible(true);
453            setIsCollapsed(false);
454            setPreferredSize(new Dimension(0, preferredHeight));
455            setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
456            titleBar.lblMinimized.setIcon(ImageProvider.get("misc", "normal"));
457        } else
458            throw new IllegalStateException();
459    }
460
461    /**
462     * Sets the visibility of all components in this toggle dialog, except the title bar
463     *
464     * @param visible true, if the components should be visible; false otherwise
465     */
466    protected void setContentVisible(boolean visible) {
467        Component[] comps = getComponents();
468        for (Component comp : comps) {
469            if (comp != titleBar && (!visible || comp != buttonsPanel || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN)) {
470                comp.setVisible(visible);
471            }
472        }
473    }
474
475    @Override
476    public void destroy() {
477        dialogsPanel = null;
478        rememberHeight();
479        closeDetachedDialog();
480        if (isShowing) {
481            hideNotify();
482        }
483        MainApplication.getMenu().windowMenu.remove(windowMenuItem);
484        try {
485            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
486        } catch (SecurityException e) {
487            Logging.log(Logging.LEVEL_ERROR, "Unable to remove AWT event listener", e);
488        }
489        Config.getPref().removePreferenceChangeListener(this);
490        GuiHelper.destroyComponents(this, false);
491        titleBar.destroy();
492        titleBar = null;
493        this.buttonActions.clear();
494    }
495
496    /**
497     * Closes the detached dialog if this toggle dialog is currently displayed in a detached dialog.
498     */
499    public void closeDetachedDialog() {
500        if (detachedDialog != null) {
501            detachedDialog.setVisible(false);
502            detachedDialog.getContentPane().removeAll();
503            detachedDialog.dispose();
504        }
505    }
506
507    /**
508     * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this
509     * method, it's a good place to register listeners needed to keep dialog updated
510     */
511    public void showNotify() {
512        // Do nothing
513    }
514
515    /**
516     * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister listeners
517     */
518    public void hideNotify() {
519        // Do nothing
520    }
521
522    /**
523     * The title bar displayed in docked mode
524     */
525    protected class TitleBar extends JPanel implements Destroyable {
526        /** the label which shows whether the toggle dialog is expanded or collapsed */
527        private final JLabel lblMinimized;
528        /** the label which displays the dialog's title **/
529        private final JLabel lblTitle;
530        private final JComponent lblTitleWeak;
531        /** the button which shows whether buttons are dynamic or not */
532        private final JButton buttonsHide;
533        /** the contextual menu **/
534        private DialogPopupMenu popupMenu;
535
536        private MouseEventHandler mouseEventHandler;
537
538        @SuppressWarnings("unchecked")
539        public TitleBar(String toggleDialogName, String iconName) {
540            setLayout(new GridBagLayout());
541
542            lblMinimized = new JLabel(ImageProvider.get("misc", "normal"));
543            add(lblMinimized);
544
545            // scale down the dialog icon
546            ImageIcon icon = ImageProvider.get("dialogs", iconName, ImageProvider.ImageSizes.SMALLICON);
547            lblTitle = new JLabel("", icon, JLabel.TRAILING);
548            lblTitle.setIconTextGap(8);
549
550            JPanel conceal = new JPanel();
551            conceal.add(lblTitle);
552            conceal.setVisible(false);
553            add(conceal, GBC.std());
554
555            // Cannot add the label directly since it would displace other elements on resize
556            lblTitleWeak = new JComponent() {
557                @Override
558                public void paintComponent(Graphics g) {
559                    lblTitle.paint(g);
560                }
561            };
562            lblTitleWeak.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20));
563            lblTitleWeak.setMinimumSize(new Dimension(0, 20));
564            add(lblTitleWeak, GBC.std().fill(GBC.HORIZONTAL));
565
566            buttonsHide = new JButton(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN
567                ? /* ICON(misc/)*/ "buttonhide" :  /* ICON(misc/)*/ "buttonshow"));
568            buttonsHide.setToolTipText(tr("Toggle dynamic buttons"));
569            buttonsHide.setBorder(BorderFactory.createEmptyBorder());
570            buttonsHide.addActionListener(e -> {
571                JRadioButtonMenuItem item = (buttonHiding == ButtonHidingType.DYNAMIC) ? alwaysShown : dynamic;
572                item.setSelected(true);
573                item.getAction().actionPerformed(null);
574            });
575            add(buttonsHide);
576
577            // show the pref button if applicable
578            if (preferenceClass != null) {
579                JButton pref = new JButton(ImageProvider.get("preference", ImageProvider.ImageSizes.SMALLICON));
580                pref.setToolTipText(tr("Open preferences for this panel"));
581                pref.setBorder(BorderFactory.createEmptyBorder());
582                pref.addActionListener(e -> {
583                    final PreferenceDialog p = new PreferenceDialog(MainApplication.getMainFrame());
584                    if (TabPreferenceSetting.class.isAssignableFrom(preferenceClass)) {
585                        p.selectPreferencesTabByClass((Class<? extends TabPreferenceSetting>) preferenceClass);
586                    } else if (SubPreferenceSetting.class.isAssignableFrom(preferenceClass)) {
587                        p.selectSubPreferencesTabByClass((Class<? extends SubPreferenceSetting>) preferenceClass);
588                    }
589                    p.setVisible(true);
590                });
591                add(pref);
592            }
593
594            // show the sticky button
595            JButton sticky = new JButton(ImageProvider.get("misc", "sticky"));
596            sticky.setToolTipText(tr("Undock the panel"));
597            sticky.setBorder(BorderFactory.createEmptyBorder());
598            sticky.addActionListener(e -> {
599                detach();
600                dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
601            });
602            add(sticky);
603
604            // show the close button
605            JButton close = new JButton(ImageProvider.get("misc", "close"));
606            close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar."));
607            close.setBorder(BorderFactory.createEmptyBorder());
608            close.addActionListener(e -> {
609                hideDialog();
610                dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
611                hideNotify();
612            });
613            add(close);
614            setToolTipText(tr("Click to minimize/maximize the panel content"));
615            setTitle(toggleDialogName);
616        }
617
618        public void setTitle(String title) {
619            lblTitle.setText(title);
620            lblTitleWeak.repaint();
621        }
622
623        public String getTitle() {
624            return lblTitle.getText();
625        }
626
627        /**
628         * This is the popup menu used for the title bar.
629         */
630        public class DialogPopupMenu extends JPopupMenu {
631
632            /**
633             * Constructs a new {@code DialogPopupMenu}.
634             */
635            DialogPopupMenu() {
636                alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN);
637                dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC);
638                alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN);
639                ButtonGroup buttonHidingGroup = new ButtonGroup();
640                JMenu buttonHidingMenu = new JMenu(tr("Side buttons"));
641                for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) {
642                    buttonHidingGroup.add(rb);
643                    buttonHidingMenu.add(rb);
644                }
645                add(buttonHidingMenu);
646                for (javax.swing.Action action: buttonActions) {
647                    add(action);
648                }
649            }
650        }
651
652        /**
653         * Registers the mouse listeners.
654         * <p>
655         * Should be called once after this title was added to the dialog.
656         */
657        public final void registerMouseListener() {
658            popupMenu = new DialogPopupMenu();
659            mouseEventHandler = new MouseEventHandler();
660            addMouseListener(mouseEventHandler);
661        }
662
663        class MouseEventHandler extends PopupMenuLauncher {
664            /**
665             * Constructs a new {@code MouseEventHandler}.
666             */
667            MouseEventHandler() {
668                super(popupMenu);
669            }
670
671            @Override
672            public void mouseClicked(MouseEvent e) {
673                if (SwingUtilities.isLeftMouseButton(e)) {
674                    if (isCollapsed) {
675                        expand();
676                        dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this);
677                    } else {
678                        collapse();
679                        dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
680                    }
681                }
682            }
683        }
684
685        @Override
686        public void destroy() {
687            removeMouseListener(mouseEventHandler);
688            this.mouseEventHandler = null;
689            this.popupMenu = null;
690        }
691    }
692
693    /**
694     * The dialog class used to display toggle dialogs in a detached window.
695     *
696     */
697    private class DetachedDialog extends JDialog {
698        DetachedDialog() {
699            super(GuiHelper.getFrameForComponent(MainApplication.getMainFrame()));
700            getContentPane().add(ToggleDialog.this);
701            addWindowListener(new WindowAdapter() {
702                @Override public void windowClosing(WindowEvent e) {
703                    rememberGeometry();
704                    getContentPane().removeAll();
705                    dispose();
706                    if (dockWhenClosingDetachedDlg()) {
707                        dock();
708                        if (isDialogInCollapsedView()) {
709                            setContentVisible(false);
710                            dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
711                        } else {
712                            dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
713                        }
714                    } else {
715                        hideDialog();
716                        hideNotify();
717                    }
718                }
719            });
720            addComponentListener(new ComponentAdapter() {
721                @Override
722                public void componentMoved(ComponentEvent e) {
723                    rememberGeometry();
724                }
725
726                @Override
727                public void componentResized(ComponentEvent e) {
728                    rememberGeometry();
729                }
730            });
731
732            try {
733                new WindowGeometry(preferencePrefix+".geometry").applySafe(this);
734            } catch (WindowGeometryException e) {
735                Logging.debug(e);
736                ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize());
737                pack();
738                setLocationRelativeTo(MainApplication.getMainFrame());
739            }
740            super.setTitle(titleBar.getTitle());
741            HelpUtil.setHelpContext(getRootPane(), helpTopic());
742        }
743
744        protected void rememberGeometry() {
745            if (detachedDialog != null && detachedDialog.isShowing()) {
746                new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry");
747            }
748        }
749    }
750
751    /**
752     * Replies the action to toggle the visible state of this toggle dialog
753     *
754     * @return the action to toggle the visible state of this toggle dialog
755     */
756    public AbstractAction getToggleAction() {
757        return toggleAction;
758    }
759
760    /**
761     * Replies the prefix for the preference settings of this dialog.
762     *
763     * @return the prefix for the preference settings of this dialog.
764     */
765    public String getPreferencePrefix() {
766        return preferencePrefix;
767    }
768
769    /**
770     * Sets the dialogsPanel managing all toggle dialogs.
771     * @param dialogsPanel The panel managing all toggle dialogs
772     */
773    public void setDialogsPanel(DialogsPanel dialogsPanel) {
774        this.dialogsPanel = dialogsPanel;
775    }
776
777    /**
778     * Replies the name of this toggle dialog
779     */
780    @Override
781    public String getName() {
782        return "toggleDialog." + preferencePrefix;
783    }
784
785    /**
786     * Sets the title.
787     * @param title The dialog's title
788     */
789    public void setTitle(String title) {
790        if (titleBar != null) {
791            titleBar.setTitle(title);
792        }
793        if (detachedDialog != null) {
794            detachedDialog.setTitle(title);
795        }
796    }
797
798    protected void setIsShowing(boolean val) {
799        isShowing = val;
800        Config.getPref().putBoolean(preferencePrefix+".visible", val);
801        stateChanged();
802    }
803
804    protected void setIsDocked(boolean val) {
805        if (buttonsPanel != null) {
806            buttonsPanel.setVisible(!val || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN);
807        }
808        isDocked = val;
809        Config.getPref().putBoolean(preferencePrefix+".docked", val);
810        stateChanged();
811    }
812
813    protected void setIsCollapsed(boolean val) {
814        isCollapsed = val;
815        Config.getPref().putBoolean(preferencePrefix+".minimized", val);
816        stateChanged();
817    }
818
819    protected void setIsButtonHiding(ButtonHidingType val) {
820        buttonHiding = val;
821        propButtonHiding.put(val);
822        refreshHidingButtons();
823    }
824
825    /**
826     * Returns the preferred height of this dialog.
827     * @return The preferred height if the toggle dialog is expanded
828     */
829    public int getPreferredHeight() {
830        return preferredHeight;
831    }
832
833    @Override
834    public String helpTopic() {
835        String help = getClass().getName();
836        help = help.substring(help.lastIndexOf('.')+1, help.length()-6);
837        return "Dialog/"+help;
838    }
839
840    @Override
841    public String toString() {
842        return name;
843    }
844
845    /**
846     * Determines if this dialog is showing either as docked or as detached dialog.
847     * @return {@code true} if this dialog is showing either as docked or as detached dialog
848     */
849    public boolean isDialogShowing() {
850        return isShowing;
851    }
852
853    /**
854     * Determines if this dialog is docked and expanded.
855     * @return {@code true} if this dialog is docked and expanded
856     */
857    public boolean isDialogInDefaultView() {
858        return isShowing && isDocked && (!isCollapsed);
859    }
860
861    /**
862     * Determines if this dialog is docked and collapsed.
863     * @return {@code true} if this dialog is docked and collapsed
864     */
865    public boolean isDialogInCollapsedView() {
866        return isShowing && isDocked && isCollapsed;
867    }
868
869    /**
870     * Sets the button from the button list that is used to display this dialog.
871     * <p>
872     * Note: This is ignored by the {@link ToggleDialog} for now.
873     * @param button The button for this dialog.
874     */
875    public void setButton(JToggleButton button) {
876        this.button = button;
877    }
878
879    /**
880     * Gets the button from the button list that is used to display this dialog.
881     * @return button The button for this dialog.
882     */
883    public JToggleButton getButton() {
884        return button;
885    }
886
887    /*
888     * The following methods are intended to be overridden, in order to customize
889     * the toggle dialog behavior.
890     */
891
892    /**
893     * Returns the default size of the detached dialog.
894     * Override this method to customize the initial dialog size.
895     * @return the default size of the detached dialog
896     */
897    protected Dimension getDefaultDetachedSize() {
898        return new Dimension(dialogsPanel.getWidth(), preferredHeight);
899    }
900
901    /**
902     * Do something when the toggleButton is pressed.
903     */
904    protected void toggleButtonHook() {
905        // Do nothing
906    }
907
908    protected boolean dockWhenClosingDetachedDlg() {
909        return dialogsPanel != null && titleBar != null;
910    }
911
912    /**
913     * primitive stateChangedListener for subclasses
914     */
915    protected void stateChanged() {
916        // Do nothing
917    }
918
919    /**
920     * Create a component with the given layout for this component.
921     * @param data The content to be displayed
922     * @param scroll <code>true</code> if it should be wrapped in a {@link JScrollPane}
923     * @param buttons The buttons to add.
924     * @return The component.
925     */
926    protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) {
927        return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null);
928    }
929
930    @SafeVarargs
931    protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons,
932            Collection<SideButton>... nextButtons) {
933        if (scroll) {
934            JScrollPane sp = new JScrollPane(data);
935            if (!(data instanceof Scrollable)) {
936                GuiHelper.setDefaultIncrement(sp);
937            }
938            data = sp;
939        }
940        LinkedList<Collection<SideButton>> buttons = new LinkedList<>();
941        buttons.addFirst(firstButtons);
942        if (nextButtons != null) {
943            buttons.addAll(Arrays.asList(nextButtons));
944        }
945        add(data, BorderLayout.CENTER);
946        if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) {
947            buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1));
948            for (Collection<SideButton> buttonRow : buttons) {
949                if (buttonRow == null) {
950                    continue;
951                }
952                final JPanel buttonRowPanel = new JPanel(Config.getPref().getBoolean("dialog.align.left", false)
953                        ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size()));
954                buttonsPanel.add(buttonRowPanel);
955                for (SideButton button : buttonRow) {
956                    buttonRowPanel.add(button);
957                    javax.swing.Action action = button.getAction();
958                    if (action != null) {
959                        buttonActions.add(action);
960                    } else {
961                        Logging.warn("Button " + button + " doesn't have action defined");
962                        Logging.error(new Exception());
963                    }
964                }
965            }
966            add(buttonsPanel, BorderLayout.SOUTH);
967            dynamicButtonsPropertyChanged();
968        } else {
969            titleBar.buttonsHide.setVisible(false);
970        }
971
972        // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu
973        titleBar.registerMouseListener();
974
975        return data;
976    }
977
978    @Override
979    public void eventDispatched(AWTEvent event) {
980        if (event instanceof MouseEvent && isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC
981                && buttonsPanel != null) {
982            Rectangle b = this.getBounds();
983            b.setLocation(getLocationOnScreen());
984            if (b.contains(((MouseEvent) event).getLocationOnScreen())) {
985                if (!buttonsPanel.isVisible()) {
986                    buttonsPanel.setVisible(true);
987                }
988            } else if (buttonsPanel.isVisible()) {
989                buttonsPanel.setVisible(false);
990            }
991        }
992    }
993
994    @Override
995    public void preferenceChanged(PreferenceChangeEvent e) {
996        if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) {
997            dynamicButtonsPropertyChanged();
998        }
999    }
1000
1001    private void dynamicButtonsPropertyChanged() {
1002        boolean propEnabled = PROP_DYNAMIC_BUTTONS.get();
1003        try {
1004            if (propEnabled) {
1005                Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK);
1006            } else {
1007                Toolkit.getDefaultToolkit().removeAWTEventListener(this);
1008            }
1009        } catch (SecurityException e) {
1010            Logging.log(Logging.LEVEL_ERROR, "Unable to add/remove AWT event listener", e);
1011        }
1012        titleBar.buttonsHide.setVisible(propEnabled);
1013        refreshHidingButtons();
1014    }
1015
1016    private void refreshHidingButtons() {
1017        titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN
1018            ?  /* ICON(misc/)*/ "buttonhide" :  /* ICON(misc/)*/ "buttonshow"));
1019        titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN);
1020        if (buttonsPanel != null) {
1021            buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked);
1022        }
1023        stateChanged();
1024    }
1025
1026    /**
1027     * @return the last used height stored in preferences or preferredHeight
1028     * @since 14425
1029     */
1030    public int getLastHeight() {
1031        return Config.getPref().getInt(preferencePrefix+".lastHeight", preferredHeight);
1032    }
1033
1034    /**
1035     * Store the current height in preferences so that we can restore it.
1036     * @since 14425
1037     */
1038    public void rememberHeight() {
1039        int h = getHeight();
1040        Config.getPref().put(preferencePrefix+".lastHeight", Integer.toString(h));
1041    }
1042}