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

Last change on this file since 4475 was 4366, checked in by stoecker, 13 years ago

see #6731 - handle collapsed menu correctly

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