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