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