diff --git a/.github/workflows/all_tests.yml b/.github/workflows/all_tests.yml
index 51e9f57763..c76de8dd40 100644
--- a/.github/workflows/all_tests.yml
+++ b/.github/workflows/all_tests.yml
@@ -11,7 +11,7 @@ jobs:
container:
image: lgomezwhl/phoebus-ci:whl-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Check coding style
run: |
cd core/commander-core
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 180bb28806..951d8a2570 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ jobs:
build-container-linux:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
@@ -39,7 +39,7 @@ jobs:
build-whl-container-linux:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
@@ -74,7 +74,7 @@ jobs:
# container:
# image: lgomezwhl/phoebus-ci:whl-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: |
git fetch --prune --unshallow --tags
- name: Set up JDK 11
diff --git a/.github/workflows/nightly_linux.yml b/.github/workflows/nightly_linux.yml
index 224868d743..4ccfdbe84f 100644
--- a/.github/workflows/nightly_linux.yml
+++ b/.github/workflows/nightly_linux.yml
@@ -6,7 +6,7 @@ jobs:
release-linux:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: |
git fetch --prune --unshallow --tags
diff --git a/.github/workflows/nightly_mac.yml b/.github/workflows/nightly_mac.yml
index c8c400d71c..5d365a8238 100644
--- a/.github/workflows/nightly_mac.yml
+++ b/.github/workflows/nightly_mac.yml
@@ -8,7 +8,7 @@ jobs:
container:
image: lgomezwhl/phoebus-ci:whl-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
diff --git a/.github/workflows/nightly_windows.yml b/.github/workflows/nightly_windows.yml
index 179d2a39da..5409508f54 100644
--- a/.github/workflows/nightly_windows.yml
+++ b/.github/workflows/nightly_windows.yml
@@ -8,7 +8,7 @@ jobs:
container:
image: lgomezwhl/phoebus-ci:whl-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
diff --git a/app/commander/Makefile b/app/commander/Makefile
new file mode 100644
index 0000000000..2e80e0e6bc
--- /dev/null
+++ b/app/commander/Makefile
@@ -0,0 +1,5 @@
+format:
+ mvn com.coveo:fmt-maven-plugin:format
+
+dev-build:
+ mvn -Dfmt.skip -DskipTests install -T6
diff --git a/app/commander/app-commander-display-model/.classpath b/app/commander/app-commander-display-model/.classpath
index b010aef042..65f270a052 100644
--- a/app/commander/app-commander-display-model/.classpath
+++ b/app/commander/app-commander-display-model/.classpath
@@ -36,5 +36,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/commander/app-commander-display-model/.project b/app/commander/app-commander-display-model/.project
index c27420ba64..944e6224d3 100644
--- a/app/commander/app-commander-display-model/.project
+++ b/app/commander/app-commander-display-model/.project
@@ -20,4 +20,15 @@
org.eclipse.jdt.core.javanature
org.eclipse.m2e.core.maven2Nature
+
+
+ 1728427135612
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
diff --git a/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/CommanderVideoWidget.java b/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/CommanderVideoWidget.java
new file mode 100644
index 0000000000..bb50b67da0
--- /dev/null
+++ b/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/CommanderVideoWidget.java
@@ -0,0 +1,292 @@
+/*******************************************************************************
+ * Copyright (c) 2015-2020 Oak Ridge National Laboratory.
+ * 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
+ *******************************************************************************/
+package com.windhoverlabs.display.model.widgets;
+
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propEnabled;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propRotationStep;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propText;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propTransparent;
+import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.runtimePropPVWritable;
+
+import java.util.List;
+import org.csstudio.display.builder.model.MacroizedWidgetProperty;
+import org.csstudio.display.builder.model.Messages;
+import org.csstudio.display.builder.model.StructuredWidgetProperty;
+import org.csstudio.display.builder.model.StructuredWidgetProperty.Descriptor;
+import org.csstudio.display.builder.model.Version;
+import org.csstudio.display.builder.model.Widget;
+import org.csstudio.display.builder.model.WidgetCategory;
+import org.csstudio.display.builder.model.WidgetConfigurator;
+import org.csstudio.display.builder.model.WidgetDescriptor;
+import org.csstudio.display.builder.model.WidgetProperty;
+import org.csstudio.display.builder.model.WidgetPropertyCategory;
+import org.csstudio.display.builder.model.WidgetPropertyDescriptor;
+import org.csstudio.display.builder.model.persist.ModelReader;
+import org.csstudio.display.builder.model.persist.NamedWidgetColors;
+import org.csstudio.display.builder.model.persist.NamedWidgetFonts;
+import org.csstudio.display.builder.model.persist.WidgetColorService;
+import org.csstudio.display.builder.model.persist.WidgetFontService;
+import org.csstudio.display.builder.model.persist.XMLTags;
+import org.csstudio.display.builder.model.properties.CommonWidgetProperties;
+import org.csstudio.display.builder.model.properties.RotationStep;
+import org.csstudio.display.builder.model.properties.StringWidgetProperty;
+import org.csstudio.display.builder.model.properties.WidgetColor;
+import org.csstudio.display.builder.model.properties.WidgetFont;
+import org.csstudio.display.builder.model.widgets.PVWidget;
+import org.phoebus.framework.persistence.XMLUtil;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Text;
+
+/**
+ * Widget that provides button for invoking actions.
+ *
+ *
The widget doesn't directly act on its primary PV. The PV is mostly used like a macro for
+ * actions that write to a "$(pv_name)" PV. It is used for the alarm sensitive border, and "text"
+ * (label) can have a special value "$(pv_value)" to update with value changes.
+ *
+ * @author Lorenzo Gomez
+ */
+@SuppressWarnings("nls")
+public class CommanderVideoWidget extends PVWidget {
+ public static final int DEFAULT_WIDTH = 100, DEFAULT_HEIGHT = 30;
+
+ // Elements of Plot Marker
+ private static final WidgetPropertyDescriptor propValue =
+ CommonWidgetProperties.newStringPropertyDescriptor(
+ WidgetPropertyCategory.RUNTIME, "value", Messages.WidgetProperties_Value);
+
+ private static final StructuredWidgetProperty.Descriptor propPv =
+ new Descriptor(WidgetPropertyCategory.DISPLAY, "argument", "Argument");
+
+ /** When "text" has this value, it will reflect the primary PV's value */
+ public static final String VALUE_LABEL = "$(pv_value)";
+
+ /** 'tooltip' property: Text to display in tooltip */
+ public static final WidgetPropertyDescriptor propVideoURL =
+ new WidgetPropertyDescriptor<>(WidgetPropertyCategory.BEHAVIOR, "Video URL", "Video URL") {
+ @Override
+ public WidgetProperty createProperty(final Widget widget, final String value) {
+ return new StringWidgetProperty(this, widget, value);
+ }
+ };
+
+ private WidgetProperty videoURL;
+
+ /** Widget descriptor */
+ public static final WidgetDescriptor WIDGET_DESCRIPTOR =
+ new WidgetDescriptor(
+ "commander_camera_button",
+ WidgetCategory.CONTROL,
+ "Video",
+ "/icons/video.png",
+ "Stream realtime video") {
+ @Override
+ public Widget createWidget() {
+ return new CommanderVideoWidget();
+ }
+ };
+
+ // The legacy MenuButton can have arbitrary actions,
+ // like an ActionButton.
+ // If, however, the "pv_name" was configured,
+ // the "label" was ignored and replaced with
+ // the current value of the PV.
+ // It was mostly used with "actions_from_pv",
+ // behaving exactly like a Combo,
+ // but sometimes with "Write PV" actions
+ // to get custom labels and values.
+
+ /** Check if XML describes a legacy Menu Button */
+ static boolean isMenuButton(final Element xml) {
+ final String typeId = xml.getAttribute("typeId");
+ return typeId.equals("org.csstudio.opibuilder.widgets.MenuButton");
+ }
+
+ /** Should legacy Menu Button be converted into Combo? */
+ static boolean shouldUseCombo(final Element xml) {
+ // Legacy Menu Button with actions_from_pv set should be handled as combo
+ if (XMLUtil.getChildBoolean(xml, "actions_from_pv").orElse(true)) return true;
+
+ // Check for actions
+ final Element el = XMLUtil.getChildElement(xml, "actions");
+ if (el != null && XMLUtil.getChildElement(el, XMLTags.ACTION) != null) {
+ // There are actions, so use Action Button
+ return false;
+ }
+ // There are no actions.
+ // Use combo because that will at least show a value for a PV,
+ // while Action Button would do nothing at all.
+ return true;
+ }
+
+ /** Custom configurator to read legacy *.opi files */
+ private static class ActionButtonConfigurator extends WidgetConfigurator {
+ public ActionButtonConfigurator(final Version xml_version) {
+ super(xml_version);
+ }
+
+ @Override
+ public boolean configureFromXML(
+ final ModelReader model_reader, final Widget widget, final Element xml) throws Exception {
+ if (isMenuButton(xml)) {
+ if (shouldUseCombo(xml)) return false;
+
+ // Menu buttons used "label" instead of text
+ final Element label_el = XMLUtil.getChildElement(xml, "label");
+
+ if (label_el != null) {
+ final Document doc = xml.getOwnerDocument();
+ final Element the_text = doc.createElement(propText.getName());
+
+ if (label_el.getFirstChild() != null)
+ the_text.appendChild(label_el.getFirstChild().cloneNode(true));
+ else {
+ Text the_label = doc.createTextNode(VALUE_LABEL);
+ the_text.appendChild(the_label);
+ }
+ xml.appendChild(the_text);
+ }
+ }
+
+ super.configureFromXML(model_reader, widget, xml);
+
+ final CommanderVideoWidget button = (CommanderVideoWidget) widget;
+ final MacroizedWidgetProperty tooltip =
+ (MacroizedWidgetProperty) button.propTooltip();
+ if (xml_version.getMajor() < 3) {
+ // See getInitialTooltip()
+ tooltip.setSpecification(tooltip.getSpecification().replace("pv_value", "actions"));
+
+ // In BOY, individual actions could have a
+ // This has been simplified to an overall confirmation setting for the button,
+ // so move the (last) confirm message from action(s) to the button.
+ final Element actions =
+ XMLUtil.getChildElement(xml, CommonWidgetProperties.propActions.getName());
+ if (actions != null) {}
+ }
+ // If there is no pv_name, remove from tool tip ..
+ // if (
+ // ((MacroizedWidgetProperty)button.propPVName()).getSpecification().isEmpty())
+ // {
+ // tooltip.setSpecification(tooltip.getSpecification().replace("$(pv_name)\n",
+ // ""));
+ // // .. and label
+ // if (
+ // ((MacroizedWidgetProperty)button.propText()).getSpecification().equals(VALUE_LABEL))
+ // button.propText().setValue("");
+ // }
+
+ return true;
+ }
+ }
+
+ @Override
+ public WidgetConfigurator getConfigurator(final Version persisted_version) throws Exception {
+ return new ActionButtonConfigurator(persisted_version);
+ }
+
+ // Has pv_name and pv_writable, but no pv_value which would make it a WritablePVWidget
+ private volatile WidgetProperty enabled;
+ private volatile WidgetProperty text;
+ private volatile WidgetProperty font;
+ private volatile WidgetProperty foreground;
+ private volatile WidgetProperty background;
+ private volatile WidgetProperty transparent;
+ private volatile WidgetProperty rotation_step;
+ private volatile WidgetProperty pv_writable;
+
+ public CommanderVideoWidget() {
+ super(WIDGET_DESCRIPTOR.getType(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
+ }
+
+ /** org.csstudio.opibuilder.widgets.ActionButton used 2.0.0 */
+ private static final Version VERSION = Version.parse("3.0.0");
+
+ /** @return Widget version number */
+ @Override
+ public Version getVersion() {
+ return VERSION;
+ }
+
+ @Override
+ protected void defineProperties(final List> properties) {
+ super.defineProperties(properties);
+ properties.add(text = propText.createProperty(this, "$(actions)"));
+ properties.add(
+ font = propFont.createProperty(this, WidgetFontService.get(NamedWidgetFonts.DEFAULT)));
+ properties.add(
+ foreground =
+ propForegroundColor.createProperty(
+ this, WidgetColorService.getColor(NamedWidgetColors.TEXT)));
+ properties.add(
+ background =
+ propBackgroundColor.createProperty(
+ this, WidgetColorService.getColor(NamedWidgetColors.BUTTON_BACKGROUND)));
+ properties.add(transparent = propTransparent.createProperty(this, false));
+ properties.add(rotation_step = propRotationStep.createProperty(this, RotationStep.NONE));
+ properties.add(enabled = propEnabled.createProperty(this, true));
+ properties.add(pv_writable = runtimePropPVWritable.createProperty(this, true));
+
+ properties.add(videoURL = propVideoURL.createProperty(this, "tcp://examplevideo.com:1235"));
+ }
+
+ @Override
+ protected String getInitialTooltip() {
+ // Default would show $(pv_value), which doesn't exist for this widget.
+ // Use $(actions) instead.
+ return "$(pv_name)\n$(actions)";
+ }
+
+ /** @return 'text' property */
+ public WidgetProperty propText() {
+ return text;
+ }
+
+ /** @return 'font' property */
+ public WidgetProperty propFont() {
+ return font;
+ }
+
+ /** @return 'foreground_color' property */
+ public WidgetProperty propForegroundColor() {
+ return foreground;
+ }
+
+ /** @return 'background_color' property */
+ public WidgetProperty propBackgroundColor() {
+ return background;
+ }
+
+ /** @return 'transparent' property */
+ public WidgetProperty propTransparent() {
+ return transparent;
+ }
+
+ /** @return 'rotation_step' property */
+ public WidgetProperty propRotationStep() {
+ return rotation_step;
+ }
+
+ /** @return 'enabled' property */
+ public WidgetProperty propEnabled() {
+ return enabled;
+ }
+
+ /** @return 'pv_writable' property */
+ public final WidgetProperty runtimePropPVWritable() {
+ return pv_writable;
+ }
+
+ public WidgetProperty propVideoURl() {
+ return videoURL;
+ }
+}
diff --git a/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/WHBaseWidgetsService.java b/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/WHBaseWidgetsService.java
index e16057ca97..3680308f96 100644
--- a/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/WHBaseWidgetsService.java
+++ b/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/WHBaseWidgetsService.java
@@ -23,6 +23,7 @@ public Collection getWidgetDescriptors() {
return List.of(
WHTextUpdateWidget.WIDGET_DESCRIPTOR,
CommanderCommandActionButtonWidget.WIDGET_DESCRIPTOR,
+ CommanderVideoWidget.WIDGET_DESCRIPTOR,
WaypointModel.WIDGET_DESCRIPTOR);
}
}
diff --git a/app/commander/app-commander-display-representation-javafx/.classpath b/app/commander/app-commander-display-representation-javafx/.classpath
index b010aef042..65f270a052 100644
--- a/app/commander/app-commander-display-representation-javafx/.classpath
+++ b/app/commander/app-commander-display-representation-javafx/.classpath
@@ -36,5 +36,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/commander/app-commander-display-representation-javafx/.project b/app/commander/app-commander-display-representation-javafx/.project
index c3fe522ee0..5f6e06153d 100644
--- a/app/commander/app-commander-display-representation-javafx/.project
+++ b/app/commander/app-commander-display-representation-javafx/.project
@@ -20,4 +20,15 @@
org.eclipse.jdt.core.javanature
org.eclipse.m2e.core.maven2Nature
+
+
+ 1728427135613
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
diff --git a/app/commander/app-commander-display-representation-javafx/Makefile b/app/commander/app-commander-display-representation-javafx/Makefile
new file mode 100644
index 0000000000..09395e6e2b
--- /dev/null
+++ b/app/commander/app-commander-display-representation-javafx/Makefile
@@ -0,0 +1,4 @@
+format:
+ mvn com.coveo:fmt-maven-plugin:format
+dev-build:
+ mvn install -T6 -DskipTests -Dfmt.skip
diff --git a/app/commander/app-commander-display-representation-javafx/pom.xml b/app/commander/app-commander-display-representation-javafx/pom.xml
index 6510492f22..a08911e974 100644
--- a/app/commander/app-commander-display-representation-javafx/pom.xml
+++ b/app/commander/app-commander-display-representation-javafx/pom.xml
@@ -11,6 +11,52 @@
app-commander-display-model
0.3.1-SNAPSHOT
+
+
+ uk.co.caprica
+ vlcj
+ 4.8.3
+
+
+
+
+ uk.co.caprica
+ vlcj-javafx
+ 1.2.0
+
+
+
+
+
+
+
+
+
+
+
app-commander-display-representation-javafx
\ No newline at end of file
diff --git a/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/BaseWidgetRepresentations.java b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/BaseWidgetRepresentations.java
index 2015100da7..2786ff1e4d 100644
--- a/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/BaseWidgetRepresentations.java
+++ b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/BaseWidgetRepresentations.java
@@ -10,6 +10,7 @@
import static java.util.Map.entry;
import com.windhoverlabs.display.model.widgets.CommanderCommandActionButtonWidget;
+import com.windhoverlabs.display.model.widgets.CommanderVideoWidget;
import com.windhoverlabs.display.model.widgets.WHTextUpdateWidget;
import com.windhoverlabs.display.model.widgets.WaypointModel;
import java.util.Map;
@@ -41,6 +42,9 @@ public class BaseWidgetRepresentations implements WidgetRepresentationsService {
() -> (WidgetRepresentation) new CommanderActionButtonRepresentation()),
entry(
WaypointModel.WIDGET_DESCRIPTOR,
- () -> (WidgetRepresentation) new WaypointRepresentation()));
+ () -> (WidgetRepresentation) new WaypointRepresentation()),
+ entry(
+ CommanderVideoWidget.WIDGET_DESCRIPTOR,
+ () -> (WidgetRepresentation) new CommanderVideoRepresentation()));
}
}
diff --git a/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/CommanderVideoRepresentation.java b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/CommanderVideoRepresentation.java
new file mode 100644
index 0000000000..ff82b43774
--- /dev/null
+++ b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/CommanderVideoRepresentation.java
@@ -0,0 +1,684 @@
+/*******************************************************************************
+ * Copyright (c) 2015-2020 Oak Ridge National Laboratory.
+ * 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
+ *******************************************************************************/
+package com.windhoverlabs.display.representation;
+
+import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger;
+
+import com.windhoverlabs.display.model.widgets.CommanderVideoWidget;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBase;
+import javafx.scene.control.MenuButton;
+import javafx.scene.control.MenuItem;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.shape.StrokeLineJoin;
+import javafx.scene.shape.StrokeType;
+import org.csstudio.display.builder.model.DirtyFlag;
+import org.csstudio.display.builder.model.UntypedWidgetPropertyListener;
+import org.csstudio.display.builder.model.WidgetProperty;
+import org.csstudio.display.builder.model.WidgetPropertyListener;
+import org.csstudio.display.builder.model.properties.ActionInfo;
+import org.csstudio.display.builder.model.properties.ActionInfos;
+import org.csstudio.display.builder.model.properties.OpenDisplayActionInfo;
+import org.csstudio.display.builder.model.properties.RotationStep;
+import org.csstudio.display.builder.model.properties.StringWidgetProperty;
+import org.csstudio.display.builder.model.properties.WritePVActionInfo;
+import org.csstudio.display.builder.model.widgets.ActionButtonWidget;
+import org.csstudio.display.builder.model.widgets.PVWidget;
+import org.csstudio.display.builder.representation.javafx.Cursors;
+import org.csstudio.display.builder.representation.javafx.JFXUtil;
+import org.csstudio.display.builder.representation.javafx.Messages;
+import org.csstudio.display.builder.representation.javafx.widgets.RegionBaseRepresentation;
+import org.csstudio.display.builder.representation.javafx.widgets.TooltipSupport;
+import org.epics.vtype.VType;
+import org.phoebus.framework.macros.MacroHandler;
+import org.phoebus.framework.macros.MacroValueProvider;
+import org.phoebus.ui.javafx.Styles;
+import org.phoebus.ui.vtype.FormatOption;
+import org.phoebus.ui.vtype.FormatOptionHandler;
+import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface;
+
+/**
+ * Creates JavaFX item for model widget
+ *
+ * @author Megan Grodowitz
+ * @author Kay Kasemir
+ */
+@SuppressWarnings("nls")
+public class CommanderVideoRepresentation
+ extends RegionBaseRepresentation {
+ // Uses a Button if there is only one action,
+ // otherwise a MenuButton so that user can select the specific action.
+ //
+ // These two types were chosen because they share the same ButtonBase base class.
+ // ChoiceBox is not derived from ButtonBase, plus it has currently selected 'value',
+ // and with action buttons it wouldn't make sense to select one of the actions.
+ //
+ // The 'base' button is wrapped in a 'pane'
+ // to allow replacing the button as actions change from single actions (or zero)
+ // to multiple actions.
+
+ private final DirtyFlag dirty_representation = new DirtyFlag();
+ private final DirtyFlag dirty_enablement = new DirtyFlag();
+ private final DirtyFlag dirty_actionls = new DirtyFlag();
+
+ private final DirtyFlag dirty_content = new DirtyFlag();
+
+ private volatile String value_text_video_url = ">";
+
+ private volatile Node base;
+ private volatile String background;
+ private volatile Color foreground;
+ private volatile String button_text;
+ private volatile boolean enabled = true;
+ private volatile boolean writable = true;
+
+ // Had to do this because the ones in GroupRepresentation are scoped to the package
+ static final BorderWidths EDIT_NONE_BORDER =
+ new BorderWidths(0.5, 0.5, 0.5, 0.5, false, false, false, false);
+ static final BorderStrokeStyle EDIT_NONE_DASHED =
+ new BorderStrokeStyle(
+ StrokeType.INSIDE,
+ StrokeLineJoin.MITER,
+ StrokeLineCap.BUTT,
+ 10,
+ 0,
+ List.of(
+ Double.valueOf(11.11),
+ Double.valueOf(7.7),
+ Double.valueOf(3.3),
+ Double.valueOf(7.7)));
+
+ /**
+ * Was there ever any transformation applied to the jfx_node?
+ *
+ * Used to optimize: If there never was a rotation, don't even _clear()_ it to keep the Node's
+ * nodeTransformation == null
+ */
+ private boolean was_ever_transformed = false;
+
+ /**
+ * Is it a 'Write PV' action?
+ *
+ *
If not, we don't have to disable the button if the PV is readonly and/or disconnected
+ */
+ private volatile boolean is_writePV = false;
+
+ /** Optional modifier of the open display 'target */
+ private Optional target_modifier = Optional.empty();
+
+ private Pane pane;
+
+ private final UntypedWidgetPropertyListener buttonChangedListener = this::buttonChanged;
+ private final UntypedWidgetPropertyListener representationChangedListener =
+ this::representationChanged;
+ private final WidgetPropertyListener enablementChangedListener = this::enablementChanged;
+ private final UntypedWidgetPropertyListener pvsListener = this::urlPVUpdate;
+
+ private final WidgetPropertyListener pvNameListener = this::pvnameChanged;
+ private final UntypedWidgetPropertyListener contentListener = this::contentChanged;
+
+ private static ImageView videoImageView;
+
+ private Node mediaPlayerInit(String mediaURL) {
+
+ System.out.println("meidiaPlayerInit**");
+
+ this.videoImageView = new ImageView();
+ this.videoImageView.setPreserveRatio(true);
+
+ VideoSingleton.getInstance()
+ .getEmbeddedMediaPlayer()
+ .videoSurface()
+ .set(new ImageViewVideoSurface(this.videoImageView));
+
+ // ---------------------------------
+
+ BorderPane root = new BorderPane();
+ root.setStyle("-fx-background-color: black;");
+
+ root.setStyle("-fx-background-color: black;" + "-fx-border-width: 2px;");
+
+ videoImageView.fitWidthProperty().bind(root.widthProperty());
+ videoImageView.fitHeightProperty().bind(root.heightProperty());
+
+ root.setPrefSize(model_widget.propWidth().getValue(), model_widget.propHeight().getValue());
+
+ root.widthProperty()
+ .addListener(
+ (observableValue, oldValue, newValue) -> {
+ // If you need to know about resizes
+ });
+
+ root.heightProperty()
+ .addListener(
+ (observableValue, oldValue, newValue) -> {
+ // If you need to know about resizes
+ });
+
+ // videoImageView
+
+ root.setCenter(videoImageView);
+
+ updateVideo(mediaURL);
+
+ VideoSingleton.getInstance().getEmbeddedMediaPlayer().controls().setPosition(0.4f);
+
+ return root;
+ }
+
+ @Override
+ protected boolean isFilteringEditModeClicks() {
+ return true;
+ }
+
+ @Override
+ public Pane createJFXNode() throws Exception {
+ updateColors();
+
+ base = mediaPlayerInit(model_widget.propVideoURl().getValue());
+
+ System.out.println(
+ "model_widget.propVideoURl().getValue()-->" + model_widget.propVideoURl().getValue());
+
+ pane = new Pane();
+ pane.getChildren().add(base);
+
+ return pane;
+ }
+
+ private String computeURLText(final VType value) {
+ Objects.requireNonNull(model_widget, "No widget");
+ if (value == null) return "<" + model_widget.propPVName().getValue() + ">";
+ if (value == PVWidget.RUNTIME_VALUE_NO_PV) return "";
+ return FormatOptionHandler.format(value, FormatOption.STRING, -1, false);
+ }
+
+ private void pvnameChanged(
+ final WidgetProperty property,
+ final String old_value,
+ final String new_value) { // PV name typically changes in edit mode.
+ // -> Show new PV name.
+ // Runtime could deal with disconnect/reconnect for new PV name
+ // -> Also OK to show disconnected state until runtime
+ // subscribes to new PV, so we eventually get values from new PV.
+ value_text_video_url = computeURLText(null);
+ dirty_content.mark();
+ toolkit.scheduleUpdate(this);
+ }
+
+ /** @param event Mouse event to check for target modifier keys */
+ private void checkModifiers(final MouseEvent event) {
+ // if (!enabled) {
+ // // Do not let the user click a disabled button
+ // event.consume();
+ // base.disarm();
+ // return;
+ // }
+ //
+ // // 'control' ('command' on Mac OS X)
+ // if (event.isShortcutDown()) target_modifier =
+ // Optional.of(OpenDisplayActionInfo.Target.TAB);
+ // else if (event.isShiftDown())
+ // target_modifier = Optional.of(OpenDisplayActionInfo.Target.WINDOW);
+ // else target_modifier = Optional.empty();
+ //
+ // // At least on Linux, a Control-click or Shift-click
+ // // will not 'arm' the button, so the click is basically ignored.
+ // // Force the 'arm', so user can Control-click or Shift-click to
+ // // invoke the button
+ // if (target_modifier.isPresent()) {
+ // logger.log(
+ // Level.FINE, "{0} modifier: {1}", new Object[] {model_widget,
+ // target_modifier.get()});
+ // base.arm();
+ // }
+ }
+
+ // private int calls = 0;
+
+ /**
+ * Create base, either single-action button or menu for selecting one out of N
+ * actions
+ */
+ private ButtonBase makeBaseButton() {
+ final ActionInfos actions = model_widget.propActions().getValue();
+ final ButtonBase result;
+ boolean has_non_writePVAction = false;
+
+ for (final ActionInfo action : actions.getActions()) {
+ if (action instanceof WritePVActionInfo) is_writePV = true;
+ else has_non_writePVAction = true;
+ }
+
+ if (actions.isExecutedAsOne() || actions.getActions().size() < 2) {
+ final Button button = new Button();
+ button.setGraphic(new ImageView("/icons/video.png"));
+ result = button;
+ } else {
+ // If there is at least one non-WritePVActionInfo then is_writePV should be false
+ is_writePV = !has_non_writePVAction;
+
+ final MenuButton button = new MenuButton();
+ // Experimenting with ways to force update of popup location,
+ // #226
+ button
+ .showingProperty()
+ .addListener(
+ (prop, old, showing) -> {
+ if (showing) {
+ // System.out.println("Showing " + model_widget + " menu: " + showing);
+ // if (++calls > 2)
+ // {
+ // System.out.println("Hack!");
+ // if (button.getPopupSide() == Side.BOTTOM)
+ // button.setPopupSide(Side.LEFT);
+ // else
+ // button.setPopupSide(Side.BOTTOM);
+ // // button.layout();
+ // }
+ }
+ });
+ for (final ActionInfo action : actions.getActions()) {
+ final MenuItem item =
+ new MenuItem(
+ makeActionText(action),
+ new ImageView(new Image(action.getType().getIconURL().toExternalForm())));
+ item.getStyleClass().add("action_button_item");
+ item.setOnAction(event -> confirm(() -> handleAction(action)));
+ button.getItems().add(item);
+ }
+ result = button;
+ }
+
+ result.setStyle(background);
+
+ // In edit mode, show dashed border for transparent/invisible widget
+ if (toolkit.isEditMode() && model_widget.propTransparent().getValue())
+ result.setBorder(
+ new Border(
+ new BorderStroke(
+ Color.BLACK, EDIT_NONE_DASHED, CornerRadii.EMPTY, EDIT_NONE_BORDER)));
+ result.getStyleClass().add("action_button");
+ result.setMnemonicParsing(false);
+
+ // Model has width/height, but JFX widget has min, pref, max size.
+ // updateChanges() will set the 'pref' size, so make min use that as well.
+ result.setMinSize(ButtonBase.USE_PREF_SIZE, ButtonBase.USE_PREF_SIZE);
+
+ // Monitor keys that modify the OpenDisplayActionInfo.Target.
+ // Use filter to capture event that's otherwise already handled.
+ if (!toolkit.isEditMode())
+ result.addEventFilter(MouseEvent.MOUSE_PRESSED, this::checkModifiers);
+
+ // Need to attach TT to the specific button, not the common jfx_node Pane
+ TooltipSupport.attach(result, model_widget.propTooltip());
+
+ return result;
+ }
+
+ /** Called by ContextMenuSupport when an action menu is selected */
+ public void handleContextMenuAction(ActionInfo action) {
+ if (action instanceof WritePVActionInfo && !writable) {
+ logger.log(Level.FINE, "{0} ignoring WritePVActionInfo because of readonly PV", model_widget);
+ return;
+ }
+
+ confirm(() -> toolkit.fireAction(model_widget, action));
+ }
+
+ private void confirm(final Runnable action) {
+ System.out.println("confirm trigger");
+ Platform.runLater(
+ () -> {
+ // If confirmation is requested..
+
+ action.run();
+ });
+ }
+
+ /** @return Should 'label' show the PV's current value? */
+ private boolean isLabelValue() {
+ final StringWidgetProperty text_prop = (StringWidgetProperty) model_widget.propText();
+ return ActionButtonWidget.VALUE_LABEL.equals(text_prop.getSpecification());
+ }
+
+ private String makeButtonText() {
+ // If text is "$(actions)", evaluate the actions ourself because
+ // a) That way we can format it beyond just "[ action1, action2, ..]"
+ // b) Macro won't be re-evaluated as actions change,
+ // while this code will always use current actions
+ final StringWidgetProperty text_prop = (StringWidgetProperty) model_widget.propText();
+ if (isLabelValue())
+ // return FormatOptionHandler.format(model_widget.runtimePropValue().getValue(),
+ // FormatOption.DEFAULT, -1, true);
+ return "dummy_pv";
+ else if ("$(actions)".equals(text_prop.getSpecification())) {
+ final List actions = model_widget.propActions().getValue().getActions();
+ if (actions.size() < 1) return Messages.ActionButton_NoActions;
+ if (actions.size() > 1) {
+ if (model_widget.propActions().getValue().isExecutedAsOne())
+ return MessageFormat.format(Messages.ActionButton_N_ActionsAsOneFmt, actions.size());
+
+ return MessageFormat.format(Messages.ActionButton_N_ActionsFmt, actions.size());
+ }
+ return makeActionText(actions.get(0));
+ } else return text_prop.getValue();
+ }
+
+ private String makeActionText(final ActionInfo action) {
+ String action_str = action.getDescription();
+ if (action_str.isEmpty()) action_str = action.toString();
+ String expanded;
+ try {
+ final MacroValueProvider macros = model_widget.getMacrosOrProperties();
+ expanded = MacroHandler.replace(macros, action_str);
+ } catch (final Exception ex) {
+ logger.log(
+ Level.WARNING,
+ model_widget + " action " + action + " cannot expand macros for " + action_str,
+ ex);
+ expanded = action_str;
+ }
+ return expanded;
+ }
+
+ /** @param actions Actions that the user invoked */
+ private void handleActions(final List actions) {
+ for (ActionInfo action : actions) handleAction(action);
+ }
+
+ /**
+ * @param action Action that the user invoked * In the context of commander, this means sending a
+ * command to the server, which in turn sends it to the vehicle.
+ */
+ private void handleAction(ActionInfo action) {
+ // Keyboard presses are not supressed so check if the widget is enabled
+ System.out.println("$$$$handleAction$$$");
+ // send command to yamcs
+ model_widget.getPropertyValue("");
+
+ if (!enabled) return;
+
+ logger.log(Level.FINE, "{0} pressed", model_widget);
+
+ if (action instanceof WritePVActionInfo && !writable) {
+ logger.log(Level.FINE, "{0} ignoring WritePVActionInfo because of readonly PV", model_widget);
+ return;
+ }
+
+ if (action instanceof OpenDisplayActionInfo && target_modifier.isPresent()) {
+ final OpenDisplayActionInfo orig = (OpenDisplayActionInfo) action;
+ action =
+ new OpenDisplayActionInfo(
+ orig.getDescription(),
+ orig.getFile(),
+ orig.getMacros(),
+ target_modifier.get(),
+ orig.getPane());
+ }
+ toolkit.fireAction(model_widget, action);
+ }
+
+ @Override
+ protected void registerListeners() {
+ updateColors();
+ super.registerListeners();
+
+ model_widget.propWidth().addUntypedPropertyListener(representationChangedListener);
+ model_widget.propHeight().addUntypedPropertyListener(representationChangedListener);
+ model_widget.propText().addUntypedPropertyListener(representationChangedListener);
+ model_widget.propFont().addUntypedPropertyListener(representationChangedListener);
+ model_widget.propRotationStep().addUntypedPropertyListener(representationChangedListener);
+
+ model_widget.propEnabled().addPropertyListener(enablementChangedListener);
+ model_widget.runtimePropPVWritable().addPropertyListener(enablementChangedListener);
+
+ model_widget.propBackgroundColor().addUntypedPropertyListener(buttonChangedListener);
+ model_widget.propForegroundColor().addUntypedPropertyListener(buttonChangedListener);
+ model_widget.propTransparent().addUntypedPropertyListener(buttonChangedListener);
+ model_widget.propActions().addUntypedPropertyListener(buttonChangedListener);
+ // model_widget.propPvs().getValue().get(0).addUntypedPropertyListener(pvsListener);
+
+ // if (! toolkit.isEditMode() && isLabelValue())
+ //
+ model_widget.runtimePropValue().addUntypedPropertyListener(pvsListener);
+
+ model_widget.propPVName().addPropertyListener(pvNameListener);
+
+ // Initial update in case runtimePropValue already has value before we registered listener
+ contentChanged(null, null, model_widget.runtimePropValue().getValue());
+
+ enablementChanged(null, null, null);
+ }
+
+ private void contentChanged(
+ final WidgetProperty> property, final Object old_value, final Object new_value) {
+ final String new_text = computeURLText(model_widget.runtimePropValue().getValue());
+ // Skip update if it's the same text
+ System.out.println("new URL1:" + value_text_video_url);
+
+ if (value_text_video_url.equals(new_text)) return;
+
+ System.out.println("new URL2:" + value_text_video_url);
+ value_text_video_url = new_text;
+ dirty_content.mark();
+ toolkit.scheduleUpdate(this);
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ // if (! toolkit.isEditMode() && isLabelValue())
+ //
+ // model_widget.runtimePropValue().removePropertyListener(representationChangedListener);
+ model_widget.propWidth().removePropertyListener(representationChangedListener);
+ model_widget.propHeight().removePropertyListener(representationChangedListener);
+ model_widget.propText().removePropertyListener(representationChangedListener);
+ model_widget.propFont().removePropertyListener(representationChangedListener);
+ model_widget.propRotationStep().removePropertyListener(representationChangedListener);
+ model_widget.propEnabled().removePropertyListener(enablementChangedListener);
+ model_widget.runtimePropPVWritable().removePropertyListener(enablementChangedListener);
+ model_widget.propBackgroundColor().removePropertyListener(buttonChangedListener);
+ model_widget.propForegroundColor().removePropertyListener(buttonChangedListener);
+ model_widget.propTransparent().removePropertyListener(buttonChangedListener);
+ model_widget.propActions().removePropertyListener(buttonChangedListener);
+ super.unregisterListeners();
+ }
+
+ @Override
+ protected void attachTooltip() {
+ // Cannot attach tool tip to the jfx_node (Pane).
+ // Needs to be attached to actual button, which
+ // is done in makeBaseButton()
+ }
+
+ /** Complete button needs to be updated */
+ private void buttonChanged(
+ final WidgetProperty> property, final Object old_value, final Object new_value) {
+ dirty_actionls.mark();
+ representationChanged(property, old_value, new_value);
+ }
+
+ public void urlPVUpdate(
+ final WidgetProperty> property, final Object old_value, final Object new_value) {
+
+ String new_value_string =
+ FormatOptionHandler.format((VType) new_value, FormatOption.STRING, -1, false);
+
+ // System.out.println("Val:" + val);
+ if (old_value != null) {
+ String old_value_string =
+ FormatOptionHandler.format((VType) old_value, FormatOption.STRING, -1, false);
+ if (!old_value_string.equals(new_value_string)) {
+ // If URL has changed, stream from new URL
+ System.out.println("New video feed.");
+ boolean success = updateVideo(new_value_string);
+ if (success) {
+ System.out.println("Play returned success");
+ } else {
+ System.out.println("Play returned error");
+ }
+ } else {
+ System.out.println("Same old video feed. Do nothing");
+ }
+ } else {
+ //
+ /**
+ * We only have a the new video URL. Check videostarted flag. If false, start video at new URL
+ */
+ System.out.println("New video feed (if videostarted flag is false)");
+
+ boolean success = updateVideo(new_value_string);
+ if (success) {
+ System.out.println("Play returned success");
+ } else {
+ System.out.println("Play returned error");
+ }
+
+ // embeddedMediaPlayer.media().info().type()
+ }
+ }
+
+ /**
+ * Updates video with new URL
+ *
+ * @param property
+ * @param old_value
+ * @param new_value
+ */
+ private void representationChanged(
+ final WidgetProperty> property, final Object old_value, final Object new_value) {
+ updateColors();
+
+ dirty_representation.mark();
+ toolkit.scheduleUpdate(this);
+ }
+
+ private boolean updateVideo(String new_value_string) {
+ return VideoSingleton.getInstance().getEmbeddedMediaPlayer().media().play(new_value_string);
+ }
+
+ /** Only details of the existing button need to be updated */
+ private void pvsChanged(
+ final WidgetProperty> property, final Object old_value, final Object new_value) {
+ System.out.println("pvsChanged");
+ // updateColors();
+ // dirty_representation.mark();
+ // toolkit.scheduleUpdate(this);
+ }
+
+ /** enabled or pv_writable changed */
+ private void enablementChanged(
+ final WidgetProperty property, final Boolean old_value, final Boolean new_value) {
+ enabled = model_widget.propEnabled().getValue();
+ writable = model_widget.runtimePropPVWritable().getValue();
+ // If clicking on the button would result in a PV write then enabled has to be false if PV is
+ // not writable
+ if (is_writePV) enabled &= writable;
+ dirty_enablement.mark();
+ toolkit.scheduleUpdate(this);
+ }
+
+ private void updateColors() {
+ foreground = JFXUtil.convert(model_widget.propForegroundColor().getValue());
+ if (model_widget.propTransparent().getValue())
+ // Set most colors to transparent, including the 'arrow' used by MenuButton
+ background =
+ "-fx-background: transparent; -fx-color: transparent; -fx-focus-color: rgba(3,158,211,0.1); -fx-mark-color: transparent; -fx-background-color: transparent;";
+ else background = JFXUtil.shadedStyle(model_widget.propBackgroundColor().getValue());
+ }
+
+ @Override
+ public void updateChanges() {
+ super.updateChanges();
+ if (dirty_actionls.checkAndClear()) {
+ // base = meidiaPlayerInit();
+ // jfx_node.getChildren().setAll(base);
+ }
+ if (dirty_representation.checkAndClear()) {
+ button_text = makeButtonText();
+ // base.setText(button_text);
+ // base.setTextFill(foreground);
+ // base.setFont(JFXUtil.convert(model_widget.propFont().getValue()));
+
+ // If widget is not wide enough to show the label, hide menu button 'arrow'.
+ // if (base instanceof MenuButton) {
+ // // Assume that desired gap and arrow occupy similar space as "__VV_".
+ // // Check if the text exceeds the width.
+ // final Dimension2D size = TextUtils.computeTextSize(base.getFont(), button_text +
+ // "__VV_");
+ // final boolean hide = size.getWidth() >= model_widget.propWidth().getValue();
+ // Styles.update(base, "hide_arrow", hide);
+ // }
+
+ final RotationStep rotation = model_widget.propRotationStep().getValue();
+ final int width = model_widget.propWidth().getValue(),
+ height = model_widget.propHeight().getValue();
+ // Button 'base' is inside 'jfx_node' Pane.
+ // Rotation needs to be applied to the Pane,
+ // which then auto-sizes to the 'base' Button dimensions.
+ // If transforming the Button instead of the Pane,
+ // it will still remain sensitive to mouse clicks in the
+ // original, un-transformed rectangle. Unclear why.
+ // Applying the transformation to the Pane does not exhibit this problem.
+ // switch (rotation) {
+ // case NONE:
+ // base.setPrefSize(width, height);
+ // if (was_ever_transformed) jfx_node.getTransforms().clear();
+ // break;
+ // case NINETY:
+ // base.setPrefSize(height, width);
+ // jfx_node
+ // .getTransforms()
+ // .setAll(new Rotate(-rotation.getAngle()), new Translate(-height, 0));
+ // was_ever_transformed = true;
+ // break;
+ // case ONEEIGHTY:
+ // base.setPrefSize(width, height);
+ // jfx_node
+ // .getTransforms()
+ // .setAll(new Rotate(-rotation.getAngle()), new Translate(-width, -height));
+ // was_ever_transformed = true;
+ // break;
+ // case MINUS_NINETY:
+ // base.setPrefSize(height, width);
+ // jfx_node
+ // .getTransforms()
+ // .setAll(new Rotate(-rotation.getAngle()), new Translate(0, -width));
+ // was_ever_transformed = true;
+ // break;
+ // }
+ }
+ if (dirty_enablement.checkAndClear()) {
+ // Don't disable the widget, because that would also remove the
+ // tooltip
+ // Just apply a style that matches the disabled look.
+ Styles.update(base, Styles.NOT_ENABLED, !enabled);
+ // Apply the cursor to the pane and not to the button
+ jfx_node.setCursor(enabled ? Cursor.HAND : Cursors.NO_WRITE);
+ }
+ }
+}
diff --git a/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/VideoSingleton.java b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/VideoSingleton.java
new file mode 100644
index 0000000000..82d2f35537
--- /dev/null
+++ b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/VideoSingleton.java
@@ -0,0 +1,72 @@
+package com.windhoverlabs.display.representation;
+
+import uk.co.caprica.vlcj.factory.MediaPlayerFactory;
+import uk.co.caprica.vlcj.player.base.MediaApi;
+import uk.co.caprica.vlcj.player.base.MediaPlayer;
+import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter;
+import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer;
+
+/**
+ * FIXME: The way this is written at the moment it will only allow for one stream at a time. Will
+ * have to implement an array of of the native objects and essentially manage the alloc/dealloc of
+ * the objects, will have to be tied to the lifetime of Phoebus unfortunately...
+ */
+public class VideoSingleton {
+
+ private MediaPlayerFactory mediaPlayerFactory;
+
+ private EmbeddedMediaPlayer embeddedMediaPlayer;
+
+ public EmbeddedMediaPlayer getEmbeddedMediaPlayer() {
+ return embeddedMediaPlayer;
+ }
+
+ private static MediaApi videoMedia;
+
+ // Step 1: Create a private static instance of the class (eager initialization)
+ private static VideoSingleton instance = null;
+
+ // Step 2: Make the constructor private so it cannot be instantiated from outside
+ private VideoSingleton() {
+ // Private constructor to prevent instantiation
+ }
+
+ // Step 3: Provide a public static method to return the instance
+ public static VideoSingleton getInstance() {
+ if (instance == null) {
+ instance = new VideoSingleton();
+
+ instance.mediaPlayerFactory = new MediaPlayerFactory();
+ instance.embeddedMediaPlayer =
+ instance.mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer();
+ instance
+ .embeddedMediaPlayer
+ .events()
+ .addMediaPlayerEventListener(
+ new MediaPlayerEventAdapter() {
+ @Override
+ public void playing(MediaPlayer mediaPlayer) {}
+
+ @Override
+ public void paused(MediaPlayer mediaPlayer) {}
+
+ @Override
+ public void stopped(MediaPlayer mediaPlayer) {}
+
+ @Override
+ public void timeChanged(MediaPlayer mediaPlayer, long newTime) {}
+ });
+ }
+ return instance;
+ }
+
+ // Example method to demonstrate singleton behavior
+ public void playVideo() {
+ System.out.println("Playing video...");
+ }
+
+ // Example method to demonstrate singleton behavior
+ public void stopVideo() {
+ System.out.println("Stopping video...");
+ }
+}
diff --git a/app/commander/app-commander-display-representation-javafx/src/main/resources/icons/video.png b/app/commander/app-commander-display-representation-javafx/src/main/resources/icons/video.png
new file mode 100644
index 0000000000..085cf498e9
Binary files /dev/null and b/app/commander/app-commander-display-representation-javafx/src/main/resources/icons/video.png differ
diff --git a/app/commander/app-commander-display-runtime/.classpath b/app/commander/app-commander-display-runtime/.classpath
index b010aef042..65f270a052 100644
--- a/app/commander/app-commander-display-runtime/.classpath
+++ b/app/commander/app-commander-display-runtime/.classpath
@@ -36,5 +36,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/commander/app-commander-display-runtime/.project b/app/commander/app-commander-display-runtime/.project
index 2a173dd9f2..baf0d36ec6 100644
--- a/app/commander/app-commander-display-runtime/.project
+++ b/app/commander/app-commander-display-runtime/.project
@@ -20,4 +20,15 @@
org.eclipse.jdt.core.javanature
org.eclipse.m2e.core.maven2Nature
+
+
+ 1728427135614
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
diff --git a/app/display/model/src/main/resources/icons/video.png b/app/display/model/src/main/resources/icons/video.png
new file mode 100644
index 0000000000..085cf498e9
Binary files /dev/null and b/app/display/model/src/main/resources/icons/video.png differ
diff --git a/app/pom.xml b/app/pom.xml
index ea33d3b74c..fba37b4ffc 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -22,7 +22,6 @@
databrowser
databrowser-timescale
display
- alarm
scan
channel
3d-viewer
diff --git a/build.xml b/build.xml
index 836bb18dd7..aac2526d8f 100644
--- a/build.xml
+++ b/build.xml
@@ -145,7 +145,6 @@
-
diff --git a/dependencies/phoebus-target/pom.xml b/dependencies/phoebus-target/pom.xml
index d3100a808a..9a8c1d2f0e 100644
--- a/dependencies/phoebus-target/pom.xml
+++ b/dependencies/phoebus-target/pom.xml
@@ -293,7 +293,7 @@
-
+
org.jfxtras
diff --git a/services/pom.xml b/services/pom.xml
index 45e8e700e4..e7441f9f3b 100644
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -8,11 +8,7 @@
4.6.10-SNAPSHOT
- alarm-server
archive-engine
- scan-server
- alarm-logger
- alarm-config-logger
save-and-restore