/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2012 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.button;
import com.mucommander.desktop.DesktopManager;
import com.mucommander.ui.action.impl.MuteProxyAction;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* PopupButton is a compound component that combines a JButton with a JPopupMenu.
*
* <p>When the mouse is held down on the button, a popup menu is displayed below the button. When the button is clicked,
* if a specific action was specified at creation time or using {@link #setAction(Action)}, this action is performed.
* If not, a popup menu is displayed below the button.
*
* <p>This class is abstract. Derived classes must implement {@link #getPopupMenu()} to return a JPopupMenu instance
* to be displayed.
*
* @author Maxence Bernard
*/
public abstract class PopupButton extends NonFocusableButton {
/** Custom action performed when the button is clicked. If null, popup menu will be displayed when mouse is clicked */
private Action buttonClickedAction;
/* Timestamp when popup menu was closed */
private long popupMenuClosedTime;
/** Non-null while popup menu is visible */
private JPopupMenu popupMenu;
/** Location of the popup menu, relative to the button */
private int popupMenuLocation = BOTTOM;
/** Controls the number of milliseconds to hold down the mouse button on the button to display the popup menu */
private final static int POPUP_DELAY = 300;
/**
* Box-orientation constant used to specify the buttom-left oriented side of a box.
*/
public static final int BUTTOM_LEFT_ORIENTED = 5;
/**
* Creates a new PopupButton with no custom action for when this button is clicked.
* When this button is clicked, the popup menu as returned by {@link #getPopupMenu()} will be displayed.
*/
public PopupButton() {
this(null);
}
/**
* Creates a new PopupButton with a custom action to performed when this button is clicked.
*
* @param buttonClickedAction custom action to performed when this button is clicked
*/
public PopupButton(Action buttonClickedAction) {
setAction(buttonClickedAction);
// Listen to mouse events on this button
addMouseListener(new PopupMenuHandler());
}
/**
* Sets the action to be performed when this button is clicked. If <code>null</code> is passed, a popup menu will
* displayed when this button is clicked.
*/
@Override
public void setAction(Action buttonClickedAction) {
if(buttonClickedAction==null) {
super.setAction(null);
}
else {
// Pass a MuteProxyAction to JButton that does nothing when the action is performed.
// We need this to keep the use the Action's properties but handle action events ourself.
super.setAction(new MuteProxyAction(buttonClickedAction));
}
this.buttonClickedAction = buttonClickedAction;
}
public int getPopupMenuLocation() {
return popupMenuLocation;
}
public void setPopupMenuLocation(int location) {
this.popupMenuLocation = location;
}
/**
* Returns true if a popup menu is currently being displayed.
*/
public boolean isPopupMenuVisible() {
return popupMenu!=null;
}
/**
* Displays the popup menu as returned by {@link #getPopupMenu()}.
*/
public synchronized void popupMenu() {
this.popupMenu = getPopupMenu();
popupMenu.addPopupMenuListener(new PopupMenuListener() {
public void popupMenuWillBecomeVisible(PopupMenuEvent popupMenuEvent) {
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent popupMenuEvent) {
popupMenuClosed();
}
public void popupMenuCanceled(PopupMenuEvent popupMenuEvent) {
popupMenuClosed();
}
private void popupMenuClosed() {
// Set popup menu reference to null once it has been made invisible so that it can be garbage collected,
// and remember the time at which the popup was closed, so that a mouse press on the button closes
// the popup menu and does not bring it back up.
popupMenuClosedTime = System.currentTimeMillis();
popupMenu = null;
setSelected(false);
}
});
// Popup up the menu underneath under this button. This has to be executed by the event thread, otherwise some
// weird repaint issue will arise under Windows at least (Note: this method can be executed by a thread other
// than the event thread).
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Dimension popupMenuSize = popupMenu.getPreferredSize();
popupMenu.show(PopupButton.this,
popupMenuLocation==RIGHT?getWidth():popupMenuLocation==LEFT?-(int)popupMenuSize.getWidth():popupMenuLocation==BUTTOM_LEFT_ORIENTED?getWidth()-(int)popupMenuSize.getWidth():0,
popupMenuLocation==BOTTOM?getHeight():popupMenuLocation==TOP?-(int)popupMenuSize.getHeight():popupMenuLocation==BUTTOM_LEFT_ORIENTED?getHeight():0
);
}
});
// Leave the button selected (shows that button has focus) while the popup menu is visible
setSelected(true);
// Note: focus MUST NOT be requested on the popup menu because:
// a/ it's not necessary, focus is automatically transfered to the popup menu
// b/ it creates a weird bug under Windows which prevents enter key from selecting any menu item
}
/**
* This method is invoked when the popup menu is about to be displayed. This method must return the JPopupMenu
* instance to display.
*/
public abstract JPopupMenu getPopupMenu();
///////////////////
// Inner classes //
///////////////////
/**
* This inner class controls this button's behavior and actions when the mouse button is clicked or held down.
*/
private class PopupMenuHandler implements MouseListener, Runnable {
/** Contains the time at which a mouse button was pressed, 0 when a mouse button is not currently being pressed */
private long pressedTime;
/** Returns true to indicate that a mouse event should currently be ignored because the popup menu
* is visible, or was closed recently (less than POPUP_DELAY ms ago) */
private boolean shouldIgnoreMouseEvent() {
return isPopupMenuVisible() || System.currentTimeMillis()- popupMenuClosedTime<POPUP_DELAY;
}
//////////////////////////////////
// MouseListener implementation //
//////////////////////////////////
public synchronized void mousePressed(MouseEvent mouseEvent) {
if(!isEnabled() || shouldIgnoreMouseEvent()) // Ignore event if button is disabled
return;
if (DesktopManager.isRightMouseButton(mouseEvent)) {
popupMenu();
}
else {
pressedTime = System.currentTimeMillis();
// Spawn a thread to check if mouse is still pressed in POPUP_DELAY ms. If that is the case, popup menu
// will be displayed.
new Thread(this).start();
}
}
public synchronized void mouseClicked(MouseEvent mouseEvent) {
if(!isEnabled() || shouldIgnoreMouseEvent()) // Ignore event if button is disabled
return;
// Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
pressedTime = 0;
if(buttonClickedAction !=null) // Perform the action if there is one
buttonClickedAction.actionPerformed(new ActionEvent(PopupButton.this, ActionEvent.ACTION_PERFORMED, "clicked"));
else // No action, popup menu
popupMenu();
}
public synchronized void mouseReleased(MouseEvent mouseEvent) {
// Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
pressedTime = 0;
}
public synchronized void mouseExited(MouseEvent mouseEvent) {
// Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
pressedTime = 0;
}
public void mouseEntered(MouseEvent mouseEvent) {
}
/////////////////////////////
// Runnable implementation //
/////////////////////////////
public void run() {
try { Thread.sleep(POPUP_DELAY); }
catch(InterruptedException e) {}
synchronized(this) {
// Popup menu if a popup menu is not already being displayed and if mouse is still pressed
if(!isPopupMenuVisible() && pressedTime!=0 && System.currentTimeMillis()-pressedTime>=POPUP_DELAY) {
popupMenu();
}
}
}
}
}