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