package ie.dcu.apps.ist.widgets; import ie.dcu.apps.ist.event.*; import java.util.ArrayList; import org.eclipse.swt.SWT; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.*; /** * A timer user interface element. Displays a component used * to implement a count-down. * * @author Kevin McGuinness */ public class SwtTimer implements TickerListener { /** * Timer is modeled as a state machine. These are the states. */ public static enum State { Initial, Ready, Running, Paused }; /** * List of listeners interested in changes to the object state. */ private final ArrayList stateListeners; /** * List of listeners interested in timeouts. */ private final ArrayList timeoutListeners; /** * The drawing canvas where the numbers are drawn. */ private final Canvas canvas; /** * The ticker posts tick events every second or so. */ private Ticker ticker; /** * The current state of the timer. */ private State state; /** * The font used to render the numbers. */ private Font font; /** * Total time for the timer, in seconds. */ private int time; /** * Remaining (displayed) time, in seconds. */ private int remaining; /** * Create the timer. Style bits are passed to the underlying canvas. * * @param parent * Parent container. * @param style * Style bits. */ public SwtTimer(Composite parent, int style) { stateListeners = new ArrayList(1); timeoutListeners = new ArrayList(1); canvas = new Canvas(parent, style); addListeners(); enter(State.Initial); } /** * Add listeners to various components. */ private void addListeners() { // Listen for paint events canvas.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { paint(e.gc); } }); // Listen for dispose event canvas.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { dispose(); } }); } /** * Set the layout data for the component. * * @param data * The layout data. */ public void setLayoutData(Object data) { canvas.setLayoutData(data); } /** * Returns the layout data for the component. * * @return The layout data. */ public Object getLayoutData() { return canvas.getLayoutData(); } /** * Get the contained canvas. * * @return The contained canvas. */ public Canvas getCanvas() { return canvas; } /** * Returns the shell associated with the canvas. * * @return The shell. */ public Shell getShell() { return canvas.getShell(); } /** * Get the state of the timer. * * @return The timer state. */ public State getState() { return state; } /** * Add a state change listener. * * @param listener * The listener. */ public void addStateListener(StateListener listener) { stateListeners.add(listener); } /** * Remove a state change listener. * * @param listener * The listener. */ public void removeStateListener(StateListener listener) { stateListeners.remove(listener); } /** * Add a time-out listener. * * @param listener * The listener. */ public void addTimeoutListener(TimeoutListener listener) { timeoutListeners.add(listener); } /** * Remove a time-out listener. * * @param listener * The listener. */ public void removeTimeoutListener(TimeoutListener listener) { timeoutListeners.remove(listener); } /** * Returns an estimate of the elapsed time, in seconds. * * @return The elapsed time. */ public int getElapsed() { return time - remaining; } /** * Set the timeout for the timer, in seconds. * * @param seconds * The number of seconds on the timer. * @throws IllegalStateException * If {@link #canSet()} is false. */ public void set(int seconds) throws IllegalStateException { switch (state) { case Initial: case Ready: time = seconds; remaining = time; break; default: throw new IllegalStateException(); } enter(State.Ready); repaint(); } /** * Returns true if set can be called. Set cannot be called when * the timer is running or paused. * * @return true if set can be called. */ public boolean canSet() { switch (state) { case Initial: case Ready: return true; } return false; } /** * Start the timer. A new thread will control the timer count-down, so this * method returns once it has started. * * @throws IllegalStateException * If {@link #canStart()} is false. */ public void start() throws IllegalStateException { switch (state) { case Ready: ticker = new Ticker(this, 1000); ticker.start(); break; case Paused: ticker.resume(); break; default: throw new IllegalStateException(); } enter(State.Running); repaint(); } /** * Returns true if start can be called. Start cannot be called * when the timer is in it's initial or running state. * * @return true if start can be called. */ public boolean canStart() { switch (state) { case Ready: case Paused: return true; } return false; } /** * Pause the count-down. Temporarily suspends the count-down thread. * * @throws IllegalStateException * If {@link #canPause()} is false. */ public void pause() throws IllegalStateException { switch (state) { case Running: ticker.pause(); break; case Paused: break; default: throw new IllegalStateException(); } enter(State.Paused); repaint(); } /** * Returns true if pause can be called. Pause cannot be called * when the timer is in it's initial or ready state. * * @return true if pause can be called. */ public boolean canPause() { switch (state) { case Running: case Paused: return true; } return false; } /** * Reset the timer. Causes the timer to stop and return to the ready * state, reseting the time-out to the last one set. * * @throws IllegalStateException * If {@link #canReset()} is false. */ public void reset() throws IllegalStateException { switch (state) { case Ready: break; case Running: case Paused: ticker.stop(); ticker = null; remaining = time; break; default: throw new IllegalStateException(); } enter(State.Ready); repaint(); } /** * Returns true if reset can be called. Reset cannot be called * when the timer is in it's initial state. * * @return true if reset can be called. */ public boolean canReset() { switch (state) { case Ready: case Running: case Paused: return true; } return false; } /** * Clear the timer. Returns the timer to its initial state, stopping * it if necessary. */ public void clear() { switch (state) { case Initial: break; case Ready: time = 0; remaining = 0; break; case Running: case Paused: ticker.stop(); ticker = null; remaining = 0; time = 0; break; } enter(State.Initial); repaint(); } /** * Returns true if clear can be called. * * @return Always returns true. */ public boolean canClear() { return true; } /** * Tell the timer to repaint itself. */ public void repaint() { if (!canvas.isDisposed()) { canvas.redraw(); } } /** * Called at each ticker tick interval. Should not be invoked by clients. */ public void tick(final TickerEvent evt) { if (canvas.isDisposed()) { // Widget is disposed, ignore timer events return; } long elapsed = evt.getElapsed(); int remaining = time - (int) (elapsed / 1000); if (remaining <= 0) { // We're done this.remaining = 0; // Stop and nullify ticker if (ticker != null) { ticker.stopLater(); ticker = null; } // Enqueue the ui update the event dispatch thread canvas.getDisplay().asyncExec(new Runnable() { public void run() { enter(State.Initial); // repaint repaint(); // fire timeout event fireTimeoutEvent(); } }); } else if (this.remaining != remaining) { // Update remaining this.remaining = remaining; // Enqueue a repaint canvas.getDisplay().asyncExec(new Runnable() { public void run() { // repaint repaint(); } }); } } /** * Enter the given state. * * @param state The state. */ private void enter(State state) { if (this.state != state) { this.state = state; fireStateChanged(); } } /** * Fires a state changed event. */ private void fireStateChanged() { if (!stateListeners.isEmpty()) { StateEvent evt = new StateEvent(this); for (StateListener s : stateListeners) { s.stateChanged(evt); } } } /** * Sends a timeout event to listeners. */ private void fireTimeoutEvent() { if (!timeoutListeners.isEmpty()) { TimeoutEvent evt = new TimeoutEvent(this); for (TimeoutListener t : timeoutListeners) { t.timeoutOccured(evt); } } } /** * Paints the timer. * * @param gc * The graphics context. */ private void paint(GC gc) { gc.setFont(getTimerFont()); int ss = remaining % 60; int mm = (remaining % 3600) / 60; int hh = remaining / 3600; String timestr = formatTime(ss, mm, hh); Point t = getStringDimensions(gc, timestr); Point c = getCanvasDimension(); int x = c.x / 2 - t.x / 2; int y = c.y / 2 - t.y / 2; if (remaining < 20 && state != State.Initial) { gc.setForeground(getWarningColor()); } else { gc.setForeground(getTimerColor()); } gc.drawString(timestr, x, y); } /** * Tidies native resources. Called automatically. */ private void dispose() { if (font != null) { font.dispose(); font = null; } } /** * Formats a time string. */ private String formatTime(int ss, int mm, int hh) { String timestr; if (hh == 0) { timestr = String.format("%02d:%02d", mm, ss); } else { timestr = String.format("%02d:%02d:%02d", hh, mm, ss); } return timestr; } /** * Returns the dimensions of a given string as drawn with the current font on * the given graphics context. */ private static Point getStringDimensions(GC gc, String str) { int x = 0; for (int i = 0; i < str.length(); i++) { x += gc.getAdvanceWidth(str.charAt(i)); } int y = gc.getFontMetrics().getHeight(); return new Point(x,y); } /** * Returns the dimensions of the canvas as a point. * * @return A point where x is the width and y is the height. */ private Point getCanvasDimension() { Rectangle bounds = canvas.getBounds(); return new Point(bounds.width, bounds.height); } private Color getWarningColor() { return getSystemColor(SWT.COLOR_RED); } private Color getTimerColor() { return getSystemColor(SWT.COLOR_DARK_BLUE); } private Font getTimerFont() { if (font == null) { font = new Font(canvas.getDisplay(), "Sans", 30, SWT.NONE); } return font; } private Color getSystemColor(int id) { return canvas.getDisplay().getSystemColor(id); } public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display, SWT.SHELL_TRIM); shell.setBounds(800, 100, 220, 200); shell.setLayout(new FillLayout()); SwtTimer timer = new SwtTimer(shell, SWT.NONE); timer.set(120); timer.start(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } }