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 try { 471 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 472 } catch (SecurityException e) { 473 Logging.log(Logging.LEVEL_ERROR, "Unable to remove AWT event listener", e); 474 } 475 Config.getPref().removePreferenceChangeListener(this); 476 destroyComponents(this, false); 477 } 478 479 private static void destroyComponents(Component component, boolean destroyItself) { 480 if (component instanceof Container) { 481 for (Component c: ((Container) component).getComponents()) { 482 destroyComponents(c, true); 483 } 484 } 485 if (destroyItself && component instanceof Destroyable) { 486 ((Destroyable) component).destroy(); 487 } 488 } 489 490 /** 491 * Closes the detached dialog if this toggle dialog is currently displayed in a detached dialog. 492 */ 493 public void closeDetachedDialog() { 494 if (detachedDialog != null) { 495 detachedDialog.setVisible(false); 496 detachedDialog.getContentPane().removeAll(); 497 detachedDialog.dispose(); 498 } 499 } 500 501 /** 502 * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this 503 * method, it's a good place to register listeners needed to keep dialog updated 504 */ 505 public void showNotify() { 506 // Do nothing 507 } 508 509 /** 510 * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister listeners 511 */ 512 public void hideNotify() { 513 // Do nothing 514 } 515 516 /** 517 * The title bar displayed in docked mode 518 */ 519 protected class TitleBar extends JPanel { 520 /** the label which shows whether the toggle dialog is expanded or collapsed */ 521 private final JLabel lblMinimized; 522 /** the label which displays the dialog's title **/ 523 private final JLabel lblTitle; 524 private final JComponent lblTitleWeak; 525 /** the button which shows whether buttons are dynamic or not */ 526 private final JButton buttonsHide; 527 /** the contextual menu **/ 528 private DialogPopupMenu popupMenu; 529 530 @SuppressWarnings("unchecked") 531 public TitleBar(String toggleDialogName, String iconName) { 532 setLayout(new GridBagLayout()); 533 534 lblMinimized = new JLabel(ImageProvider.get("misc", "normal")); 535 add(lblMinimized); 536 537 // scale down the dialog icon 538 ImageIcon icon = ImageProvider.get("dialogs", iconName, ImageProvider.ImageSizes.SMALLICON); 539 lblTitle = new JLabel("", icon, JLabel.TRAILING); 540 lblTitle.setIconTextGap(8); 541 542 JPanel conceal = new JPanel(); 543 conceal.add(lblTitle); 544 conceal.setVisible(false); 545 add(conceal, GBC.std()); 546 547 // Cannot add the label directly since it would displace other elements on resize 548 lblTitleWeak = new JComponent() { 549 @Override 550 public void paintComponent(Graphics g) { 551 lblTitle.paint(g); 552 } 553 }; 554 lblTitleWeak.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20)); 555 lblTitleWeak.setMinimumSize(new Dimension(0, 20)); 556 add(lblTitleWeak, GBC.std().fill(GBC.HORIZONTAL)); 557 558 buttonsHide = new JButton(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 559 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 560 buttonsHide.setToolTipText(tr("Toggle dynamic buttons")); 561 buttonsHide.setBorder(BorderFactory.createEmptyBorder()); 562 buttonsHide.addActionListener(e -> { 563 JRadioButtonMenuItem item = (buttonHiding == ButtonHidingType.DYNAMIC) ? alwaysShown : dynamic; 564 item.setSelected(true); 565 item.getAction().actionPerformed(null); 566 }); 567 add(buttonsHide); 568 569 // show the pref button if applicable 570 if (preferenceClass != null) { 571 JButton pref = new JButton(ImageProvider.get("preference", ImageProvider.ImageSizes.SMALLICON)); 572 pref.setToolTipText(tr("Open preferences for this panel")); 573 pref.setBorder(BorderFactory.createEmptyBorder()); 574 pref.addActionListener(e -> { 575 final PreferenceDialog p = new PreferenceDialog(Main.parent); 576 if (TabPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 577 p.selectPreferencesTabByClass((Class<? extends TabPreferenceSetting>) preferenceClass); 578 } else if (SubPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 579 p.selectSubPreferencesTabByClass((Class<? extends SubPreferenceSetting>) preferenceClass); 580 } 581 p.setVisible(true); 582 }); 583 add(pref); 584 } 585 586 // show the sticky button 587 JButton sticky = new JButton(ImageProvider.get("misc", "sticky")); 588 sticky.setToolTipText(tr("Undock the panel")); 589 sticky.setBorder(BorderFactory.createEmptyBorder()); 590 sticky.addActionListener(e -> { 591 detach(); 592 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 593 }); 594 add(sticky); 595 596 // show the close button 597 JButton close = new JButton(ImageProvider.get("misc", "close")); 598 close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar.")); 599 close.setBorder(BorderFactory.createEmptyBorder()); 600 close.addActionListener(e -> { 601 hideDialog(); 602 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 603 hideNotify(); 604 }); 605 add(close); 606 setToolTipText(tr("Click to minimize/maximize the panel content")); 607 setTitle(toggleDialogName); 608 } 609 610 public void setTitle(String title) { 611 lblTitle.setText(title); 612 lblTitleWeak.repaint(); 613 } 614 615 public String getTitle() { 616 return lblTitle.getText(); 617 } 618 619 /** 620 * This is the popup menu used for the title bar. 621 */ 622 public class DialogPopupMenu extends JPopupMenu { 623 624 /** 625 * Constructs a new {@code DialogPopupMenu}. 626 */ 627 DialogPopupMenu() { 628 alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN); 629 dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC); 630 alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN); 631 ButtonGroup buttonHidingGroup = new ButtonGroup(); 632 JMenu buttonHidingMenu = new JMenu(tr("Side buttons")); 633 for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) { 634 buttonHidingGroup.add(rb); 635 buttonHidingMenu.add(rb); 636 } 637 add(buttonHidingMenu); 638 for (javax.swing.Action action: buttonActions) { 639 add(action); 640 } 641 } 642 } 643 644 /** 645 * Registers the mouse listeners. 646 * <p> 647 * Should be called once after this title was added to the dialog. 648 */ 649 public final void registerMouseListener() { 650 popupMenu = new DialogPopupMenu(); 651 addMouseListener(new MouseEventHandler()); 652 } 653 654 class MouseEventHandler extends PopupMenuLauncher { 655 /** 656 * Constructs a new {@code MouseEventHandler}. 657 */ 658 MouseEventHandler() { 659 super(popupMenu); 660 } 661 662 @Override 663 public void mouseClicked(MouseEvent e) { 664 if (SwingUtilities.isLeftMouseButton(e)) { 665 if (isCollapsed) { 666 expand(); 667 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this); 668 } else { 669 collapse(); 670 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 671 } 672 } 673 } 674 } 675 } 676 677 /** 678 * The dialog class used to display toggle dialogs in a detached window. 679 * 680 */ 681 private class DetachedDialog extends JDialog { 682 DetachedDialog() { 683 super(GuiHelper.getFrameForComponent(Main.parent)); 684 getContentPane().add(ToggleDialog.this); 685 addWindowListener(new WindowAdapter() { 686 @Override public void windowClosing(WindowEvent e) { 687 rememberGeometry(); 688 getContentPane().removeAll(); 689 dispose(); 690 if (dockWhenClosingDetachedDlg()) { 691 dock(); 692 if (isDialogInCollapsedView()) { 693 expand(); 694 } 695 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); 696 } else { 697 hideDialog(); 698 hideNotify(); 699 } 700 } 701 }); 702 addComponentListener(new ComponentAdapter() { 703 @Override 704 public void componentMoved(ComponentEvent e) { 705 rememberGeometry(); 706 } 707 708 @Override 709 public void componentResized(ComponentEvent e) { 710 rememberGeometry(); 711 } 712 }); 713 714 try { 715 new WindowGeometry(preferencePrefix+".geometry").applySafe(this); 716 } catch (WindowGeometryException e) { 717 Logging.debug(e); 718 ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize()); 719 pack(); 720 setLocationRelativeTo(Main.parent); 721 } 722 super.setTitle(titleBar.getTitle()); 723 HelpUtil.setHelpContext(getRootPane(), helpTopic()); 724 } 725 726 protected void rememberGeometry() { 727 if (detachedDialog != null && detachedDialog.isShowing()) { 728 new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry"); 729 } 730 } 731 } 732 733 /** 734 * Replies the action to toggle the visible state of this toggle dialog 735 * 736 * @return the action to toggle the visible state of this toggle dialog 737 */ 738 public AbstractAction getToggleAction() { 739 return toggleAction; 740 } 741 742 /** 743 * Replies the prefix for the preference settings of this dialog. 744 * 745 * @return the prefix for the preference settings of this dialog. 746 */ 747 public String getPreferencePrefix() { 748 return preferencePrefix; 749 } 750 751 /** 752 * Sets the dialogsPanel managing all toggle dialogs. 753 * @param dialogsPanel The panel managing all toggle dialogs 754 */ 755 public void setDialogsPanel(DialogsPanel dialogsPanel) { 756 this.dialogsPanel = dialogsPanel; 757 } 758 759 /** 760 * Replies the name of this toggle dialog 761 */ 762 @Override 763 public String getName() { 764 return "toggleDialog." + preferencePrefix; 765 } 766 767 /** 768 * Sets the title. 769 * @param title The dialog's title 770 */ 771 public void setTitle(String title) { 772 titleBar.setTitle(title); 773 if (detachedDialog != null) { 774 detachedDialog.setTitle(title); 775 } 776 } 777 778 protected void setIsShowing(boolean val) { 779 isShowing = val; 780 Config.getPref().putBoolean(preferencePrefix+".visible", val); 781 stateChanged(); 782 } 783 784 protected void setIsDocked(boolean val) { 785 if (buttonsPanel != null) { 786 buttonsPanel.setVisible(!val || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 787 } 788 isDocked = val; 789 Config.getPref().putBoolean(preferencePrefix+".docked", val); 790 stateChanged(); 791 } 792 793 protected void setIsCollapsed(boolean val) { 794 isCollapsed = val; 795 Config.getPref().putBoolean(preferencePrefix+".minimized", val); 796 stateChanged(); 797 } 798 799 protected void setIsButtonHiding(ButtonHidingType val) { 800 buttonHiding = val; 801 propButtonHiding.put(val); 802 refreshHidingButtons(); 803 } 804 805 /** 806 * Returns the preferred height of this dialog. 807 * @return The preferred height if the toggle dialog is expanded 808 */ 809 public int getPreferredHeight() { 810 return preferredHeight; 811 } 812 813 @Override 814 public String helpTopic() { 815 String help = getClass().getName(); 816 help = help.substring(help.lastIndexOf('.')+1, help.length()-6); 817 return "Dialog/"+help; 818 } 819 820 @Override 821 public String toString() { 822 return name; 823 } 824 825 /** 826 * Determines if this dialog is showing either as docked or as detached dialog. 827 * @return {@code true} if this dialog is showing either as docked or as detached dialog 828 */ 829 public boolean isDialogShowing() { 830 return isShowing; 831 } 832 833 /** 834 * Determines if this dialog is docked and expanded. 835 * @return {@code true} if this dialog is docked and expanded 836 */ 837 public boolean isDialogInDefaultView() { 838 return isShowing && isDocked && (!isCollapsed); 839 } 840 841 /** 842 * Determines if this dialog is docked and collapsed. 843 * @return {@code true} if this dialog is docked and collapsed 844 */ 845 public boolean isDialogInCollapsedView() { 846 return isShowing && isDocked && isCollapsed; 847 } 848 849 /** 850 * Sets the button from the button list that is used to display this dialog. 851 * <p> 852 * Note: This is ignored by the {@link ToggleDialog} for now. 853 * @param button The button for this dialog. 854 */ 855 public void setButton(JToggleButton button) { 856 this.button = button; 857 } 858 859 /** 860 * Gets the button from the button list that is used to display this dialog. 861 * @return button The button for this dialog. 862 */ 863 public JToggleButton getButton() { 864 return button; 865 } 866 867 /* 868 * The following methods are intended to be overridden, in order to customize 869 * the toggle dialog behavior. 870 */ 871 872 /** 873 * Returns the default size of the detached dialog. 874 * Override this method to customize the initial dialog size. 875 * @return the default size of the detached dialog 876 */ 877 protected Dimension getDefaultDetachedSize() { 878 return new Dimension(dialogsPanel.getWidth(), preferredHeight); 879 } 880 881 /** 882 * Do something when the toggleButton is pressed. 883 */ 884 protected void toggleButtonHook() { 885 // Do nothing 886 } 887 888 protected boolean dockWhenClosingDetachedDlg() { 889 return true; 890 } 891 892 /** 893 * primitive stateChangedListener for subclasses 894 */ 895 protected void stateChanged() { 896 // Do nothing 897 } 898 899 /** 900 * Create a component with the given layout for this component. 901 * @param data The content to be displayed 902 * @param scroll <code>true</code> if it should be wrapped in a {@link JScrollPane} 903 * @param buttons The buttons to add. 904 * @return The component. 905 */ 906 protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) { 907 return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null); 908 } 909 910 @SafeVarargs 911 protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons, 912 Collection<SideButton>... nextButtons) { 913 if (scroll) { 914 JScrollPane sp = new JScrollPane(data); 915 if (!(data instanceof Scrollable)) { 916 GuiHelper.setDefaultIncrement(sp); 917 } 918 data = sp; 919 } 920 LinkedList<Collection<SideButton>> buttons = new LinkedList<>(); 921 buttons.addFirst(firstButtons); 922 if (nextButtons != null) { 923 buttons.addAll(Arrays.asList(nextButtons)); 924 } 925 add(data, BorderLayout.CENTER); 926 if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) { 927 buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1)); 928 for (Collection<SideButton> buttonRow : buttons) { 929 if (buttonRow == null) { 930 continue; 931 } 932 final JPanel buttonRowPanel = new JPanel(Config.getPref().getBoolean("dialog.align.left", false) 933 ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size())); 934 buttonsPanel.add(buttonRowPanel); 935 for (SideButton button : buttonRow) { 936 buttonRowPanel.add(button); 937 javax.swing.Action action = button.getAction(); 938 if (action != null) { 939 buttonActions.add(action); 940 } else { 941 Logging.warn("Button " + button + " doesn't have action defined"); 942 Logging.error(new Exception()); 943 } 944 } 945 } 946 add(buttonsPanel, BorderLayout.SOUTH); 947 dynamicButtonsPropertyChanged(); 948 } else { 949 titleBar.buttonsHide.setVisible(false); 950 } 951 952 // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu 953 titleBar.registerMouseListener(); 954 955 return data; 956 } 957 958 @Override 959 public void eventDispatched(AWTEvent event) { 960 if (event instanceof MouseEvent && isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC 961 && buttonsPanel != null) { 962 Rectangle b = this.getBounds(); 963 b.setLocation(getLocationOnScreen()); 964 if (b.contains(((MouseEvent) event).getLocationOnScreen())) { 965 if (!buttonsPanel.isVisible()) { 966 buttonsPanel.setVisible(true); 967 } 968 } else if (buttonsPanel.isVisible()) { 969 buttonsPanel.setVisible(false); 970 } 971 } 972 } 973 974 @Override 975 public void preferenceChanged(PreferenceChangeEvent e) { 976 if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) { 977 dynamicButtonsPropertyChanged(); 978 } 979 } 980 981 private void dynamicButtonsPropertyChanged() { 982 boolean propEnabled = PROP_DYNAMIC_BUTTONS.get(); 983 try { 984 if (propEnabled) { 985 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK); 986 } else { 987 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 988 } 989 } catch (SecurityException e) { 990 Logging.log(Logging.LEVEL_ERROR, "Unable to add/remove AWT event listener", e); 991 } 992 titleBar.buttonsHide.setVisible(propEnabled); 993 refreshHidingButtons(); 994 } 995 996 private void refreshHidingButtons() { 997 titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 998 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 999 titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 1000 if (buttonsPanel != null) { 1001 buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked); 1002 } 1003 stateChanged(); 1004 } 1005}