// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
/**
* Provides a button component that pops up an associated window when the button is pressed.
* It works similar to {@code ButtonPopupMenu}.
*/
public class ButtonPopupWindow extends JButton
{
public enum Align {
/** Use in {@link #setWindowAlignment(int)}. Places the popup window below the button control. */
BOTTOM,
/** Use in {@link #setWindowAlignment(int)}. Places the popup window on top of the button control. */
TOP,
/** Use in {@link #setWindowAlignment(int)}. Places the popup window to the right of the button control. */
RIGHT,
/** Use in {@link #setWindowAlignment(int)}. Places the popup window to the left of the button control. */
LEFT,
}
private final PopupWindow window = new PopupWindow(this);
private final List<PopupWindowListener> listeners = new ArrayList<PopupWindowListener>();
private PopupWindow ignoredWindow; // used to determine whether to hide the current window on lost focus
private Align windowAlign;
private Component content;
public ButtonPopupWindow()
{
super();
init(null, Align.BOTTOM);
}
public ButtonPopupWindow(Component content)
{
super();
init(content, Align.BOTTOM);
}
public ButtonPopupWindow(Component content, Align align)
{
super();
init(content, align);
}
public ButtonPopupWindow(Action a)
{
super(a);
init(null, Align.BOTTOM);
}
public ButtonPopupWindow(Action a, Component content)
{
super(a);
init(content, Align.BOTTOM);
}
public ButtonPopupWindow(Action a, Component content, Align align)
{
super(a);
init(content, align);
}
public ButtonPopupWindow(Icon icon)
{
super(icon);
init(null, Align.BOTTOM);
}
public ButtonPopupWindow(Icon icon, Component content)
{
super(icon);
init(content, Align.BOTTOM);
}
public ButtonPopupWindow(Icon icon, Component content, Align align)
{
super(icon);
init(content, align);
}
public ButtonPopupWindow(String text)
{
super(text);
init(null, Align.BOTTOM);
}
public ButtonPopupWindow(String text, Component content)
{
super(text);
init(content, Align.BOTTOM);
}
public ButtonPopupWindow(String text, Component content, Align align)
{
super(text);
init(content, align);
}
public ButtonPopupWindow(String text, Icon icon)
{
super(text, icon);
init(null, Align.BOTTOM);
}
public ButtonPopupWindow(String text, Icon icon, Component content)
{
super(text, icon);
init(content, Align.BOTTOM);
}
public ButtonPopupWindow(String text, Icon icon, Component content, Align align)
{
super(text, icon);
init(content, align);
}
/** Adds a new PopupWindowListener to this component. */
public void addPopupWindowListener(PopupWindowListener listener)
{
if (listener != null) {
if (listeners.indexOf(listener) < 0) {
listeners.add(listener);
}
}
}
/** Returns all registered PopupWindowListener object. */
public PopupWindowListener[] getPopupWindowListeners()
{
PopupWindowListener[] retVal = new PopupWindowListener[listeners.size()];
for (int i = 0, size = listeners.size(); i < size; i++) {
retVal[i] = listeners.get(i);
}
return retVal;
}
/** Removes a PopupWindowListener from this component. */
public void removePopupWindowListener(PopupWindowListener listener)
{
if (listener != null) {
int idx = listeners.indexOf(listener);
if (idx >= 0) {
listeners.remove(idx);
}
}
}
/**
* Returns the popup window.
* @return The popup window.
*/
public Window getPopupWindow()
{
return window;
}
/**
* Sets new content to the popup window. Old content will be removed.
* @param content The new content of the popup window.
*/
public void setContent(Component content)
{
displayWindow(false);
window.getContentPane().removeAll();
this.content = content;
if (this.content != null) {
window.getContentPane().add(this.content, BorderLayout.CENTER);
}
window.pack();
}
/**
* Returns the currently assigned content of the popup window.
* @return Current content of the popup window. Can be {@code null}.
*/
public Component getContent()
{
return content;
}
/**
* Shows the popup window if it hasn't been activated already.
*/
public void showPopupWindow()
{
if (!window.isVisible()) {
displayWindow(true);
}
}
/**
* Hides the popup window if it isn't hidden already.
*/
public void hidePopupWindow()
{
if (window.isVisible()) {
displayWindow(false);
}
}
/**
* Returns the default alignment of the popup window relative to the associated button control.
* @return The default alignment of the popup window.
*/
public Align getWindowAlignment()
{
return windowAlign;
}
/**
* Specify a new default alignment of the popup window relative to the associated button control.
* Use one of the constants ({@code Align.Bottom}, {@code Align.Top},
* {@code Align.Left}, {@code Align.Right}).
* {@code ButtonPopupWindow.BOTTOM} is the default.
* @param align The new default alignment of the popup window.
*/
public void setWindowAlignment(Align align)
{
switch (align) {
case TOP:
case LEFT:
case RIGHT:
windowAlign = align;
break;
default:
windowAlign = Align.BOTTOM;
}
}
/**
* Registers a custom action for a specific keystroke.
* @param key A unique key to link the keystroke to the action.
* @param keyStroke The keystroke object defining the keyboard input sequence.
* @param action The action to process.
*/
public void addGlobalKeyStroke(Object key, KeyStroke keyStroke, Action action)
{
if (key != null && keyStroke != null && action != null) {
final InputMap inputMap = window.getRootPane().getInputMap(WHEN_IN_FOCUSED_WINDOW);
final ActionMap actionMap = window.getRootPane().getActionMap();
inputMap.put(keyStroke, key);
actionMap.put(key, action);
}
}
/**
* Removes a keystroke action from the window.
* @param key The key which identifies the action.
* @param keyStroke The keystroke which triggers the action.
*/
public void removeGlobalKeyStroke(Object key, KeyStroke keyStroke)
{
if (key != null && keyStroke != null) {
final InputMap inputMap = window.getRootPane().getInputMap(WHEN_IN_FOCUSED_WINDOW);
final ActionMap actionMap = window.getRootPane().getActionMap();
inputMap.remove(keyStroke);
actionMap.remove(key);
}
}
protected void firePopupWindowListener(boolean becomeVisible)
{
PopupWindowEvent event = null;
for (int i = 0, size = listeners.size(); i < size; i++) {
if (event == null) {
event = new PopupWindowEvent(this);
}
if (becomeVisible) {
listeners.get(i).popupWindowWillBecomeVisible(event);
} else {
listeners.get(i).popupWindowWillBecomeInvisible(event);
}
}
}
private void init(Component content, Align align)
{
ignoredWindow = window;
window.addWindowFocusListener(new ButtonWindowListener());
setWindowAlignment(align);
// close popup window on ESC
JRootPane pane = window.getRootPane();
pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), pane);
pane.getActionMap().put(pane, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event)
{
displayWindow(false);
}
});
setContent(content);
addMouseListener(new ButtonPopupListener());
}
private void displayWindow(boolean state)
{
if (state == true) {
showWindow();
} else {
hideWindow();
}
}
private void showWindow()
{
if (!window.isVisible()) {
firePopupWindowListener(true);
// notify the parent window to stay open if of type PopupWindow
PopupWindow parent = getParentPopupWindow(window);
if (parent != null) {
parent.getButton().setIgnoredWindow(window);
}
// determine correct window location
DisplayMode dm = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDisplayMode();
Dimension dimScreen = new Dimension(dm.getWidth(), dm.getHeight());
Rectangle rectButton = new Rectangle(getLocationOnScreen(), getSize());
Dimension dimWin = window.getSize();
Point location = new Point();
if (windowAlign == Align.RIGHT) {
if (dimWin.width >= dimScreen.width - rectButton.x - rectButton.width) {
// show left of the button
location.x = rectButton.x - dimWin.width;
} else {
// show right of the button
location.x = rectButton.x + rectButton.width;
}
} else if (windowAlign == Align.LEFT) {
if (dimWin.width > rectButton.x) {
// show right of the button
location.x = rectButton.x + rectButton.width;
} else {
// show left of the button
location.x = rectButton.x - dimWin.width;
}
} else if (windowAlign == Align.TOP) {
if (dimWin.height > rectButton.y) {
// show below button
location.y = rectButton.y + rectButton.height;
} else {
// show below button
location.y = rectButton.y - dimWin.height;
}
} else { // defaults to Align.Bottom
if (dimWin.height >= dimScreen.height - rectButton.y - rectButton.height) {
// show on top of button
location.y = rectButton.y - dimWin.height;
} else {
// show below button
location.y = rectButton.y + rectButton.height;
}
}
if (windowAlign == Align.RIGHT || windowAlign == Align.LEFT) {
if (dimWin.height < dimScreen.height - rectButton.y) {
// align with button vertically
location.y = rectButton.y;
} else {
location.y = dimScreen.height - dimWin.height;
}
} else {
// considering locale-specific horizontal orientations
if (ComponentOrientation.getOrientation(Locale.getDefault()) == ComponentOrientation.RIGHT_TO_LEFT) {
if (rectButton.x + rectButton.width >= dimWin.width) {
// align with button horizontally
location.x = rectButton.x + rectButton.width - dimWin.width;
} else {
location.x = 0;
}
} else { // default: left-to-right orientation
if (dimWin.width < dimScreen.width - rectButton.x) {
// align with button horizontally
location.x = rectButton.x;
} else {
location.x = dimScreen.width - dimWin.width;
}
}
}
// translate absolute to relative coordinates
if (window.getParent() != null) {
location.x -= window.getParent().getLocation().x;
location.y -= window.getParent().getLocation().y;
}
window.setLocation(location);
window.setVisible(true);
window.requestFocusInWindow();
}
}
private void hideWindow()
{
if (window.isVisible()) {
firePopupWindowListener(false);
window.setVisible(false);
window.getButton().requestFocusInWindow();
}
}
// Returns the direct parent of the specified window if of type PopupWindow, or null if not available
private PopupWindow getParentPopupWindow(PopupWindow wnd)
{
if (wnd != null) {
Window parent = SwingUtilities.getWindowAncestor(wnd.getButton());
if (parent != null && parent instanceof PopupWindow) {
return (PopupWindow)parent;
}
}
return null;
}
private void setIgnoredWindow(PopupWindow wnd)
{
ignoredWindow = wnd;
}
private void notifyCloseWindow(Window wnd)
{
if (wnd != window) {
displayWindow(false);
PopupWindow parent = getParentPopupWindow(window);
if (parent != null) {
parent.getButton().notifyCloseWindow(wnd);
}
}
}
//-------------------------- INNER CLASSES --------------------------
private static final class PopupWindow extends JFrame
{
private ButtonPopupWindow button;
public PopupWindow(ButtonPopupWindow button)
{
this.button = button;
setUndecorated(true);
getRootPane().setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
}
public ButtonPopupWindow getButton()
{
return button;
}
}
private final class ButtonPopupListener extends MouseAdapter
{
public ButtonPopupListener() { super(); }
@Override
public void mousePressed(MouseEvent event)
{
if (event.getSource() instanceof ButtonPopupWindow &&
event.getButton() == MouseEvent.BUTTON1 &&
!event.isPopupTrigger() &&
event.getComponent().isEnabled() &&
window != null) {
displayWindow(!window.isVisible());
}
}
}
private final class ButtonWindowListener extends WindowAdapter
{
public ButtonWindowListener() { super(); }
@Override
public void windowLostFocus(WindowEvent event)
{
if (event.getWindow() == window &&
event.getOppositeWindow() != ignoredWindow) {
notifyCloseWindow(event.getOppositeWindow());
}
ignoredWindow = window; // reset state
}
}
}