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