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