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