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