001/* [{
002Copyright 2007, 2008 Nicolas Carranza <nicarran at gmail.com>
003
004This file is part of jpen.
005
006jpen is free software: you can redistribute it and/or modify
007it under the terms of the GNU Lesser General Public License as published by
008the Free Software Foundation, either version 3 of the License,
009or (at your option) any later version.
010
011jpen is distributed in the hope that it will be useful,
012but WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014GNU Lesser General Public License for more details.
015
016You should have received a copy of the GNU Lesser General Public License
017along with jpen.  If not, see <http://www.gnu.org/licenses/>.
018}] */
019package jpen;
020
021import java.lang.reflect.InvocationTargetException;
022import java.security.AccessController;
023import java.security.PrivilegedAction;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.EnumMap;
027import java.util.List;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030import java.util.Queue;
031import javax.swing.SwingUtilities;
032import jpen.event.PenListener;
033import jpen.internal.ThreadUtils;
034import jpen.internal.ThrowableUtils;
035
036public class Pen extends PenState {
037        private static final Logger L=Logger.getLogger(Pen.class.getName());
038        //static{L.setLevel(Level.ALL);}
039
040        public static final int DEFAULT_FREQUENCY=60; // TODO: 50 is a better default or less??
041
042        public final PenManager penManager;
043        private int frequency;
044        private volatile MyThread thread;
045
046        /** Tail of event queue. */
047        private PenEvent lastDispatchedEvent=new PenEvent.Dummy();
048        final PenScheduler scheduler;
049        public final PenState lastScheduledState;
050        private final List<PenListener> listeners=new ArrayList<PenListener>();
051        private PenListener[] listenersArray;
052        private boolean firePenTockOnSwing;
053        public final PLevelEmulator levelEmulator;
054
055        private final class MyThread
056                extends Thread {
057                final int periodMillis;
058                long beforeTime;
059                long waitTime;
060                long availablePeriod;
061                PenEvent event;
062                boolean waitedNewEvents;
063                Exception exception;
064                private final Waiter waiter=new Waiter();
065                volatile boolean stopRunning;
066                Thread oldThread;
067
068                final class Waiter
069                        extends Object{
070
071                        boolean waitForNewEvent() throws InterruptedException{
072                                if(lastDispatchedEvent.next!=null)
073                                        return false;
074                                synchronized(this){
075                                        if(lastDispatchedEvent.next!=null)
076                                                return false;
077                                        if(!stopRunning)
078                                                wait(0);
079                                        return true;
080                                }
081                        }
082                        synchronized void notifyNewEvent(){
083                                notify();
084                        }
085                }
086
087                MyThread(Thread oldThread){
088                        periodMillis=1000/Pen.this.frequency;
089                        this.oldThread=oldThread;
090                        setName("jpen-Pen-["+periodMillis+"ms]");
091                        AccessController.doPrivileged(new PrivilegedAction<Object>(){
092                                                //@Override
093                                                public Object run(){
094                                                        setDaemon(true);
095                                                        return null;
096                                                }
097                                        });
098                }
099                private final Runnable penTockFirer=new Runnable(){
100                                        //@Override
101                                        public void run(){
102                                                //System.out.println("firing tocks "+System.currentTimeMillis());
103                                                for(PenListener l:getListenersArray()){
104                                                        //System.out.println("firing pentock, procTime="+evalCurrentProcTime()+", l="+l);
105                                                        l.penTock(availablePeriodLeft());
106                                                }
107                                        }
108                                };
109
110                public void run() {
111                        try {
112                                L.finest("v");
113                                if(oldThread!=null)
114                                        oldThread.join();
115                                oldThread=null;
116                                while(!stopRunning) {
117                                        waitedNewEvents=waiter.waitForNewEvent();
118                                        beforeTime=System.currentTimeMillis();
119                                        if(waitedNewEvents)
120                                                waitTime=0;
121                                        boolean eventDispatched=false;
122                                        while((event=lastDispatchedEvent.next)!=null && event.getTime()<=beforeTime) {
123                                                event.copyTo(Pen.this);
124                                                event.dispatch();
125                                                lastDispatchedEvent.next=null;
126                                                lastDispatchedEvent=event;
127                                                eventDispatched=true;
128                                        }
129                                        //System.out.println("after event dispatching, procTime="+evalCurrentProcTime());
130                                        availablePeriod=periodMillis+waitTime; // waitTime here is always <=0, if it is <0 then the whole processing of the previous round took longer than the time available.
131                                        //System.out.println("going to fire tock "+System.currentTimeMillis());
132                                        if(eventDispatched)
133                                                firePenTock();
134                                        //System.out.println("after penTock, procTime="+evalCurrentProcTime());
135                                        waitTime=availablePeriodLeft();// the same:  (periodMillis-evalCurrentProcTime())+waitTime;
136                                        if(waitTime>0) {
137                                                //System.out.println("going to wait: "+waitTime);
138                                                ThreadUtils.sleepUninterrupted(waitTime);
139                                                //waiter.doWait(waitTime); // in some cases, this (instead of sleepUninterrupted ^ ) gives better overall performance (wintab-pulling, jpen demo). We can put this as an alternate behavior if  needed.
140                                                waitTime=0;
141                                        }
142                                }
143                        } catch(Exception ex) {
144                                L.severe("jpen-Pen thread threw an exception: "+ThrowableUtils.evalStackTraceString(ex));
145                                exception=ex;
146                        }
147                        L.finest("^");
148                }
149
150                private long evalCurrentProcTime(){
151                        return System.currentTimeMillis()-beforeTime;
152                }
153
154                private long availablePeriodLeft(){
155                        return availablePeriod-evalCurrentProcTime();
156                }
157
158                private void firePenTock() throws InterruptedException, InvocationTargetException{
159                        if(getListenersArray().length==0)
160                                return;
161                        if(firePenTockOnSwing)
162                                SwingUtilities.invokeAndWait(penTockFirer);
163                        else
164                                penTockFirer.run();
165                }
166
167                void stop(boolean join){
168                        stopRunning=true;
169                        processNewEvents(); // because it may be waiting for new events.
170                        if(join)
171                                try{
172                                        join();
173                                }
174                                catch(InterruptedException ex) {
175                                        throw new Error(ex);
176                                }
177                }
178        }
179
180        Pen(PenManager penManager) {
181                this.penManager=penManager;
182                this.scheduler=new PenScheduler(this);
183                this.lastScheduledState=scheduler.lastScheduledState;
184                this.levelEmulator=new PLevelEmulator(this);
185                setFrequencyLater(DEFAULT_FREQUENCY);
186        }
187
188        void processNewEvents(){
189                thread.waiter.notifyNewEvent();
190        }
191
192        PenEvent getLastDispatchedEvent(){
193                return lastDispatchedEvent;
194        }
195
196        public boolean getFirePenTockOnSwing() {
197                return firePenTockOnSwing;
198        }
199
200        /**
201        @param firePenTockOnSwing If {@code true} then {@link PenListener#penTock(long)} is called from the event dispatch thread. {@code false} by default.
202        */
203        public void setFirePenTockOnSwing(boolean firePenTockOnSwing){
204                this.firePenTockOnSwing = firePenTockOnSwing;
205        }
206
207        /**
208        Changes the event firing frequency. The pen collects device (tablet) data points and stores them in a buffer. The data  points are taken from this buffer and fired as {@link PenEvent}s at this frequency.<p> 
209
210        This method returns immediately, the change of frequency will happen after all the pending events are processed.
211
212        @see #addListener(PenListener) 
213        @see #removeListener(PenListener)
214        */
215        public void setFrequencyLater(int frequency){
216                setFrequency(frequency, false);
217        }
218
219        private synchronized void setFrequency(int frequency, boolean wait) {
220                if(frequency<=0)
221                        throw new IllegalArgumentException();
222                if(frequency==this.frequency)
223                        return;
224                if(wait && SwingUtilities.isEventDispatchThread())
225                        throw new Error("Cannot call setFrequency(int, <true>) from the event dispatcher thread");
226                L.finest("v");
227                MyThread oldThread=this.thread;
228                if(oldThread!=null){
229                        oldThread.stop(wait);
230                }
231                this.frequency=frequency;
232                thread=new MyThread(oldThread);
233                thread.start();
234                L.finest("^");
235        }
236
237        public int getFrequency() {
238                return frequency;
239        }
240
241        public int getPeriodMillis(){
242                return thread.periodMillis;
243        }
244
245        public synchronized Exception getThreadException(){
246                return thread.exception;
247        }
248
249        /**
250        Adds a {@link PenListener} for {@link PenEvent}s fired by this pen.
251        */
252        public void addListener(PenListener l) {
253                synchronized(listeners) {
254                        listeners.add(l);
255                        listenersArray=null;
256                }
257        }
258
259        /**
260        Removes a {@link PenListener} previously added using {@link #addListener(PenListener)}.
261        */
262        public void removeListener(PenListener l) {
263                synchronized(listeners) {
264                        listeners.remove(l);
265                        listenersArray=null;
266                }
267        }
268
269        PenListener[] getListenersArray() {
270                synchronized(listeners){
271                        if(listenersArray==null)
272                                listenersArray=listeners.toArray(new PenListener[listeners.size()]);
273                        return listenersArray;
274                }
275        }
276}