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

Last change on this file since 3146 was 3087, checked in by mjulius, 14 years ago

see #4674 - NPE in ToggleDialog

now throws IllegalArgumentException when ToggleDialogAction() gets no ToggleDialog to toggle.

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