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 super(new BorderLayout()); 233 this.preferencePrefix = iconName; 234 this.name = name; 235 this.preferenceClass = prefClass; 236 237 /** Use the full width of the parent element */ 238 setPreferredSize(new Dimension(0, preferredHeight)); 239 /** Override any minimum sizes of child elements so the user can resize freely */ 240 setMinimumSize(new Dimension(0, 0)); 241 this.preferredHeight = Config.getPref().getInt(preferencePrefix+".preferredHeight", preferredHeight); 242 toggleAction = new ToggleDialogAction(name, "dialogs/"+iconName, tooltip, shortcut, helpTopic()); 243 244 isShowing = Config.getPref().getBoolean(preferencePrefix+".visible", defShow); 245 isDocked = Config.getPref().getBoolean(preferencePrefix+".docked", true); 246 isCollapsed = Config.getPref().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 MainApplication.redirectToMainContentPane(this); 256 Config.getPref().addPreferenceChangeListener(this); 257 258 registerInWindowMenu(); 259 } 260 261 /** 262 * Registers this dialog in the window menu. Called in the constructor. 263 * @since 10467 264 */ 265 protected void registerInWindowMenu() { 266 windowMenuItem = MainMenu.addWithCheckbox(MainApplication.getMenu().windowMenu, 267 (JosmAction) getToggleAction(), 268 MainMenu.WINDOW_MENU_GROUP.TOGGLE_DIALOG); 269 } 270 271 /** 272 * The action to toggle the visibility state of this toggle dialog. 273 * 274 * Emits {@link PropertyChangeEvent}s for the property <code>selected</code>: 275 * <ul> 276 * <li>true, if the dialog is currently visible</li> 277 * <li>false, if the dialog is currently invisible</li> 278 * </ul> 279 * 280 */ 281 public final class ToggleDialogAction extends JosmAction { 282 283 private ToggleDialogAction(String name, String iconName, String tooltip, Shortcut shortcut, String helpId) { 284 super(name, iconName, tooltip, shortcut, false, false); 285 setHelpId(helpId); 286 } 287 288 @Override 289 public void actionPerformed(ActionEvent e) { 290 toggleButtonHook(); 291 if (getValue("toolbarbutton") instanceof JButton) { 292 ((JButton) getValue("toolbarbutton")).setSelected(!isShowing); 293 } 294 if (isShowing) { 295 hideDialog(); 296 if (dialogsPanel != null) { 297 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 298 } 299 hideNotify(); 300 } else { 301 showDialog(); 302 if (isDocked && isCollapsed) { 303 expand(); 304 } 305 if (isDocked && dialogsPanel != null) { 306 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); 307 } 308 showNotify(); 309 } 310 } 311 312 @Override 313 public String toString() { 314 return "ToggleDialogAction [" + ToggleDialog.this + ']'; 315 } 316 } 317 318 /** 319 * Shows the dialog 320 */ 321 public void showDialog() { 322 setIsShowing(true); 323 if (!isDocked) { 324 detach(); 325 } else { 326 dock(); 327 this.setVisible(true); 328 } 329 // toggling the selected value in order to enforce PropertyChangeEvents 330 setIsShowing(true); 331 if (windowMenuItem != null) { 332 windowMenuItem.setState(true); 333 } 334 toggleAction.putValue("selected", Boolean.FALSE); 335 toggleAction.putValue("selected", Boolean.TRUE); 336 } 337 338 /** 339 * Changes the state of the dialog such that the user can see the content. 340 * (takes care of the panel reconstruction) 341 */ 342 public void unfurlDialog() { 343 if (isDialogInDefaultView()) 344 return; 345 if (isDialogInCollapsedView()) { 346 expand(); 347 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this); 348 } else if (!isDialogShowing()) { 349 showDialog(); 350 if (isDocked && isCollapsed) { 351 expand(); 352 } 353 if (isDocked) { 354 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, this); 355 } 356 showNotify(); 357 } 358 } 359 360 @Override 361 public void buttonHidden() { 362 if ((Boolean) toggleAction.getValue("selected")) { 363 toggleAction.actionPerformed(null); 364 } 365 } 366 367 @Override 368 public void buttonShown() { 369 unfurlDialog(); 370 } 371 372 /** 373 * Hides the dialog 374 */ 375 public void hideDialog() { 376 closeDetachedDialog(); 377 this.setVisible(false); 378 if (windowMenuItem != null) { 379 windowMenuItem.setState(false); 380 } 381 setIsShowing(false); 382 toggleAction.putValue("selected", Boolean.FALSE); 383 } 384 385 /** 386 * Displays the toggle dialog in the toggle dialog view on the right 387 * of the main map window. 388 * 389 */ 390 protected void dock() { 391 detachedDialog = null; 392 titleBar.setVisible(true); 393 setIsDocked(true); 394 } 395 396 /** 397 * Display the dialog in a detached window. 398 * 399 */ 400 protected void detach() { 401 setContentVisible(true); 402 this.setVisible(true); 403 titleBar.setVisible(false); 404 if (!GraphicsEnvironment.isHeadless()) { 405 detachedDialog = new DetachedDialog(); 406 detachedDialog.setVisible(true); 407 } 408 setIsShowing(true); 409 setIsDocked(false); 410 } 411 412 /** 413 * Collapses the toggle dialog to the title bar only 414 * 415 */ 416 public void collapse() { 417 if (isDialogInDefaultView()) { 418 setContentVisible(false); 419 setIsCollapsed(true); 420 setPreferredSize(new Dimension(0, 20)); 421 setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); 422 setMinimumSize(new Dimension(Integer.MAX_VALUE, 20)); 423 titleBar.lblMinimized.setIcon(ImageProvider.get("misc", "minimized")); 424 } else 425 throw new IllegalStateException(); 426 } 427 428 /** 429 * Expands the toggle dialog 430 */ 431 protected void expand() { 432 if (isDialogInCollapsedView()) { 433 setContentVisible(true); 434 setIsCollapsed(false); 435 setPreferredSize(new Dimension(0, preferredHeight)); 436 setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); 437 titleBar.lblMinimized.setIcon(ImageProvider.get("misc", "normal")); 438 } else 439 throw new IllegalStateException(); 440 } 441 442 /** 443 * Sets the visibility of all components in this toggle dialog, except the title bar 444 * 445 * @param visible true, if the components should be visible; false otherwise 446 */ 447 protected void setContentVisible(boolean visible) { 448 Component[] comps = getComponents(); 449 for (Component comp : comps) { 450 if (comp != titleBar && (!visible || comp != buttonsPanel || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN)) { 451 comp.setVisible(visible); 452 } 453 } 454 } 455 456 @Override 457 public void destroy() { 458 dialogsPanel = null; 459 rememberHeight(); 460 closeDetachedDialog(); 461 if (isShowing) { 462 hideNotify(); 463 } 464 MainApplication.getMenu().windowMenu.remove(windowMenuItem); 465 try { 466 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 467 } catch (SecurityException e) { 468 Logging.log(Logging.LEVEL_ERROR, "Unable to remove AWT event listener", e); 469 } 470 Config.getPref().removePreferenceChangeListener(this); 471 GuiHelper.destroyComponents(this, false); 472 titleBar.destroy(); 473 titleBar = null; 474 this.buttonActions.clear(); 475 } 476 477 /** 478 * Closes the detached dialog if this toggle dialog is currently displayed in a detached dialog. 479 */ 480 public void closeDetachedDialog() { 481 if (detachedDialog != null) { 482 detachedDialog.setVisible(false); 483 detachedDialog.getContentPane().removeAll(); 484 detachedDialog.dispose(); 485 } 486 } 487 488 /** 489 * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this 490 * method, it's a good place to register listeners needed to keep dialog updated 491 */ 492 public void showNotify() { 493 // Do nothing 494 } 495 496 /** 497 * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister listeners 498 */ 499 public void hideNotify() { 500 // Do nothing 501 } 502 503 /** 504 * The title bar displayed in docked mode 505 */ 506 protected class TitleBar extends JPanel implements Destroyable { 507 /** the label which shows whether the toggle dialog is expanded or collapsed */ 508 private final JLabel lblMinimized; 509 /** the label which displays the dialog's title **/ 510 private final JLabel lblTitle; 511 private final JComponent lblTitleWeak; 512 /** the button which shows whether buttons are dynamic or not */ 513 private final JButton buttonsHide; 514 /** the contextual menu **/ 515 private DialogPopupMenu popupMenu; 516 517 private MouseEventHandler mouseEventHandler; 518 519 @SuppressWarnings("unchecked") 520 public TitleBar(String toggleDialogName, String iconName) { 521 setLayout(new GridBagLayout()); 522 523 lblMinimized = new JLabel(ImageProvider.get("misc", "normal")); 524 add(lblMinimized); 525 526 // scale down the dialog icon 527 ImageIcon icon = ImageProvider.get("dialogs", iconName, ImageProvider.ImageSizes.SMALLICON); 528 lblTitle = new JLabel("", icon, JLabel.TRAILING); 529 lblTitle.setIconTextGap(8); 530 531 JPanel conceal = new JPanel(); 532 conceal.add(lblTitle); 533 conceal.setVisible(false); 534 add(conceal, GBC.std()); 535 536 // Cannot add the label directly since it would displace other elements on resize 537 lblTitleWeak = new JComponent() { 538 @Override 539 public void paintComponent(Graphics g) { 540 lblTitle.paint(g); 541 } 542 }; 543 lblTitleWeak.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20)); 544 lblTitleWeak.setMinimumSize(new Dimension(0, 20)); 545 add(lblTitleWeak, GBC.std().fill(GBC.HORIZONTAL)); 546 547 buttonsHide = new JButton(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 548 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 549 buttonsHide.setToolTipText(tr("Toggle dynamic buttons")); 550 buttonsHide.setBorder(BorderFactory.createEmptyBorder()); 551 buttonsHide.addActionListener(e -> { 552 JRadioButtonMenuItem item = (buttonHiding == ButtonHidingType.DYNAMIC) ? alwaysShown : dynamic; 553 item.setSelected(true); 554 item.getAction().actionPerformed(null); 555 }); 556 add(buttonsHide); 557 558 // show the pref button if applicable 559 if (preferenceClass != null) { 560 JButton pref = new JButton(ImageProvider.get("preference", ImageProvider.ImageSizes.SMALLICON)); 561 pref.setToolTipText(tr("Open preferences for this panel")); 562 pref.setBorder(BorderFactory.createEmptyBorder()); 563 pref.addActionListener(e -> { 564 final PreferenceDialog p = new PreferenceDialog(MainApplication.getMainFrame()); 565 if (TabPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 566 p.selectPreferencesTabByClass((Class<? extends TabPreferenceSetting>) preferenceClass); 567 } else if (SubPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 568 p.selectSubPreferencesTabByClass((Class<? extends SubPreferenceSetting>) preferenceClass); 569 } 570 p.setVisible(true); 571 }); 572 add(pref); 573 } 574 575 // show the sticky button 576 JButton sticky = new JButton(ImageProvider.get("misc", "sticky")); 577 sticky.setToolTipText(tr("Undock the panel")); 578 sticky.setBorder(BorderFactory.createEmptyBorder()); 579 sticky.addActionListener(e -> { 580 detach(); 581 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 582 }); 583 add(sticky); 584 585 // show the close button 586 JButton close = new JButton(ImageProvider.get("misc", "close")); 587 close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar.")); 588 close.setBorder(BorderFactory.createEmptyBorder()); 589 close.addActionListener(e -> { 590 hideDialog(); 591 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 592 hideNotify(); 593 }); 594 add(close); 595 setToolTipText(tr("Click to minimize/maximize the panel content")); 596 setTitle(toggleDialogName); 597 } 598 599 public void setTitle(String title) { 600 lblTitle.setText(title); 601 lblTitleWeak.repaint(); 602 } 603 604 public String getTitle() { 605 return lblTitle.getText(); 606 } 607 608 /** 609 * This is the popup menu used for the title bar. 610 */ 611 public class DialogPopupMenu extends JPopupMenu { 612 613 /** 614 * Constructs a new {@code DialogPopupMenu}. 615 */ 616 DialogPopupMenu() { 617 alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN); 618 dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC); 619 alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN); 620 ButtonGroup buttonHidingGroup = new ButtonGroup(); 621 JMenu buttonHidingMenu = new JMenu(tr("Side buttons")); 622 for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) { 623 buttonHidingGroup.add(rb); 624 buttonHidingMenu.add(rb); 625 } 626 add(buttonHidingMenu); 627 for (javax.swing.Action action: buttonActions) { 628 add(action); 629 } 630 } 631 } 632 633 /** 634 * Registers the mouse listeners. 635 * <p> 636 * Should be called once after this title was added to the dialog. 637 */ 638 public final void registerMouseListener() { 639 popupMenu = new DialogPopupMenu(); 640 mouseEventHandler = new MouseEventHandler(); 641 addMouseListener(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 @Override 667 public void destroy() { 668 removeMouseListener(mouseEventHandler); 669 this.mouseEventHandler = null; 670 this.popupMenu = null; 671 } 672 } 673 674 /** 675 * The dialog class used to display toggle dialogs in a detached window. 676 * 677 */ 678 private class DetachedDialog extends JDialog { 679 DetachedDialog() { 680 super(GuiHelper.getFrameForComponent(MainApplication.getMainFrame())); 681 getContentPane().add(ToggleDialog.this); 682 addWindowListener(new WindowAdapter() { 683 @Override public void windowClosing(WindowEvent e) { 684 rememberGeometry(); 685 getContentPane().removeAll(); 686 dispose(); 687 if (dockWhenClosingDetachedDlg()) { 688 dock(); 689 if (isDialogInCollapsedView()) { 690 setContentVisible(false); 691 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 692 } else { 693 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); 694 } 695 } else { 696 hideDialog(); 697 hideNotify(); 698 } 699 } 700 }); 701 addComponentListener(new ComponentAdapter() { 702 @Override 703 public void componentMoved(ComponentEvent e) { 704 rememberGeometry(); 705 } 706 707 @Override 708 public void componentResized(ComponentEvent e) { 709 rememberGeometry(); 710 } 711 }); 712 713 try { 714 new WindowGeometry(preferencePrefix+".geometry").applySafe(this); 715 } catch (WindowGeometryException e) { 716 Logging.debug(e); 717 ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize()); 718 pack(); 719 setLocationRelativeTo(MainApplication.getMainFrame()); 720 } 721 super.setTitle(titleBar.getTitle()); 722 HelpUtil.setHelpContext(getRootPane(), helpTopic()); 723 } 724 725 protected void rememberGeometry() { 726 if (detachedDialog != null && detachedDialog.isShowing()) { 727 new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry"); 728 } 729 } 730 } 731 732 /** 733 * Replies the action to toggle the visible state of this toggle dialog 734 * 735 * @return the action to toggle the visible state of this toggle dialog 736 */ 737 public AbstractAction getToggleAction() { 738 return toggleAction; 739 } 740 741 /** 742 * Replies the prefix for the preference settings of this dialog. 743 * 744 * @return the prefix for the preference settings of this dialog. 745 */ 746 public String getPreferencePrefix() { 747 return preferencePrefix; 748 } 749 750 /** 751 * Sets the dialogsPanel managing all toggle dialogs. 752 * @param dialogsPanel The panel managing all toggle dialogs 753 */ 754 public void setDialogsPanel(DialogsPanel dialogsPanel) { 755 this.dialogsPanel = dialogsPanel; 756 } 757 758 /** 759 * Replies the name of this toggle dialog 760 */ 761 @Override 762 public String getName() { 763 return "toggleDialog." + preferencePrefix; 764 } 765 766 /** 767 * Sets the title. 768 * @param title The dialog's title 769 */ 770 public void setTitle(String title) { 771 if (titleBar != null) { 772 titleBar.setTitle(title); 773 } 774 if (detachedDialog != null) { 775 detachedDialog.setTitle(title); 776 } 777 } 778 779 protected void setIsShowing(boolean val) { 780 isShowing = val; 781 Config.getPref().putBoolean(preferencePrefix+".visible", val); 782 stateChanged(); 783 } 784 785 protected void setIsDocked(boolean val) { 786 if (buttonsPanel != null) { 787 buttonsPanel.setVisible(!val || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 788 } 789 isDocked = val; 790 Config.getPref().putBoolean(preferencePrefix+".docked", val); 791 stateChanged(); 792 } 793 794 protected void setIsCollapsed(boolean val) { 795 isCollapsed = val; 796 Config.getPref().putBoolean(preferencePrefix+".minimized", val); 797 stateChanged(); 798 } 799 800 protected void setIsButtonHiding(ButtonHidingType val) { 801 buttonHiding = val; 802 propButtonHiding.put(val); 803 refreshHidingButtons(); 804 } 805 806 /** 807 * Returns the preferred height of this dialog. 808 * @return The preferred height if the toggle dialog is expanded 809 */ 810 public int getPreferredHeight() { 811 return preferredHeight; 812 } 813 814 @Override 815 public String helpTopic() { 816 String help = getClass().getName(); 817 help = help.substring(help.lastIndexOf('.')+1, help.length()-6); 818 return "Dialog/"+help; 819 } 820 821 @Override 822 public String toString() { 823 return name; 824 } 825 826 /** 827 * Determines if this dialog is showing either as docked or as detached dialog. 828 * @return {@code true} if this dialog is showing either as docked or as detached dialog 829 */ 830 public boolean isDialogShowing() { 831 return isShowing; 832 } 833 834 /** 835 * Determines if this dialog is docked and expanded. 836 * @return {@code true} if this dialog is docked and expanded 837 */ 838 public boolean isDialogInDefaultView() { 839 return isShowing && isDocked && (!isCollapsed); 840 } 841 842 /** 843 * Determines if this dialog is docked and collapsed. 844 * @return {@code true} if this dialog is docked and collapsed 845 */ 846 public boolean isDialogInCollapsedView() { 847 return isShowing && isDocked && isCollapsed; 848 } 849 850 /** 851 * Sets the button from the button list that is used to display this dialog. 852 * <p> 853 * Note: This is ignored by the {@link ToggleDialog} for now. 854 * @param button The button for this dialog. 855 */ 856 public void setButton(JToggleButton button) { 857 this.button = button; 858 } 859 860 /** 861 * Gets the button from the button list that is used to display this dialog. 862 * @return button The button for this dialog. 863 */ 864 public JToggleButton getButton() { 865 return button; 866 } 867 868 /* 869 * The following methods are intended to be overridden, in order to customize 870 * the toggle dialog behavior. 871 */ 872 873 /** 874 * Returns the default size of the detached dialog. 875 * Override this method to customize the initial dialog size. 876 * @return the default size of the detached dialog 877 */ 878 protected Dimension getDefaultDetachedSize() { 879 return new Dimension(dialogsPanel.getWidth(), preferredHeight); 880 } 881 882 /** 883 * Do something when the toggleButton is pressed. 884 */ 885 protected void toggleButtonHook() { 886 // Do nothing 887 } 888 889 protected boolean dockWhenClosingDetachedDlg() { 890 return dialogsPanel != null && titleBar != null; 891 } 892 893 /** 894 * primitive stateChangedListener for subclasses 895 */ 896 protected void stateChanged() { 897 // Do nothing 898 } 899 900 /** 901 * Create a component with the given layout for this component. 902 * @param data The content to be displayed 903 * @param scroll <code>true</code> if it should be wrapped in a {@link JScrollPane} 904 * @param buttons The buttons to add. 905 * @return The component. 906 */ 907 protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) { 908 return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null); 909 } 910 911 @SafeVarargs 912 protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons, 913 Collection<SideButton>... nextButtons) { 914 if (scroll) { 915 JScrollPane sp = new JScrollPane(data); 916 if (!(data instanceof Scrollable)) { 917 GuiHelper.setDefaultIncrement(sp); 918 } 919 data = sp; 920 } 921 LinkedList<Collection<SideButton>> buttons = new LinkedList<>(); 922 buttons.addFirst(firstButtons); 923 if (nextButtons != null) { 924 buttons.addAll(Arrays.asList(nextButtons)); 925 } 926 add(data, BorderLayout.CENTER); 927 if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) { 928 buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1)); 929 for (Collection<SideButton> buttonRow : buttons) { 930 if (buttonRow == null) { 931 continue; 932 } 933 final JPanel buttonRowPanel = new JPanel(Config.getPref().getBoolean("dialog.align.left", false) 934 ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size())); 935 buttonsPanel.add(buttonRowPanel); 936 for (SideButton button : buttonRow) { 937 buttonRowPanel.add(button); 938 javax.swing.Action action = button.getAction(); 939 if (action != null) { 940 buttonActions.add(action); 941 } else { 942 Logging.warn("Button " + button + " doesn't have action defined"); 943 Logging.error(new Exception()); 944 } 945 } 946 } 947 add(buttonsPanel, BorderLayout.SOUTH); 948 dynamicButtonsPropertyChanged(); 949 } else { 950 titleBar.buttonsHide.setVisible(false); 951 } 952 953 // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu 954 titleBar.registerMouseListener(); 955 956 return data; 957 } 958 959 @Override 960 public void eventDispatched(AWTEvent event) { 961 if (event instanceof MouseEvent && isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC 962 && buttonsPanel != null) { 963 Rectangle b = this.getBounds(); 964 b.setLocation(getLocationOnScreen()); 965 if (b.contains(((MouseEvent) event).getLocationOnScreen())) { 966 if (!buttonsPanel.isVisible()) { 967 buttonsPanel.setVisible(true); 968 } 969 } else if (buttonsPanel.isVisible()) { 970 buttonsPanel.setVisible(false); 971 } 972 } 973 } 974 975 @Override 976 public void preferenceChanged(PreferenceChangeEvent e) { 977 if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) { 978 dynamicButtonsPropertyChanged(); 979 } 980 } 981 982 private void dynamicButtonsPropertyChanged() { 983 boolean propEnabled = PROP_DYNAMIC_BUTTONS.get(); 984 try { 985 if (propEnabled) { 986 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK); 987 } else { 988 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 989 } 990 } catch (SecurityException e) { 991 Logging.log(Logging.LEVEL_ERROR, "Unable to add/remove AWT event listener", e); 992 } 993 titleBar.buttonsHide.setVisible(propEnabled); 994 refreshHidingButtons(); 995 } 996 997 private void refreshHidingButtons() { 998 titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 999 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 1000 titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 1001 if (buttonsPanel != null) { 1002 buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked); 1003 } 1004 stateChanged(); 1005 } 1006 1007 /** 1008 * @return the last used height stored in preferences or preferredHeight 1009 * @since 14425 1010 */ 1011 public int getLastHeight() { 1012 return Config.getPref().getInt(preferencePrefix+".lastHeight", preferredHeight); 1013 } 1014 1015 /** 1016 * Store the current height in preferences so that we can restore it. 1017 * @since 14425 1018 */ 1019 public void rememberHeight() { 1020 int h = getHeight(); 1021 Config.getPref().put(preferencePrefix+".lastHeight", Integer.toString(h)); 1022 } 1023}