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