/*
* Copyright (c) 2005-2010 Flamingo Kirill Grouchnikov. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of Flamingo Kirill Grouchnikov nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.pushingpixels.flamingo.internal.ui.common.popup;
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.ComboPopup;
import org.pushingpixels.flamingo.api.common.JCommandButton;
import org.pushingpixels.flamingo.api.common.popup.JPopupPanel;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager.PopupEvent;
import org.pushingpixels.flamingo.api.ribbon.JRibbon;
import org.pushingpixels.flamingo.internal.ui.ribbon.JRibbonTaskToggleButton;
import org.pushingpixels.flamingo.internal.ui.ribbon.appmenu.JRibbonApplicationMenuPopupPanel;
import org.pushingpixels.flamingo.internal.utils.FlamingoUtilities;
import org.pushingpixels.flamingo.internal.utils.KeyTipManager;
/**
* Basic UI for popup panel {@link JPopupPanel}.
*
* @author Kirill Grouchnikov
*/
public class BasicPopupPanelUI extends PopupPanelUI {
/**
* The associated popup panel.
*/
protected JPopupPanel popupPanel;
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
*/
public static ComponentUI createUI(JComponent c) {
return new BasicPopupPanelUI();
}
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#installUI(javax.swing.JComponent)
*/
@Override
public void installUI(JComponent c) {
this.popupPanel = (JPopupPanel) c;
super.installUI(this.popupPanel);
installDefaults();
installComponents();
installListeners();
}
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#uninstallUI(javax.swing.JComponent)
*/
@Override
public void uninstallUI(JComponent c) {
uninstallListeners();
uninstallComponents();
uninstallDefaults();
super.uninstallUI(this.popupPanel);
}
/**
* Installs default settings for the associated command popup menu.
*/
protected void installDefaults() {
Color bg = this.popupPanel.getBackground();
if (bg == null || bg instanceof UIResource) {
this.popupPanel.setBackground(FlamingoUtilities.getColor(
Color.lightGray, "PopupPanel.background",
"Panel.background"));
}
Border b = this.popupPanel.getBorder();
if (b == null || b instanceof UIResource) {
Border toSet = UIManager.getBorder("PopupPanel.border");
if (toSet == null)
toSet = new BorderUIResource.CompoundBorderUIResource(
new LineBorder(FlamingoUtilities.getBorderColor()),
new EmptyBorder(1, 1, 1, 1));
this.popupPanel.setBorder(toSet);
}
LookAndFeel.installProperty(this.popupPanel, "opaque", Boolean.TRUE);
}
/**
* Installs listeners on the associated command popup menu.
*/
protected void installListeners() {
initiliazeGlobalListeners();
}
/**
* Installs components on the associated command popup menu.
*/
protected void installComponents() {
}
/**
* Uninstalls default settings from the associated command popup menu.
*/
protected void uninstallDefaults() {
LookAndFeel.uninstallBorder(this.popupPanel);
}
/**
* Uninstalls listeners from the associated command popup menu.
*/
protected void uninstallListeners() {
}
/**
* Uninstalls subcomponents from the associated command popup menu.
*/
protected void uninstallComponents() {
}
/**
* The global listener that tracks the ESC key action on the root panes of
* windows that show popup panels.
*/
static PopupPanelManager.PopupListener popupPanelManagerListener;
/**
* Initializes the global listeners.
*/
protected static synchronized void initiliazeGlobalListeners() {
if (popupPanelManagerListener != null) {
return;
}
popupPanelManagerListener = new PopupPanelEscapeDismisser();
PopupPanelManager.defaultManager().addPopupListener(
popupPanelManagerListener);
new WindowTracker();
}
/**
* This class is used to trace the changes in the shown popup panels and
* install ESC key listener on the matching root pane so that the popup
* panels can be dismissed with the ESC key.
*
* @author Kirill Grouchnikov
*/
protected static class PopupPanelEscapeDismisser implements
PopupPanelManager.PopupListener {
/**
* The currently installed action map on the {@link #tracedRootPane}.
*/
private ActionMap newActionMap;
/**
* The currently installed input map on the {@link #tracedRootPane}.
*/
private InputMap newInputMap;
/**
* The last shown popup panel sequence.
*/
List<PopupPanelManager.PopupInfo> lastPathSelected;
/**
* Currently traced root pane. It is the root pane of the originating
* component of the first popup panel in the currently shown sequence of
* {@link PopupPanelManager}.
*/
private JRootPane tracedRootPane;
/**
* Creates a new tracer for popup panels to be dismissed with ESC key.
*/
public PopupPanelEscapeDismisser() {
PopupPanelManager popupPanelManager = PopupPanelManager
.defaultManager();
this.lastPathSelected = popupPanelManager.getShownPath();
if (this.lastPathSelected.size() != 0) {
traceRootPane(this.lastPathSelected);
}
}
@Override
public void popupHidden(PopupEvent event) {
PopupPanelManager msm = PopupPanelManager.defaultManager();
List<PopupPanelManager.PopupInfo> p = msm.getShownPath();
if (lastPathSelected.size() != 0 && p.size() == 0) {
// if it is the last popup panel to be dismissed, untrace the
// root pane
untraceRootPane();
}
lastPathSelected = p;
}
/**
* Removes the installed maps on the currently traced root pane.
*/
private void untraceRootPane() {
if (this.tracedRootPane != null) {
removeUIActionMap(this.tracedRootPane, this.newActionMap);
removeUIInputMap(this.tracedRootPane, this.newInputMap);
}
}
@Override
public void popupShown(PopupEvent event) {
PopupPanelManager msm = PopupPanelManager.defaultManager();
List<PopupPanelManager.PopupInfo> p = msm.getShownPath();
if (lastPathSelected.size() == 0 && p.size() != 0) {
// if it is the first popup panel to be shown, trace the root
// panel
traceRootPane(p);
}
lastPathSelected = p;
}
/**
* Installs the maps on the root pane of the originating component of
* the first popup panel of the specified sequence to trace the ESC key
* and dismiss the shown popup panels.
*
* @param shownPath
* Popup panel sequence.
*/
private void traceRootPane(List<PopupPanelManager.PopupInfo> shownPath) {
JComponent originator = shownPath.get(0).getPopupOriginator();
this.tracedRootPane = SwingUtilities.getRootPane(originator);
if (this.tracedRootPane != null) {
newInputMap = new ComponentInputMapUIResource(tracedRootPane);
newInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
"hidePopupPanel");
newActionMap = new ActionMapUIResource();
newActionMap.put("hidePopupPanel", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
// Hide the last sequence popup for every ESC keystroke.
// There is special case - if the keytips are shown
// for the *second* panel of the app menu popup panel,
// do not dismiss the popup
List<PopupPanelManager.PopupInfo> popups = PopupPanelManager
.defaultManager().getShownPath();
if (popups.size() > 0) {
PopupPanelManager.PopupInfo lastPopup = popups
.get(popups.size() - 1);
if (lastPopup.getPopupPanel() instanceof JRibbonApplicationMenuPopupPanel) {
JRibbonApplicationMenuPopupPanel appMenuPopupPanel = (JRibbonApplicationMenuPopupPanel) lastPopup
.getPopupPanel();
KeyTipManager.KeyTipChain currentlyShownKeyTipChain = KeyTipManager
.defaultManager()
.getCurrentlyShownKeyTipChain();
if ((currentlyShownKeyTipChain != null)
&& (currentlyShownKeyTipChain.chainParentComponent == appMenuPopupPanel
.getPanelLevel2()))
return;
}
}
PopupPanelManager.defaultManager().hideLastPopup();
}
});
addUIInputMap(tracedRootPane, newInputMap);
addUIActionMap(tracedRootPane, newActionMap);
}
}
/**
* Adds the specified input map to the specified component.
*
* @param c
* Component.
* @param map
* Input map to add.
*/
void addUIInputMap(JComponent c, InputMap map) {
InputMap lastNonUI = null;
InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
while (parent != null && !(parent instanceof UIResource)) {
lastNonUI = parent;
parent = parent.getParent();
}
if (lastNonUI == null) {
c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
} else {
lastNonUI.setParent(map);
}
map.setParent(parent);
}
/**
* Adds the specified action map to the specified component.
*
* @param c
* Component.
* @param map
* Action map to add.
*/
void addUIActionMap(JComponent c, ActionMap map) {
ActionMap lastNonUI = null;
ActionMap parent = c.getActionMap();
while (parent != null && !(parent instanceof UIResource)) {
lastNonUI = parent;
parent = parent.getParent();
}
if (lastNonUI == null) {
c.setActionMap(map);
} else {
lastNonUI.setParent(map);
}
map.setParent(parent);
}
/**
* Removes the specified input map from the specified component.
*
* @param c
* Component.
* @param map
* Input map to remove.
*/
void removeUIInputMap(JComponent c, InputMap map) {
InputMap im = null;
InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
while (parent != null) {
if (parent == map) {
if (im == null) {
c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map
.getParent());
} else {
im.setParent(map.getParent());
}
break;
}
im = parent;
parent = parent.getParent();
}
}
/**
* Removes the specified action map from the specified component.
*
* @param c
* Component.
* @param map
* Action map to remove.
*/
void removeUIActionMap(JComponent c, ActionMap map) {
ActionMap im = null;
ActionMap parent = c.getActionMap();
while (parent != null) {
if (parent == map) {
if (im == null) {
c.setActionMap(map.getParent());
} else {
im.setParent(map.getParent());
}
break;
}
im = parent;
parent = parent.getParent();
}
}
}
/**
* This class is used to dismiss popup panels on the following events:
*
* <ul>
* <li>Mouse click outside any shown popup panel.</li>
* <li>Closing, iconifying or deactivation of a top-level window.</li>
* <li>Any change in the component hierarchy of a top-level window.</li>
* </ul>
*
* Only one top-level window is tracked at any time. The assumption is that
* the {@link PopupPanelManager} only shows popup panels originating from
* one top-level window.
*
* @author Kirill Grouchnikov
*/
protected static class WindowTracker implements
PopupPanelManager.PopupListener, AWTEventListener,
ComponentListener, WindowListener {
/**
* The currently tracked window. It is the window of the originating
* component of the first popup panel in the currently shown sequence of
* {@link PopupPanelManager}.
*/
Window grabbedWindow;
/**
* Last selected path in the {@link PopupPanelManager}.
*/
List<PopupPanelManager.PopupInfo> lastPathSelected;
/**
* Creates the new window tracker.
*/
public WindowTracker() {
PopupPanelManager popupPanelManager = PopupPanelManager
.defaultManager();
popupPanelManager.addPopupListener(this);
this.lastPathSelected = popupPanelManager.getShownPath();
if (this.lastPathSelected.size() != 0) {
grabWindow(this.lastPathSelected);
}
}
/**
* Grabs the window of the first popup panel in the specified popup
* panel sequence.
*
* @param shownPath
* Sequence of the currently shown popup panels.
*/
void grabWindow(List<PopupPanelManager.PopupInfo> shownPath) {
final Toolkit tk = Toolkit.getDefaultToolkit();
java.security.AccessController
.doPrivileged(new java.security.PrivilegedAction() {
@Override
public Object run() {
tk.addAWTEventListener(WindowTracker.this,
AWTEvent.MOUSE_EVENT_MASK
| AWTEvent.MOUSE_MOTION_EVENT_MASK
| AWTEvent.MOUSE_WHEEL_EVENT_MASK
| AWTEvent.WINDOW_EVENT_MASK);
return null;
}
});
Component invoker = shownPath.get(0).getPopupOriginator();
grabbedWindow = invoker instanceof Window ? (Window) invoker
: SwingUtilities.getWindowAncestor(invoker);
if (grabbedWindow != null) {
grabbedWindow.addComponentListener(this);
grabbedWindow.addWindowListener(this);
}
}
/**
* Ungrabs the currently tracked window.
*/
void ungrabWindow() {
final Toolkit tk = Toolkit.getDefaultToolkit();
// The grab should be removed
java.security.AccessController
.doPrivileged(new java.security.PrivilegedAction() {
@Override
public Object run() {
tk.removeAWTEventListener(WindowTracker.this);
return null;
}
});
if (grabbedWindow != null) {
grabbedWindow.removeComponentListener(this);
grabbedWindow.removeWindowListener(this);
grabbedWindow = null;
}
}
@Override
public void popupShown(PopupEvent event) {
PopupPanelManager msm = PopupPanelManager.defaultManager();
List<PopupPanelManager.PopupInfo> p = msm.getShownPath();
if (lastPathSelected.size() == 0 && p.size() != 0) {
// if it is the first popup panel to be shown, grab its window
grabWindow(p);
}
lastPathSelected = p;
}
@Override
public void popupHidden(PopupEvent event) {
PopupPanelManager msm = PopupPanelManager.defaultManager();
List<PopupPanelManager.PopupInfo> p = msm.getShownPath();
if (lastPathSelected.size() != 0 && p.size() == 0) {
// if it is the last popup panel to be hidden, ungrab its window
ungrabWindow();
}
lastPathSelected = p;
}
@Override
public void eventDispatched(AWTEvent ev) {
if (!(ev instanceof MouseEvent)) {
// We are interested in MouseEvents only
return;
}
MouseEvent me = (MouseEvent) ev;
final Component src = me.getComponent();
JPopupPanel popupPanelParent = (JPopupPanel) SwingUtilities
.getAncestorOfClass(JPopupPanel.class, src);
switch (me.getID()) {
case MouseEvent.MOUSE_PRESSED:
boolean wasCommandButtonPopupShowing = false;
if (src instanceof JCommandButton) {
wasCommandButtonPopupShowing = ((JCommandButton) src)
.getPopupModel().isPopupShowing();
}
if (!wasCommandButtonPopupShowing && (popupPanelParent != null)) {
// close all popups until this parent and return
PopupPanelManager.defaultManager().hidePopups(
popupPanelParent);
return;
}
if (src instanceof JRibbonTaskToggleButton) {
JRibbon ribbon = (JRibbon) SwingUtilities
.getAncestorOfClass(JRibbon.class, src);
if ((ribbon != null)
&& FlamingoUtilities
.isShowingMinimizedRibbonInPopup(ribbon)) {
// This will be handled in the action listener installed
// on ribbon task toggle buttons in BasicRibbonUI.
// There the ribbon popup will be hidden.
return;
}
}
// if the popup of command button was showing, it will be hidden
// in BasicCommandButtonUI.processPopupAction() - via
// BasicCommandButtonUI.createPopupActionListener().
if (!wasCommandButtonPopupShowing) {
// special case - ignore mouse press on an item in a combo popup
if (SwingUtilities
.getAncestorOfClass(ComboPopup.class, src) == null) {
PopupPanelManager.defaultManager().hidePopups(src);
}
}
// pass the event so that it gets processed by the controls
break;
case MouseEvent.MOUSE_RELEASED:
// special case - mouse release on an item in a combo popup
if (SwingUtilities.getAncestorOfClass(ComboPopup.class, src) != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
PopupPanelManager.defaultManager().hidePopups(src);
}
});
}
// pass the event so that it gets processed by the controls
break;
case MouseEvent.MOUSE_WHEEL:
if (popupPanelParent != null) {
// close all popups until this parent and return
PopupPanelManager.defaultManager().hidePopups(
popupPanelParent);
return;
}
PopupPanelManager.defaultManager().hidePopups(src);
break;
}
}
/**
* Checks whether the specified component lies inside a
* {@link JPopupPanel}.
*
* @param src
* Component.
* @return <code>true</code> if the specified component lies inside a
* {@link JPopupPanel}.
*/
boolean isInPopupPanel(Component src) {
for (Component c = src; c != null; c = c.getParent()) {
if (c instanceof Applet || c instanceof Window) {
break;
} else if (c instanceof JPopupPanel) {
return true;
}
}
return false;
}
@Override
public void componentResized(ComponentEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void componentMoved(ComponentEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void componentShown(ComponentEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void componentHidden(ComponentEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void windowClosing(WindowEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void windowClosed(WindowEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void windowIconified(WindowEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void windowDeactivated(WindowEvent e) {
PopupPanelManager.defaultManager().hidePopups(null);
}
@Override
public void windowOpened(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
}
}