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        try {
471            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
472        } catch (SecurityException e) {
473            Logging.log(Logging.LEVEL_ERROR, "Unable to remove AWT event listener", e);
474        }
475        Config.getPref().removePreferenceChangeListener(this);
476        destroyComponents(this, false);
477    }
478
479    private static void destroyComponents(Component component, boolean destroyItself) {
480        if (component instanceof Container) {
481            for (Component c: ((Container) component).getComponents()) {
482                destroyComponents(c, true);
483            }
484        }
485        if (destroyItself && component instanceof Destroyable) {
486            ((Destroyable) component).destroy();
487        }
488    }
489
490    /**
491     * Closes the detached dialog if this toggle dialog is currently displayed in a detached dialog.
492     */
493    public void closeDetachedDialog() {
494        if (detachedDialog != null) {
495            detachedDialog.setVisible(false);
496            detachedDialog.getContentPane().removeAll();
497            detachedDialog.dispose();
498        }
499    }
500
501    /**
502     * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this
503     * method, it's a good place to register listeners needed to keep dialog updated
504     */
505    public void showNotify() {
506        // Do nothing
507    }
508
509    /**
510     * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister listeners
511     */
512    public void hideNotify() {
513        // Do nothing
514    }
515
516    /**
517     * The title bar displayed in docked mode
518     */
519    protected class TitleBar extends JPanel {
520        /** the label which shows whether the toggle dialog is expanded or collapsed */
521        private final JLabel lblMinimized;
522        /** the label which displays the dialog's title **/
523        private final JLabel lblTitle;
524        private final JComponent lblTitleWeak;
525        /** the button which shows whether buttons are dynamic or not */
526        private final JButton buttonsHide;
527        /** the contextual menu **/
528        private DialogPopupMenu popupMenu;
529
530        @SuppressWarnings("unchecked")
531        public TitleBar(String toggleDialogName, String iconName) {
532            setLayout(new GridBagLayout());
533
534            lblMinimized = new JLabel(ImageProvider.get("misc", "normal"));
535            add(lblMinimized);
536
537            // scale down the dialog icon
538            ImageIcon icon = ImageProvider.get("dialogs", iconName, ImageProvider.ImageSizes.SMALLICON);
539            lblTitle = new JLabel("", icon, JLabel.TRAILING);
540            lblTitle.setIconTextGap(8);
541
542            JPanel conceal = new JPanel();
543            conceal.add(lblTitle);
544            conceal.setVisible(false);
545            add(conceal, GBC.std());
546
547            // Cannot add the label directly since it would displace other elements on resize
548            lblTitleWeak = new JComponent() {
549                @Override
550                public void paintComponent(Graphics g) {
551                    lblTitle.paint(g);
552                }
553            };
554            lblTitleWeak.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20));
555            lblTitleWeak.setMinimumSize(new Dimension(0, 20));
556            add(lblTitleWeak, GBC.std().fill(GBC.HORIZONTAL));
557
558            buttonsHide = new JButton(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN
559                ? /* ICON(misc/)*/ "buttonhide" :  /* ICON(misc/)*/ "buttonshow"));
560            buttonsHide.setToolTipText(tr("Toggle dynamic buttons"));
561            buttonsHide.setBorder(BorderFactory.createEmptyBorder());
562            buttonsHide.addActionListener(e -> {
563                JRadioButtonMenuItem item = (buttonHiding == ButtonHidingType.DYNAMIC) ? alwaysShown : dynamic;
564                item.setSelected(true);
565                item.getAction().actionPerformed(null);
566            });
567            add(buttonsHide);
568
569            // show the pref button if applicable
570            if (preferenceClass != null) {
571                JButton pref = new JButton(ImageProvider.get("preference", ImageProvider.ImageSizes.SMALLICON));
572                pref.setToolTipText(tr("Open preferences for this panel"));
573                pref.setBorder(BorderFactory.createEmptyBorder());
574                pref.addActionListener(e -> {
575                    final PreferenceDialog p = new PreferenceDialog(Main.parent);
576                    if (TabPreferenceSetting.class.isAssignableFrom(preferenceClass)) {
577                        p.selectPreferencesTabByClass((Class<? extends TabPreferenceSetting>) preferenceClass);
578                    } else if (SubPreferenceSetting.class.isAssignableFrom(preferenceClass)) {
579                        p.selectSubPreferencesTabByClass((Class<? extends SubPreferenceSetting>) preferenceClass);
580                    }
581                    p.setVisible(true);
582                });
583                add(pref);
584            }
585
586            // show the sticky button
587            JButton sticky = new JButton(ImageProvider.get("misc", "sticky"));
588            sticky.setToolTipText(tr("Undock the panel"));
589            sticky.setBorder(BorderFactory.createEmptyBorder());
590            sticky.addActionListener(e -> {
591                detach();
592                dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
593            });
594            add(sticky);
595
596            // show the close button
597            JButton close = new JButton(ImageProvider.get("misc", "close"));
598            close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar."));
599            close.setBorder(BorderFactory.createEmptyBorder());
600            close.addActionListener(e -> {
601                hideDialog();
602                dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
603                hideNotify();
604            });
605            add(close);
606            setToolTipText(tr("Click to minimize/maximize the panel content"));
607            setTitle(toggleDialogName);
608        }
609
610        public void setTitle(String title) {
611            lblTitle.setText(title);
612            lblTitleWeak.repaint();
613        }
614
615        public String getTitle() {
616            return lblTitle.getText();
617        }
618
619        /**
620         * This is the popup menu used for the title bar.
621         */
622        public class DialogPopupMenu extends JPopupMenu {
623
624            /**
625             * Constructs a new {@code DialogPopupMenu}.
626             */
627            DialogPopupMenu() {
628                alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN);
629                dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC);
630                alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN);
631                ButtonGroup buttonHidingGroup = new ButtonGroup();
632                JMenu buttonHidingMenu = new JMenu(tr("Side buttons"));
633                for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) {
634                    buttonHidingGroup.add(rb);
635                    buttonHidingMenu.add(rb);
636                }
637                add(buttonHidingMenu);
638                for (javax.swing.Action action: buttonActions) {
639                    add(action);
640                }
641            }
642        }
643
644        /**
645         * Registers the mouse listeners.
646         * <p>
647         * Should be called once after this title was added to the dialog.
648         */
649        public final void registerMouseListener() {
650            popupMenu = new DialogPopupMenu();
651            addMouseListener(new MouseEventHandler());
652        }
653
654        class MouseEventHandler extends PopupMenuLauncher {
655            /**
656             * Constructs a new {@code MouseEventHandler}.
657             */
658            MouseEventHandler() {
659                super(popupMenu);
660            }
661
662            @Override
663            public void mouseClicked(MouseEvent e) {
664                if (SwingUtilities.isLeftMouseButton(e)) {
665                    if (isCollapsed) {
666                        expand();
667                        dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this);
668                    } else {
669                        collapse();
670                        dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
671                    }
672                }
673            }
674        }
675    }
676
677    /**
678     * The dialog class used to display toggle dialogs in a detached window.
679     *
680     */
681    private class DetachedDialog extends JDialog {
682        DetachedDialog() {
683            super(GuiHelper.getFrameForComponent(Main.parent));
684            getContentPane().add(ToggleDialog.this);
685            addWindowListener(new WindowAdapter() {
686                @Override public void windowClosing(WindowEvent e) {
687                    rememberGeometry();
688                    getContentPane().removeAll();
689                    dispose();
690                    if (dockWhenClosingDetachedDlg()) {
691                        dock();
692                        if (isDialogInCollapsedView()) {
693                            expand();
694                        }
695                        dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
696                    } else {
697                        hideDialog();
698                        hideNotify();
699                    }
700                }
701            });
702            addComponentListener(new ComponentAdapter() {
703                @Override
704                public void componentMoved(ComponentEvent e) {
705                    rememberGeometry();
706                }
707
708                @Override
709                public void componentResized(ComponentEvent e) {
710                    rememberGeometry();
711                }
712            });
713
714            try {
715                new WindowGeometry(preferencePrefix+".geometry").applySafe(this);
716            } catch (WindowGeometryException e) {
717                Logging.debug(e);
718                ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize());
719                pack();
720                setLocationRelativeTo(Main.parent);
721            }
722            super.setTitle(titleBar.getTitle());
723            HelpUtil.setHelpContext(getRootPane(), helpTopic());
724        }
725
726        protected void rememberGeometry() {
727            if (detachedDialog != null && detachedDialog.isShowing()) {
728                new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry");
729            }
730        }
731    }
732
733    /**
734     * Replies the action to toggle the visible state of this toggle dialog
735     *
736     * @return the action to toggle the visible state of this toggle dialog
737     */
738    public AbstractAction getToggleAction() {
739        return toggleAction;
740    }
741
742    /**
743     * Replies the prefix for the preference settings of this dialog.
744     *
745     * @return the prefix for the preference settings of this dialog.
746     */
747    public String getPreferencePrefix() {
748        return preferencePrefix;
749    }
750
751    /**
752     * Sets the dialogsPanel managing all toggle dialogs.
753     * @param dialogsPanel The panel managing all toggle dialogs
754     */
755    public void setDialogsPanel(DialogsPanel dialogsPanel) {
756        this.dialogsPanel = dialogsPanel;
757    }
758
759    /**
760     * Replies the name of this toggle dialog
761     */
762    @Override
763    public String getName() {
764        return "toggleDialog." + preferencePrefix;
765    }
766
767    /**
768     * Sets the title.
769     * @param title The dialog's title
770     */
771    public void setTitle(String title) {
772        titleBar.setTitle(title);
773        if (detachedDialog != null) {
774            detachedDialog.setTitle(title);
775        }
776    }
777
778    protected void setIsShowing(boolean val) {
779        isShowing = val;
780        Config.getPref().putBoolean(preferencePrefix+".visible", val);
781        stateChanged();
782    }
783
784    protected void setIsDocked(boolean val) {
785        if (buttonsPanel != null) {
786            buttonsPanel.setVisible(!val || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN);
787        }
788        isDocked = val;
789        Config.getPref().putBoolean(preferencePrefix+".docked", val);
790        stateChanged();
791    }
792
793    protected void setIsCollapsed(boolean val) {
794        isCollapsed = val;
795        Config.getPref().putBoolean(preferencePrefix+".minimized", val);
796        stateChanged();
797    }
798
799    protected void setIsButtonHiding(ButtonHidingType val) {
800        buttonHiding = val;
801        propButtonHiding.put(val);
802        refreshHidingButtons();
803    }
804
805    /**
806     * Returns the preferred height of this dialog.
807     * @return The preferred height if the toggle dialog is expanded
808     */
809    public int getPreferredHeight() {
810        return preferredHeight;
811    }
812
813    @Override
814    public String helpTopic() {
815        String help = getClass().getName();
816        help = help.substring(help.lastIndexOf('.')+1, help.length()-6);
817        return "Dialog/"+help;
818    }
819
820    @Override
821    public String toString() {
822        return name;
823    }
824
825    /**
826     * Determines if this dialog is showing either as docked or as detached dialog.
827     * @return {@code true} if this dialog is showing either as docked or as detached dialog
828     */
829    public boolean isDialogShowing() {
830        return isShowing;
831    }
832
833    /**
834     * Determines if this dialog is docked and expanded.
835     * @return {@code true} if this dialog is docked and expanded
836     */
837    public boolean isDialogInDefaultView() {
838        return isShowing && isDocked && (!isCollapsed);
839    }
840
841    /**
842     * Determines if this dialog is docked and collapsed.
843     * @return {@code true} if this dialog is docked and collapsed
844     */
845    public boolean isDialogInCollapsedView() {
846        return isShowing && isDocked && isCollapsed;
847    }
848
849    /**
850     * Sets the button from the button list that is used to display this dialog.
851     * <p>
852     * Note: This is ignored by the {@link ToggleDialog} for now.
853     * @param button The button for this dialog.
854     */
855    public void setButton(JToggleButton button) {
856        this.button = button;
857    }
858
859    /**
860     * Gets the button from the button list that is used to display this dialog.
861     * @return button The button for this dialog.
862     */
863    public JToggleButton getButton() {
864        return button;
865    }
866
867    /*
868     * The following methods are intended to be overridden, in order to customize
869     * the toggle dialog behavior.
870     */
871
872    /**
873     * Returns the default size of the detached dialog.
874     * Override this method to customize the initial dialog size.
875     * @return the default size of the detached dialog
876     */
877    protected Dimension getDefaultDetachedSize() {
878        return new Dimension(dialogsPanel.getWidth(), preferredHeight);
879    }
880
881    /**
882     * Do something when the toggleButton is pressed.
883     */
884    protected void toggleButtonHook() {
885        // Do nothing
886    }
887
888    protected boolean dockWhenClosingDetachedDlg() {
889        return true;
890    }
891
892    /**
893     * primitive stateChangedListener for subclasses
894     */
895    protected void stateChanged() {
896        // Do nothing
897    }
898
899    /**
900     * Create a component with the given layout for this component.
901     * @param data The content to be displayed
902     * @param scroll <code>true</code> if it should be wrapped in a {@link JScrollPane}
903     * @param buttons The buttons to add.
904     * @return The component.
905     */
906    protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) {
907        return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null);
908    }
909
910    @SafeVarargs
911    protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons,
912            Collection<SideButton>... nextButtons) {
913        if (scroll) {
914            JScrollPane sp = new JScrollPane(data);
915            if (!(data instanceof Scrollable)) {
916                GuiHelper.setDefaultIncrement(sp);
917            }
918            data = sp;
919        }
920        LinkedList<Collection<SideButton>> buttons = new LinkedList<>();
921        buttons.addFirst(firstButtons);
922        if (nextButtons != null) {
923            buttons.addAll(Arrays.asList(nextButtons));
924        }
925        add(data, BorderLayout.CENTER);
926        if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) {
927            buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1));
928            for (Collection<SideButton> buttonRow : buttons) {
929                if (buttonRow == null) {
930                    continue;
931                }
932                final JPanel buttonRowPanel = new JPanel(Config.getPref().getBoolean("dialog.align.left", false)
933                        ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size()));
934                buttonsPanel.add(buttonRowPanel);
935                for (SideButton button : buttonRow) {
936                    buttonRowPanel.add(button);
937                    javax.swing.Action action = button.getAction();
938                    if (action != null) {
939                        buttonActions.add(action);
940                    } else {
941                        Logging.warn("Button " + button + " doesn't have action defined");
942                        Logging.error(new Exception());
943                    }
944                }
945            }
946            add(buttonsPanel, BorderLayout.SOUTH);
947            dynamicButtonsPropertyChanged();
948        } else {
949            titleBar.buttonsHide.setVisible(false);
950        }
951
952        // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu
953        titleBar.registerMouseListener();
954
955        return data;
956    }
957
958    @Override
959    public void eventDispatched(AWTEvent event) {
960        if (event instanceof MouseEvent && isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC
961                && buttonsPanel != null) {
962            Rectangle b = this.getBounds();
963            b.setLocation(getLocationOnScreen());
964            if (b.contains(((MouseEvent) event).getLocationOnScreen())) {
965                if (!buttonsPanel.isVisible()) {
966                    buttonsPanel.setVisible(true);
967                }
968            } else if (buttonsPanel.isVisible()) {
969                buttonsPanel.setVisible(false);
970            }
971        }
972    }
973
974    @Override
975    public void preferenceChanged(PreferenceChangeEvent e) {
976        if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) {
977            dynamicButtonsPropertyChanged();
978        }
979    }
980
981    private void dynamicButtonsPropertyChanged() {
982        boolean propEnabled = PROP_DYNAMIC_BUTTONS.get();
983        try {
984            if (propEnabled) {
985                Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK);
986            } else {
987                Toolkit.getDefaultToolkit().removeAWTEventListener(this);
988            }
989        } catch (SecurityException e) {
990            Logging.log(Logging.LEVEL_ERROR, "Unable to add/remove AWT event listener", e);
991        }
992        titleBar.buttonsHide.setVisible(propEnabled);
993        refreshHidingButtons();
994    }
995
996    private void refreshHidingButtons() {
997        titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN
998            ?  /* ICON(misc/)*/ "buttonhide" :  /* ICON(misc/)*/ "buttonshow"));
999        titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN);
1000        if (buttonsPanel != null) {
1001            buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked);
1002        }
1003        stateChanged();
1004    }
1005}