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