From 3e513c512f0c1178201d51959283c9916bcdbf4a Mon Sep 17 00:00:00 2001 From: lgomez Date: Tue, 8 Oct 2024 18:05:59 -0500 Subject: [PATCH 01/12] -Video widget. WIP. --- .../app-commander-display-model/.classpath | 17 + .../app-commander-display-model/.project | 11 + .../model/widgets/CommanderVideoWidget.java | 362 +++++++++++ .../model/widgets/WHBaseWidgetsService.java | 1 + .../.classpath | 17 + .../.project | 11 + .../pom.xml | 46 ++ .../BaseWidgetRepresentations.java | 7 +- .../CommanderVideoRepresentation.java | 582 ++++++++++++++++++ .../src/main/resources/icons/video.png | Bin 0 -> 615 bytes .../app-commander-display-runtime/.classpath | 17 + .../app-commander-display-runtime/.project | 11 + .../model/src/main/resources/icons/video.png | Bin 0 -> 615 bytes 13 files changed, 1081 insertions(+), 1 deletion(-) create mode 100644 app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/CommanderVideoWidget.java create mode 100644 app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/CommanderVideoRepresentation.java create mode 100644 app/commander/app-commander-display-representation-javafx/src/main/resources/icons/video.png create mode 100644 app/display/model/src/main/resources/icons/video.png 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..57cdf6bba5 --- /dev/null +++ b/app/commander/app-commander-display-model/src/main/java/com/windhoverlabs/display/model/widgets/CommanderVideoWidget.java @@ -0,0 +1,362 @@ +/******************************************************************************* + * 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.propCommandArgument; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmDialog; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmMessage; +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.propPVName; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; +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.Arrays; +import java.util.Collections; +import java.util.List; +import org.csstudio.display.builder.model.ArrayWidgetProperty; +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.WidgetColor; +import org.csstudio.display.builder.model.properties.WidgetFont; +import org.csstudio.display.builder.model.widgets.VisibleWidget; +import org.epics.vtype.VType; +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 VisibleWidget { + 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)"; + + /** Structure for Plot Marker */ + public static class PvArgProperty extends StructuredWidgetProperty { + protected PvArgProperty(final Widget widget, final String name) { + super( + propPv, + widget, + Arrays.asList( + propPVName.createProperty(widget, ""), + propValue.createProperty(widget, ""), + propCommandArgument.createProperty(widget, ""))); + } + + public WidgetProperty pv() { + return getElement(0); + } + + public WidgetProperty value() { + return getElement(1); + } + + public WidgetProperty argumentName() { + return getElement(2); + } + } + ; + + /** 'Arguments' array */ + private static final ArrayWidgetProperty.Descriptor propPVs = + new ArrayWidgetProperty.Descriptor<>( + WidgetPropertyCategory.MISC, + "argument", + "Arguments", + (widget, index) -> new PvArgProperty(widget, "Argument " + index), + 0); + + /** 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) + for (Element action : XMLUtil.getChildElements(actions, XMLTags.ACTION)) { + final String message = + XMLUtil.getChildString(action, propConfirmMessage.getName()).orElse(""); + if (!message.isBlank()) { + button.propConfirmMessage().setValue(message); + button.propConfirmDialog().setValue(true); + } + } + } + // 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; + private volatile WidgetProperty confirm_dialog; + private volatile WidgetProperty confirm_message; + private volatile WidgetProperty password; + private volatile ArrayWidgetProperty PVs; + private volatile WidgetProperty command; + + 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(confirm_dialog = propConfirmDialog.createProperty(this, false)); + properties.add( + confirm_message = + propConfirmMessage.createProperty(this, "Are your sure you want to do this?")); + properties.add(password = propPassword.createProperty(this, "")); + properties.add(PVs = propPVs.createProperty(this, Collections.emptyList())); + properties.add( + command = CommonWidgetProperties.propCommand.createProperty(this, "command_name")); + } + + @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; + } + + /** @return 'confirm_dialog' property */ + public WidgetProperty propConfirmDialog() { + return confirm_dialog; + } + + /** @return 'confirm_message' property */ + public WidgetProperty propConfirmMessage() { + return confirm_message; + } + + public WidgetProperty propCommand() { + return command; + } + + /** @return 'password' property */ + public WidgetProperty propPassword() { + return password; + } + + public ArrayWidgetProperty propPvs() { + return PVs; + } +} 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/pom.xml b/app/commander/app-commander-display-representation-javafx/pom.xml index d511c166c9..65fd8f1952 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.2.6-SNAPSHOT + + + + + + + + + + + + + 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..bdbac29832 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 @@ -41,6 +41,11 @@ 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..c9639abc51 --- /dev/null +++ b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/CommanderVideoRepresentation.java @@ -0,0 +1,582 @@ +/******************************************************************************* + * 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.Optional; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.geometry.Dimension2D; +import javafx.scene.Cursor; +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.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 javafx.scene.transform.Rotate; +import javafx.scene.transform.Translate; +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.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.phoebus.framework.macros.MacroHandler; +import org.phoebus.framework.macros.MacroValueProvider; +import org.phoebus.ui.javafx.Styles; +import org.phoebus.ui.javafx.TextUtils; +// import uk.co.caprica.vlcj.factory.MediaPlayerFactory; +// import uk.co.caprica.vlcj.player.base.MediaPlayer; +// import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter; +// import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; + +/** + * 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 volatile ButtonBase 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::pvsChanged; + + // private MediaPlayerFactory mediaPlayerFactory; + // + // private EmbeddedMediaPlayer embeddedMediaPlayer; + // + // private ImageView videoImageView; + + private void meidiaPlayerInit() { + + // System.out.println("meidiaPlayerInit"); + // this.mediaPlayerFactory = new MediaPlayerFactory(); + // this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); + // this.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) {} + // }); + } + + @Override + protected boolean isFilteringEditModeClicks() { + return true; + } + + @Override + public Pane createJFXNode() throws Exception { + updateColors(); + base = makeBaseButton(); + + meidiaPlayerInit(); + + pane = new Pane(); + pane.getChildren().add(base); + + return pane; + } + + /** @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")); + button.setOnAction(event -> sendCommand()); + 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.. + if (model_widget.propConfirmDialog().getValue()) { + final String message = model_widget.propConfirmMessage().getValue(); + final String password = model_widget.propPassword().getValue(); + // .. check either with password or generic Ok/Cancel prompt + if (password.length() > 0) { + if (toolkit.showPasswordDialog(model_widget, message, password) == null) return; + } else if (!toolkit.showConfirmationDialog(model_widget, message)) return; + } + + action.run(); + }); + } + + /** + * Triggered by button click. It will gather command arguments(if any) and send the specified + * command to commander. + */ + private void sendCommand() { + System.out.println("sendCommand"); + System.out.println("command name:" + model_widget.propCommand().getValue()); + System.out.println("pv name :" + model_widget.propPvs().getValue().get(0).pv().getValue()); + System.out.println("pv value:" + model_widget.propPvs().getValue().get(0).value().getValue()); + } + + /** @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(representationChangedListener); + + enablementChanged(null, null, null); + } + + @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); + } + + /** Only details of the existing button need to be updated */ + private void representationChanged( + final WidgetProperty property, final Object old_value, final Object new_value) { + updateColors(); + dirty_representation.mark(); + toolkit.scheduleUpdate(this); + } + + /** 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 = makeBaseButton(); + 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/resources/icons/video.png b/app/commander/app-commander-display-representation-javafx/src/main/resources/icons/video.png new file mode 100644 index 0000000000000000000000000000000000000000..085cf498e9b5e8be16e1f81ff6d5c970a049d67e GIT binary patch literal 615 zcmV-t0+{`YP)EX>4Tx04R}tkv&MmKpe$iQ^is$B6bjQ$WXgzK~%(1t5Adrp;l;vxAeOiK+|nA zm57U(?5f!FiXZ~$M--znvy3@OO2Tt|-NVP%yBN>%KKJM7RdW^td?N8IGfbO!gLrz= zHaPDShgnfpiO-40Ou8WPBiEHCzi}?OEbz>*nNH0Uhl#~P2P+-Sil#<9MI2Ezo$`fD zlU2@JoV9Y5weHDZ7|Q7@%Uq{9j06_31PLM(R8c}1He$5uq*zGNe%!-9yfK1P6^U7%5OobO}DX`BH5XW&Y2`zv)|=9BbV zTZg?t`(KcjET0t2@|aIM)}Yagc%K$^Nr-T()O zK(s*F>mKh8boTb|nO1*4mG5%3QsaBr00006VoOIv0RI600RN!9r;`8x0E|gQK~y-) zV`N}p_|E_Z^XJ2vctK(!Bb?0yr5PDW2aL!DU|9Y4?;k=2{KxBploVFVT!8LZl3jpo z$j6T_$#Ve=6ck8cGvq%>u~1MT!SMh8e+(C}5aR+IX@qzeG&Wu!KMWchFEGf->5~@= znVG9_X(k*7Q>VUX;NlV>KOGRzOj + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..085cf498e9b5e8be16e1f81ff6d5c970a049d67e GIT binary patch literal 615 zcmV-t0+{`YP)EX>4Tx04R}tkv&MmKpe$iQ^is$B6bjQ$WXgzK~%(1t5Adrp;l;vxAeOiK+|nA zm57U(?5f!FiXZ~$M--znvy3@OO2Tt|-NVP%yBN>%KKJM7RdW^td?N8IGfbO!gLrz= zHaPDShgnfpiO-40Ou8WPBiEHCzi}?OEbz>*nNH0Uhl#~P2P+-Sil#<9MI2Ezo$`fD zlU2@JoV9Y5weHDZ7|Q7@%Uq{9j06_31PLM(R8c}1He$5uq*zGNe%!-9yfK1P6^U7%5OobO}DX`BH5XW&Y2`zv)|=9BbV zTZg?t`(KcjET0t2@|aIM)}Yagc%K$^Nr-T()O zK(s*F>mKh8boTb|nO1*4mG5%3QsaBr00006VoOIv0RI600RN!9r;`8x0E|gQK~y-) zV`N}p_|E_Z^XJ2vctK(!Bb?0yr5PDW2aL!DU|9Y4?;k=2{KxBploVFVT!8LZl3jpo z$j6T_$#Ve=6ck8cGvq%>u~1MT!SMh8e+(C}5aR+IX@qzeG&Wu!KMWchFEGf->5~@= znVG9_X(k*7Q>VUX;NlV>KOGRzOj Date: Wed, 9 Oct 2024 10:24:32 -0500 Subject: [PATCH 02/12] -Video widget. WIP. -TODO:Solve JNA issues. --- .../Makefile | 4 ++++ .../pom.xml | 14 ++++++------ .../BaseWidgetRepresentations.java | 11 +++++----- .../CommanderVideoRepresentation.java | 22 +++++++++---------- 4 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 app/commander/app-commander-display-representation-javafx/Makefile 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 65fd8f1952..af4643cf7f 100644 --- a/app/commander/app-commander-display-representation-javafx/pom.xml +++ b/app/commander/app-commander-display-representation-javafx/pom.xml @@ -11,20 +11,20 @@ app-commander-display-model 0.2.6-SNAPSHOT - - - + + +--> - - + +--> 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 From cfe057a17b5f19f877752f72fd76506537e21b1d Mon Sep 17 00:00:00 2001 From: lgomez Date: Wed, 9 Oct 2024 11:59:02 -0500 Subject: [PATCH 04/12] -Minimally functional video widget. -TODO:Add width/height props to widget. -TODO:Address memory issues with vlc lib --- .../CommanderVideoRepresentation.java | 221 +++++++++++------- 1 file changed, 135 insertions(+), 86 deletions(-) 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 index 06de876b50..e1ddab8f68 100644 --- 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 @@ -17,6 +17,7 @@ import javafx.application.Platform; import javafx.geometry.Dimension2D; import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.control.MenuButton; @@ -25,6 +26,7 @@ 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; @@ -59,6 +61,10 @@ import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; +import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; +import uk.co.caprica.vlcj.player.base.MediaPlayer; +import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter; + /** * Creates JavaFX item for model widget * @@ -83,7 +89,7 @@ public class CommanderVideoRepresentation private final DirtyFlag dirty_enablement = new DirtyFlag(); private final DirtyFlag dirty_actionls = new DirtyFlag(); - private volatile ButtonBase base; + private volatile Node base; private volatile String background; private volatile Color foreground; private volatile String button_text; @@ -138,27 +144,70 @@ public class CommanderVideoRepresentation private ImageView videoImageView; - private void meidiaPlayerInit() { + private Node meidiaPlayerInit() { - System.out.println("meidiaPlayerInit"); + System.out.println("meidiaPlayerInit**"); this.mediaPlayerFactory = new MediaPlayerFactory(); - // this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); - // this.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) {} - // }); + this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); + this.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) {} + }); + +// ------------------------------ + this.videoImageView = new ImageView(); + this.videoImageView.setPreserveRatio(true); + + embeddedMediaPlayer.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(400, 300); + + 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 + }); + + root.setCenter(videoImageView); + + + + embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); + + embeddedMediaPlayer.controls().setPosition(0.4f); + + + return root; + + } @Override @@ -169,9 +218,9 @@ protected boolean isFilteringEditModeClicks() { @Override public Pane createJFXNode() throws Exception { updateColors(); - base = makeBaseButton(); +// base = makeBaseButton(); - meidiaPlayerInit(); + base = meidiaPlayerInit(); pane = new Pane(); pane.getChildren().add(base); @@ -181,28 +230,28 @@ public Pane createJFXNode() throws Exception { /** @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(); - } +// 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; @@ -322,9 +371,9 @@ private void confirm(final Runnable action) { */ private void sendCommand() { System.out.println("sendCommand"); - System.out.println("command name:" + model_widget.propCommand().getValue()); - System.out.println("pv name :" + model_widget.propPvs().getValue().get(0).pv().getValue()); - System.out.println("pv value:" + model_widget.propPvs().getValue().get(0).value().getValue()); +// System.out.println("command name:" + model_widget.propCommand().getValue()); +// System.out.println("pv name :" + model_widget.propPvs().getValue().get(0).pv().getValue()); +// System.out.println("pv value:" + model_widget.propPvs().getValue().get(0).value().getValue()); } /** @return Should 'label' show the PV's current value? */ @@ -512,23 +561,23 @@ private void updateColors() { public void updateChanges() { super.updateChanges(); if (dirty_actionls.checkAndClear()) { - base = makeBaseButton(); - jfx_node.getChildren().setAll(base); +// 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())); +// 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); - } +// 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(), @@ -540,33 +589,33 @@ public void updateChanges() { // 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; - } +// 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 From 91dfcf1fe1d4f2e65dc04a2b8ab5952fbb0ce449 Mon Sep 17 00:00:00 2001 From: lgomez Date: Wed, 9 Oct 2024 12:11:18 -0500 Subject: [PATCH 05/12] -Update github actions checkout plugin --- .github/workflows/all_tests.yml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/nightly_linux.yml | 2 +- .github/workflows/nightly_mac.yml | 2 +- .github/workflows/nightly_windows.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) 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: From c064eebab7c3bc05306c633134fb22d7e6e67d16 Mon Sep 17 00:00:00 2001 From: lgomez Date: Wed, 9 Oct 2024 12:17:42 -0500 Subject: [PATCH 06/12] -Format files --- .../CommanderVideoRepresentation.java | 259 +++++++++--------- 1 file changed, 127 insertions(+), 132 deletions(-) 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 index e1ddab8f68..59fa4269a4 100644 --- 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 @@ -15,7 +15,6 @@ import java.util.Optional; import java.util.logging.Level; import javafx.application.Platform; -import javafx.geometry.Dimension2D; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Button; @@ -36,8 +35,6 @@ import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; import javafx.scene.shape.StrokeType; -import javafx.scene.transform.Rotate; -import javafx.scene.transform.Translate; import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; import org.csstudio.display.builder.model.WidgetProperty; @@ -57,13 +54,11 @@ import org.phoebus.framework.macros.MacroHandler; import org.phoebus.framework.macros.MacroValueProvider; import org.phoebus.ui.javafx.Styles; -import org.phoebus.ui.javafx.TextUtils; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; -import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; - import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; import uk.co.caprica.vlcj.player.base.MediaPlayer; import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter; +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; /** * Creates JavaFX item for model widget @@ -146,68 +141,63 @@ public class CommanderVideoRepresentation private Node meidiaPlayerInit() { - System.out.println("meidiaPlayerInit**"); - this.mediaPlayerFactory = new MediaPlayerFactory(); - this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); - this.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) {} - }); - -// ------------------------------ - this.videoImageView = new ImageView(); - this.videoImageView.setPreserveRatio(true); - - embeddedMediaPlayer.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(400, 300); - - root.widthProperty().addListener((observableValue, oldValue, newValue) -> { - // If you need to know about resizes - }); + System.out.println("meidiaPlayerInit**"); + this.mediaPlayerFactory = new MediaPlayerFactory(); + this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); + this.embeddedMediaPlayer + .events() + .addMediaPlayerEventListener( + new MediaPlayerEventAdapter() { + @Override + public void playing(MediaPlayer mediaPlayer) {} - root.heightProperty().addListener((observableValue, oldValue, newValue) -> { - // If you need to know about resizes - }); + @Override + public void paused(MediaPlayer mediaPlayer) {} + + @Override + public void stopped(MediaPlayer mediaPlayer) {} + + @Override + public void timeChanged(MediaPlayer mediaPlayer, long newTime) {} + }); + + // ------------------------------ + this.videoImageView = new ImageView(); + this.videoImageView.setPreserveRatio(true); + + embeddedMediaPlayer.videoSurface().set(new ImageViewVideoSurface(this.videoImageView)); + + // --------------------------------- + + BorderPane root = new BorderPane(); + root.setStyle("-fx-background-color: black;"); - root.setCenter(videoImageView); - - - - embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); - - embeddedMediaPlayer.controls().setPosition(0.4f); - - - return root; - - + root.setStyle("-fx-background-color: black;" + "-fx-border-width: 2px;"); + + videoImageView.fitWidthProperty().bind(root.widthProperty()); + videoImageView.fitHeightProperty().bind(root.heightProperty()); + + root.setPrefSize(400, 300); + + 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 + }); + + root.setCenter(videoImageView); + + embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); + + embeddedMediaPlayer.controls().setPosition(0.4f); + + return root; } @Override @@ -218,9 +208,9 @@ protected boolean isFilteringEditModeClicks() { @Override public Pane createJFXNode() throws Exception { updateColors(); -// base = makeBaseButton(); + // base = makeBaseButton(); - base = meidiaPlayerInit(); + base = meidiaPlayerInit(); pane = new Pane(); pane.getChildren().add(base); @@ -230,28 +220,30 @@ public Pane createJFXNode() throws Exception { /** @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(); -// } + // 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; @@ -371,9 +363,11 @@ private void confirm(final Runnable action) { */ private void sendCommand() { System.out.println("sendCommand"); -// System.out.println("command name:" + model_widget.propCommand().getValue()); -// System.out.println("pv name :" + model_widget.propPvs().getValue().get(0).pv().getValue()); -// System.out.println("pv value:" + model_widget.propPvs().getValue().get(0).value().getValue()); + // System.out.println("command name:" + model_widget.propCommand().getValue()); + // System.out.println("pv name :" + + // model_widget.propPvs().getValue().get(0).pv().getValue()); + // System.out.println("pv value:" + + // model_widget.propPvs().getValue().get(0).value().getValue()); } /** @return Should 'label' show the PV's current value? */ @@ -561,23 +555,24 @@ private void updateColors() { public void updateChanges() { super.updateChanges(); if (dirty_actionls.checkAndClear()) { -// base = meidiaPlayerInit(); -// jfx_node.getChildren().setAll(base); + // 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())); + // 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); -// } + // 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(), @@ -589,33 +584,33 @@ public void updateChanges() { // 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; -// } + // 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 From 804cc89dd0d9d20c66e8ad87350713356de40169 Mon Sep 17 00:00:00 2001 From: lgomez Date: Wed, 23 Oct 2024 17:55:41 -0500 Subject: [PATCH 07/12] -Get Video URL from PV. WIP. --- .../model/widgets/CommanderVideoWidget.java | 24 +++- .../CommanderVideoRepresentation.java | 118 +++++++++++++++++- 2 files changed, 138 insertions(+), 4 deletions(-) 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 index 57cdf6bba5..f15e2a9304 100644 --- 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 @@ -18,6 +18,7 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; 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.propTooltip; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propTransparent; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.runtimePropPVWritable; @@ -45,8 +46,10 @@ 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.csstudio.display.builder.model.widgets.VisibleWidget; import org.epics.vtype.VType; import org.phoebus.framework.persistence.XMLUtil; @@ -64,7 +67,7 @@ * @author Lorenzo Gomez */ @SuppressWarnings("nls") -public class CommanderVideoWidget extends VisibleWidget { +public class CommanderVideoWidget extends PVWidget { public static final int DEFAULT_WIDTH = 100, DEFAULT_HEIGHT = 30; // Elements of Plot Marker @@ -77,6 +80,20 @@ public class CommanderVideoWidget extends VisibleWidget { /** 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", Messages.WidgetProperties_Tooltip) + { + @Override + public WidgetProperty createProperty(final Widget widget, final String value) + { + return new StringWidgetProperty(this, widget, value); + } + }; + + + private WidgetProperty videoURL; /** Structure for Plot Marker */ public static class PvArgProperty extends StructuredWidgetProperty { @@ -159,6 +176,8 @@ static boolean shouldUseCombo(final Element xml) { // while Action Button would do nothing at all. return true; } + + /** Custom configurator to read legacy *.opi files */ private static class ActionButtonConfigurator extends WidgetConfigurator { @@ -288,6 +307,9 @@ protected void defineProperties(final List> properties) { properties.add(PVs = propPVs.createProperty(this, Collections.emptyList())); properties.add( command = CommonWidgetProperties.propCommand.createProperty(this, "command_name")); + + properties.add(videoURL = propVideoURL.createProperty(this, "tcp://172.16.100.179:1235")); + } @Override 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 index 59fa4269a4..e7c0c325e5 100644 --- 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 @@ -12,6 +12,7 @@ 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; @@ -46,14 +47,19 @@ 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.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; import uk.co.caprica.vlcj.player.base.MediaPlayer; @@ -83,6 +89,10 @@ public class CommanderVideoRepresentation 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; @@ -132,6 +142,10 @@ public class CommanderVideoRepresentation this::representationChanged; private final WidgetPropertyListener enablementChangedListener = this::enablementChanged; private final UntypedWidgetPropertyListener pvsListener = this::pvsChanged; + + private final WidgetPropertyListener pvNameListener = this::pvnameChanged; + private final UntypedWidgetPropertyListener contentListener = this::contentChanged; + private MediaPlayerFactory mediaPlayerFactory; @@ -193,7 +207,12 @@ public void timeChanged(MediaPlayer mediaPlayer, long newTime) {} root.setCenter(videoImageView); - embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); + embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); +// embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); + +// model_widget. + +// model_widget.get embeddedMediaPlayer.controls().setPosition(0.4f); @@ -217,6 +236,30 @@ public Pane createJFXNode() throws Exception { 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) { @@ -475,10 +518,31 @@ protected void registerListeners() { // if (! toolkit.isEditMode() && isLabelValue()) // - // model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); + model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); + + + 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() { @@ -513,10 +577,58 @@ private void buttonChanged( representationChanged(property, old_value, new_value); } - /** Only details of the existing button need to be updated */ + /** + * 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(); + + System.out.println("old value:" + old_value); + System.out.println("new_value:" + new_value); + if(new_value != null) + { + + 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."); + } + 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)"); + } + + } + dirty_representation.mark(); toolkit.scheduleUpdate(this); } From 9b6e6c0433243a17d3944aa68dd2ecd45c366ce0 Mon Sep 17 00:00:00 2001 From: lgomez Date: Thu, 24 Oct 2024 10:03:57 -0500 Subject: [PATCH 08/12] -Keep a reference to MediaAPI. Prevents video widget from crashing Phoebus. --- .../model/widgets/CommanderVideoWidget.java | 25 +-- .../CommanderVideoRepresentation.java | 174 +++++++++--------- 2 files changed, 94 insertions(+), 105 deletions(-) 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 index f15e2a9304..56e85d9321 100644 --- 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 @@ -18,7 +18,6 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; 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.propTooltip; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propTransparent; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.runtimePropPVWritable; @@ -50,7 +49,6 @@ 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.csstudio.display.builder.model.widgets.VisibleWidget; import org.epics.vtype.VType; import org.phoebus.framework.persistence.XMLUtil; import org.w3c.dom.Document; @@ -80,19 +78,17 @@ public class CommanderVideoWidget extends PVWidget { /** 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", Messages.WidgetProperties_Tooltip) - { - @Override - public WidgetProperty createProperty(final Widget widget, final String value) - { + new WidgetPropertyDescriptor<>( + WidgetPropertyCategory.BEHAVIOR, "Video URL", Messages.WidgetProperties_Tooltip) { + @Override + public WidgetProperty createProperty(final Widget widget, final String value) { return new StringWidgetProperty(this, widget, value); - } - }; - - + } + }; + private WidgetProperty videoURL; /** Structure for Plot Marker */ @@ -176,8 +172,6 @@ static boolean shouldUseCombo(final Element xml) { // while Action Button would do nothing at all. return true; } - - /** Custom configurator to read legacy *.opi files */ private static class ActionButtonConfigurator extends WidgetConfigurator { @@ -307,9 +301,8 @@ protected void defineProperties(final List> properties) { properties.add(PVs = propPVs.createProperty(this, Collections.emptyList())); properties.add( command = CommonWidgetProperties.propCommand.createProperty(this, "command_name")); - - properties.add(videoURL = propVideoURL.createProperty(this, "tcp://172.16.100.179:1235")); + properties.add(videoURL = propVideoURL.createProperty(this, "tcp://172.16.100.179:1235")); } @Override 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 index e7c0c325e5..9d62f7a17e 100644 --- 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 @@ -59,9 +59,9 @@ import org.phoebus.ui.javafx.Styles; import org.phoebus.ui.vtype.FormatOption; import org.phoebus.ui.vtype.FormatOptionHandler; - import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; +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; @@ -89,9 +89,9 @@ public class CommanderVideoRepresentation 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; @@ -142,18 +142,19 @@ public class CommanderVideoRepresentation this::representationChanged; private final WidgetPropertyListener enablementChangedListener = this::enablementChanged; private final UntypedWidgetPropertyListener pvsListener = this::pvsChanged; - + private final WidgetPropertyListener pvNameListener = this::pvnameChanged; private final UntypedWidgetPropertyListener contentListener = this::contentChanged; - private MediaPlayerFactory mediaPlayerFactory; private EmbeddedMediaPlayer embeddedMediaPlayer; + private MediaApi videoMedia; + private ImageView videoImageView; - private Node meidiaPlayerInit() { + private Node mediaPlayerInit(String mediaURL) { System.out.println("meidiaPlayerInit**"); this.mediaPlayerFactory = new MediaPlayerFactory(); @@ -207,12 +208,13 @@ public void timeChanged(MediaPlayer mediaPlayer, long newTime) {} root.setCenter(videoImageView); - embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); -// embeddedMediaPlayer.media().play("tcp://172.16.100.208:8080"); - -// model_widget. - -// model_widget.get + // embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); + videoMedia = embeddedMediaPlayer.media(); + videoMedia.play(mediaURL); + + // model_widget. + + // model_widget.get embeddedMediaPlayer.controls().setPosition(0.4f); @@ -227,39 +229,34 @@ protected boolean isFilteringEditModeClicks() { @Override public Pane createJFXNode() throws Exception { updateColors(); - // base = makeBaseButton(); - base = meidiaPlayerInit(); + base = mediaPlayerInit("tcp://172.16.100.208:8080"); 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); - } - + 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); - } + 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) { @@ -518,8 +515,7 @@ protected void registerListeners() { // if (! toolkit.isEditMode() && isLabelValue()) // - model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); - + model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); model_widget.propPVName().addPropertyListener(pvNameListener); @@ -528,21 +524,20 @@ protected void registerListeners() { 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); + 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); - } + 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() { @@ -579,6 +574,7 @@ private void buttonChanged( /** * Updates video with new URL + * * @param property * @param old_value * @param new_value @@ -586,47 +582,47 @@ private void buttonChanged( private void representationChanged( final WidgetProperty property, final Object old_value, final Object new_value) { updateColors(); - + System.out.println("old value:" + old_value); System.out.println("new_value:" + new_value); - if(new_value != null) - { - - 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."); - } - 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)"); - } - + if (new_value != null) { + + 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 = videoMedia.play(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 = videoMedia.play(new_value_string); + if (success) { + System.out.println("Play returned success"); + } else { + System.out.println("Play returned error"); + } + + // embeddedMediaPlayer.media().info().type() + } } dirty_representation.mark(); From 09d5de2d1c01990608a50b41b93bda19c963303e Mon Sep 17 00:00:00 2001 From: lgomez Date: Thu, 24 Oct 2024 10:19:45 -0500 Subject: [PATCH 09/12] -Add Makefile to app-commander --- app/commander/Makefile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/commander/Makefile 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 From 84ea5f210ecfb87c74a994b7f443221a86f1db0b Mon Sep 17 00:00:00 2001 From: lgomez Date: Thu, 24 Oct 2024 11:28:39 -0500 Subject: [PATCH 10/12] -Use PV as video URL. WIP. -Cleanup --- .../model/widgets/CommanderVideoWidget.java | 86 ++----------------- .../CommanderVideoRepresentation.java | 37 +++----- 2 files changed, 17 insertions(+), 106 deletions(-) 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 index 56e85d9321..2e2d192b66 100644 --- 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 @@ -8,21 +8,15 @@ 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.propCommandArgument; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmDialog; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propConfirmMessage; 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.propPVName; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; 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.Arrays; -import java.util.Collections; +import com.windhoverlabs.display.model.widgets.CommanderCommandActionButtonWidget.PvArgProperty; import java.util.List; import org.csstudio.display.builder.model.ArrayWidgetProperty; import org.csstudio.display.builder.model.MacroizedWidgetProperty; @@ -49,7 +43,6 @@ 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.epics.vtype.VType; import org.phoebus.framework.persistence.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -81,8 +74,7 @@ public class CommanderVideoWidget extends PVWidget { /** 'tooltip' property: Text to display in tooltip */ public static final WidgetPropertyDescriptor propVideoURL = - new WidgetPropertyDescriptor<>( - WidgetPropertyCategory.BEHAVIOR, "Video URL", Messages.WidgetProperties_Tooltip) { + new WidgetPropertyDescriptor<>(WidgetPropertyCategory.BEHAVIOR, "Video URL", "Video URL") { @Override public WidgetProperty createProperty(final Widget widget, final String value) { return new StringWidgetProperty(this, widget, value); @@ -91,32 +83,6 @@ public WidgetProperty createProperty(final Widget widget, final String v private WidgetProperty videoURL; - /** Structure for Plot Marker */ - public static class PvArgProperty extends StructuredWidgetProperty { - protected PvArgProperty(final Widget widget, final String name) { - super( - propPv, - widget, - Arrays.asList( - propPVName.createProperty(widget, ""), - propValue.createProperty(widget, ""), - propCommandArgument.createProperty(widget, ""))); - } - - public WidgetProperty pv() { - return getElement(0); - } - - public WidgetProperty value() { - return getElement(1); - } - - public WidgetProperty argumentName() { - return getElement(2); - } - } - ; - /** 'Arguments' array */ private static final ArrayWidgetProperty.Descriptor propPVs = new ArrayWidgetProperty.Descriptor<>( @@ -216,15 +182,7 @@ public boolean configureFromXML( // so move the (last) confirm message from action(s) to the button. final Element actions = XMLUtil.getChildElement(xml, CommonWidgetProperties.propActions.getName()); - if (actions != null) - for (Element action : XMLUtil.getChildElements(actions, XMLTags.ACTION)) { - final String message = - XMLUtil.getChildString(action, propConfirmMessage.getName()).orElse(""); - if (!message.isBlank()) { - button.propConfirmMessage().setValue(message); - button.propConfirmDialog().setValue(true); - } - } + if (actions != null) {} } // If there is no pv_name, remove from tool tip .. // if ( @@ -256,11 +214,6 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) throw private volatile WidgetProperty transparent; private volatile WidgetProperty rotation_step; private volatile WidgetProperty pv_writable; - private volatile WidgetProperty confirm_dialog; - private volatile WidgetProperty confirm_message; - private volatile WidgetProperty password; - private volatile ArrayWidgetProperty PVs; - private volatile WidgetProperty command; public CommanderVideoWidget() { super(WIDGET_DESCRIPTOR.getType(), DEFAULT_WIDTH, DEFAULT_HEIGHT); @@ -293,16 +246,8 @@ protected void defineProperties(final List> properties) { 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(confirm_dialog = propConfirmDialog.createProperty(this, false)); - properties.add( - confirm_message = - propConfirmMessage.createProperty(this, "Are your sure you want to do this?")); - properties.add(password = propPassword.createProperty(this, "")); - properties.add(PVs = propPVs.createProperty(this, Collections.emptyList())); - properties.add( - command = CommonWidgetProperties.propCommand.createProperty(this, "command_name")); - properties.add(videoURL = propVideoURL.createProperty(this, "tcp://172.16.100.179:1235")); + properties.add(videoURL = propVideoURL.createProperty(this, "tcp://examplevideo.com:1235")); } @Override @@ -352,26 +297,7 @@ public final WidgetProperty runtimePropPVWritable() { return pv_writable; } - /** @return 'confirm_dialog' property */ - public WidgetProperty propConfirmDialog() { - return confirm_dialog; - } - - /** @return 'confirm_message' property */ - public WidgetProperty propConfirmMessage() { - return confirm_message; - } - - public WidgetProperty propCommand() { - return command; - } - - /** @return 'password' property */ - public WidgetProperty propPassword() { - return password; - } - - public ArrayWidgetProperty propPvs() { - return PVs; + public WidgetProperty propVideoURl() { + return videoURL; } } 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 index 9d62f7a17e..dc82767f03 100644 --- 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 @@ -210,7 +210,7 @@ public void timeChanged(MediaPlayer mediaPlayer, long newTime) {} // embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); videoMedia = embeddedMediaPlayer.media(); - videoMedia.play(mediaURL); + updateVideo(mediaURL); // model_widget. @@ -230,7 +230,10 @@ protected boolean isFilteringEditModeClicks() { public Pane createJFXNode() throws Exception { updateColors(); - base = mediaPlayerInit("tcp://172.16.100.208:8080"); + base = mediaPlayerInit(model_widget.propVideoURl().getValue()); + + System.out.println( + "model_widget.propVideoURl().getValue()-->" + model_widget.propVideoURl().getValue()); pane = new Pane(); pane.getChildren().add(base); @@ -305,7 +308,6 @@ private ButtonBase makeBaseButton() { if (actions.isExecutedAsOne() || actions.getActions().size() < 2) { final Button button = new Button(); button.setGraphic(new ImageView("/icons/video.png")); - button.setOnAction(event -> sendCommand()); result = button; } else { // If there is at least one non-WritePVActionInfo then is_writePV should be false @@ -384,32 +386,11 @@ private void confirm(final Runnable action) { Platform.runLater( () -> { // If confirmation is requested.. - if (model_widget.propConfirmDialog().getValue()) { - final String message = model_widget.propConfirmMessage().getValue(); - final String password = model_widget.propPassword().getValue(); - // .. check either with password or generic Ok/Cancel prompt - if (password.length() > 0) { - if (toolkit.showPasswordDialog(model_widget, message, password) == null) return; - } else if (!toolkit.showConfirmationDialog(model_widget, message)) return; - } action.run(); }); } - /** - * Triggered by button click. It will gather command arguments(if any) and send the specified - * command to commander. - */ - private void sendCommand() { - System.out.println("sendCommand"); - // System.out.println("command name:" + model_widget.propCommand().getValue()); - // System.out.println("pv name :" + - // model_widget.propPvs().getValue().get(0).pv().getValue()); - // System.out.println("pv value:" + - // model_widget.propPvs().getValue().get(0).value().getValue()); - } - /** @return Should 'label' show the PV's current value? */ private boolean isLabelValue() { final StringWidgetProperty text_prop = (StringWidgetProperty) model_widget.propText(); @@ -597,7 +578,7 @@ private void representationChanged( if (!old_value_string.equals(new_value_string)) { // If URL has changed, stream from new URL System.out.println("New video feed."); - boolean success = videoMedia.play(new_value_string); + boolean success = updateVideo(new_value_string); if (success) { System.out.println("Play returned success"); } else { @@ -614,7 +595,7 @@ private void representationChanged( */ System.out.println("New video feed (if videostarted flag is false)"); - boolean success = videoMedia.play(new_value_string); + boolean success = updateVideo(new_value_string); if (success) { System.out.println("Play returned success"); } else { @@ -629,6 +610,10 @@ private void representationChanged( toolkit.scheduleUpdate(this); } + private boolean updateVideo(String new_value_string) { + return videoMedia.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) { From e2888a81ed2b6cc1cbdc8fce951faee3b7065349 Mon Sep 17 00:00:00 2001 From: lgomez Date: Thu, 24 Oct 2024 12:26:23 -0500 Subject: [PATCH 11/12] -Add VideoSingleton to tie vlc native objects to app lifetime. This should prevent whole app crashing because of garbage collection. https://capricasoftware.co.uk/tutorials/vlcj/4/garbage-collection --- .../model/widgets/CommanderVideoWidget.java | 11 --- .../CommanderVideoRepresentation.java | 40 +++-------- .../representation/VideoSingleton.java | 67 +++++++++++++++++++ 3 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/VideoSingleton.java 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 index 2e2d192b66..bb50b67da0 100644 --- 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 @@ -16,9 +16,7 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propTransparent; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.runtimePropPVWritable; -import com.windhoverlabs.display.model.widgets.CommanderCommandActionButtonWidget.PvArgProperty; import java.util.List; -import org.csstudio.display.builder.model.ArrayWidgetProperty; import org.csstudio.display.builder.model.MacroizedWidgetProperty; import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.StructuredWidgetProperty; @@ -83,15 +81,6 @@ public WidgetProperty createProperty(final Widget widget, final String v private WidgetProperty videoURL; - /** 'Arguments' array */ - private static final ArrayWidgetProperty.Descriptor propPVs = - new ArrayWidgetProperty.Descriptor<>( - WidgetPropertyCategory.MISC, - "argument", - "Arguments", - (widget, index) -> new PvArgProperty(widget, "Argument " + index), - 0); - /** Widget descriptor */ public static final WidgetDescriptor WIDGET_DESCRIPTOR = new WidgetDescriptor( 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 index dc82767f03..0f7c9b0cb1 100644 --- 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 @@ -59,12 +59,8 @@ import org.phoebus.ui.javafx.Styles; import org.phoebus.ui.vtype.FormatOption; import org.phoebus.ui.vtype.FormatOptionHandler; -import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; 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; /** * Creates JavaFX item for model widget @@ -146,41 +142,21 @@ public class CommanderVideoRepresentation private final WidgetPropertyListener pvNameListener = this::pvnameChanged; private final UntypedWidgetPropertyListener contentListener = this::contentChanged; - private MediaPlayerFactory mediaPlayerFactory; + private static MediaApi videoMedia; - private EmbeddedMediaPlayer embeddedMediaPlayer; - - private MediaApi videoMedia; - - private ImageView videoImageView; + private static ImageView videoImageView; private Node mediaPlayerInit(String mediaURL) { System.out.println("meidiaPlayerInit**"); - this.mediaPlayerFactory = new MediaPlayerFactory(); - this.embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); - this.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) {} - }); - // ------------------------------ this.videoImageView = new ImageView(); this.videoImageView.setPreserveRatio(true); - embeddedMediaPlayer.videoSurface().set(new ImageViewVideoSurface(this.videoImageView)); + VideoSingleton.getInstance() + .getEmbeddedMediaPlayer() + .videoSurface() + .set(new ImageViewVideoSurface(this.videoImageView)); // --------------------------------- @@ -209,14 +185,14 @@ public void timeChanged(MediaPlayer mediaPlayer, long newTime) {} root.setCenter(videoImageView); // embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); - videoMedia = embeddedMediaPlayer.media(); + videoMedia = VideoSingleton.getInstance().getEmbeddedMediaPlayer().media(); updateVideo(mediaURL); // model_widget. // model_widget.get - embeddedMediaPlayer.controls().setPosition(0.4f); + VideoSingleton.getInstance().getEmbeddedMediaPlayer().controls().setPosition(0.4f); return root; } 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..218cb85d46 --- /dev/null +++ b/app/commander/app-commander-display-representation-javafx/src/main/java/com/windhoverlabs/display/representation/VideoSingleton.java @@ -0,0 +1,67 @@ +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; + +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..."); + } +} From 068a1ef43dc735103acb1bcf8e598505f200a70d Mon Sep 17 00:00:00 2001 From: lgomez Date: Thu, 24 Oct 2024 13:07:49 -0500 Subject: [PATCH 12/12] -Width and height of video from props. -Cleanup --- .../CommanderVideoRepresentation.java | 99 +++++++++---------- .../representation/VideoSingleton.java | 5 + 2 files changed, 50 insertions(+), 54 deletions(-) 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 index 0f7c9b0cb1..ff82b43774 100644 --- 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 @@ -60,7 +60,6 @@ import org.phoebus.ui.vtype.FormatOption; import org.phoebus.ui.vtype.FormatOptionHandler; import uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurface; -import uk.co.caprica.vlcj.player.base.MediaApi; /** * Creates JavaFX item for model widget @@ -137,13 +136,11 @@ public class CommanderVideoRepresentation private final UntypedWidgetPropertyListener representationChangedListener = this::representationChanged; private final WidgetPropertyListener enablementChangedListener = this::enablementChanged; - private final UntypedWidgetPropertyListener pvsListener = this::pvsChanged; + private final UntypedWidgetPropertyListener pvsListener = this::urlPVUpdate; private final WidgetPropertyListener pvNameListener = this::pvnameChanged; private final UntypedWidgetPropertyListener contentListener = this::contentChanged; - private static MediaApi videoMedia; - private static ImageView videoImageView; private Node mediaPlayerInit(String mediaURL) { @@ -168,7 +165,7 @@ private Node mediaPlayerInit(String mediaURL) { videoImageView.fitWidthProperty().bind(root.widthProperty()); videoImageView.fitHeightProperty().bind(root.heightProperty()); - root.setPrefSize(400, 300); + root.setPrefSize(model_widget.propWidth().getValue(), model_widget.propHeight().getValue()); root.widthProperty() .addListener( @@ -182,16 +179,12 @@ private Node mediaPlayerInit(String mediaURL) { // If you need to know about resizes }); + // videoImageView + root.setCenter(videoImageView); - // embeddedMediaPlayer.media().play("udp://@0.0.0.0:5000"); - videoMedia = VideoSingleton.getInstance().getEmbeddedMediaPlayer().media(); updateVideo(mediaURL); - // model_widget. - - // model_widget.get - VideoSingleton.getInstance().getEmbeddedMediaPlayer().controls().setPosition(0.4f); return root; @@ -472,7 +465,7 @@ protected void registerListeners() { // if (! toolkit.isEditMode() && isLabelValue()) // - model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); + model_widget.runtimePropValue().addUntypedPropertyListener(pvsListener); model_widget.propPVName().addPropertyListener(pvNameListener); @@ -529,65 +522,63 @@ private void buttonChanged( representationChanged(property, old_value, new_value); } - /** - * Updates video with new URL - * - * @param property - * @param old_value - * @param new_value - */ - private void representationChanged( + public void urlPVUpdate( final WidgetProperty property, final Object old_value, final Object new_value) { - updateColors(); - System.out.println("old value:" + old_value); - System.out.println("new_value:" + new_value); - if (new_value != null) { - - 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)"); + 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"); } - - // embeddedMediaPlayer.media().info().type() + } 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 videoMedia.play(new_value_string); + return VideoSingleton.getInstance().getEmbeddedMediaPlayer().media().play(new_value_string); } /** Only details of the existing button need to be updated */ 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 index 218cb85d46..82d2f35537 100644 --- 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 @@ -6,6 +6,11 @@ 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;