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