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

Last change on this file since 4947 was 4932, checked in by stoecker, 12 years ago

fix 6833 - use WindowGeometry for toggle dialogs and mainwindow replacing old custom methods, improve dual screen handling

  • Property svn:eol-style set to native
File size: 23.1 KB
Line 
1// License: GPL. See LICENSE file for details.
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.GridBagLayout;
13import java.awt.GridLayout;
14import java.awt.Image;
15import java.awt.Rectangle;
16import java.awt.Toolkit;
17import java.awt.event.AWTEventListener;
18import java.awt.event.ActionEvent;
19import java.awt.event.ActionListener;
20import java.awt.event.ComponentAdapter;
21import java.awt.event.ComponentEvent;
22import java.awt.event.MouseAdapter;
23import java.awt.event.MouseEvent;
24import java.awt.event.WindowAdapter;
25import java.awt.event.WindowEvent;
26
27import java.util.Collection;
28
29import javax.swing.AbstractAction;
30import javax.swing.BorderFactory;
31import javax.swing.ImageIcon;
32import javax.swing.JButton;
33import javax.swing.JCheckBoxMenuItem;
34import javax.swing.JComponent;
35import javax.swing.JDialog;
36import javax.swing.JLabel;
37import javax.swing.JOptionPane;
38import javax.swing.JScrollPane;
39import javax.swing.JPanel;
40import javax.swing.JToggleButton;
41
42import org.openstreetmap.josm.Main;
43import org.openstreetmap.josm.actions.JosmAction;
44import org.openstreetmap.josm.gui.MainMenu;
45import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
46import org.openstreetmap.josm.gui.help.HelpUtil;
47import org.openstreetmap.josm.gui.help.Helpful;
48import org.openstreetmap.josm.gui.ShowHideButtonListener;
49import org.openstreetmap.josm.gui.SideButton;
50import org.openstreetmap.josm.tools.GBC;
51import org.openstreetmap.josm.tools.ImageProvider;
52import org.openstreetmap.josm.tools.Shortcut;
53import org.openstreetmap.josm.tools.WindowGeometry;
54import org.openstreetmap.josm.tools.WindowGeometry.WindowGeometryException;
55
56/**
57 * This class is a toggle dialog that can be turned on and off.
58 *
59 */
60public class ToggleDialog extends JPanel implements ShowHideButtonListener, Helpful, AWTEventListener {
61
62 /** The action to toggle this dialog */
63 protected ToggleDialogAction toggleAction;
64 protected String preferencePrefix;
65 final protected String name;
66
67 /** DialogsPanel that manages all ToggleDialogs */
68 protected DialogsPanel dialogsPanel;
69
70 protected TitleBar titleBar;
71
72 /**
73 * Indicates whether the dialog is showing or not.
74 */
75 protected boolean isShowing;
76 /**
77 * If isShowing is true, indicates whether the dialog is docked or not, e. g.
78 * shown as part of the main window or as a separate dialog window.
79 */
80 protected boolean isDocked;
81 /**
82 * If isShowing and isDocked are true, indicates whether the dialog is
83 * currently minimized or not.
84 */
85 protected boolean isCollapsed;
86 /**
87 * Indicates whether dynamic button hiding is active or not.
88 */
89 protected boolean isButtonHiding;
90
91 /** the preferred height if the toggle dialog is expanded */
92 private int preferredHeight;
93
94 /** the label in the title bar which shows whether the toggle dialog is expanded or collapsed */
95 private JLabel lblMinimized;
96
97 /** the label in the title bar which shows whether buttons are dynamic or not */
98 private JButton buttonsHide = null;
99
100 /** the JDialog displaying the toggle dialog as undocked dialog */
101 protected JDialog detachedDialog;
102
103 protected JToggleButton button;
104 private JPanel buttonsPanel;
105
106 /** holds the menu entry in the windows menu. Required to properly
107 * toggle the checkbox on show/hide
108 */
109 protected JCheckBoxMenuItem windowMenuItem;
110
111 /**
112 * Constructor
113 * (see below)
114 */
115 public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight) {
116 this(name, iconName, tooltip, shortcut, preferredHeight, false);
117 }
118 /**
119 * Constructor
120 *
121 * @param name the name of the dialog
122 * @param iconName the name of the icon to be displayed
123 * @param tooltip the tool tip
124 * @param shortcut the shortcut
125 * @param preferredHeight the preferred height for the dialog
126 * @param defShow if the dialog should be shown by default, if there is no preference
127 */
128 public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow) {
129 super(new BorderLayout());
130 this.preferencePrefix = iconName;
131 this.name = name;
132
133 /** Use the full width of the parent element */
134 setPreferredSize(new Dimension(0, preferredHeight));
135 /** Override any minimum sizes of child elements so the user can resize freely */
136 setMinimumSize(new Dimension(0,0));
137 this.preferredHeight = preferredHeight;
138 toggleAction = new ToggleDialogAction(name, "dialogs/"+iconName, tooltip, shortcut, iconName);
139 String helpId = "Dialog/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
140 toggleAction.putValue("help", helpId.substring(0, helpId.length()-6));
141
142 isShowing = Main.pref.getBoolean(preferencePrefix+".visible", defShow);
143 isDocked = Main.pref.getBoolean(preferencePrefix+".docked", true);
144 isCollapsed = Main.pref.getBoolean(preferencePrefix+".minimized", false);
145 isButtonHiding = Main.pref.getBoolean(preferencePrefix+".buttonhiding", true);
146
147 /** show the minimize button */
148 titleBar = new TitleBar(name, iconName);
149 add(titleBar, BorderLayout.NORTH);
150
151 setBorder(BorderFactory.createEtchedBorder());
152
153 Main.redirectToMainContentPane(this);
154
155 windowMenuItem = MainMenu.addWithCheckbox(Main.main.menu.windowMenu,
156 (JosmAction) getToggleAction(),
157 MainMenu.WINDOW_MENU_GROUP.TOGGLE_DIALOG);
158 }
159
160 /**
161 * The action to toggle the visibility state of this toggle dialog.
162 *
163 * Emits {@see PropertyChangeEvent}s for the property <tt>selected</tt>:
164 * <ul>
165 * <li>true, if the dialog is currently visible</li>
166 * <li>false, if the dialog is currently invisible</li>
167 * </ul>
168 *
169 */
170 public final class ToggleDialogAction extends JosmAction {
171
172 private ToggleDialogAction(String name, String iconName, String tooltip, Shortcut shortcut, String prefname) {
173 super(name, iconName, tooltip, shortcut, false);
174 }
175
176 public void actionPerformed(ActionEvent e) {
177 toggleButtonHook();
178 if(getValue("toolbarbutton") != null && getValue("toolbarbutton") instanceof JButton) {
179 ((JButton) getValue("toolbarbutton")).setSelected(!isShowing);
180 }
181 if (isShowing) {
182 hideDialog();
183 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
184 hideNotify();
185 } else {
186 showDialog();
187 if (isDocked && isCollapsed) {
188 expand();
189 }
190 if (isDocked) {
191 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
192 }
193 showNotify();
194 }
195 }
196
197 @Override
198 public void destroy() {
199 super.destroy();
200 }
201 }
202
203 /**
204 * Shows the dialog
205 */
206 public void showDialog() {
207 setIsShowing(true);
208 if (!isDocked) {
209 detach();
210 } else {
211 dock();
212 this.setVisible(true);
213 }
214 // toggling the selected value in order to enforce PropertyChangeEvents
215 setIsShowing(true);
216 windowMenuItem.setState(true);
217 toggleAction.putValue("selected", false);
218 toggleAction.putValue("selected", true);
219 }
220
221 /**
222 * Changes the state of the dialog such that the user can see the content.
223 * (takes care of the panel reconstruction)
224 */
225 public void unfurlDialog() {
226 if (isDialogInDefaultView())
227 return;
228 if (isDialogInCollapsedView()) {
229 expand();
230 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this);
231 } else if (!isDialogShowing()) {
232 showDialog();
233 if (isDocked && isCollapsed) {
234 expand();
235 }
236 if (isDocked) {
237 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, this);
238 }
239 showNotify();
240 }
241 }
242
243 @Override
244 public void buttonHidden() {
245 if ((Boolean) toggleAction.getValue("selected")) {
246 toggleAction.actionPerformed(null);
247 }
248 }
249
250 public void buttonShown() {
251 unfurlDialog();
252 }
253
254
255 /**
256 * Hides the dialog
257 */
258 public void hideDialog() {
259 closeDetachedDialog();
260 this.setVisible(false);
261 windowMenuItem.setState(false);
262 setIsShowing(false);
263 toggleAction.putValue("selected", false);
264 }
265
266 /**
267 * Displays the toggle dialog in the toggle dialog view on the right
268 * of the main map window.
269 *
270 */
271 protected void dock() {
272 detachedDialog = null;
273 titleBar.setVisible(true);
274 setIsDocked(true);
275 }
276
277 /**
278 * Display the dialog in a detached window.
279 *
280 */
281 protected void detach() {
282 setContentVisible(true);
283 this.setVisible(true);
284 titleBar.setVisible(false);
285 detachedDialog = new DetachedDialog();
286 detachedDialog.setVisible(true);
287 setIsShowing(true);
288 setIsDocked(false);
289 }
290
291 /**
292 * Collapses the toggle dialog to the title bar only
293 *
294 */
295 public void collapse() {
296 if (isDialogInDefaultView()) {
297 setContentVisible(false);
298 setIsCollapsed(true);
299 setPreferredSize(new Dimension(0,20));
300 setMaximumSize(new Dimension(Integer.MAX_VALUE,20));
301 setMinimumSize(new Dimension(Integer.MAX_VALUE,20));
302 lblMinimized.setIcon(ImageProvider.get("misc", "minimized"));
303 }
304 else throw new IllegalStateException();
305 }
306
307 /**
308 * Expands the toggle dialog
309 */
310 protected void expand() {
311 if (isDialogInCollapsedView()) {
312 setContentVisible(true);
313 setIsCollapsed(false);
314 setPreferredSize(new Dimension(0,preferredHeight));
315 setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
316 lblMinimized.setIcon(ImageProvider.get("misc", "normal"));
317 }
318 else throw new IllegalStateException();
319 }
320
321 /**
322 * Sets the visibility of all components in this toggle dialog, except the title bar
323 *
324 * @param visible true, if the components should be visible; false otherwise
325 */
326 protected void setContentVisible(boolean visible) {
327 Component comps[] = getComponents();
328 for(int i=0; i<comps.length; i++) {
329 if(comps[i] != titleBar) {
330 comps[i].setVisible(visible);
331 }
332 }
333 }
334
335 public void destroy() {
336 closeDetachedDialog();
337 hideNotify();
338 Main.main.menu.windowMenu.remove(windowMenuItem);
339 }
340
341 /**
342 * Closes the detached dialog if this toggle dialog is currently displayed
343 * in a detached dialog.
344 *
345 */
346 public void closeDetachedDialog() {
347 if (detachedDialog != null) {
348 detachedDialog.setVisible(false);
349 detachedDialog.getContentPane().removeAll();
350 detachedDialog.dispose();
351 }
352 }
353
354 /**
355 * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this
356 * method, it's a good place to register listeners needed to keep dialog updated
357 */
358 public void showNotify() {
359
360 }
361
362 /**
363 * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister
364 * listeners
365 */
366 public void hideNotify() {
367
368 }
369
370 /**
371 * The title bar displayed in docked mode
372 *
373 */
374 protected class TitleBar extends JPanel {
375 final private JLabel lblTitle;
376 final private JComponent lblTitle_weak;
377
378 public TitleBar(String toggleDialogName, String iconName) {
379 setLayout(new GridBagLayout());
380
381 lblMinimized = new JLabel(ImageProvider.get("misc", "normal"));
382 add(lblMinimized);
383
384 // scale down the dialog icon
385 ImageIcon inIcon = ImageProvider.get("dialogs", iconName);
386 ImageIcon smallIcon = new ImageIcon(inIcon.getImage().getScaledInstance(16 , 16, Image.SCALE_SMOOTH));
387 lblTitle = new JLabel("",smallIcon, JLabel.TRAILING);
388 lblTitle.setIconTextGap(8);
389
390 JPanel conceal = new JPanel();
391 conceal.add(lblTitle);
392 conceal.setVisible(false);
393 add(conceal, GBC.std());
394
395 // Cannot add the label directly since it would displace other elements on resize
396 lblTitle_weak = new JComponent() {
397 @Override
398 public void paintComponent(Graphics g) {
399 lblTitle.paint(g);
400 }
401 };
402 lblTitle_weak.setPreferredSize(new Dimension(Integer.MAX_VALUE,20));
403 lblTitle_weak.setMinimumSize(new Dimension(0,20));
404 add(lblTitle_weak, GBC.std().fill(GBC.HORIZONTAL));
405
406 addMouseListener(
407 new MouseAdapter() {
408 @Override
409 public void mouseClicked(MouseEvent e) {
410 if (isCollapsed) {
411 expand();
412 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this);
413 } else {
414 collapse();
415 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
416 }
417 }
418 }
419 );
420
421 if(Main.pref.getBoolean("dialog.dynamic.buttons", true)) {
422 buttonsHide = new JButton(ImageProvider.get("misc", isButtonHiding ? "buttonhide" : "buttonshow"));
423 buttonsHide.setToolTipText(tr("Toggle dynamic buttons"));
424 buttonsHide.setBorder(BorderFactory.createEmptyBorder());
425 buttonsHide.addActionListener(
426 new ActionListener(){
427 public void actionPerformed(ActionEvent e) {
428 setIsButtonHiding(!isButtonHiding);
429 }
430 }
431 );
432 add(buttonsHide);
433 }
434
435 // show the sticky button
436 JButton sticky = new JButton(ImageProvider.get("misc", "sticky"));
437 sticky.setToolTipText(tr("Undock the panel"));
438 sticky.setBorder(BorderFactory.createEmptyBorder());
439 sticky.addActionListener(
440 new ActionListener(){
441 public void actionPerformed(ActionEvent e) {
442 detach();
443 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
444 }
445 }
446 );
447 add(sticky);
448
449 // show the close button
450 JButton close = new JButton(ImageProvider.get("misc", "close"));
451 close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar."));
452 close.setBorder(BorderFactory.createEmptyBorder());
453 close.addActionListener(
454 new ActionListener(){
455 public void actionPerformed(ActionEvent e) {
456 hideDialog();
457 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null);
458 hideNotify();
459 }
460 }
461 );
462 add(close);
463 setToolTipText(tr("Click to minimize/maximize the panel content"));
464 setTitle(toggleDialogName);
465 }
466
467 public void setTitle(String title) {
468 lblTitle.setText(title);
469 lblTitle_weak.repaint();
470 }
471
472 public String getTitle() {
473 return lblTitle.getText();
474 }
475 }
476
477 /**
478 * The dialog class used to display toggle dialogs in a detached window.
479 *
480 */
481 private class DetachedDialog extends JDialog{
482 public DetachedDialog() {
483 super(JOptionPane.getFrameForComponent(Main.parent));
484 getContentPane().add(ToggleDialog.this);
485 addWindowListener(new WindowAdapter(){
486 @Override public void windowClosing(WindowEvent e) {
487 rememberGeometry();
488 getContentPane().removeAll();
489 dispose();
490 if (dockWhenClosingDetachedDlg()) {
491 dock();
492 if (isDialogInCollapsedView()) {
493 expand();
494 }
495 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this);
496 } else {
497 hideDialog();
498 hideNotify();
499 }
500 }
501 });
502 addComponentListener(new ComponentAdapter() {
503 @Override public void componentMoved(ComponentEvent e) {
504 rememberGeometry();
505 }
506 @Override public void componentResized(ComponentEvent e) {
507 rememberGeometry();
508 }
509 });
510
511 try {
512 new WindowGeometry(preferencePrefix+".geometry").applySafe(this);
513 } catch (WindowGeometryException e) {
514 ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize());
515 pack();
516 setLocationRelativeTo(Main.parent);
517 }
518 setTitle(titleBar.getTitle());
519 HelpUtil.setHelpContext(getRootPane(), helpTopic());
520 }
521
522 protected void rememberGeometry() {
523 if (detachedDialog != null) {
524 new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry");
525 }
526 }
527 }
528
529 /**
530 * Replies the action to toggle the visible state of this toggle dialog
531 *
532 * @return the action to toggle the visible state of this toggle dialog
533 */
534 public AbstractAction getToggleAction() {
535 return toggleAction;
536 }
537
538 /**
539 * Replies the prefix for the preference settings of this dialog.
540 *
541 * @return the prefix for the preference settings of this dialog.
542 */
543 public String getPreferencePrefix() {
544 return preferencePrefix;
545 }
546
547 /**
548 * Sets the dialogsPanel managing all toggle dialogs
549 */
550 public void setDialogsPanel(DialogsPanel dialogsPanel) {
551 this.dialogsPanel = dialogsPanel;
552 }
553
554 /**
555 * Replies the name of this toggle dialog
556 */
557 @Override
558 public String getName() {
559 return "toggleDialog." + preferencePrefix;
560 }
561
562 /**
563 * Sets the title
564 */
565 public void setTitle(String title) {
566 titleBar.setTitle(title);
567 if (detachedDialog != null) {
568 detachedDialog.setTitle(title);
569 }
570 }
571
572 protected void setIsShowing(boolean val) {
573 isShowing = val;
574 Main.pref.put(preferencePrefix+".visible", val);
575 stateChanged();
576 }
577
578 protected void setIsDocked(boolean val) {
579 if(buttonsPanel != null && buttonsHide != null) {
580 buttonsPanel.setVisible(val ? !isButtonHiding : true);
581 }
582 isDocked = val;
583 Main.pref.put(preferencePrefix+".docked", val);
584 stateChanged();
585 }
586
587 protected void setIsCollapsed(boolean val) {
588 isCollapsed = val;
589 Main.pref.put(preferencePrefix+".minimized", val);
590 stateChanged();
591 }
592
593 protected void setIsButtonHiding(boolean val) {
594 isButtonHiding = val;
595 Main.pref.put(preferencePrefix+".buttonhiding", val);
596 buttonsHide.setIcon(ImageProvider.get("misc", val ? "buttonhide" : "buttonshow"));
597 stateChanged();
598 }
599
600 public int getPreferredHeight() {
601 return preferredHeight;
602 }
603
604 public String helpTopic() {
605 String help = getClass().getName();
606 help = help.substring(help.lastIndexOf('.')+1, help.length()-6);
607 return "Dialog/"+help;
608 }
609
610 @Override
611 public String toString() {
612 return name;
613 }
614
615 /**
616 * Replies true if this dialog is showing either as docked or as detached dialog
617 */
618 public boolean isDialogShowing() {
619 return isShowing;
620 }
621
622 /**
623 * Replies true if this dialog is docked and expanded
624 */
625 public boolean isDialogInDefaultView() {
626 return isShowing && isDocked && (! isCollapsed);
627 }
628
629 /**
630 * Replies true if this dialog is docked and collapsed
631 */
632 public boolean isDialogInCollapsedView() {
633 return isShowing && isDocked && isCollapsed;
634 }
635
636 public void setButton(JToggleButton button) {
637 this.button = button;
638 }
639
640 public JToggleButton getButton() {
641 return button;
642 }
643
644 /***
645 * The following methods are intended to be overridden, in order to customize
646 * the toggle dialog behavior.
647 **/
648
649 /**
650 * Change the Geometry of the detached dialog to better fit the content.
651 */
652 protected Rectangle getDetachedGeometry(Rectangle last) {
653 return last;
654 }
655
656 /**
657 * Default size of the detached dialog.
658 * Override this method to customize the initial dialog size.
659 */
660 protected Dimension getDefaultDetachedSize() {
661 return new Dimension(dialogsPanel.getWidth(), preferredHeight);
662 }
663
664 /**
665 * Do something when the toggleButton is pressed.
666 */
667 protected void toggleButtonHook() {
668 }
669
670 protected boolean dockWhenClosingDetachedDlg() {
671 return true;
672 }
673
674 /**
675 * primitive stateChangedListener for subclasses
676 */
677 protected void stateChanged() {
678 }
679
680 protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) {
681 if(scroll)
682 data = new JScrollPane(data);
683 add(data, BorderLayout.CENTER);
684 if(buttons != null && buttons.size() != 0) {
685 buttonsPanel = new JPanel(Main.pref.getBoolean("dialog.align.left", false)
686 ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1,buttons.size()));
687 for(SideButton button : buttons)
688 buttonsPanel.add(button);
689 add(buttonsPanel, BorderLayout.SOUTH);
690 if(Main.pref.getBoolean("dialog.dynamic.buttons", true)) {
691 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK);
692 buttonsPanel.setVisible(!isButtonHiding || !isDocked);
693 }
694 } else if(buttonsHide != null) {
695 buttonsHide.setVisible(false);
696 }
697 return data;
698 }
699
700 @Override
701 public void eventDispatched(AWTEvent event) {
702 if(isShowing() && !isCollapsed && isDocked && isButtonHiding) {
703 Rectangle b = this.getBounds();
704 b.setLocation(getLocationOnScreen());
705 if (b.contains(((MouseEvent)event).getLocationOnScreen())) {
706 if(!buttonsPanel.isVisible()) {
707 buttonsPanel.setVisible(true);
708 }
709 } else if (buttonsPanel.isVisible()) {
710 buttonsPanel.setVisible(false);
711 }
712 }
713 }
714}
Note: See TracBrowser for help on using the repository browser.