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