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
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            lblTitle_weak.repaint();
600        }
601
602        public String getTitle() {
603            return lblTitle.getText();
604        }
605
606        public class DialogPopupMenu extends JPopupMenu {
607            private final ButtonGroup buttonHidingGroup = new ButtonGroup();
608            private final JMenu buttonHidingMenu = new JMenu(tr("Side buttons"));
609
610            public DialogPopupMenu() {
611                alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN);
612                dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC);
613                alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN);
614                for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) {
615                    buttonHidingGroup.add(rb);
616                    buttonHidingMenu.add(rb);
617                }
618                add(buttonHidingMenu);
619                for (javax.swing.Action action: buttonActions) {
620                    add(action);
621                }
622            }
623        }
624
625        public final void registerMouseListener() {
626            popupMenu = new DialogPopupMenu();
627            addMouseListener(new MouseEventHandler());
628        }
629
630        class MouseEventHandler extends PopupMenuLauncher {
631            public MouseEventHandler() {
632                super(popupMenu);
633            }
634            @Override
635            public void mouseClicked(MouseEvent e) {
636                if (SwingUtilities.isLeftMouseButton(e)) {
637                    if (isCollapsed) {
638                        expand();
639                        dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this);
640                    } else {
641                        collapse();
642                        dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
643                    }
644                }
645            }
646        }
647    }
648
649    /**
650     * The dialog class used to display toggle dialogs in a detached window.
651     *
652     */
653    private class DetachedDialog extends JDialog {
654        public DetachedDialog() {
655            super(JOptionPane.getFrameForComponent(Main.parent));
656            getContentPane().add(ToggleDialog.this);
657            addWindowListener(new WindowAdapter(){
658                @Override public void windowClosing(WindowEvent e) {
659                    rememberGeometry();
660                    getContentPane().removeAll();
661                    dispose();
662                    if (dockWhenClosingDetachedDlg()) {
663                        dock();
664                        if (isDialogInCollapsedView()) {
665                            expand();
666                        }
667                        dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
668                    } else {
669                        hideDialog();
670                        hideNotify();
671                    }
672                }
673            });
674            addComponentListener(new ComponentAdapter() {
675                @Override public void componentMoved(ComponentEvent e) {
676                    rememberGeometry();
677                }
678                @Override public void componentResized(ComponentEvent e) {
679                    rememberGeometry();
680                }
681            });
682
683            try {
684                new WindowGeometry(preferencePrefix+".geometry").applySafe(this);
685            } catch (WindowGeometryException e) {
686                ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize());
687                pack();
688                setLocationRelativeTo(Main.parent);
689            }
690            setTitle(titleBar.getTitle());
691            HelpUtil.setHelpContext(getRootPane(), helpTopic());
692        }
693
694        protected void rememberGeometry() {
695            if (detachedDialog != null) {
696                new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry");
697            }
698        }
699    }
700
701    /**
702     * Replies the action to toggle the visible state of this toggle dialog
703     *
704     * @return the action to toggle the visible state of this toggle dialog
705     */
706    public AbstractAction getToggleAction() {
707        return toggleAction;
708    }
709
710    /**
711     * Replies the prefix for the preference settings of this dialog.
712     *
713     * @return the prefix for the preference settings of this dialog.
714     */
715    public String getPreferencePrefix() {
716        return preferencePrefix;
717    }
718
719    /**
720     * Sets the dialogsPanel managing all toggle dialogs.
721     * @param dialogsPanel The panel managing all toggle dialogs
722     */
723    public void setDialogsPanel(DialogsPanel dialogsPanel) {
724        this.dialogsPanel = dialogsPanel;
725    }
726
727    /**
728     * Replies the name of this toggle dialog
729     */
730    @Override
731    public String getName() {
732        return "toggleDialog." + preferencePrefix;
733    }
734
735    /**
736     * Sets the title.
737     * @param title The dialog's title
738     */
739    public void setTitle(String title) {
740        titleBar.setTitle(title);
741        if (detachedDialog != null) {
742            detachedDialog.setTitle(title);
743        }
744    }
745
746    protected void setIsShowing(boolean val) {
747        isShowing = val;
748        Main.pref.put(preferencePrefix+".visible", val);
749        stateChanged();
750    }
751
752    protected void setIsDocked(boolean val) {
753        if (buttonsPanel != null) {
754            buttonsPanel.setVisible(val ? buttonHiding != ButtonHidingType.ALWAYS_HIDDEN : true);
755        }
756        isDocked = val;
757        Main.pref.put(preferencePrefix+".docked", val);
758        stateChanged();
759    }
760
761    protected void setIsCollapsed(boolean val) {
762        isCollapsed = val;
763        Main.pref.put(preferencePrefix+".minimized", val);
764        stateChanged();
765    }
766
767    protected void setIsButtonHiding(ButtonHidingType val) {
768        buttonHiding = val;
769        PROP_BUTTON_HIDING.put(val);
770        refreshHidingButtons();
771    }
772
773    /**
774     * Returns the preferred height of this dialog.
775     * @return The preferred height if the toggle dialog is expanded
776     */
777    public int getPreferredHeight() {
778        return preferredHeight;
779    }
780
781    @Override
782    public String helpTopic() {
783        String help = getClass().getName();
784        help = help.substring(help.lastIndexOf('.')+1, help.length()-6);
785        return "Dialog/"+help;
786    }
787
788    @Override
789    public String toString() {
790        return name;
791    }
792
793    /**
794     * Determines if this dialog is showing either as docked or as detached dialog.
795     * @return {@code true} if this dialog is showing either as docked or as detached dialog
796     */
797    public boolean isDialogShowing() {
798        return isShowing;
799    }
800
801    /**
802     * Determines if this dialog is docked and expanded.
803     * @return {@code true} if this dialog is docked and expanded
804     */
805    public boolean isDialogInDefaultView() {
806        return isShowing && isDocked && (! isCollapsed);
807    }
808
809    /**
810     * Determines if this dialog is docked and collapsed.
811     * @return {@code true} if this dialog is docked and collapsed
812     */
813    public boolean isDialogInCollapsedView() {
814        return isShowing && isDocked && isCollapsed;
815    }
816
817    public void setButton(JToggleButton button) {
818        this.button = button;
819    }
820
821    public JToggleButton getButton() {
822        return button;
823    }
824
825    /***
826     * The following methods are intended to be overridden, in order to customize
827     * the toggle dialog behavior.
828     **/
829
830    /**
831     * Change the Geometry of the detached dialog to better fit the content.
832     */
833    protected Rectangle getDetachedGeometry(Rectangle last) {
834        return last;
835    }
836
837    /**
838     * Default size of the detached dialog.
839     * Override this method to customize the initial dialog size.
840     */
841    protected Dimension getDefaultDetachedSize() {
842        return new Dimension(dialogsPanel.getWidth(), preferredHeight);
843    }
844
845    /**
846     * Do something when the toggleButton is pressed.
847     */
848    protected void toggleButtonHook() {
849    }
850
851    protected boolean dockWhenClosingDetachedDlg() {
852        return true;
853    }
854
855    /**
856     * primitive stateChangedListener for subclasses
857     */
858    protected void stateChanged() {
859    }
860
861    protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) {
862        return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null);
863    }
864
865    @SafeVarargs
866    protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons, Collection<SideButton>... nextButtons) {
867        if (scroll) {
868            data = new JScrollPane(data);
869        }
870        LinkedList<Collection<SideButton>> buttons = new LinkedList<>();
871        buttons.addFirst(firstButtons);
872        if (nextButtons != null) {
873            buttons.addAll(Arrays.asList(nextButtons));
874        }
875        add(data, BorderLayout.CENTER);
876        if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) {
877            buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1));
878            for (Collection<SideButton> buttonRow : buttons) {
879                if (buttonRow == null) {
880                    continue;
881                }
882                final JPanel buttonRowPanel = new JPanel(Main.pref.getBoolean("dialog.align.left", false)
883                        ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size()));
884                buttonsPanel.add(buttonRowPanel);
885                for (SideButton button : buttonRow) {
886                    buttonRowPanel.add(button);
887                    javax.swing.Action action = button.getAction();
888                    if (action != null) {
889                        buttonActions.add(action);
890                    } else {
891                        Main.warn("Button " + button + " doesn't have action defined");
892                        Main.error(new Exception());
893                    }
894                }
895            }
896            add(buttonsPanel, BorderLayout.SOUTH);
897            dynamicButtonsPropertyChanged();
898        } else {
899            titleBar.buttonsHide.setVisible(false);
900        }
901
902        // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu
903        titleBar.registerMouseListener();
904
905        return data;
906    }
907
908    @Override
909    public void eventDispatched(AWTEvent event) {
910        if(isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC) {
911            if (buttonsPanel != null) {
912                Rectangle b = this.getBounds();
913                b.setLocation(getLocationOnScreen());
914                if (b.contains(((MouseEvent)event).getLocationOnScreen())) {
915                    if(!buttonsPanel.isVisible()) {
916                        buttonsPanel.setVisible(true);
917                    }
918                } else if (buttonsPanel.isVisible()) {
919                    buttonsPanel.setVisible(false);
920                }
921            }
922        }
923    }
924
925    @Override
926    public void preferenceChanged(PreferenceChangeEvent e) {
927        if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) {
928            dynamicButtonsPropertyChanged();
929        }
930    }
931
932    private void dynamicButtonsPropertyChanged() {
933        boolean propEnabled = PROP_DYNAMIC_BUTTONS.get();
934        if (propEnabled) {
935            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK);
936        } else {
937            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
938        }
939        titleBar.buttonsHide.setVisible(propEnabled);
940        refreshHidingButtons();
941    }
942
943    private void refreshHidingButtons() {
944        titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN
945            ?  /* ICON(misc/)*/ "buttonhide" :  /* ICON(misc/)*/ "buttonshow"));
946        titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN);
947        if (buttonsPanel != null) {
948            buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked);
949        }
950        stateChanged();
951    }
952}