From 0e2660b2cd018bcbf1c0e744fe040518cdb4e1ea Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 16 Nov 2015 10:48:48 -0500 Subject: [PATCH 01/10] Sources should not throw exceptions when they fail to load --- .../main/java/edu/wpi/grip/core/Source.java | 13 ++ .../grip/core/events/SourceStartedEvent.java | 19 ++ .../grip/core/events/SourceStoppedEvent.java | 18 ++ .../wpi/grip/core/sources/CameraSource.java | 176 +++++++++--------- .../grip/core/sources/ImageFileSource.java | 21 ++- .../core/sources/ImageFileSourceTest.java | 6 +- .../wpi/grip/ui/pipeline/AddSourceView.java | 58 ++++-- 7 files changed, 206 insertions(+), 105 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/events/SourceStartedEvent.java create mode 100644 core/src/main/java/edu/wpi/grip/core/events/SourceStoppedEvent.java diff --git a/core/src/main/java/edu/wpi/grip/core/Source.java b/core/src/main/java/edu/wpi/grip/core/Source.java index c8aae468ab..dba12e739e 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -45,4 +45,17 @@ public final OutputSocket[] getOutputSockets() { * @see #getProperties() */ public abstract void createFromProperties(EventBus eventBus, Properties properties) throws IOException; + + + public T start(EventBus eventBus) throws IOException { + final T source = (T) start(); + eventBus.register(source); + return source; + }; + + protected abstract Source start() throws IOException; + + public abstract Source stop() throws Exception; + + public abstract boolean isRunning(); } diff --git a/core/src/main/java/edu/wpi/grip/core/events/SourceStartedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SourceStartedEvent.java new file mode 100644 index 0000000000..eef5ca115f --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/events/SourceStartedEvent.java @@ -0,0 +1,19 @@ +package edu.wpi.grip.core.events; + + +import edu.wpi.grip.core.Source; + +/** + * An event that occurs when a source is started. + */ +public class SourceStartedEvent { + private final Source source; + + public SourceStartedEvent(Source source){ + this.source = source; + } + + public Source getSource() { + return this.source; + } +} diff --git a/core/src/main/java/edu/wpi/grip/core/events/SourceStoppedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SourceStoppedEvent.java new file mode 100644 index 0000000000..28a9d9639b --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/events/SourceStoppedEvent.java @@ -0,0 +1,18 @@ +package edu.wpi.grip.core.events; + +import edu.wpi.grip.core.Source; + +/** + * An event that occurs when a source is stopped. + */ +public class SourceStoppedEvent { + private final Source source; + + public SourceStoppedEvent(Source source){ + this.source = source; + } + + public Source getSource() { + return this.source; + } +} \ No newline at end of file diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index 05046e4408..2407ff3496 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -11,6 +11,8 @@ import edu.wpi.grip.core.Source; import edu.wpi.grip.core.events.UnexpectedThrowableEvent; import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.events.SourceStartedEvent; +import edu.wpi.grip.core.events.SourceStoppedEvent; import org.bytedeco.javacpp.opencv_core.Mat; import org.bytedeco.javacv.*; @@ -42,7 +44,7 @@ public class CameraSource extends Source { private OutputSocket frameOutputSocket; private OutputSocket frameRateOutputSocket; private Optional frameThread; - private Optional grabber; + private FrameGrabber grabber; /** * Creates a camera source that can be used as an input to a pipeline @@ -72,7 +74,6 @@ public CameraSource(EventBus eventBus, String address) throws IOException { * Used for serialization */ public CameraSource() { - this.grabber = Optional.empty(); this.frameThread = Optional.empty(); } @@ -81,9 +82,8 @@ private void initialize(EventBus eventBus, FrameGrabber frameGrabber, String nam this.name = name; this.frameOutputSocket = new OutputSocket<>(eventBus, imageOutputHint); this.frameRateOutputSocket = new OutputSocket<>(eventBus, frameRateOutputHint); - - this.eventBus.register(this); - this.startVideo(frameGrabber); + this.grabber = frameGrabber; + eventBus.register(this); } @Override @@ -127,109 +127,117 @@ public void createFromProperties(EventBus eventBus, Properties properties) throw } /** - * Starts the video capture from the - * - * @param grabber A JavaCV {@link FrameGrabber} instance to capture from + * Starts the video capture from the source device */ - private synchronized void startVideo(FrameGrabber grabber) throws IOException { + public CameraSource start() throws IOException, IllegalStateException { final OpenCVFrameConverter.ToMat convertToMat = new OpenCVFrameConverter.ToMat(); - if (this.frameThread.isPresent()) { - throw new IllegalStateException("The video retrieval thread has already been started."); - } - if (this.grabber.isPresent()) { - throw new IllegalStateException("The Frame Grabber has already been started."); - } - try { - grabber.start(); - } catch (FrameGrabber.Exception e) { - throw new IOException("A problem occurred trying to start the frame grabber for " + this.name, e); - } + synchronized (this.frameThread) { + if (this.frameThread.isPresent()) { + throw new IllegalStateException("The video retrieval thread has already been started."); + } + try { + grabber.start(); + } catch (FrameGrabber.Exception e) { + throw new IOException("A problem occurred trying to start the frame grabber for " + this.name, e); + } - // Store the grabber only once it has been started in the case that there is an exception. - this.grabber = Optional.of(grabber); + final Thread frameExecutor = new Thread(() -> { + long lastFrame = System.currentTimeMillis(); + while (!Thread.interrupted()) { + final Frame videoFrame; + try { + videoFrame = grabber.grab(); + } catch (FrameGrabber.Exception e) { + throw new IllegalStateException("Failed to grab image", e); + } - final Thread frameExecutor = new Thread(() -> { - long lastFrame = System.currentTimeMillis(); - while (!Thread.interrupted()) { - final Frame videoFrame; - try { - videoFrame = grabber.grab(); - } catch (FrameGrabber.Exception e) { - throw new IllegalStateException("Failed to grab image", e); - } - final Mat frameMat = convertToMat.convert(videoFrame); + final Mat frameMat = convertToMat.convert(videoFrame); - if (frameMat == null || frameMat.isNull()) { - throw new IllegalStateException("The camera returned a null frame Mat"); + if (frameMat == null || frameMat.isNull()) { + throw new IllegalStateException("The camera returned a null frame Mat"); + } + + frameMat.copyTo(frameOutputSocket.getValue().get()); + frameOutputSocket.setValue(frameOutputSocket.getValue().get()); + long thisMoment = System.currentTimeMillis(); + frameRateOutputSocket.setValue(1000 / (thisMoment - lastFrame)); + lastFrame = thisMoment; } + }, "Camera"); - frameMat.copyTo(frameOutputSocket.getValue().get()); - frameOutputSocket.setValue(frameOutputSocket.getValue().get()); - long thisMoment = System.currentTimeMillis(); - frameRateOutputSocket.setValue(1000 / (thisMoment - lastFrame)); - lastFrame = thisMoment; - } - }, "Camera"); - frameExecutor.setUncaughtExceptionHandler( - (thread, exception) -> { - eventBus.post(new UnexpectedThrowableEvent(exception, "Webcam Frame Grabber Thread crashed with uncaught exception")); - try { - stopVideo(); - } catch (TimeoutException e) { - eventBus.post(new UnexpectedThrowableEvent(e, "Webcam Frame Grabber could not be stopped!")); + frameExecutor.setUncaughtExceptionHandler( + (thread, exception) -> { + eventBus.post(new UnexpectedThrowableEvent(exception, "Camera Frame Grabber Thread crashed with uncaught exception")); + try { + stop(); + } catch (TimeoutException e) { + eventBus.post(new UnexpectedThrowableEvent(e, "Camera Frame Grabber could not be stopped!")); + } } - } - ); - frameExecutor.setDaemon(true); - frameExecutor.start(); - frameThread = Optional.of(frameExecutor); + ); + frameExecutor.setDaemon(true); + frameExecutor.start(); + this.frameThread = Optional.of(frameExecutor); + } + eventBus.post(new SourceStartedEvent(this)); + return this; } /** * Stops the video feed from updating the output socket. * - * @throws TimeoutException If the thread running the Webcam fails to join this one after a timeout. + * @throws TimeoutException If the thread running the Webcam fails to join this one after a timeout. + * @throws IllegalStateException If the camera was already stopped */ - private void stopVideo() throws TimeoutException { - if (frameThread.isPresent()) { - final Thread ex = frameThread.get(); - ex.interrupt(); - try { - ex.join(TimeUnit.SECONDS.toMillis(2)); - if (ex.isAlive()) { - throw new TimeoutException("Unable to terminate video feed from Web Camera"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - //TODO: Move this into a logging framework - System.out.println("Caught Exception:"); - e.printStackTrace(); - } finally { - frameThread = Optional.empty(); - // This will always run even if a timeout exception occurs + public CameraSource stop() throws TimeoutException, IllegalStateException { + synchronized (this.frameThread) { + if (frameThread.isPresent()) { + final Thread ex = frameThread.get(); + ex.interrupt(); try { - grabber.ifPresent(grabber -> { - try { - grabber.stop(); - } catch (FrameGrabber.Exception e) { - throw new IllegalStateException("A problem occurred trying to stop the frame grabber", e); - } - }); + ex.join(TimeUnit.SECONDS.toMillis(500)); + if (ex.isAlive()) { + throw new TimeoutException("Unable to terminate video feed from Web Camera"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + //TODO: Move this into a logging framework + System.out.println("Caught Exception:"); + e.printStackTrace(); } finally { - // This will always run even if we fail to stop the grabber - grabber = Optional.empty(); + // Clean up this resource as you can't restart a stopped thread + this.frameThread = Optional.empty(); + // This will always run even if a timeout exception occurs + try { + grabber.stop(); + } catch (FrameGrabber.Exception e) { + throw new IllegalStateException("A problem occurred trying to stop the frame grabber", e); + } } + } else { + throw new IllegalStateException("Tried to stop a Webcam that is already stopped."); } - } else { - throw new IllegalStateException("Tried to stop a Webcam that is already stopped."); + } + eventBus.post(new SourceStoppedEvent(this)); + frameRateOutputSocket.setValue(0); + return this; + } + + @Override + public boolean isRunning() { + synchronized (this.frameThread) { + return this.frameThread.isPresent() && this.frameThread.get().isAlive(); } } @Subscribe public void onSourceRemovedEvent(SourceRemovedEvent event) throws TimeoutException { if (event.getSource() == this) { - this.stopVideo(); - this.eventBus.unregister(this); + try { + this.stop(); + } finally { + this.eventBus.unregister(this); + } } } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java index 74c8e0d047..509e9a734c 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java @@ -7,6 +7,7 @@ import edu.wpi.grip.core.SocketHint; import edu.wpi.grip.core.SocketHints; import edu.wpi.grip.core.Source; +import edu.wpi.grip.core.events.SourceStartedEvent; import org.bytedeco.javacpp.opencv_core.Mat; import org.bytedeco.javacpp.opencv_imgcodecs; @@ -31,6 +32,8 @@ public class ImageFileSource extends Source { private String path; private final SocketHint imageOutputHint = SocketHints.Inputs.createMatSocketHint("Image", true); private OutputSocket outputSocket; + private EventBus eventBus; + private boolean started = false; /** * @param eventBus The event bus for the pipeline. @@ -80,10 +83,25 @@ public void createFromProperties(EventBus eventBus, Properties properties) throw if (path == null) { throw new IllegalArgumentException("Cannot create ImageFileSource without a path."); } - this.initialize(eventBus, path); } + public ImageFileSource start() throws IOException { + this.started = true; + loadImage(this.path); + return this; + } + + @Override + public ImageFileSource stop() { + return this; + } + + @Override + public boolean isRunning() { + return this.started; + } + /** * Loads the image and posts an update to the {@link EventBus} * @@ -108,5 +126,6 @@ private void loadImage(String path, final int flags) throws IOException { // TODO Output Error to GUI about invalid url throw new IOException("Error loading image " + path); } + this.eventBus.post(new SourceStartedEvent(this)); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sources/ImageFileSourceTest.java b/core/src/test/java/edu/wpi/grip/core/sources/ImageFileSourceTest.java index 3c6f767f20..94c04eaa8d 100644 --- a/core/src/test/java/edu/wpi/grip/core/sources/ImageFileSourceTest.java +++ b/core/src/test/java/edu/wpi/grip/core/sources/ImageFileSourceTest.java @@ -32,7 +32,7 @@ public void setUp() throws URISyntaxException { public void testLoadImageToMat() throws IOException { // Given above setup // When - final ImageFileSource fileSource = new ImageFileSource(eventBus, this.imageFile); + final ImageFileSource fileSource = new ImageFileSource(eventBus, this.imageFile).start(eventBus); OutputSocket outputSocket = fileSource.getOutputSockets()[0]; // Then @@ -45,7 +45,7 @@ public void testLoadImageToMat() throws IOException { @Test(expected = IOException.class) public void testReadInTextFile() throws IOException { - final ImageFileSource fileSource = new ImageFileSource(eventBus, this.textFile); + final ImageFileSource fileSource = new ImageFileSource(eventBus, this.textFile).start(eventBus); OutputSocket outputSocket = fileSource.getOutputSockets()[0]; assertTrue("No matrix should have been returned.", outputSocket.getValue().get().empty()); } @@ -54,7 +54,7 @@ public void testReadInTextFile() throws IOException { public void testReadInFileWithoutExtension() throws MalformedURLException, IOException { final File testFile = new File("temp" + File.separator +"fdkajdl3eaf"); - final ImageFileSource fileSource = new ImageFileSource(eventBus, testFile); + final ImageFileSource fileSource = new ImageFileSource(eventBus, testFile).start(eventBus); OutputSocket outputSocket = fileSource.getOutputSockets()[0]; assertTrue("No matrix should have been returned.", outputSocket.getValue().get().empty()); } diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java index bdb045d154..4451b166ed 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java @@ -6,6 +6,7 @@ import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.ui.util.DPIUtility; +import javafx.application.Platform; import javafx.event.EventHandler; import javafx.scene.Parent; import javafx.scene.control.*; @@ -21,6 +22,8 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.Predicate; /** @@ -30,6 +33,11 @@ */ public class AddSourceView extends HBox { + @FunctionalInterface + private interface SupplierWithIO { + T getWithIO() throws IOException; + } + private final EventBus eventBus; public AddSourceView(EventBus eventBus) { @@ -49,7 +57,7 @@ public AddSourceView(EventBus eventBus) { // Add a new source for each image . imageFiles.forEach(file -> { try { - eventBus.post(new SourceAddedEvent(new ImageFileSource(eventBus, file))); + eventBus.post(new SourceAddedEvent(new ImageFileSource(eventBus, file).start(eventBus))); } catch (IOException e) { eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source")); } @@ -72,14 +80,11 @@ public AddSourceView(EventBus eventBus) { dialog.getDialogPane().getStylesheets().addAll(root.getStylesheets()); // If the user clicks OK, add a new camera source - dialog.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).ifPresent(result -> { - try { - final CameraSource source = new CameraSource(eventBus, cameraIndex.getValue()); - eventBus.post(new SourceAddedEvent(source)); - } catch (IOException e) { - eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source")); - } - }); + loadCamera(dialog, + () -> new CameraSource(eventBus, cameraIndex.getValue()).start(eventBus), + e -> { + // TODO: Indicate to user that the camera source was invalid + }); }); addButton("Add IP\nCamera", getClass().getResource("/edu/wpi/grip/ui/icons/add-webcam.png"), mouseEvent -> { @@ -113,17 +118,36 @@ public AddSourceView(EventBus eventBus) { dialog.getDialogPane().getStylesheets().addAll(root.getStylesheets()); // If the user clicks OK, add a new camera source - dialog.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).ifPresent(result -> { - try { - final CameraSource source = new CameraSource(eventBus, cameraAddress.getText()); - eventBus.post(new SourceAddedEvent(source)); - } catch (IOException e) { - eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source")); - } - }); + loadCamera(dialog, + () -> new CameraSource(eventBus, cameraAddress.getText()).start(eventBus), + e -> { + // TODO: Indicate to user that the camera source was invalid + }); }); } + /** + * + * @param dialog The dialog to load the camera with + * @param cameraSourceSupplier The supplier that will create the camera + * @param failureCallback The handler for when the camera source supplier throws an IO Exception + */ + private void loadCamera(Dialog dialog, SupplierWithIO cameraSourceSupplier, Consumer failureCallback){ + assert Platform.isFxApplicationThread() : "Should only run in FX thread"; + dialog.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).ifPresent(result -> { + try { + // Will try to create the camera with the values from the supplier + final CameraSource source = cameraSourceSupplier.getWithIO(); + eventBus.post(new SourceAddedEvent(source)); + } catch (IOException e) { + // This will run it again with the new values retrieved by the supplier + failureCallback.accept(e); + Platform.runLater(() -> loadCamera(dialog, cameraSourceSupplier, failureCallback)); + } + }); + } + + /** * Add a new button for adding a source. This method takes care of setting the event handler. */ From 59aa466193194c554ecf1dc7d23534240d8dc1be Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 23 Nov 2015 19:21:25 -0500 Subject: [PATCH 02/10] Sources can now be stopped and started from the UI --- .../main/java/edu/wpi/grip/core/Source.java | 29 ++++++- .../wpi/grip/core/sources/CameraSource.java | 8 +- .../grip/core/sources/ImageFileSource.java | 5 ++ .../resources/edu/wpi/grip/ui/icons/start.png | Bin 0 -> 362 bytes .../resources/edu/wpi/grip/ui/icons/stop.png | Bin 0 -> 188 bytes .../edu/wpi/grip/ui/pipeline/SourceView.java | 76 ++++++++++++++++++ .../java/edu/wpi/grip/ui/util/DPIUtility.java | 1 + .../main/resources/edu/wpi/grip/ui/GRIP.css | 6 +- .../edu/wpi/grip/ui/pipeline/Source.fxml | 38 ++++++--- 9 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 src/main/resources/edu/wpi/grip/ui/icons/start.png create mode 100644 src/main/resources/edu/wpi/grip/ui/icons/stop.png diff --git a/core/src/main/java/edu/wpi/grip/core/Source.java b/core/src/main/java/edu/wpi/grip/core/Source.java index dba12e739e..e275618a74 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -51,11 +51,38 @@ public T start(EventBus eventBus) throws IOException { final T source = (T) start(); eventBus.register(source); return source; - }; + } + /** + * Starts this source. + * A source whose {@link #isRestartable()} returns true can also be stopped and started and stopped multiple times. + * + * @return The source object that created the camera + * @throws IOException If the source fails to be started + */ protected abstract Source start() throws IOException; + /** + * Stops this source. + * This will stop the source publishing new socket values after this method returns. + * A source whose {@link #isRestartable()} returns true can also be stopped and started and stopped multiple times. + * + * @return The source that was stopped + * @throws Exception + */ public abstract Source stop() throws Exception; + /** + * Used to indicate if the source is running or stopped + * + * @return true if this source is running + */ public abstract boolean isRunning(); + + /** + * Used to flag to the UI if this source should have the start/stop button displayed + * + * @return true if this source can be restarted once created + */ + public abstract boolean isRestartable(); } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index 2407ff3496..0b4c69b6e4 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -83,7 +83,6 @@ private void initialize(EventBus eventBus, FrameGrabber frameGrabber, String nam this.frameOutputSocket = new OutputSocket<>(eventBus, imageOutputHint); this.frameRateOutputSocket = new OutputSocket<>(eventBus, frameRateOutputHint); this.grabber = frameGrabber; - eventBus.register(this); } @Override @@ -230,11 +229,16 @@ public boolean isRunning() { } } + @Override + public boolean isRestartable() { + return true; + } + @Subscribe public void onSourceRemovedEvent(SourceRemovedEvent event) throws TimeoutException { if (event.getSource() == this) { try { - this.stop(); + if (this.isRunning()) this.stop(); } finally { this.eventBus.unregister(this); } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java index 509e9a734c..06387f34d7 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java @@ -102,6 +102,11 @@ public boolean isRunning() { return this.started; } + @Override + public boolean isRestartable() { + return false; + } + /** * Loads the image and posts an update to the {@link EventBus} * diff --git a/src/main/resources/edu/wpi/grip/ui/icons/start.png b/src/main/resources/edu/wpi/grip/ui/icons/start.png new file mode 100644 index 0000000000000000000000000000000000000000..d8de5e04e76a5543378472ecd6c103fa01d6d4d9 GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt<0zT^vIq zTK7)a&C6scz_DjrnrCO73(K+q;2Fim%cXf)z5AWWIVE#Z979!RC6?g;aWw_h85W! zTnewGZZUPNXxql1v??)@A@TYKorVqB6`~GprhB*)4oeBLPVnvRU^=nPZ3~0o#pPSS z{j=>mIRAQSZ2s13s!<7wi*DPT|N37*aYLl6>V;TYMT3~Png^oZsvd~T)!k6Nb!kJ! z*4Bostziy(qqGvXMJXlhs_JhzowcsvcGeSaC%sK3Gn9N|fnH|tboFyt=akR{03PRs Au>b%7 literal 0 HcmV?d00001 diff --git a/src/main/resources/edu/wpi/grip/ui/icons/stop.png b/src/main/resources/edu/wpi/grip/ui/icons/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..b113484d8e046585173125e8d11af8785c432efd GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt-0GE{-7) zt#8jQWMoj_VcB?S(%g-~O>C_zmDKOoD1E7ZEN4_)Di?P3qowuvI3}P5D41aO`w5G| S2@dZ>kbtMFpUXO@geCwpnl$tP literal 0 HcmV?d00001 diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/SourceView.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/SourceView.java index 775e0df9a5..71730da0f8 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/SourceView.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/SourceView.java @@ -1,13 +1,27 @@ package edu.wpi.grip.ui.pipeline; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import edu.wpi.grip.core.OutputSocket; import edu.wpi.grip.core.Source; +import edu.wpi.grip.core.events.FatalErrorEvent; import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.events.SourceStartedEvent; +import edu.wpi.grip.core.events.SourceStoppedEvent; +import edu.wpi.grip.ui.util.DPIUtility; +import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import java.io.IOException; @@ -19,12 +33,21 @@ */ public class SourceView extends VBox { + private static final Image startImage = new Image(SourceView.class.getResourceAsStream("/edu/wpi/grip/ui/icons/start.png")); + private static final Image stopImage = new Image(SourceView.class.getResourceAsStream("/edu/wpi/grip/ui/icons/stop.png")); + @FXML private Label name; @FXML private VBox sockets; + @FXML + private ToggleButton startStopButton; + + @FXML + private Tooltip startStopTooltip; + private final EventBus eventBus; private final Source source; @@ -41,11 +64,45 @@ public SourceView(EventBus eventBus, Source source) { throw new RuntimeException(e); } + this.name.setText(source.getName()); + if (!source.isRestartable()) startStopButton.setVisible(false); + else { + HBox.setHgrow(startStopButton, Priority.NEVER); + startStopButton.setContentDisplay(ContentDisplay.RIGHT); + + startStopButton.addEventFilter(MouseEvent.MOUSE_RELEASED, (event) -> { + event.consume(); + if (!startStopButton.isSelected()) try { + source.start(eventBus); + } catch (IOException e) { + eventBus.post(new FatalErrorEvent(e)); + } + else try { + source.stop(); + } catch (Exception e) { + eventBus.post(new FatalErrorEvent(e)); + } + }); + startStopButton.selectedProperty().addListener((o, oldV, newV) -> { + final String stopCameraText = "Stop Camera"; + final String startCameraText = "Start Camera"; + final ImageView icon = newV ? new ImageView(stopImage) : new ImageView(startImage); + icon.setFitHeight(DPIUtility.MINI_ICON_SIZE); + icon.setFitWidth(DPIUtility.MINI_ICON_SIZE); + startStopButton.setGraphic(icon); + startStopTooltip.setText(newV ? stopCameraText : startCameraText); + startStopButton.setAccessibleText(newV ? stopCameraText : startCameraText); + }); + startStopButton.setSelected(source.isRunning()); + } + for (OutputSocket socket : source.getOutputSockets()) { this.sockets.getChildren().add(new OutputSocketView(eventBus, socket)); } + + eventBus.register(this); } public Source getSource() { @@ -64,4 +121,23 @@ public ObservableList getOutputSockets() { public void delete() { this.eventBus.post(new SourceRemovedEvent(this.getSource())); } + + + @Subscribe + public void onSourceStarted(SourceStartedEvent event) { + if (source == event.getSource()) { + Platform.runLater(() -> { + startStopButton.setSelected(true); + }); + } + } + + @Subscribe + public void onSourceStopped(SourceStoppedEvent event) { + if (source == event.getSource()) { + Platform.runLater(() -> { + startStopButton.setSelected(false); + }); + } + } } diff --git a/ui/src/main/java/edu/wpi/grip/ui/util/DPIUtility.java b/ui/src/main/java/edu/wpi/grip/ui/util/DPIUtility.java index 4e94e0da52..1418dd647e 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/util/DPIUtility.java +++ b/ui/src/main/java/edu/wpi/grip/ui/util/DPIUtility.java @@ -13,6 +13,7 @@ public class DPIUtility { public final static double FONT_SIZE = 11.0 * (isManualHiDPI() ? HIDPI_SCALE : 1.0); public final static double SMALL_ICON_SIZE = 16.0 * (isManualHiDPI() ? HIDPI_SCALE : 1.0); public final static double LARGE_ICON_SIZE = 48.0 * (isManualHiDPI() ? HIDPI_SCALE : 1.0); + public final static double MINI_ICON_SIZE = SMALL_ICON_SIZE / 2.0; public final static double STROKE_WIDTH = 2.0 * (isManualHiDPI() ? HIDPI_SCALE : 1.0); private static boolean isManualHiDPI() { diff --git a/ui/src/main/resources/edu/wpi/grip/ui/GRIP.css b/ui/src/main/resources/edu/wpi/grip/ui/GRIP.css index ac0d55ae16..2f4f0bc84d 100644 --- a/ui/src/main/resources/edu/wpi/grip/ui/GRIP.css +++ b/ui/src/main/resources/edu/wpi/grip/ui/GRIP.css @@ -123,7 +123,7 @@ Button.add-source { -fx-spacing: 0.5em; } -Button.delete, Button.move-left, Button.move-right { +Button.delete, Button.move-left, Button.move-right, Button.start-stop { -fx-padding: 0.2em 0.8em; -fx-font-weight: bold; -fx-font-color: -fx-dark-color; @@ -181,6 +181,10 @@ VBox.sockets { -fx-padding: 0.5em; } +.source-header-box { + -fx-padding: 0 0 0.5em 0; +} + .preview-box { -fx-spacing: 0.5em; } diff --git a/ui/src/main/resources/edu/wpi/grip/ui/pipeline/Source.fxml b/ui/src/main/resources/edu/wpi/grip/ui/pipeline/Source.fxml index 9e218253db..2748cfc4dd 100644 --- a/ui/src/main/resources/edu/wpi/grip/ui/pipeline/Source.fxml +++ b/ui/src/main/resources/edu/wpi/grip/ui/pipeline/Source.fxml @@ -1,16 +1,32 @@ - - - - + + + -