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