source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/ToggleDialog.java

Last change on this file was 19106, checked in by taylor.smock, 13 months ago

Cleanup some new PMD warnings from PMD 7.x (followup of r19101)

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