Java tutorial
/******************************************************************************* * Copyright (c) 2015 UT-Battelle, LLC. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Jordan Deyton (UT-Battelle, LLC.) - Initial API and implementation and/or * initial documentation * Jordan Deyton - bug 471166 * Jordan Deyton - bug 471248 * Jordan Deyton - bug 471749 * Jordan Deyton - bug 471750 *******************************************************************************/ package org.eclipse.eavp.viz.service.widgets; import java.net.URL; import java.util.ArrayList; import java.util.List; import org.eclipse.eavp.viz.datastructures.VizActionTree; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Scale; import org.eclipse.swt.widgets.Text; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; /** * This class provides a widget with three separate means of selecting times * from a list of allowed times: * <ul> * <li>dragging a {@link Scale} widget (coarse-grained control)</li> * <li>pressing arrow keys (fine-grained control)</li> * <li>typing a value near an existing timestep (very fine-grained control)</li> * </ul> * As input, the widget takes an ordered list of doubles. If the input list is * <i>not</i> in ascending order, then the widget will order them on its own and * provide timesteps based on the ordered list. * <p> * To receive notifications of changes to the selected timestep, a * {@link SelectionListener} must be registered as with a typical SWT widget. * Notifications will be posted on the UI thread. * </p> * * @author Jordan Deyton * */ public class TimeSliderComposite extends Composite { /** * A minimum FPS of 1 frame per minute. */ private static final double minFPS = 0.0167; /** * The string to use in the text box when there are no times configured. */ private static final String NO_TIMES = "N/A"; /** * The widget used for coarse-grained timestep control. */ private final Scale scale; /** * The text widget used for exact timestep control. */ private final Text text; /** * Sets the timestep to the next available timestep. This is a fine-grained * control. */ private final Button nextButton; /** * Sets the timestep to the previous available timestep. This is a * fine-grained control. */ private final Button prevButton; /** * Starts or pauses playback of the timesteps. */ private final Button playButton; /** * Opens a context menu for changing playback settings for the widget. */ private final Button optionsButton; /** * The Menu that appears beneath the options button. */ private final MenuManager optionsMenuManager; /** * The Action in the options menu for quickly getting to the first timestep. */ private Action firstStepAction; /** * The Action in the options menu for quickly getting to the last timestep. */ private Action lastStepAction; /** * The Action in the options menu for toggling looped playback. */ private Action loopPlaybackAction; /** * The current frames per second for playback. */ private double fps = 1.0; /** * The current delay between playback events produced by the FPS. This is * used when scheduling the next event, which requires an int to represent * the delay in milliseconds. */ private int fpsDelay = 1000; /** * Whether or not the playback operation is currently running. */ private boolean isPlaying = false; /** * Whether or not to loop playback. This does not apply to the previous/next * timestep buttons. */ private boolean loopPlayback = true; /** * The runnable operation used for "playback". It increments the timestep * and schedules itself to execute later based on the value of * {@link #playbackInterval}. */ private Runnable playbackRunnable; /** * The current timestep, or -1 if there are no times available. */ private int timestep; /** * A tree that contains the times associated with the timesteps. */ private BinarySearchTree times; /** * A list of created ImageDescriptors. These will be used to allocate shared * Image resources for buttons in the widget. */ private List<ImageDescriptor> imageDescriptors = new ArrayList<ImageDescriptor>(); /** * A list of created Image resources. These must be released when disposed. */ private List<Image> images = new ArrayList<Image>(); /** * A reference to the play image. This will be displayed when the widget is * paused. */ private Image playImage; /** * A reference to the pause image. This will be displayed when the widget is * playing. */ private Image pauseImage; /** * A list of listeners that will be notified if the timestep changes to a * new value. */ private final List<SelectionListener> listeners; /** * The default constructor. * * @param parent * a widget which will be the parent of the new instance (cannot * be null) * @param style * the style of widget to construct */ public TimeSliderComposite(Composite parent, int style) { super(parent, style); // Iniitalize the list of selection listeners. listeners = new ArrayList<SelectionListener>(); // Create the widgets. prevButton = createPrevButton(this); playButton = createPlayButton(this); nextButton = createNextButton(this); optionsButton = createOptionsButton(this); text = createText(this); scale = createScale(this); // Create the Menu for the options button. optionsMenuManager = createOptionsMenuManager(this); // Layout the widgets. The scale should take up all horizontal space on // the right. The text widget should grab whatever space remains, while // the normal buttons take up only the space they require. setLayout(new GridLayout(6, false)); GridData gridData = new GridData(SWT.CENTER, SWT.CENTER, false, true); prevButton.setLayoutData(gridData); playButton.setLayoutData(GridDataFactory.copyData(gridData)); nextButton.setLayoutData(GridDataFactory.copyData(gridData)); optionsButton.setLayoutData(GridDataFactory.copyData(gridData)); text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, true)); scale.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); // The default focus should be on the play button. playButton.setFocus(); // Refresh the widgets. This will enable/disable them as necessary. setTimes(new ArrayList<Double>()); return; } /** * Adds a new listener to be notified when the selected time changes. * * @param listener * The new listener to add. * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public void addSelectionListener(SelectionListener listener) { // Check that this widget can be accessed. Also check the listener is // not null. checkWidget(); if (listener == null) { throw new SWTException(SWT.ERROR_NULL_ARGUMENT); } listeners.add(listener); } /** * Creates a blank SelectionEvent that can be used when pausing the * playback. * * @return A new, empty SelectionEvent associated with this widget. */ private SelectionEvent createBlankSelectionEvent() { Event event = new Event(); event.widget = this; return new SelectionEvent(event); } /** * Creates the "next" button that increments the timestep. * * @param parent * The parent Composite for this widget. Assumed not to be * {@code null}. * @return The new widget. */ private Button createNextButton(Composite parent) { Button nextButton = new Button(parent, SWT.PUSH); // When the button is clicked, playback should be stopped and the // timestep should be incremented. nextButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Disable playback. setPlayback(false, e); // Increment the timestep. if (incrementTimestep()) { notifyListeners(e); } } }); // Load the button image as necessary. ImageDescriptor imgd = getImageDescriptor("nav_forward.gif"); Image img = (Image) imgd.createResource(getDisplay()); imageDescriptors.add(imgd); images.add(img); // Set the initial tool tip and image. nextButton.setToolTipText("Next"); nextButton.setImage(img); return nextButton; } /** * Creates the "options" button that can be used to configure playback * behavior. * * @param parent * The parent Composite for this widget. Assumed not to be * {@code null}. * @return The new widget. */ private Button createOptionsButton(Composite parent) { final Button optionsButton = new Button(parent, SWT.PUSH); // When the button is clicked, playback should be stopped and the // timestep should be incremented. optionsButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Open up the menu below this button. Rectangle r = optionsButton.getBounds(); Point p = new Point(r.x, r.y + r.height); p = optionsButton.getParent().toDisplay(p.x, p.y); Menu menu = optionsMenuManager.getMenu(); menu.setLocation(p.x, p.y); menu.setVisible(true); } }); // Load the button image as necessary. ImageDescriptor imgd = getImageDescriptor("thread_obj.gif"); Image img = (Image) imgd.createResource(getDisplay()); imageDescriptors.add(imgd); images.add(img); // Set the initial tool tip and image. optionsButton.setToolTipText("Playback settings"); optionsButton.setImage(img); return optionsButton; } /** * Creates the context menu that appears when the "options" button is * clicked. * * @param parent * The parent Composite for this widget (a context Menu). Assumed * not to be {@code null}. * @return The new MenuManager for the "options" context Menu. */ private MenuManager createOptionsMenuManager(Composite parent) { MenuManager manager = new MenuManager(); // Add a sub-menu for selecting the playback rate. VizActionTree playbackRate = new VizActionTree("Playback Rate"); // Set up the list of previous rates. This will be used to keep track of // previous values when selecting a new custom framerate. final List<String> previousRates = new ArrayList<String>(); previousRates.add("1"); previousRates.add("12"); previousRates.add("24"); previousRates.add("30"); // Add some default framerates for 1, 12, 24, and 30 fps. playbackRate.add(new VizActionTree(new Action("1 fps") { @Override public void run() { setFPS(1.0); } })); playbackRate.add(new VizActionTree(new Action("12 fps") { @Override public void run() { setFPS(12.0); } })); playbackRate.add(new VizActionTree(new Action("24 fps") { @Override public void run() { setFPS(24.0); } })); playbackRate.add(new VizActionTree(new Action("30 fps") { @Override public void run() { setFPS(30.0); } })); // Add an action for selecting a custom framerate. playbackRate.add(new VizActionTree(new Action("Custom...") { @Override public void run() { // Set up a dialog based on the previous and current framerate // values. It should use a Combo for selection, and should allow // the user to enter a new double framerate. ComboDialog dialog = new ComboDialog(getShell(), false) { @Override protected String validateSelection(String text) { String validatedText = super.validateSelection(text); // Accept double values that are greater than the min. if (validatedText == null) { try { double newValue = Double.parseDouble(text); if (Double.compare(newValue, minFPS) >= 0) { validatedText = text; } } catch (NullPointerException | NumberFormatException exception) { // Nothing to do. } } return validatedText; } }; // Customize the dialog's appearance. dialog.setTitle("Custom Framerate"); dialog.setInfoText("Enter a new frame rate or\n" + "select a previous rate.\n" + "Values must be greater than 0.0"); dialog.setErrorText("Please enter a number greater than 60\n" + "seconds per frame (0.0167 FPS)."); // Set the dialog Combo's allowed values and initial value. dialog.setAllowedValues(previousRates); dialog.setInitialValue(Double.toString(fps)); // Open the dialog and get the results. The selection has // already been validated if OK is clicked. if (dialog.open() == Window.OK) { // Get the resulting value as a string. String value = dialog.getValue(); previousRates.add(value); // Convert it to a double and set it as the new rate. double newRate = Double.parseDouble(value); setFPS(newRate); } return; } })); manager.add(playbackRate.getContributionItem()); // Add a menu item for toggling looped playback. loopPlaybackAction = new Action("Loop Playback", IAction.AS_CHECK_BOX) { @Override public void run() { setLoopPlayback(!loopPlayback); } }; loopPlaybackAction.setImageDescriptor(getImageDescriptor("loop.gif")); // Toggle the initial state of the button depending on whether playback // is looped by default. loopPlaybackAction.setChecked(loopPlayback); manager.add(loopPlaybackAction); // Add a menu item for skipping to the first timestep. firstStepAction = new Action("First Step", IAction.AS_PUSH_BUTTON) { @Override public void run() { SelectionEvent e = createBlankSelectionEvent(); // Disable playback. setPlayback(false, e); // Increment the timestep. if (setValidTimestep(0)) { notifyListeners(e); } } }; firstStepAction.setImageDescriptor(getImageDescriptor("skip_backward.gif")); manager.add(firstStepAction); // Add a menu item for skipping to the last timestep. lastStepAction = new Action("Last Step", IAction.AS_PUSH_BUTTON) { @Override public void run() { SelectionEvent e = createBlankSelectionEvent(); // Disable playback. setPlayback(false, e); // Increment the timestep. if (setValidTimestep(times.size() - 1)) { notifyListeners(e); } } }; lastStepAction.setImageDescriptor(getImageDescriptor("skip_forward.gif")); manager.add(lastStepAction); // Create the manager's context Menu. manager.createContextMenu(parent); return manager; } /** * Creates the "play" button that toggles the playback operation. * * @param parent * The parent Composite for this widget. Assumed not to be * {@code null}. * @return The new widget. */ private Button createPlayButton(Composite parent) { Button playButton = new Button(parent, SWT.PUSH); // When the button is clicked, playback should be toggled. playButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Toggle playback. setPlayback(!isPlaying, e); } }); // Load the play image. ImageDescriptor imgd = getImageDescriptor("nav_go.gif"); playImage = (Image) imgd.createResource(getDisplay()); imageDescriptors.add(imgd); images.add(playImage); // Load the pause image. imgd = getImageDescriptor("suspend_co.gif"); pauseImage = (Image) imgd.createResource(getDisplay()); imageDescriptors.add(imgd); images.add(pauseImage); // Set the initial tool tip and image. playButton.setToolTipText("Play"); playButton.setImage(playImage); return playButton; } /** * Creates the "previous" button that deccrements the timestep. * * @param parent * The parent Composite. Assumed not to be {@code null}. * @return The new button. */ private Button createPrevButton(Composite parent) { Button prevButton = new Button(parent, SWT.PUSH); // When the button is clicked, playback should be stopped and the // timestep should be decremented. prevButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Disable playback. setPlayback(false, e); // Decrement the timestep. if (decrementTimestep()) { notifyListeners(e); } } }); // Load the button image as necessary. ImageDescriptor imgd = getImageDescriptor("nav_backward.gif"); Image img = (Image) imgd.createResource(getDisplay()); imageDescriptors.add(imgd); images.add(img); // Set the initial tool tip and image. prevButton.setToolTipText("Previous"); prevButton.setImage(img); return prevButton; } /** * Creates the scale or slider widget that can be used to quickly traverse * the timesteps. * * @param parent * The parent Composite for this widget. Assumed not to be * {@code null}. * @return The new widget. */ private Scale createScale(Composite parent) { final Scale scale = new Scale(this, SWT.HORIZONTAL); scale.setMinimum(0); scale.setIncrement(1); scale.setMaximum(0); scale.setToolTipText("Traverses the timesteps"); scale.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Disable playback. setPlayback(false, e); // Get the timestep from the scale widget. if (setValidTimestep(scale.getSelection())) { notifyListeners(e); } } }); return scale; } /** * Creates the text widget for setting this time widget to a specific time. * * @param parent * The parent Composite for this widget. Assumed not to be * {@code null}. * @return The new widget. */ private Text createText(Composite parent) { final Text text = new Text(this, SWT.SINGLE | SWT.BORDER); text.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent e) { widgetSelected(e); } @Override public void widgetSelected(SelectionEvent e) { // Disable playback. setPlayback(false, e); // Try to find the value from the text widget's new text. try { double value = Double.parseDouble(text.getText()); // Set the value to the nearest allowed time. if (setValidTimestep(times.findNearestIndex(value))) { notifyListeners(e); } else { // Update the text to the set time regardless of whether // it changed. This lets the user know their input was // accepted. text.setText(Double.toString(getTime())); } } catch (NumberFormatException exception) { // If the number was invalid, revert to the previous text. text.setText(Double.toString(getTime())); } return; } }); // Tweak the default appearance. text.setFont(parent.getFont()); // Set the initial tool tip. text.setToolTipText("The current time"); return text; } /** * Decrements the timestep. Updates all widgets as necessary. This will loop * back around to the last timestep if necessary. * * @return True if the timestep changed, false otherwise. */ private boolean decrementTimestep() { int size = times.size(); return setValidTimestep((timestep - 1 + size) % size); } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Widget#dispose() */ @Override public void dispose() { // Dispose all ImageDescriptors and their used resources (Images). for (int i = images.size() - 1; i >= 0; i--) { imageDescriptors.remove(0).destroyResource(images.remove(0)); } // The play and pause images are now disposed. playImage = null; pauseImage = null; super.dispose(); } /** * Gets the current playback rate in frames per second. * * @return The current frames per second for playback. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public double getFPS() { // Check that this widget can be accessed. checkWidget(); return fps; } /** * A convenient operation for loading a custom image resource from this * widget's path. * * @param name * The name of (path to) the image to load, like "/image1.gif". * @return The loaded image, or {@code null} if the image could not be * loaded. */ private ImageDescriptor getImageDescriptor(String name) { Bundle bundle = FrameworkUtil.getBundle(TimeSliderComposite.class); URL url = bundle.getEntry("icons/" + name); return ImageDescriptor.createFromURL(url); } /** * Gets whether or not playback will loop back to the first timestep after * the last timestep is reached. * * @return True if playback will be looped, false otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean getLoopPlayback() { // Check that this widget can be accessed. checkWidget(); return loopPlayback; } /** * Gets the currently selected time. * * @return The selected time. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public double getTime() { // Check that this widget can be accessed. checkWidget(); return timestep >= 0 ? times.get(timestep) : 0.0; } /** * Gets the currently selected timestep (the index of the selected time). * * @return The selected timestep. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public int getTimestep() { // Check that this widget can be accessed. checkWidget(); return timestep; } /** * Increments the timestep. Updates all widgets as necessary. This will loop * back around to the first timestep if necessary. * * @return True if the timestep changed, false otherwise. */ private boolean incrementTimestep() { return setValidTimestep((timestep + 1) % times.size()); } /** * Gets whether the widget is currently playing. * * @return True if the widget is playing, false if it is paused. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean isPlaying() { // Check that this widget can be accessed. checkWidget(); return isPlaying; } /** * Notifies listeners that the selected time has changed. This is performed * on the UI thread as with all SWT selection events. * * @param e * The event triggering the updated time. */ private void notifyListeners(SelectionEvent e) { e.data = getTime(); for (SelectionListener listener : listeners) { listener.widgetSelected(e); } } /** * Stops playback on the widget at its current timestep. * * @return True if the widget was playing and is now paused, false * otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean pause() { // Check that this widget can be accessed. checkWidget(); boolean changed = false; if (isPlaying) { setPlayback(false, createBlankSelectionEvent()); changed = true; } return changed; } /** * Starts playback on the widget. Calling this method <i>will not itself * notify SelectionListeners</i>, but the ensuing playback <i>will</i>. * * @return True if the widget was paused and is now playing, false * otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean play() { // Check that this widget can be accessed. checkWidget(); boolean changed = false; if (!isPlaying) { setPlayback(true, createBlankSelectionEvent()); changed = true; } return changed; } /** * Removes the specified listener from this widget. It will no longer be * notified of time changes from this widget unless it has been registered * multiple times. * * @param listener * The listener to remove. * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public void removeSelectionListener(SelectionListener listener) { // Check that this widget can be accessed. Also check the listener is // not null. checkWidget(); if (listener == null) { throw new SWTException(SWT.ERROR_NULL_ARGUMENT); } listeners.remove(listener); } /* * (non-Javadoc) * * @see * org.eclipse.swt.widgets.Control#setBackground(org.eclipse.swt.graphics. * Color) */ @Override public void setBackground(Color color) { super.setBackground(color); // Update the background colors for all child widgets. scale.setBackground(color); playButton.setBackground(color); nextButton.setBackground(color); prevButton.setBackground(color); return; } /** * Sets the current playback rate in frames per second. The slowest allowed * framerate is defined by {@link #minFPS}. * * @param fps * The new framerate. Must be greater than the minimum allowed * framerate. * @return True if the FPS was changed, false otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean setFPS(double fps) { // Check that this widget can be accessed. checkWidget(); boolean changed = false; if (Double.compare(fps, minFPS) >= 0 && Math.abs(fps - this.fps) > 1e-7) { this.fps = fps; // Convert the FPS into a millisecond delay. fpsDelay = (int) (Math.round(1000.0 / this.fps)); changed = true; } return changed; } /** * Sets whether or not playback will loop back to the first timestep after * the last timestep is reached. * * @param loop * If true, then the playback will be looped. If false, playback * will not be looped. * @return True if the property changed, false otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean setLoopPlayback(boolean loop) { // Check that this widget can be accessed. checkWidget(); boolean changed = false; if (loop != loopPlayback) { // Update the variable and the associated options menu item. this.loopPlayback = loop; loopPlaybackAction.setChecked(loop); changed = true; } return changed; } /** * Starts or stops the playback operation. * * @param play * If true, playback will be started. If false, playback will be * stopped. * @param e * The selection event that triggered the playback operation. * This is only used if play is true. Otherwise, you may use * {@code null}. */ private void setPlayback(boolean play, final SelectionEvent e) { if (play != this.isPlaying) { this.isPlaying = play; // Determine the text and image for the play/pause button as well as // whether the playback event should be scheduled or cancelled. final String text; final Image image; final int time; if (play) { // Set the text for the pause button. text = "Pause"; image = pauseImage; // If play is clicked on the last timestep, immediately start // from the first (next) timestep. Otherwise, insert a delay. time = (timestep < times.size() - 1 ? fpsDelay : 0); // The playback runnable should be created. playbackRunnable = new Runnable() { @Override public void run() { // If the last timestep is reached and playback // shouldn't be looped, halt playback. if (!loopPlayback && timestep == times.size() - 2) { setPlayback(false, null); } // Otherwise, schedule the next frame change. else { getDisplay().timerExec(fpsDelay, this); } // Increment the timestep. if (incrementTimestep()) { notifyListeners(e); } return; } }; } else { // Set the text for the play button. text = "Play"; image = playImage; // The playback runnable should be cancelled. time = -1; } // Update the tool tip and image for the play button. playButton.setToolTipText(text); playButton.setImage(image); // Schedule or cancel the playback task. getDisplay().timerExec(time, playbackRunnable); } return; } /** * Updates the timestep and all embedded widgets based on the nearest known * time to the specified time. Calling this method <i>will not notify * SelectionListeners!</i> * <p> * <b>Note:</b> Calling this method from the UI thread with valid arguments * will pause the widget if it is currently playing. * </p> * * @param time * The new time. * @return True if the timestep changed, false otherwise. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean setTime(double time) { // Check that this widget can be accessed. checkWidget(); // Halt playback. SelectionEvent event = createBlankSelectionEvent(); setPlayback(false, event); // Set the timestep. return setValidTimestep(times.findNearestIndex(time)); } /** * Sets the list of timesteps used by this widget. * <p> * <b>Note:</b> Calling this method from the UI thread with valid arguments * will pause the widget if it is currently playing. * </p> * * @param times * The list of timesteps. Null values and duplicates will be * ignored. The list will be ordered. * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the list of times is null * </li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public void setTimes(List<Double> times) { // Check that this widget can be accessed. Also check the list is not // null. checkWidget(); if (times == null) { throw new SWTException(SWT.ERROR_NULL_ARGUMENT); } // Halt playback. setPlayback(false, createBlankSelectionEvent()); // Try to recreate the tree of times based on the new list of times. try { this.times = new BinarySearchTree(times); timestep = 0; } // If the list has no non-null values, set the timestep to -1. catch (IllegalArgumentException e) { this.times = null; timestep = -1; } // Refresh the widgets. final int size = this.times != null ? this.times.size() : 0; boolean widgetsEnabled = size > 1; // Enable/disable the widgets as necessary. scale.setEnabled(widgetsEnabled); text.setEnabled(widgetsEnabled); nextButton.setEnabled(widgetsEnabled); prevButton.setEnabled(widgetsEnabled); playButton.setEnabled(widgetsEnabled); firstStepAction.setEnabled(widgetsEnabled); lastStepAction.setEnabled(widgetsEnabled); // Refresh the scale widget's max value. scale.setMaximum(widgetsEnabled ? size - 1 : 0); // Reset the selection of the widgets. scale.setSelection(0); text.setText(size > 0 ? Double.toString(getTime()) : NO_TIMES); return; } /** * Updates the timestep and all embedded widgets based on the new timestep * value. Calling this method <i>will not notify SelectionListeners!</i> * <p> * <b>Note:</b> Calling this method from the UI thread with valid arguments * will pause the widget if it is currently playing. * </p> * * @param index * The new index of the timestep. * @return True if the timestep changed, false otherwise. * @throws IndexOutOfBoundsException * If the specified index is invalid. * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the receiver</li> * </ul> */ public boolean setTimestep(int index) throws IndexOutOfBoundsException { // Check that this widget can be accessed. checkWidget(); // Check that the index is valid. if (times == null || index < 0 || index >= times.size()) { throw new IndexOutOfBoundsException(); } // Halt playback. SelectionEvent event = createBlankSelectionEvent(); setPlayback(false, event); // Set the timestep. return setValidTimestep(index); } /** * Updates the timestep and all embedded widgets based on the new timestep * value. * <p> * <b>Note:</b> This method should be used when on the UI thread and the * timestep is known to be valid (e.g., from a widget's selection). * </p> * * @param index * The new timestep. <i>Assumed to be valid.</i> * @return True if the timestep changed to a new value, false otherwise. */ private boolean setValidTimestep(int index) { boolean changed = false; if (index != timestep) { changed = true; // Update the timestep. timestep = index; // Update the selections on the widgets. scale.setSelection(timestep); text.setText(Double.toString(times.get(timestep))); } return changed; } }