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