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