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.ActionListener; 021import java.awt.event.ComponentAdapter; 022import java.awt.event.ComponentEvent; 023import java.awt.event.MouseEvent; 024import java.awt.event.WindowAdapter; 025import java.awt.event.WindowEvent; 026import java.beans.PropertyChangeEvent; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.LinkedList; 031import java.util.List; 032 033import javax.swing.AbstractAction; 034import javax.swing.BorderFactory; 035import javax.swing.ButtonGroup; 036import javax.swing.ImageIcon; 037import javax.swing.JButton; 038import javax.swing.JCheckBoxMenuItem; 039import javax.swing.JComponent; 040import javax.swing.JDialog; 041import javax.swing.JLabel; 042import javax.swing.JMenu; 043import javax.swing.JPanel; 044import javax.swing.JPopupMenu; 045import javax.swing.JRadioButtonMenuItem; 046import javax.swing.JScrollPane; 047import javax.swing.JToggleButton; 048import javax.swing.Scrollable; 049import javax.swing.SwingUtilities; 050 051import org.openstreetmap.josm.Main; 052import org.openstreetmap.josm.actions.JosmAction; 053import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 054import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 055import org.openstreetmap.josm.data.preferences.BooleanProperty; 056import org.openstreetmap.josm.data.preferences.ParametrizedEnumProperty; 057import org.openstreetmap.josm.gui.MainMenu; 058import org.openstreetmap.josm.gui.ShowHideButtonListener; 059import org.openstreetmap.josm.gui.SideButton; 060import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action; 061import org.openstreetmap.josm.gui.help.HelpUtil; 062import org.openstreetmap.josm.gui.help.Helpful; 063import org.openstreetmap.josm.gui.preferences.PreferenceDialog; 064import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 065import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting; 066import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting; 067import org.openstreetmap.josm.gui.util.GuiHelper; 068import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 069import org.openstreetmap.josm.tools.Destroyable; 070import org.openstreetmap.josm.tools.GBC; 071import org.openstreetmap.josm.tools.ImageProvider; 072import org.openstreetmap.josm.tools.Shortcut; 073import org.openstreetmap.josm.tools.WindowGeometry; 074import org.openstreetmap.josm.tools.WindowGeometry.WindowGeometryException; 075 076/** 077 * This class is a toggle dialog that can be turned on and off. 078 * @since 8 079 */ 080public class ToggleDialog extends JPanel implements ShowHideButtonListener, Helpful, AWTEventListener, Destroyable, PreferenceChangedListener { 081 082 /** 083 * The button-hiding strategy in toggler dialogs. 084 */ 085 public enum ButtonHidingType { 086 /** Buttons are always shown (default) **/ 087 ALWAYS_SHOWN, 088 /** Buttons are always hidden **/ 089 ALWAYS_HIDDEN, 090 /** Buttons are dynamically hidden, i.e. only shown when mouse cursor is in dialog */ 091 DYNAMIC 092 } 093 094 /** 095 * Property to enable dynamic buttons globally. 096 * @since 6752 097 */ 098 public static final BooleanProperty PROP_DYNAMIC_BUTTONS = new BooleanProperty("dialog.dynamic.buttons", false); 099 100 private final transient ParametrizedEnumProperty<ButtonHidingType> propButtonHiding = 101 new ParametrizedEnumProperty<ToggleDialog.ButtonHidingType>(ButtonHidingType.class, ButtonHidingType.DYNAMIC) { 102 @Override 103 protected String getKey(String... params) { 104 return preferencePrefix + ".buttonhiding"; 105 } 106 107 @Override 108 protected ButtonHidingType parse(String s) { 109 try { 110 return super.parse(s); 111 } catch (IllegalArgumentException e) { 112 // Legacy settings 113 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 public TitleBar(String toggleDialogName, String iconName) { 516 setLayout(new GridBagLayout()); 517 518 lblMinimized = new JLabel(ImageProvider.get("misc", "normal")); 519 add(lblMinimized); 520 521 // scale down the dialog icon 522 ImageIcon icon = ImageProvider.get("dialogs", iconName, ImageProvider.ImageSizes.SMALLICON); 523 lblTitle = new JLabel("", icon, JLabel.TRAILING); 524 lblTitle.setIconTextGap(8); 525 526 JPanel conceal = new JPanel(); 527 conceal.add(lblTitle); 528 conceal.setVisible(false); 529 add(conceal, GBC.std()); 530 531 // Cannot add the label directly since it would displace other elements on resize 532 lblTitleWeak = new JComponent() { 533 @Override 534 public void paintComponent(Graphics g) { 535 lblTitle.paint(g); 536 } 537 }; 538 lblTitleWeak.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20)); 539 lblTitleWeak.setMinimumSize(new Dimension(0, 20)); 540 add(lblTitleWeak, GBC.std().fill(GBC.HORIZONTAL)); 541 542 buttonsHide = new JButton(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 543 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 544 buttonsHide.setToolTipText(tr("Toggle dynamic buttons")); 545 buttonsHide.setBorder(BorderFactory.createEmptyBorder()); 546 buttonsHide.addActionListener( 547 new ActionListener() { 548 @Override 549 public void actionPerformed(ActionEvent e) { 550 JRadioButtonMenuItem item = (buttonHiding == ButtonHidingType.DYNAMIC) ? alwaysShown : dynamic; 551 item.setSelected(true); 552 item.getAction().actionPerformed(null); 553 } 554 } 555 ); 556 add(buttonsHide); 557 558 // show the pref button if applicable 559 if (preferenceClass != null) { 560 JButton pref = new JButton(ImageProvider.get("preference", ImageProvider.ImageSizes.SMALLICON)); 561 pref.setToolTipText(tr("Open preferences for this panel")); 562 pref.setBorder(BorderFactory.createEmptyBorder()); 563 pref.addActionListener( 564 new ActionListener() { 565 @Override 566 @SuppressWarnings("unchecked") 567 public void actionPerformed(ActionEvent e) { 568 final PreferenceDialog p = new PreferenceDialog(Main.parent); 569 if (TabPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 570 p.selectPreferencesTabByClass((Class<? extends TabPreferenceSetting>) preferenceClass); 571 } else if (SubPreferenceSetting.class.isAssignableFrom(preferenceClass)) { 572 p.selectSubPreferencesTabByClass((Class<? extends SubPreferenceSetting>) preferenceClass); 573 } 574 p.setVisible(true); 575 } 576 } 577 ); 578 add(pref); 579 } 580 581 // show the sticky button 582 JButton sticky = new JButton(ImageProvider.get("misc", "sticky")); 583 sticky.setToolTipText(tr("Undock the panel")); 584 sticky.setBorder(BorderFactory.createEmptyBorder()); 585 sticky.addActionListener( 586 new ActionListener() { 587 @Override 588 public void actionPerformed(ActionEvent e) { 589 detach(); 590 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 591 } 592 } 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( 601 new ActionListener() { 602 @Override 603 public void actionPerformed(ActionEvent e) { 604 hideDialog(); 605 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 606 hideNotify(); 607 } 608 } 609 ); 610 add(close); 611 setToolTipText(tr("Click to minimize/maximize the panel content")); 612 setTitle(toggleDialogName); 613 } 614 615 public void setTitle(String title) { 616 lblTitle.setText(title); 617 lblTitleWeak.repaint(); 618 } 619 620 public String getTitle() { 621 return lblTitle.getText(); 622 } 623 624 /** 625 * This is the popup menu used for the title bar. 626 */ 627 public class DialogPopupMenu extends JPopupMenu { 628 629 /** 630 * Constructs a new {@code DialogPopupMenu}. 631 */ 632 DialogPopupMenu() { 633 alwaysShown.setSelected(buttonHiding == ButtonHidingType.ALWAYS_SHOWN); 634 dynamic.setSelected(buttonHiding == ButtonHidingType.DYNAMIC); 635 alwaysHidden.setSelected(buttonHiding == ButtonHidingType.ALWAYS_HIDDEN); 636 ButtonGroup buttonHidingGroup = new ButtonGroup(); 637 JMenu buttonHidingMenu = new JMenu(tr("Side buttons")); 638 for (JRadioButtonMenuItem rb : new JRadioButtonMenuItem[]{alwaysShown, dynamic, alwaysHidden}) { 639 buttonHidingGroup.add(rb); 640 buttonHidingMenu.add(rb); 641 } 642 add(buttonHidingMenu); 643 for (javax.swing.Action action: buttonActions) { 644 add(action); 645 } 646 } 647 } 648 649 /** 650 * Registers the mouse listeners. 651 * <p> 652 * Should be called once after this title was added to the dialog. 653 */ 654 public final void registerMouseListener() { 655 popupMenu = new DialogPopupMenu(); 656 addMouseListener(new MouseEventHandler()); 657 } 658 659 class MouseEventHandler extends PopupMenuLauncher { 660 /** 661 * Constructs a new {@code MouseEventHandler}. 662 */ 663 MouseEventHandler() { 664 super(popupMenu); 665 } 666 667 @Override 668 public void mouseClicked(MouseEvent e) { 669 if (SwingUtilities.isLeftMouseButton(e)) { 670 if (isCollapsed) { 671 expand(); 672 dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this); 673 } else { 674 collapse(); 675 dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); 676 } 677 } 678 } 679 } 680 } 681 682 /** 683 * The dialog class used to display toggle dialogs in a detached window. 684 * 685 */ 686 private class DetachedDialog extends JDialog { 687 DetachedDialog() { 688 super(GuiHelper.getFrameForComponent(Main.parent)); 689 getContentPane().add(ToggleDialog.this); 690 addWindowListener(new WindowAdapter() { 691 @Override public void windowClosing(WindowEvent e) { 692 rememberGeometry(); 693 getContentPane().removeAll(); 694 dispose(); 695 if (dockWhenClosingDetachedDlg()) { 696 dock(); 697 if (isDialogInCollapsedView()) { 698 expand(); 699 } 700 dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); 701 } else { 702 hideDialog(); 703 hideNotify(); 704 } 705 } 706 }); 707 addComponentListener(new ComponentAdapter() { 708 @Override 709 public void componentMoved(ComponentEvent e) { 710 rememberGeometry(); 711 } 712 713 @Override 714 public void componentResized(ComponentEvent e) { 715 rememberGeometry(); 716 } 717 }); 718 719 try { 720 new WindowGeometry(preferencePrefix+".geometry").applySafe(this); 721 } catch (WindowGeometryException e) { 722 ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize()); 723 pack(); 724 setLocationRelativeTo(Main.parent); 725 } 726 super.setTitle(titleBar.getTitle()); 727 HelpUtil.setHelpContext(getRootPane(), helpTopic()); 728 } 729 730 protected void rememberGeometry() { 731 if (detachedDialog != null && detachedDialog.isShowing()) { 732 new WindowGeometry(detachedDialog).remember(preferencePrefix+".geometry"); 733 } 734 } 735 } 736 737 /** 738 * Replies the action to toggle the visible state of this toggle dialog 739 * 740 * @return the action to toggle the visible state of this toggle dialog 741 */ 742 public AbstractAction getToggleAction() { 743 return toggleAction; 744 } 745 746 /** 747 * Replies the prefix for the preference settings of this dialog. 748 * 749 * @return the prefix for the preference settings of this dialog. 750 */ 751 public String getPreferencePrefix() { 752 return preferencePrefix; 753 } 754 755 /** 756 * Sets the dialogsPanel managing all toggle dialogs. 757 * @param dialogsPanel The panel managing all toggle dialogs 758 */ 759 public void setDialogsPanel(DialogsPanel dialogsPanel) { 760 this.dialogsPanel = dialogsPanel; 761 } 762 763 /** 764 * Replies the name of this toggle dialog 765 */ 766 @Override 767 public String getName() { 768 return "toggleDialog." + preferencePrefix; 769 } 770 771 /** 772 * Sets the title. 773 * @param title The dialog's title 774 */ 775 public void setTitle(String title) { 776 titleBar.setTitle(title); 777 if (detachedDialog != null) { 778 detachedDialog.setTitle(title); 779 } 780 } 781 782 protected void setIsShowing(boolean val) { 783 isShowing = val; 784 Main.pref.put(preferencePrefix+".visible", val); 785 stateChanged(); 786 } 787 788 protected void setIsDocked(boolean val) { 789 if (buttonsPanel != null) { 790 buttonsPanel.setVisible(!val || buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 791 } 792 isDocked = val; 793 Main.pref.put(preferencePrefix+".docked", val); 794 stateChanged(); 795 } 796 797 protected void setIsCollapsed(boolean val) { 798 isCollapsed = val; 799 Main.pref.put(preferencePrefix+".minimized", val); 800 stateChanged(); 801 } 802 803 protected void setIsButtonHiding(ButtonHidingType val) { 804 buttonHiding = val; 805 propButtonHiding.put(val); 806 refreshHidingButtons(); 807 } 808 809 /** 810 * Returns the preferred height of this dialog. 811 * @return The preferred height if the toggle dialog is expanded 812 */ 813 public int getPreferredHeight() { 814 return preferredHeight; 815 } 816 817 @Override 818 public String helpTopic() { 819 String help = getClass().getName(); 820 help = help.substring(help.lastIndexOf('.')+1, help.length()-6); 821 return "Dialog/"+help; 822 } 823 824 @Override 825 public String toString() { 826 return name; 827 } 828 829 /** 830 * Determines if this dialog is showing either as docked or as detached dialog. 831 * @return {@code true} if this dialog is showing either as docked or as detached dialog 832 */ 833 public boolean isDialogShowing() { 834 return isShowing; 835 } 836 837 /** 838 * Determines if this dialog is docked and expanded. 839 * @return {@code true} if this dialog is docked and expanded 840 */ 841 public boolean isDialogInDefaultView() { 842 return isShowing && isDocked && (!isCollapsed); 843 } 844 845 /** 846 * Determines if this dialog is docked and collapsed. 847 * @return {@code true} if this dialog is docked and collapsed 848 */ 849 public boolean isDialogInCollapsedView() { 850 return isShowing && isDocked && isCollapsed; 851 } 852 853 /** 854 * Sets the button from the button list that is used to display this dialog. 855 * <p> 856 * Note: This is ignored by the {@link ToggleDialog} for now. 857 * @param button The button for this dialog. 858 */ 859 public void setButton(JToggleButton button) { 860 this.button = button; 861 } 862 863 /** 864 * Gets the button from the button list that is used to display this dialog. 865 * @return button The button for this dialog. 866 */ 867 public JToggleButton getButton() { 868 return button; 869 } 870 871 /* 872 * The following methods are intended to be overridden, in order to customize 873 * the toggle dialog behavior. 874 */ 875 876 /** 877 * Returns the default size of the detached dialog. 878 * Override this method to customize the initial dialog size. 879 * @return the default size of the detached dialog 880 */ 881 protected Dimension getDefaultDetachedSize() { 882 return new Dimension(dialogsPanel.getWidth(), preferredHeight); 883 } 884 885 /** 886 * Do something when the toggleButton is pressed. 887 */ 888 protected void toggleButtonHook() { 889 // Do nothing 890 } 891 892 protected boolean dockWhenClosingDetachedDlg() { 893 return true; 894 } 895 896 /** 897 * primitive stateChangedListener for subclasses 898 */ 899 protected void stateChanged() { 900 // Do nothing 901 } 902 903 /** 904 * Create a component with the given layout for this component. 905 * @param data The content to be displayed 906 * @param scroll <code>true</code> if it should be wrapped in a {@link JScrollPane} 907 * @param buttons The buttons to add. 908 * @return The component. 909 */ 910 protected Component createLayout(Component data, boolean scroll, Collection<SideButton> buttons) { 911 return createLayout(data, scroll, buttons, (Collection<SideButton>[]) null); 912 } 913 914 @SafeVarargs 915 protected final Component createLayout(Component data, boolean scroll, Collection<SideButton> firstButtons, 916 Collection<SideButton>... nextButtons) { 917 if (scroll) { 918 JScrollPane sp = new JScrollPane(data); 919 if (!(data instanceof Scrollable)) { 920 GuiHelper.setDefaultIncrement(sp); 921 } 922 data = sp; 923 } 924 LinkedList<Collection<SideButton>> buttons = new LinkedList<>(); 925 buttons.addFirst(firstButtons); 926 if (nextButtons != null) { 927 buttons.addAll(Arrays.asList(nextButtons)); 928 } 929 add(data, BorderLayout.CENTER); 930 if (!buttons.isEmpty() && buttons.get(0) != null && !buttons.get(0).isEmpty()) { 931 buttonsPanel = new JPanel(new GridLayout(buttons.size(), 1)); 932 for (Collection<SideButton> buttonRow : buttons) { 933 if (buttonRow == null) { 934 continue; 935 } 936 final JPanel buttonRowPanel = new JPanel(Main.pref.getBoolean("dialog.align.left", false) 937 ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1, buttonRow.size())); 938 buttonsPanel.add(buttonRowPanel); 939 for (SideButton button : buttonRow) { 940 buttonRowPanel.add(button); 941 javax.swing.Action action = button.getAction(); 942 if (action != null) { 943 buttonActions.add(action); 944 } else { 945 Main.warn("Button " + button + " doesn't have action defined"); 946 Main.error(new Exception()); 947 } 948 } 949 } 950 add(buttonsPanel, BorderLayout.SOUTH); 951 dynamicButtonsPropertyChanged(); 952 } else { 953 titleBar.buttonsHide.setVisible(false); 954 } 955 956 // Register title bar mouse listener only after buttonActions has been initialized to have a complete popup menu 957 titleBar.registerMouseListener(); 958 959 return data; 960 } 961 962 @Override 963 public void eventDispatched(AWTEvent event) { 964 if (isShowing() && !isCollapsed && isDocked && buttonHiding == ButtonHidingType.DYNAMIC) { 965 if (buttonsPanel != null) { 966 Rectangle b = this.getBounds(); 967 b.setLocation(getLocationOnScreen()); 968 if (b.contains(((MouseEvent) event).getLocationOnScreen())) { 969 if (!buttonsPanel.isVisible()) { 970 buttonsPanel.setVisible(true); 971 } 972 } else if (buttonsPanel.isVisible()) { 973 buttonsPanel.setVisible(false); 974 } 975 } 976 } 977 } 978 979 @Override 980 public void preferenceChanged(PreferenceChangeEvent e) { 981 if (e.getKey().equals(PROP_DYNAMIC_BUTTONS.getKey())) { 982 dynamicButtonsPropertyChanged(); 983 } 984 } 985 986 private void dynamicButtonsPropertyChanged() { 987 boolean propEnabled = PROP_DYNAMIC_BUTTONS.get(); 988 if (propEnabled) { 989 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_MOTION_EVENT_MASK); 990 } else { 991 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 992 } 993 titleBar.buttonsHide.setVisible(propEnabled); 994 refreshHidingButtons(); 995 } 996 997 private void refreshHidingButtons() { 998 titleBar.buttonsHide.setIcon(ImageProvider.get("misc", buttonHiding != ButtonHidingType.ALWAYS_SHOWN 999 ? /* ICON(misc/)*/ "buttonhide" : /* ICON(misc/)*/ "buttonshow")); 1000 titleBar.buttonsHide.setEnabled(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN); 1001 if (buttonsPanel != null) { 1002 buttonsPanel.setVisible(buttonHiding != ButtonHidingType.ALWAYS_HIDDEN || !isDocked); 1003 } 1004 stateChanged(); 1005 } 1006}