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