/*
* EventManager.java
* de.sciss.app package
*
* Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved.
*
* This software 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 2, june 1991 of the License, or (at your option) any later version.
*
* This software 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 (gpl.txt) along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 20-May-05 created from from de.sciss.meloncillo.util.EventManager
* 06-Aug-05 added dispose()
* 19-Mar-07 collapsing calls to invokeLater
* 08-Apr-08 fixed potential locking issue in run
*/
package de.sciss.app;
import java.awt.EventQueue;
import java.util.ArrayList;
/**
* A custom event dispatcher which
* carefully deals with synchronization issues.
* Assuming, the synchronization requests specified for
* some methods are fulfilled, this class is completely
* thread safe.
* <p>
* It is constructed using a second object, the manager's
* processor which will be invoked whenever new events are
* available in the event FIFO queue. the processor is then
* responsible for querying all registered listeners and
* calling their appropriate event listening methods.
* <p>
* Event dispatching is deferred to the Swing thread execution
* time since this makes the whole application much more
* predictable and easily synchronizable.
*
* @author Hanns Holger Rutz
* @version 0.18, 08-Apr-08
*/
public class EventManager
implements Runnable
{
public static final boolean DEBUG_EVENTS = false;
private final ArrayList collListeners = new ArrayList(); // sync'ed because always in Swing thread
private final ArrayList collQueue = new ArrayList(); // sync'ed through synchronized( this )
private boolean paused = false;
private volatile boolean invoked = false;
protected EventManager.Processor eventProcessor;
private Object[] events = new Object[ 2 ];
public EventManager( EventManager.Processor eventProcessor )
{
this.eventProcessor = eventProcessor;
}
protected EventManager() { /* empty */ }
public void dispose()
{
synchronized( this ) {
collListeners.clear();
collQueue.clear();
}
}
/**
* Adds a new listener. The listener
* will receive all events queued after this
* method is called. Events already in queue
* at the moment this method is called are not
* passed to the listener.
*
* @param listener the listener to add
*/
public void addListener( Object listener )
{
if( listener != null ) {
synchronized( this ) {
// since methods executed within the eventProcessor's run method
// are possible candidates for calling addListener(), we postpone
// the adding so it is acertained that the getListener() calls
// in the eventProcessor's run method won't be disturbed!!
collQueue.add( new PostponedAction( listener, true ));
EventQueue.invokeLater( this );
}
}
}
/**
* Removes a listener. Similar to the
* adding process, the listener won't receive
* any events queued after this method is called.
* However, when there are events in the queue
* at the moment when this method is called, they
* will still be past to the old listener.
*
* @param listener the listener to remove. <code>null</code>
* is allowed (no op).
*/
public void removeListener( Object listener )
{
if( listener != null ) {
synchronized( this ) {
// since methods executed within the eventProcessor's run method
// are possible candidates for calling removeListener(), we postpone
// the adding so it is ascertained that the getListener() calls
// in the eventProcessor's run method won't be disturbed!!
collQueue.add( new PostponedAction( listener, false ));
EventQueue.invokeLater( this );
}
}
}
/**
* Called by add/removeListener and dispatchEvent.
* This method makes the postponed
* collection modifications permanent.
* It calls the eventProcessor as long as there
* are events in the queue.
*/
public void run()
{
final int numEvents;
synchronized( this ) {
invoked = false;
if( paused ) return;
// we only process that many events
// we find NOW in the queue. if the
// event processor or its listeners
// add new events they will be processed
// in the next later invocation
// eventsInCycle = collQueue.size();
numEvents = collQueue.size();
events = collQueue.toArray( events );
collQueue.clear();
}
for( int i = 0; i < numEvents; i++ ) {
if( events[ i ] instanceof BasicEvent ) {
try {
eventProcessor.processEvent( (BasicEvent) events[ i ]);
}
catch( Exception e ) {
e.printStackTrace();
}
} else {
// assert events[ i ] instanceof PostponedAction;
final PostponedAction pa = (PostponedAction) events[ i ];
if( pa.state ) {
if( !collListeners.contains( pa.listener )) {
collListeners.add( pa.listener );
}
} else {
collListeners.remove( pa.listener );
}
// } else {
// assert false : o.getClass().getName();
}
events[ i ] = null;
}
}
/**
* Gets a listener from the list
*
* @synchronization MUST BE CALLED FROM THE EVENT DISPATCH THREAD
*/
public Object getListener( int index )
{
return( collListeners.get( index ));
}
/**
* Get the number of listeners
*
* @synchronization MUST BE CALLED FROM THE EVENT DISPATCH THREAD
*/
public int countListeners()
{
return( collListeners.size() );
}
public void debugDump()
{
for( int i = 0; i < collListeners.size(); i++ ) {
System.err.println( "listen "+i+" = "+collListeners.get( i ).toString() );
}
}
/**
* Puts a new event in the queue.
* If the most recent event can
* be incorporated by the new event,
* it will be replaced, otherwise the new
* one is appended to the end. The
* eventProcessor is invoked asynchronously
* in the Swing event thread
*
* @param e the event to add to the queue.
* before it's added, the event's incorporate
* method will be checked against the most
* recent event in the queue.
*/
public void dispatchEvent( BasicEvent e )
{
final int i;
final boolean invoke;
final Object o;
sync: synchronized( this ) {
invoke = !(paused || invoked);
i = collQueue.size() - 1;
if( i >= 0 ) {
o = collQueue.get( i );
if( (o instanceof BasicEvent) && e.incorporate( (BasicEvent) o )) {
collQueue.set( i, e );
break sync;
}
}
collQueue.add( e );
} // synchronized( this )
if( invoke ) {
invoked = true;
EventQueue.invokeLater( this );
}
}
/**
* Pauses event dispatching.
* Events will still be queued, but the
* dispatcher will wait to call any processors
* until resume() is called.
*/
public void pause()
{
synchronized( this ) {
//System.err.println( "pause" );
paused = true;
} // synchronized( this )
}
/**
* Resumes event dispatching.
* Any events in the queue will be
* distributed as normal.
*/
public void resume()
{
boolean invoke;
synchronized( this ) {
//System.err.println( "resume" );
paused = false;
invoke = !collQueue.isEmpty();
} // synchronized( this )
if( invoke ) EventQueue.invokeLater( this );
}
// -------------------- processor interface --------------------
/**
* Callers of the EventManager constructor
* must provide an object implementing this interface
*/
public interface Processor
{
/**
* Processes the next event in the queue.
* This gets called in the event thread.
* Usually implementing classes should
* loop through all listeners by calling
* elm.countListeners() and elm.getListener(),
* and invoke specific dispatching methods
* on these listeners.
*/
public void processEvent( BasicEvent e );
}
// -------------------- postpone helper class --------------------
private class PostponedAction
{
protected final Object listener;
protected final boolean state;
protected PostponedAction( Object listener, boolean state )
{
this.listener = listener;
this.state = state;
}
}
}