/*
Copyright (c) 2003-2008 ITerative Consulting Pty Ltd. 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 This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 DisplayProject;
import java.awt.AWTException;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.util.Stack;
import java.util.WeakHashMap;
import javax.swing.JComponent;
import javax.swing.Timer;
import org.apache.log4j.Logger;
import Framework.EventManager;
import Framework.IntegerData;
/**
* This class manages the cursors used in the java application. The default
* logic used is:
* <li>When the first event occurs</li>
*/
public class CursorMgr {
/**
* Get the logger associated with the current class
*/
private static Logger _log = Logger.getLogger(CursorMgr.class);
/**
* The time between when an action is fired and when the cursor changes to an hourglass
*/
private static final int cCURSOR_CHANGE_DELAY_START = 500;
/**
* The time between when an action ends and the cursor changes back from being an hourglass (this will only happen if there is another event on the queue)
*/
private static final int cCURSOR_CHANGE_DELAY_END = 10;
private static Cursor hourglassCursor = null;
private static Cursor invalidCursor = null;
/**
* A blank mouse adapter which is used to obtain all the input on the glass pane, otherwise
* the children of the glass pane will process the mouse events as normal
*/
private static final MouseAdapter emptyMouseAdapter = new MouseAdapter() {};
/**
* A Blank key listener to intercept all key events going to the glass pane
*/
private static final KeyListener emptyKeyListener = new KeyListener() {
public void keyPressed(KeyEvent ke) {};
public void keyReleased(KeyEvent ke) {
// Magic shortcut to remove the glass pane in case of calamity
Component c = ke.getComponent();
if (ke.isAltDown() && ke.isControlDown() && ke.getKeyCode() == KeyEvent.VK_F12) {
if (c instanceof JComponent) {
Window window = (Window)((JComponent)c).getTopLevelAncestor();
UIutils.getGlassPane(window);
window.removeMouseListener(emptyMouseAdapter);
window.removeKeyListener(emptyKeyListener);
Component glass = UIutils.getGlassPane(window);
if (glass != null) {
glass.removeMouseListener(emptyMouseAdapter);
glass.removeKeyListener(emptyKeyListener);
glass.setVisible(false);
}
}
System.out.println("Forcibly removed glass pane!");
}
};
public void keyTyped(KeyEvent ke) {};
};
protected static class WindowData {
private static final int cCURSOR_CHANGE_HOURGLASS = 1;
private static final int cCURSOR_CHANGE_RESTORE = 2;
private static final int cINACTIVE = 3;
private static final int cINACTIVE_CHANGE_HOURGLASS = 4;
private Timer timer = null;
private Cursor cursorToRestore = null;
private Stack<Cursor> oldCursors = new Stack<Cursor>();
private Window window = null;
private CursorChanger cursorChanger = new CursorChanger();
/**
* The mode the timer is currently in. Marked as volatile as this one attribute will be referenced
* from both the application thread and the EDT. We use an integerData to get a lockable object
*/
private volatile IntegerData timerMode = new IntegerData(cINACTIVE);
private class CursorChanger implements ActionListener {
public void actionPerformed(ActionEvent evt) {
// This action will be invoked on the EDT
synchronized (WindowData.this.timerMode) {
switch (WindowData.this.timerMode.getValue()) {
case cINACTIVE:
_log.debug("TimerTick::ignored");
break;
case cINACTIVE_CHANGE_HOURGLASS:
// We were waiting to change the cursor to the hourglass, but this
// was boycotted. We don't restore the cursor because it never changed, but
// do still need to remove the glass pane.
// TF:20/6/07 introduced a new cursor handling stack, so it is appropriate
// to restore the old cursor here. (Yes, this is now the same as the last case)
_log.debug("TimerTick::removing glass pane");
WindowData.this.restoreOldCursor();
WindowData.this.removeGlassPane();
break;
case cCURSOR_CHANGE_HOURGLASS:
_log.debug("TimerTick::changing cursor to hourglass");
WindowData.this.changeCursorToHourglass();
break;
case cCURSOR_CHANGE_RESTORE:
_log.debug("TimerTick::restoring old cursor and removing glass pane");
WindowData.this.restoreOldCursor();
WindowData.this.removeGlassPane();
break;
}
WindowData.this.timer = null;
WindowData.this.timerMode.setValue(cINACTIVE);
}
}
}
public WindowData(Window pWindow) {
window = pWindow;
}
public void finishedAction() {
// If we're in starting an action mode (ie the hourglass hasn't changed from the pointer)
// we leave the timer going incase another action starts immediately and we're going to
// still want to change the cursor (but not after a new 0.3 seconds, otherwise a lot of
// quick actions will never change the cursor
popOldCursor();
synchronized (timerMode) {
if (timer == null) {
// No active timer, set one up to restore the cursor
timer = new Timer(cCURSOR_CHANGE_DELAY_END, cursorChanger);
timer.setRepeats(false);
timerMode.setValue(cCURSOR_CHANGE_RESTORE);
timer.start();
}
else {
switch (timerMode.getValue()) {
case cCURSOR_CHANGE_HOURGLASS:
// Still waiting to change the cursor. We want to boycott this action, unless it
// comes in again. Hence we change to INACTIVE_CHANGE_HOURGLASS
timerMode.setValue(cINACTIVE_CHANGE_HOURGLASS);
// TF:28/07/2009:Reset the timer so that we trigger the end cursor.
timer.setDelay(cCURSOR_CHANGE_DELAY_END);
timer.setInitialDelay(cCURSOR_CHANGE_DELAY_END);
timer.restart();
break;
// Otherwise, we're either waiting to change the cursor back to the default, which
// is what we want to do here anyway (and logically shouldn't happen) or it's
// inactive in which case the timer will do nothing. So just let it expire.
}
}
// CraigM:08/05/2008 - If there are no more events to be processed, we can just change the cursor back immediately.
if (EventManager.hasPendingEvents() == false) {
// TF:27/08/2008:Changed this to actually trigger the timer immediately. We set the delay to something
// finite (ie non-zero) to ensure we don't get multiple possible firings of the timer.
timer.setDelay(10000);
timer.setInitialDelay(0);
timer.restart();
}
}
// This is now done by the timer
// removeGlassPane();
}
public void startAction() {
installGlassPane();
synchronized (timerMode) {
pushOldCursor();
if (timer == null) {
// No active timer, set one up to change the cursor
timer = new Timer(cCURSOR_CHANGE_DELAY_START, cursorChanger);
timer.setRepeats(false);
timerMode.setValue(cCURSOR_CHANGE_HOURGLASS);
timer.start();
}
else {
switch (timerMode.getValue()) {
case cINACTIVE_CHANGE_HOURGLASS:
// The cursor was going to change to an hourglass, but this operation got
// cancelled, but is still pending. Just restore it, so quick events will
// not flicker the cursor.
timerMode.setValue(cCURSOR_CHANGE_HOURGLASS);
break;
case cCURSOR_CHANGE_RESTORE:
// The timer is active to restore the cursor. However, now we want the cursor
// as the hourglass (which it currently is), so kill the timer. Do not set it
// to INACTIVE as this will not restore the glass pane later.
timer.stop();
timer = null;
timerMode.setValue(cINACTIVE);
}
}
}
}
public void installGlassPane() {
final GlassPaneWithEvents glass = UIutils.getGlassPane(window);
if (!glass.isVisible()) {
UIutils.invokeOnGuiThread(new Runnable() {
public void run() {
if (glass != null && !glass.isVisible()) {
// TF:19/9/07:Commented this out because it's not the way Forte works, and introduces problems
// CraigM: 31/03/2008: Added back as we now have GlassPaneWithEvents.
glass.setVisible(true);
glass.setEnabled(true);
glass.addMouseListener(emptyMouseAdapter);
glass.addKeyListener(emptyKeyListener);
}
window.addMouseListener(emptyMouseAdapter);
window.addKeyListener(emptyKeyListener);
}
});
}
}
public void removeGlassPane() {
UIutils.processGUIActions();
UIutils.invokeOnGuiThread( new Runnable() {
public void run() {
window.removeMouseListener(emptyMouseAdapter);
window.removeKeyListener(emptyKeyListener);
Component glass = UIutils.getGlassPane(window);
if (glass != null) {
glass.removeMouseListener(emptyMouseAdapter);
glass.removeKeyListener(emptyKeyListener);
glass.setVisible(false);
}
}
});
}
public synchronized void pushOldCursor() {
_log.debug("pushing old cursor");
if (!this.oldCursors.isEmpty()) {
// This is a nested start action, so we must use a Timer cursor
if (hourglassCursor == null) {
loadCursors();
}
this.oldCursors.push(hourglassCursor);
_log.debug(" stack depth currently " + (this.oldCursors.size() - 1) + ", pushing an hourglass");
}
else if (this.cursorToRestore != null) {
// We're about to change the cursor back to the default, stop this
this.oldCursors.push(this.cursorToRestore);
_log.debug(" pushing " + this.cursorToRestore);
}
else {
this.oldCursors.push(window.getCursor());
_log.debug(" pushing " + window.getCursor());
}
this.cursorToRestore = null;
}
public synchronized void popOldCursor() {
_log.debug("popping old cursor");
if(!this.oldCursors.isEmpty()) // Should never happen, here as a safeguard
{
Cursor nextCursor = this.oldCursors.pop();
// JVM 1.4 Cursor nextCursor = (Cursor)this.oldCursor.pop();
if (this.oldCursors.isEmpty()) {
this.cursorToRestore = nextCursor;
_log.debug(" setting cursor to restore to be " + nextCursor);
}
else {
_log.debug(" stack not empty, leaving as default");
}
}
}
public synchronized void restoreOldCursor() {
if (this.cursorToRestore != null) {
_log.debug("Restoring old cursor to " + this.cursorToRestore);
window.setCursor(this.cursorToRestore);
this.cursorToRestore = null;
}
// CraigM:08/05/2008 - We need to clear out the old cursor on the glass pane, as when the
// glass pane comes up again we still want a delay before the hourglassCursor appears.
setCursor(null);
}
public void changeCursorToHourglass() {
if (hourglassCursor == null) {
loadCursors();
}
setCursor(hourglassCursor);
}
public void changeCursorToInvalid() {
if (invalidCursor == null) {
loadCursors();
}
// We must kill off the timer first, so as we don't change it straight back to an hourglass when the timer ticks
if (timer != null) {
timer.stop();
timer = null;
timerMode.setValue(cINACTIVE);
}
setCursor(invalidCursor);
}
private void setCursor(final Cursor cursor) {
UIutils.invokeOnGuiThread(new Runnable() {
public void run() {
_log.debug("Setting cursor to " + cursor);
// TF:16/11/07:Removed this as it can conflict with user code which sets the window cursor explicitly
//window.setCursor(cursor);
Component glass = UIutils.getGlassPane(window);
if (glass != null) {
glass.setCursor(cursor);
}
}
});
}
}
private static WeakHashMap<Window, WindowData> windowData = new WeakHashMap<Window, WindowData>();
protected static void loadCursors() {
if (invalidCursor == null) {
try {
invalidCursor = Cursor.getSystemCustomCursor("Invalid.32x32");
}
catch (AWTException e) {
invalidCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
}
if (hourglassCursor == null) {
hourglassCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
}
}
private static synchronized WindowData create(Window pWindow) {
WindowData data = new WindowData(pWindow);
windowData.put(pWindow, data);
return data;
}
private static synchronized WindowData find(Window pWindow) {
return (WindowData)windowData.get(pWindow);
}
private static synchronized void destroy(Window pWindow) {
WindowData data = find(pWindow);
if (data != null) {
windowData.remove(pWindow);
}
}
/**
* Invoke this method when the user begins an action on the window. This method should not run on the EDT
*/
public static void startEvent() {
Window window = WindowManager.getWindowToActivate(true);
if (window != null) {
WindowData data = find(window);
if (data == null) {
data = create(window);
}
data.startAction();
}
}
/**
* Invoke this method when the user begins an action on the window.
*/
public static void startEvent(Window win) {
Window window = WindowManager.getWindowToActivate(win, true);
if (window != null) {
WindowData data = find(window);
if (data == null) {
data = create(window);
}
data.startAction();
}
}
/**
* Invoke this method when the user finishes the action on the window. This method should not run on the EDT
*/
public static void endEvent() {
Window window = WindowManager.getWindowToActivate(true);
if (window != null) {
WindowData data = find(window);
if (data != null) {
data.finishedAction();
}
}
}
/**
* Invoke this method when the user finishes the action on the window.
*/
public static void endEvent(Window win) {
Window window = WindowManager.getWindowToActivate(win, true);
if (window != null) {
WindowData data = find(window);
if (data != null) {
data.finishedAction();
}
}
}
/**
* Invoked when a window is created. This method is assumed to run not on the EDT
* @param pWindow
*/
public static void createWindow(Window pWindow) {
Window prevWin = WindowManager.getPreviousWindow(pWindow);
// if there is a previous window set the cursor to be disabled
if (prevWin != null) {
WindowData data = find(prevWin);
if (data == null) {
data = create(pWindow);
}
data.changeCursorToInvalid();
}
create(pWindow);
}
/**
* Invoked when a window is destroyed. This method is assumed to be run not on the EDT
* @param pWindow
*/
public static void destroyWindow(Window pWindow) {
Window prevWin = WindowManager.getPreviousWindow(pWindow);
// if there is a previous window set the cursor to be an hourglass (Not the default pointer
// because we're still in an event loop or wait state.)
if (prevWin != null) {
WindowData data = find(prevWin);
if (data != null) {
data.changeCursorToHourglass();
}
}
destroy(pWindow);
}
}