// License: GPL. See LICENSE file for details. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Image; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.help.Helpful; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; /** * This class is a toggle dialog that can be turned on and off. * */ public class ToggleDialog extends JPanel implements Helpful { /** The action to toggle this dialog */ protected ToggleDialogAction toggleAction; protected String preferencePrefix; final protected String name; /** DialogsPanel that manages all ToggleDialogs */ protected DialogsPanel dialogsPanel; protected TitleBar titleBar; /** * Indicates whether the dialog is showing or not. */ protected boolean isShowing; /** * If isShowing is true, indicates whether the dialog is docked or not, e. g. * shown as part of the main window or as a separate dialog window. */ protected boolean isDocked; /** * If isShowing and isDocked are true, indicates whether the dialog is * currently minimized or not. */ protected boolean isCollapsed; /** the preferred height if the toggle dialog is expanded */ private int preferredHeight; /** the label in the title bar which shows whether the toggle dialog is expanded or collapsed */ private JLabel lblMinimized; /** the JDialog displaying the toggle dialog as undocked dialog */ protected JDialog detachedDialog; /** * Constructor * (see below) */ public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight) { this(name, iconName, tooltip, shortcut, preferredHeight, false); } /** * Constructor * * @param name the name of the dialog * @param iconName the name of the icon to be displayed * @param tooltip the tool tip * @param shortcut the shortcut * @param preferredHeight the preferred height for the dialog * @param defShow if the dialog should be shown by default, if there is no preference */ public ToggleDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow) { super(new BorderLayout()); this.preferencePrefix = iconName; this.name = name; /** Use the full width of the parent element */ setPreferredSize(new Dimension(0, preferredHeight)); /** Override any minimum sizes of child elements so the user can resize freely */ setMinimumSize(new Dimension(0,0)); this.preferredHeight = preferredHeight; toggleAction = new ToggleDialogAction(name, "dialogs/"+iconName, tooltip, shortcut, iconName); String helpId = "Dialog/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); toggleAction.putValue("help", helpId.substring(0, helpId.length()-6)); setLayout(new BorderLayout()); /** show the minimize button */ lblMinimized = new JLabel(ImageProvider.get("misc", "normal")); titleBar = new TitleBar(name, iconName); add(titleBar, BorderLayout.NORTH); setBorder(BorderFactory.createEtchedBorder()); isShowing = Main.pref.getBoolean(preferencePrefix+".visible", defShow); isDocked = Main.pref.getBoolean(preferencePrefix+".docked", true); isCollapsed = Main.pref.getBoolean(preferencePrefix+".minimized", false); } /** * The action to toggle the visibility state of this toggle dialog. * * Emits {@see PropertyChangeEvent}s for the property <tt>selected</tt>: * <ul> * <li>true, if the dialog is currently visible</li> * <li>false, if the dialog is currently invisible</li> * </ul> * */ public final class ToggleDialogAction extends JosmAction { private ToggleDialogAction(String name, String iconName, String tooltip, Shortcut shortcut, String prefname) { super(name, iconName, tooltip, shortcut, false); } public void actionPerformed(ActionEvent e) { toggleButtonHook(); if (isShowing) { hideDialog(); dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); } else { showDialog(); if (isDocked && isCollapsed) { expand(); } if (isDocked) { dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); } } } @Override public void destroy() { super.destroy(); } } /** * Shows the dialog */ public void showDialog() { setIsShowing(true); if (!isDocked) { detach(); } else { dock(); this.setVisible(true); } // toggling the selected value in order to enforce PropertyChangeEvents setIsShowing(true); toggleAction.putValue("selected", false); toggleAction.putValue("selected", true); showNotify(); } /** * Changes the state of the dialog such that the user can see the content * and takes care of the panel reconstruction. */ public void unfurlDialog() { if (isDialogInDefaultView()) return; if (isDialogInCollapsedView()) { expand(); dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, this); } else if (!isDialogShowing()) { showDialog(); if (isDocked && isCollapsed) { expand(); } if (isDocked) { dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, this); } } } /** * Hides the dialog */ public void hideDialog() { closeDetachedDialog(); this.setVisible(false); setIsShowing(false); toggleAction.putValue("selected", false); hideNotify(); } /** * Displays the toggle dialog in the toggle dialog view on the right * of the main map window. * */ protected void dock() { detachedDialog = null; titleBar.setVisible(true); setIsDocked(true); } /** * Display the dialog in a detached window. * */ protected void detach() { setContentVisible(true); this.setVisible(true); titleBar.setVisible(false); detachedDialog = new DetachedDialog(); detachedDialog.setVisible(true); setIsShowing(true); setIsDocked(false); } /** * Collapses the toggle dialog to the title bar only * */ public void collapse() { // if (isShowing && isDocked && !isCollapsed) { if (isDialogInDefaultView()) { setContentVisible(false); setIsCollapsed(true); setPreferredSize(new Dimension(0,20)); setMaximumSize(new Dimension(Integer.MAX_VALUE,20)); setMinimumSize(new Dimension(Integer.MAX_VALUE,20)); lblMinimized.setIcon(ImageProvider.get("misc", "minimized")); } else throw new IllegalStateException(); } /** * Expands the toggle dialog */ protected void expand() { // if (isShowing && isDocked && isCollapsed) { if (isDialogInCollapsedView()) { setContentVisible(true); setIsCollapsed(false); setPreferredSize(new Dimension(0,preferredHeight)); setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); lblMinimized.setIcon(ImageProvider.get("misc", "normal")); } else throw new IllegalStateException(); } /** * Sets the visibility of all components in this toggle dialog, except the title bar * * @param visible true, if the components should be visible; false otherwise */ protected void setContentVisible(boolean visible) { Component comps[] = getComponents(); for(int i=0; i<comps.length; i++) { if(comps[i] != titleBar) { comps[i].setVisible(visible); } } } public void destroy() { closeDetachedDialog(); hideNotify(); } /** * Closes the detached dialog if this toggle dialog is currently displayed * in a detached dialog. * */ public void closeDetachedDialog() { if (detachedDialog != null) { detachedDialog.setVisible(false); detachedDialog.getContentPane().removeAll(); detachedDialog.dispose(); } } /** * Called when toggle dialog is shown (after it was created or expanded). Descendants may overwrite this * method, it's a good place to register listeners needed to keep dialog updated */ public void showNotify() { } /** * Called when toggle dialog is hidden (collapsed, removed, MapFrame is removed, ...). Good place to unregister * listeners */ public void hideNotify() { } /** * The title bar displayed in docked mode * */ protected class TitleBar extends JPanel { final private JLabel lblTitle; final private JComponent lblTitle_weak; public TitleBar(String toggleDialogName, String iconName) { setLayout(new GridBagLayout()); lblMinimized = new JLabel(ImageProvider.get("misc", "normal")); add(lblMinimized); // scale down the dialog icon ImageIcon inIcon = ImageProvider.get("dialogs", iconName); ImageIcon smallIcon = new ImageIcon(inIcon.getImage().getScaledInstance(16 , 16, Image.SCALE_SMOOTH)); lblTitle = new JLabel("",smallIcon, JLabel.TRAILING); lblTitle.setIconTextGap(8); JPanel conceal = new JPanel(); conceal.add(lblTitle); conceal.setVisible(false); add(conceal, GBC.std()); // Cannot add the label directly since it would displace other elements on resize lblTitle_weak = new JComponent() { @Override public void paintComponent(Graphics g) { lblTitle.paint(g); } }; lblTitle_weak.setPreferredSize(new Dimension(Integer.MAX_VALUE,20)); lblTitle_weak.setMinimumSize(new Dimension(0,20)); add(lblTitle_weak, GBC.std().fill(GBC.HORIZONTAL)); addMouseListener( new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { // toggleExpandedState if (isCollapsed) { expand(); dialogsPanel.reconstruct(Action.COLLAPSED_TO_DEFAULT, ToggleDialog.this); } else { collapse(); dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); } } } ); // show the sticky button JButton sticky = new JButton(ImageProvider.get("misc", "sticky")); sticky.setToolTipText(tr("Undock the panel")); sticky.setBorder(BorderFactory.createEmptyBorder()); sticky.addActionListener( new ActionListener(){ public void actionPerformed(ActionEvent e) { detach(); dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); } } ); add(sticky); // show the close button JButton close = new JButton(ImageProvider.get("misc", "close")); close.setToolTipText(tr("Close this panel. You can reopen it with the buttons in the left toolbar.")); close.setBorder(BorderFactory.createEmptyBorder()); close.addActionListener( new ActionListener(){ public void actionPerformed(ActionEvent e) { hideDialog(); dialogsPanel.reconstruct(Action.ELEMENT_SHRINKS, null); } } ); add(close); setToolTipText(tr("Click to minimize/maximize the panel content")); setTitle(toggleDialogName); } public void setTitle(String title) { lblTitle.setText(title); lblTitle_weak.repaint(); } public String getTitle() { return lblTitle.getText(); } } /** * The dialog class used to display toggle dialogs in a detached window. * */ private class DetachedDialog extends JDialog{ public DetachedDialog() { super(JOptionPane.getFrameForComponent(Main.parent)); getContentPane().add(ToggleDialog.this); addWindowListener(new WindowAdapter(){ @Override public void windowClosing(WindowEvent e) { rememberGeometry(); getContentPane().removeAll(); dispose(); if (dockWhenClosingDetachedDlg()) { dock(); if (isDialogInCollapsedView()) { expand(); } dialogsPanel.reconstruct(Action.INVISIBLE_TO_DEFAULT, ToggleDialog.this); } else { hideDialog(); } } }); addComponentListener(new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { rememberGeometry(); } @Override public void componentResized(ComponentEvent e) { rememberGeometry(); } }); String bounds = Main.pref.get(preferencePrefix+".bounds",null); if (bounds != null) { String[] b = bounds.split(","); setBounds(getDetachedGeometry(new Rectangle( Integer.parseInt(b[0]),Integer.parseInt(b[1]),Integer.parseInt(b[2]),Integer.parseInt(b[3])))); } else { ToggleDialog.this.setPreferredSize(ToggleDialog.this.getDefaultDetachedSize()); pack(); setLocationRelativeTo(Main.parent); } setTitle(titleBar.getTitle()); HelpUtil.setHelpContext(getRootPane(), helpTopic()); } protected void rememberGeometry() { Main.pref.put(preferencePrefix+".bounds", detachedDialog.getX()+","+detachedDialog.getY()+","+detachedDialog.getWidth()+","+detachedDialog.getHeight()); } } /** * Replies the action to toggle the visible state of this toggle dialog * * @return the action to toggle the visible state of this toggle dialog */ public AbstractAction getToggleAction() { return toggleAction; } /** * Replies the prefix for the preference settings of this dialog. * * @return the prefix for the preference settings of this dialog. */ public String getPreferencePrefix() { return preferencePrefix; } /** * Sets the dialogsPanel managing all toggle dialogs */ public void setDialogsPanel(DialogsPanel dialogsPanel) { this.dialogsPanel = dialogsPanel; } /** * Replies the name of this toggle dialog */ @Override public String getName() { return "toggleDialog." + preferencePrefix; } /** * Sets the title */ public void setTitle(String title) { titleBar.setTitle(title); if (detachedDialog != null) { detachedDialog.setTitle(title); } } protected void setIsShowing(boolean val) { isShowing = val; Main.pref.put(preferencePrefix+".visible", val); stateChanged(); } protected void setIsDocked(boolean val) { isDocked = val; Main.pref.put(preferencePrefix+".docked", val); stateChanged(); } protected void setIsCollapsed(boolean val) { isCollapsed = val; Main.pref.put(preferencePrefix+".minimized", val); stateChanged(); } public int getPreferredHeight() { return preferredHeight; } public String helpTopic() { String help = getClass().getName(); help = help.substring(help.lastIndexOf('.')+1, help.length()-6); return "Dialog/"+help; } @Override public String toString() { return name; } /** * Replies true if this dialog is showing either as docked or as detached dialog */ public boolean isDialogShowing() { return isShowing; } /** * Replies true if this dialog is docked and expanded */ public boolean isDialogInDefaultView() { return isShowing && isDocked && (! isCollapsed); } /** * Replies true if this dialog is docked and collapsed */ public boolean isDialogInCollapsedView() { return isShowing && isDocked && isCollapsed; } /*** * The following methods are intended to be overridden, in order to customize * the toggle dialog behavior. **/ /** * Change the Geometry of the detached dialog to better fit the content. */ protected Rectangle getDetachedGeometry(Rectangle last) { return last; } /** * Default size of the detached dialog. * Override this method to customize the initial dialog size. */ protected Dimension getDefaultDetachedSize() { return new Dimension(dialogsPanel.getWidth(), preferredHeight); } /** * Do something when the toggleButton is pressed. */ protected void toggleButtonHook() { } protected boolean dockWhenClosingDetachedDlg() { return true; } /** * primitive stateChangedListener for subclasses */ protected void stateChanged() { } protected JPanel getButtonPanel(int columns) { JPanel pnl = new JPanel(); pnl.setLayout(Main.pref.getBoolean("dialog.align.left", false) ? new FlowLayout(FlowLayout.LEFT) : new GridLayout(1,columns)); return pnl; } }