diff --git a/app/display/actions/build.xml b/app/display/actions/build.xml new file mode 100644 index 0000000000..6097bbd2d3 --- /dev/null +++ b/app/display/actions/build.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/display/actions/pom.xml b/app/display/actions/pom.xml new file mode 100644 index 0000000000..04ab3eb583 --- /dev/null +++ b/app/display/actions/pom.xml @@ -0,0 +1,35 @@ + + + + + 4.0.0 + + org.phoebus + app-display + 4.7.4-SNAPSHOT + + + app-display-actions + + + 17 + 17 + UTF-8 + + + + org.phoebus + app-display-representation-javafx + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-model + 4.7.4-SNAPSHOT + + + \ No newline at end of file diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandAction.java new file mode 100644 index 0000000000..e6c38fe572 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandAction.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ExecuteCommandAction extends ActionInfoBase { + + public static final String EXECUTE_COMMAND = "command"; + private String command; + private ExecuteCommandActionController executeCommandActionController; + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public ExecuteCommandAction() { + this.description = Messages.ActionExecuteCommand; + this.type = EXECUTE_COMMAND; + } + + public ExecuteCommandAction(String description, String command) { + this.description = description; + this.command = command; + this.type = EXECUTE_COMMAND; + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/execute_script.png"); + } + + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) { + // Legacy: + // + // echo Hello + // $(user.home) + // 10 + // Hello + // + // + // New: + // + // echo Hello + // Hello + // + command = XMLUtil.getChildString(actionXml, XMLTags.COMMAND).orElse(""); + String directory = XMLUtil.getChildString(actionXml, "command_directory") + .orElse(null); + // Legacy allowed "opi.dir" as magic macro. + // Commands are now by default resolved relative to the display file. + if ("$(opi.dir)".equals(directory)) + directory = null; + // Legacy allowed user.home as a 'current working directory'. + // Commands are now executed with their location as cwd. + if ("$(user.home)".equals(directory)) + directory = null; + // If a legacy directory was provided, locate command there + if (directory != null && !directory.isEmpty()) + command = directory + "/" + command; + if (description.isEmpty()) + description = Messages.ActionExecuteCommand; + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, EXECUTE_COMMAND); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.COMMAND); + writer.writeCharacters(command); + writer.writeEndElement(); + } + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(EXECUTE_COMMAND) || + "EXECUTE_CMD".equalsIgnoreCase(type); + } + + public String getCommand() { + return command; + } + + @Override + public Node getEditor(Widget widget) { + if (editorUi != null) { + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("ExecuteCommandAction.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(Widget.class, ActionInfo.class).newInstance(widget, this); + } catch (Exception e) { + Logger.getLogger(ExecuteCommandAction.class.getName()).log(Level.SEVERE, "Failed to construct ExecuteCommandActionDetailsController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + executeCommandActionController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert() { + executeCommandActionController.setDescription(this.description); + executeCommandActionController.setCommand(command); + } + + @Override + public ActionInfo commit() { + description = executeCommandActionController.getDescription(); + command = executeCommandActionController.getCommand(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandActionController.java new file mode 100644 index 0000000000..4f91ec3221 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteCommandActionController.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.FilenameSupport; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * FXML Controller for the execute command action editor + */ +public class ExecuteCommandActionController extends ActionControllerBase { + + private final ExecuteCommandAction executeCommandActionInfo; + + private final Widget widget; + + @FXML + private TextField command; + + private final StringProperty commandProperty = new SimpleStringProperty(); + + private static final Logger logger = + Logger.getLogger(ExecuteCommandActionController.class.getName()); + + /** + * @param widget Widget + * @param actionInfo {@link ActionInfo} + */ + public ExecuteCommandActionController(Widget widget, ActionInfo actionInfo) { + this.widget = widget; + this.executeCommandActionInfo = (ExecuteCommandAction) actionInfo; + this.descriptionProperty.set(actionInfo.getDescription()); + } + + /** + * Init + */ + @FXML + public void initialize() { + super.initialize(); + commandProperty.set(executeCommandActionInfo.getCommand()); + command.textProperty().bindBidirectional(commandProperty); + } + + /** + * Prompt for command to execute + */ + @FXML + public void selectCommand() { + try { + final String path = FilenameSupport.promptForRelativePath(widget, commandProperty.get()); + if (path != null) { + commandProperty.set(path); + } + } catch (Exception ex) { + logger.log(Level.WARNING, "Cannot prompt for command/filename", ex); + } + } + + public String getCommand(){ + return commandProperty.get(); + } + + public void setCommand(String command){ + commandProperty.set(command); + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptAction.java new file mode 100644 index 0000000000..a2dab1e47d --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptAction.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.properties.ScriptInfo; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.Collections; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ExecuteScriptAction extends ActionInfoBase { + + public static final String EXECUTE_SCRIPT = "execute"; + public static final String EXECUTE_PYTHONSCRIPT = "EXECUTE_PYTHONSCRIPT"; + public static final String EXECUTE_JAVASCRIPT = "EXECUTE_JAVASCRIPT"; + + private ScriptInfo scriptInfo; + private String text; + private String path; + + private ExecuteScriptActionController executeScriptController; + + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public ExecuteScriptAction() { + this.description = Messages.ActionExecuteScript; + this.type = EXECUTE_SCRIPT; + this.scriptInfo = new ScriptInfo(ScriptInfo.EMBEDDED_PYTHON, + ScriptInfo.EXAMPLE_PYTHON, + false, + Collections.emptyList()); + } + + public ExecuteScriptAction(String description, ScriptInfo scriptInfo) { + this.description = description; + this.type = EXECUTE_SCRIPT; + this.scriptInfo = scriptInfo; + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/execute_script.png"); + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) throws Exception { + if(type.equalsIgnoreCase(EXECUTE_SCRIPT)){ + // + final Element el = XMLUtil.getChildElement(actionXml, XMLTags.SCRIPT); + if (el == null) { + throw new Exception("Missing "); + } else { + path = el.getAttribute(XMLTags.FILE); + if(ScriptInfo.EMBEDDED_PYTHON.equals(path) || ScriptInfo.EMBEDDED_JAVASCRIPT.equals(path)){ + text = XMLUtil.getChildString(el, XMLTags.TEXT).orElse(null); + } + scriptInfo = new ScriptInfo(path, text, false, Collections.emptyList()); + if (description.isEmpty()) { + description = Messages.ActionExecuteScript; + } + } + } + else if(type.equalsIgnoreCase(EXECUTE_JAVASCRIPT) || + type.equalsIgnoreCase(EXECUTE_PYTHONSCRIPT)){ + // Legacy XML: + // .. or "EXECUTE_JAVASCRIPT" + // script.py + // + // false + // A script + // + final boolean embed = Boolean.parseBoolean(XMLUtil.getChildString(actionXml, "embedded").orElse("false")); + path = XMLUtil.getChildString(actionXml, XMLTags.PATH).orElse(""); + text = XMLUtil.getChildString(actionXml, "scriptText").orElse(""); + if (embed) + { + final String dialect = type.contains("PYTHON") + ? ScriptInfo.EMBEDDED_PYTHON : ScriptInfo.EMBEDDED_JAVASCRIPT; + scriptInfo = new ScriptInfo(dialect, text, false, Collections.emptyList()); + } + else + scriptInfo = new ScriptInfo(path, null, false, Collections.emptyList()); + if (description.isEmpty()){ + description = Messages.ActionExecuteScript; + } + } + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, EXECUTE_SCRIPT); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.SCRIPT); + writer.writeAttribute(XMLTags.FILE, path); + if(scriptInfo.getPath().equals(ScriptInfo.EMBEDDED_PYTHON) || + scriptInfo.getPath().equals(ScriptInfo.EMBEDDED_JAVASCRIPT)){ + final String text = scriptInfo.getText(); + if (text != null) { + writer.writeStartElement(XMLTags.TEXT); + writer.writeCData(text); + writer.writeEndElement(); + } + } + else{ + writer.writeAttribute(XMLTags.FILE, path); + } + writer.writeEndElement(); + } + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(EXECUTE_SCRIPT) || + actionId.equalsIgnoreCase(EXECUTE_JAVASCRIPT) || + actionId.equalsIgnoreCase(EXECUTE_PYTHONSCRIPT); + } + + public ScriptInfo getScriptInfo() { + return scriptInfo; + } + + @Override + public Node getEditor(Widget widget){ + if(editorUi != null){ + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("ExecuteScriptActionDetails.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(Widget.class, ActionInfo.class).newInstance(widget, this); + } catch (Exception e) { + Logger.getLogger(ExecuteScriptAction.class.getName()).log(Level.SEVERE, "Failed to construct ExecuteScriptActionController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + executeScriptController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert(){ + executeScriptController.setScriptPath(path); + executeScriptController.setScriptBody(text); + } + + @Override + public ActionInfo commit(){ + path = executeScriptController.getScriptPath(); + text = executeScriptController.getScriptBody(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptActionController.java new file mode 100644 index 0000000000..f64cde5c58 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/ExecuteScriptActionController.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + + +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javafx.beans.binding.Bindings; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.properties.ScriptInfo; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.model.util.ModelResourceUtil; +import org.csstudio.display.builder.representation.javafx.FilenameSupport; +import org.phoebus.framework.macros.MacroHandler; +import org.phoebus.framework.util.ResourceParser; +import org.phoebus.ui.application.ApplicationLauncherService; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import org.phoebus.ui.javafx.ImageCache; + +/** + * FXML Controller for the execute script action editor. + */ +public class ExecuteScriptActionController extends ActionControllerBase { + + private final ExecuteScriptAction executeScriptActionInfo; + + private final Widget widget; + + @FXML + private TextField scriptPath; + @FXML + private TextArea scriptBody; + @FXML + private Button embedPyButton; + @FXML + private Button embedJsButton; + @FXML + private Button openExternalEditorButton; + + private final StringProperty scriptPathProperty = new SimpleStringProperty(); + private final StringProperty scriptBodyProperty = new SimpleStringProperty(); + + private final Logger logger = + Logger.getLogger(ExecuteScriptActionController.class.getName()); + + /** @param widget Widget + * @param actionInfo Action info + */ + public ExecuteScriptActionController(Widget widget, ActionInfo actionInfo){ + this.widget = widget; + this.executeScriptActionInfo = (ExecuteScriptAction)actionInfo; + descriptionProperty.set(actionInfo.getDescription()); + } + + /** Init */ + @FXML + public void initialize(){ + super.initialize(); + + scriptPathProperty.set(executeScriptActionInfo.getScriptInfo().getPath()); + scriptBodyProperty.set(executeScriptActionInfo.getScriptInfo().getText()); + embedPyButton + .setGraphic(ImageCache.getImageView(ExecuteScriptActionController.class, "/icons/embedded_script.png")); + embedJsButton.setGraphic(ImageCache.getImageView(ExecuteScriptActionController.class, "/icons/embedded_script.png")); + openExternalEditorButton.setGraphic(ImageCache.getImageView(ExecuteScriptActionController.class, "/icons/embedded_script.png")); + openExternalEditorButton.setDisable(!externalEditorExists()); + + scriptPath.textProperty().bindBidirectional(scriptPathProperty); + scriptBody.textProperty().bindBidirectional(scriptBodyProperty); + scriptBody.disableProperty().bind(Bindings.createBooleanBinding(() -> scriptPathProperty.get() == null || + scriptPathProperty.get().isEmpty() || + (!scriptPathProperty.get().equals(ScriptInfo.EMBEDDED_PYTHON) && + !scriptPathProperty.get().equals(ScriptInfo.EMBEDDED_JAVASCRIPT)), scriptPathProperty)); + } + + /** Select embedded Py */ + @FXML + public void embedPy(){ + scriptPathProperty.setValue(ScriptInfo.EMBEDDED_PYTHON); + final String text = scriptBodyProperty.get(); + if (text == null || + text.trim().isEmpty() || + text.trim().equals(ScriptInfo.EXAMPLE_JAVASCRIPT) ) + scriptBodyProperty.setValue(ScriptInfo.EXAMPLE_PYTHON); + openExternalEditorButton.setDisable(true); + } + + /** Select embedded Js */ + @FXML + public void embedJs(){ + scriptPathProperty.setValue(ScriptInfo.EMBEDDED_JAVASCRIPT); + final String text = scriptBodyProperty.get(); + if (text == null || + text.trim().isEmpty() || + text.trim().equals(ScriptInfo.EXAMPLE_PYTHON) ) + scriptBodyProperty.setValue(ScriptInfo.EXAMPLE_JAVASCRIPT); + openExternalEditorButton.setDisable(true); + } + + /** Open external script file */ + @FXML + public void openExternalEditor(){ + String resolved; + try + { + String path = MacroHandler.replace(widget.getMacrosOrProperties(), scriptPathProperty.get()); + resolved = ModelResourceUtil.resolveResource(widget.getDisplayModel(), path); + File file = new File(resolved); + ApplicationLauncherService.openFile(file, true, null); + } + catch (Exception e) + { + logger.warning("Cannot resolve resource " + scriptPathProperty.get()); + } + } + + /** Select external script file */ + @FXML + public void selectScriptFile(){ + try + { + final String path = FilenameSupport.promptForRelativePath(widget, scriptPathProperty.get()); + if (path != null) + { + scriptPathProperty.setValue(path); + openExternalEditorButton.setDisable(!externalEditorExists()); + scriptBody.setDisable(true); + scriptBodyProperty.set(""); + } + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot prompt for filename", ex); + } + } + + /** @return Whether an external Python or Javascript editor is configured */ + private boolean externalEditorExists() + { + return null != + ApplicationLauncherService.findApplication(ResourceParser.getURI(new File(scriptPathProperty.get())), false, null); + } + + public String getScriptPath(){ + return scriptPathProperty.get(); + } + + public String getScriptBody(){ + return scriptBodyProperty.get(); + } + + public void setScriptPath(String scriptPath) { + this.scriptPathProperty.set(scriptPath); + } + + public void setScriptBody(String scriptBody) { + this.scriptBodyProperty.set(scriptBody); + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/Messages.java b/app/display/actions/src/main/java/org/csstudio/display/actions/Messages.java new file mode 100644 index 0000000000..988bfec2cf --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/Messages.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * 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 org.csstudio.display.actions; + +import org.phoebus.framework.nls.NLS; + +/** + * Externalized Strings + * + * @author Kay Kasemir + */ +public class Messages { + // Keep in alphabetical order, synchronized with messages.properties + /** + * Externalized Strings + */ + public static String ActionExecuteCommand, + ActionExecuteScript, + ActionOpenDisplay, + ActionOpenFile, + ActionOpenWebPage, + ActionWritePV, + ActionsDialog_Actions, + ActionsDialog_Command, + ActionsDialog_Description, + ActionsDialog_Detail, + ActionsDialog_DisplayPath, + ActionsDialog_ExecuteAll, + ActionsDialog_FilePath, + ActionsDialog_Info, + ActionsDialog_PVName, + ActionsDialog_ScriptPath, + ActionsDialog_ScriptText, + ActionsDialog_URL, + ActionsDialog_Value, + Add, + AddEmbeddedJavaScript, + AddEmbeddedPython, + AddJavaScriptFile, + AddPythonFile, + Alpha, + BoolButtonError_Body, + BoolButtonError_Title, + Blue, + ColorDialog_Current, + ColorDialog_Custom, + ColorDialog_Default, + ColorDialog_Info, + ColorDialog_Original, + ColorDialog_Predefined, + ColorDialog_Title_FMT, + ColorMap_Custom, + ColorMapDialog_Add, + ColorMapDialog_Color, + ColorMapDialog_Info, + ColorMapDialog_PredefinedMap, + ColorMapDialog_Remove, + ColorMapDialog_Result, + ColorMapDialog_Title, + ColorMapDialog_Value, + Column, + ConvertToEmbeddedJavaScript, + ConvertToEmbeddedPython, + ConvertToScriptFile, + Edit, + ExportWidgetInfo, + ExportFailed, + ExportDone, + Copy, + Duplicate, + FileTypeAll, + FileTypeDisplays, + FontDialog_ExampleText, + FontDialog_Family, + FontDialog_Info, + FontDialog_Predefined, + FontDialog_Preview, + FontDialog_Size, + FontDialog_Style, + Green, + HIGH, + HIHI, + LOLO, + LOW, + MacrosDialog_Info, + MacrosDialog_NameCol, + MacrosDialog_Title, + MacrosDialog_ValueCol, + MacrosTable_NameHint, + MacrosTable_ToolTip, + MacrosTable_ValueHint, + MoveDown, + MoveUp, + NotSet, + OpenInExternalEditor, + Password, + Password_Caption, + Password_Error, + Password_Prompt, + PointsDialog_Info, + PointsDialog_Title, + PointsTable_Empty, + PointsTable_X, + PointsTable_Y, + Red, + Remove, + Row, + RulesDialog_ColName, + RulesDialog_ColBoolExp, + RulesDialog_ColValExp, + RulesDialog_DefaultRuleName, + RulesDialog_ExpressionsTT, + RulesDialog_Info, + RulesDialog_NoExpressions, + RulesDialog_NoPVs, + RulesDialog_NoRules, + RulesDialog_PVsTT, + RulesDialog_RulesTT, + RulesDialog_SelectRule, + RulesDialog_ShowScript, + RulesDialog_Title, + ScriptsDialog_BtnEmbedJS, + ScriptsDialog_BtnEmbedPy, + ScriptsDialog_BtnFile, + ScriptsDialog_CheckConnections, + ScriptsDialog_ColPV, + ScriptsDialog_ColScript, + ScriptsDialog_ColTrigger, + ScriptsDialog_DefaultScriptFile, + ScriptsDialog_Info, + ScriptsDialog_JavaScriptScriptFile, + ScriptsDialog_NoScripts, + ScriptsDialog_NoPVs, + ScriptsDialog_PVsTT, + ScriptsDialog_PythonScriptFile, + ScriptsDialog_ScriptsTT, + ScriptsDialog_Title, + Select, + ShowConfirmationDialogTitle, + ShowErrorDialogTitle, + ShowMessageDialogTitle, + ShowSaveAsDialogTitle, + Target_Replace, + Target_Standalone, + Target_Tab, + Target_Window, + WidgetColorPopOver_Alpha, + WidgetColorPopOver_Blue, + WidgetColorPopOver_Color, + WidgetColorPopOver_CustomColor, + WidgetColorPopOver_Default, + WidgetColorPopOver_DefaultButton, + WidgetColorPopOver_Green, + WidgetColorPopOver_Info, + WidgetColorPopOver_Original, + WidgetColorPopOver_PredefinedColors, + WidgetColorPopOver_Red, + WidgetColorPopOver_SearchField, + WidgetColorPopOver_SearchFieldTT, + WidgetFontPopOver_ExampleText, + WidgetFontPopOver_FontsFamilies, + WidgetFontPopOver_Info, + WidgetFontPopOver_PredefinedFonts, + WidgetFontPopOver_Preview, + WidgetFontPopOver_PreviewPrompt, + WidgetFontPopOver_SearchPrompt, + WidgetFontPopOver_SearchPromptTT, + WidgetFontPopOver_SizeCaption, + WidgetFontPopOver_SizePrompt, + WidgetFontPopOver_Sizes, + WidgetFontPopOver_StyleCaption, + WidgetFontPopOver_StylePrompt, + WidgetFontPopOver_Styles, + WidgetInfoDialog_Category, + WidgetInfoDialog_Count, + WidgetInfoDialog_Disconnected, + WidgetInfoDialog_Info_Fmt, + WidgetInfoDialog_Name, + WidgetInfoDialog_Path, + WidgetInfoDialog_Property, + WidgetInfoDialog_State, + WidgetInfoDialog_WidgetStats, + WidgetInfoDialog_TabMacros, + WidgetInfoDialog_TabProperties, + WidgetInfoDialog_TabPVs, + WidgetInfoDialog_Title, + WidgetInfoDialog_Total, + WidgetInfoDialog_Value, + WidgetInfoDialog_WidgetType, + Zoom_All, + Zoom_Height, + Zoom_Width, + Reset_Axis_Ranges; + + static { + // initialize resource bundle + NLS.initializeMessages(Messages.class); + } + + private Messages() { + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayAction.java new file mode 100644 index 0000000000..8eb8edaa74 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayAction.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.MenuItem; +import javafx.scene.image.Image; +import javafx.scene.input.MouseEvent; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.macros.MacroXMLUtil; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.macros.Macros; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OpenDisplayAction extends ActionInfoBase { + + /** + * Default {@link Target} is new tab. + */ + private Target target = Target.TAB; + + private Macros macros; + + private String pane; + + private String file; + + public static final String OPEN_DISPLAY = "open_display"; + + private OpenDisplayActionController openDisplayActionController; + + private static final Logger logger = Logger.getLogger(OpenDisplayAction.class.getName()); + + public enum Target { + /** + * Replace current display + */ + REPLACE(Messages.Target_Replace), + + /** + * Open a new tab in existing window + */ + TAB(Messages.Target_Tab), + + /** + * Open a new window + */ + WINDOW(Messages.Target_Window), + + /** + * Open standalone window + * + * @deprecated Was only used in RCP version. + */ + @Deprecated + STANDALONE(Messages.Target_Standalone); + + private final String name; + + Target(final String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public OpenDisplayAction() { + this.description = Messages.ActionOpenDisplay; + this.type = OPEN_DISPLAY; + } + + /** @param description Action description + * @param file Path to the display + * @param macros Macros + * @param target Where to show the display + */ + public OpenDisplayAction(final String description, final String file, final Macros macros, final OpenDisplayAction.Target target) + { + this(description, file, macros, target, ""); + } + + /** @param description Action description + * @param file Path to the display + * @param macros Macros + * @param target Where to show the display + * @param pane Pane in which to open (for target==TAB) + */ + public OpenDisplayAction(final String description, final String file, final Macros macros, final OpenDisplayAction.Target target, final String pane) + { + this.description = description; + this.file = Objects.requireNonNull(file); + this.macros = macros; + this.target = target; + this.pane = pane; + this.type = OPEN_DISPLAY; + } + + public Target getTarget() { + return target; + } + + public Macros getMacros() { + return macros == null ? new Macros() : macros; + } + + public String getPane() { + return pane; + } + + public String getFile() { + return file; + } + + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(OPEN_DISPLAY) || actionId.equalsIgnoreCase("OPEN_OPI_IN_VIEW"); + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) { + Optional targetOptional = XMLUtil.getChildString(actionXml, XMLTags.TARGET); + if (targetOptional.isPresent()) { + target = OpenDisplayAction.Target.valueOf(targetOptional.get().toUpperCase()); + } else { + Optional replace = XMLUtil.getChildString(actionXml, "replace"); + if (replace.isPresent()) { + if ("0".equals(replace.get())) { + target = OpenDisplayAction.Target.TAB; + } else if ("2".equals(replace.get())) { + target = OpenDisplayAction.Target.WINDOW; + } + } else { + Optional mode = XMLUtil.getChildString(actionXml, "mode"); + mode.ifPresent(s -> target = modeToTargetConvert(Integer.parseInt(s))); + } + } + + // Use , falling back to legacy + file = XMLUtil.getChildString(actionXml, XMLTags.FILE) + .orElse(XMLUtil.getChildString(actionXml, XMLTags.PATH) + .orElse("")); + + final Element macroXml = XMLUtil.getChildElement(actionXml, XMLTags.MACROS); + if (macroXml != null) { + macros = MacroXMLUtil.readMacros(macroXml); + } else { + macros = new Macros(); + } + + pane = XMLUtil.getChildString(actionXml, XMLTags.NAME).orElse(""); + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, OPEN_DISPLAY); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.FILE); + writer.writeCharacters(file); + writer.writeEndElement(); + if (macros != null && !macros.getNames().isEmpty()) { + writer.writeStartElement(XMLTags.MACROS); + MacroXMLUtil.writeMacros(writer, macros); + writer.writeEndElement(); + } + writer.writeStartElement(XMLTags.TARGET); + writer.writeCharacters(target.name().toLowerCase()); + writer.writeEndElement(); + if (pane != null && !pane.isEmpty()) { + writer.writeStartElement(XMLTags.NAME); + writer.writeCharacters(pane); + writer.writeEndElement(); + } + } + + @Override + public void setModifiers(final MouseEvent event) { + boolean middle_click = event.isMiddleButtonDown(); + if (event.isShortcutDown() || middle_click) { + target = Target.TAB; + } else if (event.isShiftDown()) { + target = Target.WINDOW; + } + } + + @Override + public List getContextMenuItems(Widget widget) { + List items = new ArrayList<>(); + + items.add(createMenuItem(widget, description)); + + // Add variant for all the available Target types: Replace, new Tab, ... + for (OpenDisplayAction.Target target : OpenDisplayAction.Target.values()) { + if (target == OpenDisplayAction.Target.STANDALONE || target == this.target) + continue; + // Mention non-default targets in the description + items.add(createMenuItem(widget, description + " (" + target + ")")); + } + + return items; + } + + private OpenDisplayAction.Target modeToTargetConvert(int mode) { + return switch (mode) { + // 0 - REPLACE + case 0 -> Target.REPLACE; + // 7 - NEW_WINDOW + // 8 - NEW_SHELL + case 7, 8 -> Target.WINDOW; + // 1 - NEW_TAB + // 2 - NEW_TAB_LEFT + // 3 - NEW_TAB_RIGHT + // 4 - NEW_TAB_TOP + // 5 - NEW_TAB_BOTTOM + // 6 - NEW_TAB_DETACHED + default -> Target.TAB; + }; + } + + @Override + public String toString() { + if (getDescription().isEmpty()) + return "Open " + file; + else + return description; + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/open_display.png"); + } + + @Override + public Node getEditor(Widget widget) { + if (editorUi != null) { + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("/org/csstudio/display/actions/OpenDisplayAction.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(Widget.class, ActionInfo.class).newInstance(widget, this); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to construct OpenDisplayActionController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + openDisplayActionController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert(){ + openDisplayActionController.setDisplayPath(file); + openDisplayActionController.setPane(pane); + openDisplayActionController.setDescription(description); + openDisplayActionController.setTarget(target); + openDisplayActionController.setMacros(macros); + } + + @Override + public ActionInfo commit(){ + description = openDisplayActionController.getDescription(); + file = openDisplayActionController.getDisplayPath(); + pane = openDisplayActionController.getPane(); + macros = openDisplayActionController.getMacros(); + target = openDisplayActionController.getTarget(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayActionController.java new file mode 100644 index 0000000000..e6594f914b --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenDisplayActionController.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.FilenameSupport; +import org.csstudio.display.builder.representation.javafx.MacrosTable; +import org.phoebus.framework.macros.Macros; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * FXML controller for the open display script action editor. + */ +public class OpenDisplayActionController extends ActionControllerBase { + @FXML + private RadioButton replaceRadioButton; + @FXML + private RadioButton newTabRadioButton; + @FXML + private RadioButton newWindowRadioButton; + @FXML + private TextField displayPath; + @FXML + private TextField pane; + @FXML + private VBox macrosTablePlaceholder; + + private MacrosTable macrosTable; + + private final OpenDisplayAction openDisplayActionInfo; + + private final StringProperty paneProperty = new SimpleStringProperty(); + private final StringProperty displayPathProperty = new SimpleStringProperty(); + + private OpenDisplayAction.Target target; + + private final Widget widget; + + /** + * @param widget Widget + * @param actionInfo {@link ActionInfo} + */ + public OpenDisplayActionController(Widget widget, ActionInfo actionInfo) { + this.widget = widget; + this.openDisplayActionInfo = (OpenDisplayAction) actionInfo; + descriptionProperty.set(actionInfo.getDescription()); + } + + /** + * Init + */ + @FXML + public void initialize() { + super.initialize(); + + paneProperty.set(openDisplayActionInfo.getPane()); + + replaceRadioButton.setUserData(OpenDisplayAction.Target.REPLACE); + newTabRadioButton.setUserData(OpenDisplayAction.Target.TAB); + newWindowRadioButton.setUserData(OpenDisplayAction.Target.WINDOW); + + ToggleGroup toggleGroup = new ToggleGroup(); + toggleGroup.getToggles().addAll(replaceRadioButton, newTabRadioButton, newWindowRadioButton); + + target = openDisplayActionInfo.getTarget(); + /* + * Standalone is a deprecated name for Window + */ + if (target == OpenDisplayAction.Target.STANDALONE) { + target = OpenDisplayAction.Target.WINDOW; + } + + toggleGroup.selectToggle(toggleGroup.getToggles().stream() + .filter(t -> t.getUserData().equals(target)).findFirst().get()); + toggleGroup.selectedToggleProperty().addListener((obs, o, n) -> { + target = (OpenDisplayAction.Target)n.getUserData(); + }); + + paneProperty.set(openDisplayActionInfo.getPane()); + pane.textProperty().bindBidirectional(paneProperty); + pane.disableProperty().bind(newTabRadioButton.selectedProperty().not()); + + displayPathProperty.setValue(openDisplayActionInfo.getFile()); + displayPath.textProperty().bindBidirectional(displayPathProperty); + + macrosTable = new MacrosTable(openDisplayActionInfo.getMacros()); + macrosTablePlaceholder.getChildren().add(macrosTable.getNode()); + GridPane.setHgrow(macrosTable.getNode(), Priority.ALWAYS); + VBox.setVgrow(macrosTable.getNode(), Priority.ALWAYS); + } + + /** + * Prompt for filename + */ + @FXML + public void selectDisplayPath() { + try { + final String path = FilenameSupport.promptForRelativePath(widget, openDisplayActionInfo.getFile()); + if (path != null) { + displayPathProperty.set(path); + } + } catch (Exception e) { + Logger.getLogger(OpenDisplayActionController.class.getName()) + .log(Level.WARNING, "Cannot prompt for filename", e); + } + } + + public String getDisplayPath(){ + return displayPathProperty.get(); + } + + public String getPane(){ + return paneProperty.get(); + } + + public OpenDisplayAction.Target getTarget(){ + return target; + } + + public Macros getMacros(){ + return macrosTable.getMacros(); + } + + public void setDisplayPath(String displayPath) { + this.displayPathProperty.set(displayPath); + } + + public void setPane(String pane) { + this.paneProperty.set(pane); + } + + public void setMacros(Macros macros) { + if(macros != null){ + this.macrosTable.setMacros(macros); + } + } + + public void setTarget(OpenDisplayAction.Target target) { + this.target = target; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileAction.java new file mode 100644 index 0000000000..f40efbba03 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileAction.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OpenFileAction extends ActionInfoBase { + + public static final String OPEN_FILE = "open_file"; + private String file; + + private OpenFileActionController openFileActionController; + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public OpenFileAction() { + this.description = Messages.ActionOpenFile; + this.type = OPEN_FILE; + + } + + /** @param description Action description + * @param file Path to file to open + */ + public OpenFileAction(final String description, final String file) + { + this.description = description; + this.file = file; + this.type = OPEN_FILE; + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) { + file = XMLUtil.getChildString(actionXml, XMLTags.FILE) + .orElse(XMLUtil.getChildString(actionXml, XMLTags.PATH) + .orElse("")); + if (description.isEmpty()) { + description = Messages.ActionOpenFile; + } + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, OPEN_FILE); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.FILE); + writer.writeCharacters(file); + writer.writeEndElement(); + } + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(OPEN_FILE); + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/open_file.png"); + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + @Override + public Node getEditor(Widget widget){ + if(editorUi != null){ + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("OpenFileAction.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(Widget.class, ActionInfo.class).newInstance(widget, this); + } catch (Exception e) { + Logger.getLogger(OpenFileAction.class.getName()) + .log(Level.SEVERE, "Failed to construct OpenFileActionController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + openFileActionController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert(){ + openFileActionController.setFilePath(file); + openFileActionController.setDescription(description); + } + + public ActionInfo commit(){ + description = openFileActionController.getDescription(); + file = openFileActionController.getFilePath(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileActionController.java new file mode 100644 index 0000000000..a4b4c7eabe --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenFileActionController.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.FilenameSupport; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * FXML Controller for the open file action editor. + */ +public class OpenFileActionController extends ActionControllerBase { + + private final OpenFileAction openFileActionInfo; + private final Widget widget; + + @FXML + private TextField filePath; + + private final StringProperty filePathProperty = new SimpleStringProperty(); + + private final Logger logger = + Logger.getLogger(OpenFileActionController.class.getName()); + + /** + * @param widget Widget + * @param actionInfo {@link ActionInfo} + */ + public OpenFileActionController(Widget widget, ActionInfo actionInfo) { + this.widget = widget; + this.openFileActionInfo = (OpenFileAction) actionInfo; + descriptionProperty.set(actionInfo.getDescription()); + } + + /** + * Init + */ + @FXML + public void initialize() { + super.initialize(); + filePathProperty.set(openFileActionInfo.getFile()); + filePath.textProperty().bindBidirectional(filePathProperty); + } + + /** + * Prompt for file + */ + @FXML + public void selectFile() { + try { + final String path = FilenameSupport.promptForRelativePath(widget, filePathProperty.get()); + if (path != null) { + filePathProperty.setValue(path); + } + } catch (Exception ex) { + logger.log(Level.WARNING, "Cannot prompt for filename", ex); + } + } + + public String getFilePath(){ + return filePathProperty.get(); + } + + public void setFilePath(String filePath) { + this.filePathProperty.set(filePath); + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageAction.java new file mode 100644 index 0000000000..eeb7120cc7 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageAction.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OpenWebPageAction extends ActionInfoBase { + + public static final String OPEN_WEBPAGE = "open_webpage"; + private String url; + private OpenWebPageActionController openWebPageController; + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public OpenWebPageAction() { + this.description = Messages.ActionOpenWebPage; + this.type = OPEN_WEBPAGE; + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) { + url = XMLUtil.getChildString(actionXml, XMLTags.URL) + .orElse(XMLUtil.getChildString(actionXml, "hyperlink") + .orElse("")); + if (description.isEmpty()) { + description = Messages.ActionOpenWebPage; + } + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, OPEN_WEBPAGE); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.URL); + writer.writeCharacters(url); + writer.writeEndElement(); + } + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(OPEN_WEBPAGE); + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/web_browser.png"); + } + + public String getURL() { + return url; + } + + public void setURL(String url) { + this.url = url; + } + + @Override + public Node getEditor(Widget widget){ + if(editorUi != null){ + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("OpenWebPageAction.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(ActionInfo.class).newInstance(this); + } catch (Exception e) { + Logger.getLogger(OpenWebPageAction.class.getName()) + .log(Level.SEVERE, "Failed to construct OpenWebPageActionController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + openWebPageController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert(){ + openWebPageController.setDescription(description); + openWebPageController.setUrl(url); + } + + @Override + public ActionInfo commit(){ + description = openWebPageController.getDescription(); + url = openWebPageController.getUrl(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageActionController.java new file mode 100644 index 0000000000..c6cf528d35 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/OpenWebPageActionController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.spi.ActionInfo; + +/** + * FXML Controller for the open web page action editor. + */ +public class OpenWebPageActionController extends ActionControllerBase { + + private final OpenWebPageAction openWebpageActionInfo; + + @FXML + private TextField url; + + private final StringProperty urlProperty = new SimpleStringProperty(); + + /** + * @param actionInfo {@link ActionInfo} + */ + public OpenWebPageActionController(ActionInfo actionInfo) { + this.openWebpageActionInfo = (OpenWebPageAction) actionInfo; + descriptionProperty.set(openWebpageActionInfo.getDescription()); + } + + /** + * Init + */ + @FXML + public void initialize() { + super.initialize(); + + urlProperty.setValue(openWebpageActionInfo.getURL()); + url.textProperty().bindBidirectional(urlProperty); + } + + public String getUrl(){ + return urlProperty.get(); + } + + public void setUrl(String url){ + urlProperty.set(url); + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java new file mode 100644 index 0000000000..8c860a78a3 --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.ActionInfoBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.nls.NLS; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.javafx.ImageCache; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class WritePVAction extends ActionInfoBase { + + private String pv = "$(pv_name)"; + private String value = "0"; + + public static final String WRITE_PV = "write_pv"; + private final Logger logger = Logger.getLogger(WritePVAction.class.getName()); + + private WritePVActionController writePVActionController; + + @SuppressWarnings("unused") + /** + * Do not remove, needed by SPI framework. + */ + public WritePVAction() { + this.description = Messages.ActionWritePV; + this.type = WRITE_PV; + } + + public WritePVAction(String description, String pv, String value) { + this.description = description; + this.pv = pv; + this.value = value; + this.type = WRITE_PV; + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) { + // PV Name should be set. + pv = XMLUtil.getChildString(actionXml, XMLTags.PV_NAME).orElse(""); + if (pv.isEmpty()) { + logger.log(Level.WARNING, "Ignoring with empty on widget"); + } + + // PV may be empty to write "". + // In contrast to legacy opibuilder the value is _not_ trimmed, + // so it's possible to write " " (which opibuilder wrote as "") + value = XMLUtil.getChildString(actionXml, XMLTags.VALUE).orElse(""); + if (description.isEmpty()) { + description = Messages.ActionWritePV; + } + } + + @Override + public void writeToXML(ModelWriter modelWriter, XMLStreamWriter writer) throws Exception { + + writer.writeAttribute(XMLTags.TYPE, WRITE_PV); + writeDescriptionToXML(writer, description); + writer.writeStartElement(XMLTags.PV_NAME); + writer.writeCharacters(pv); + writer.writeEndElement(); + writer.writeStartElement(XMLTags.VALUE); + writer.writeCharacters(value); + writer.writeEndElement(); + } + + @Override + public Image getImage() { + return ImageCache.getImage(ActionsDialog.class, "/icons/write_pv.png"); + } + + @Override + public boolean matchesAction(String actionId) { + return actionId.equalsIgnoreCase(WRITE_PV); + } + + public String getPV() { + return pv; + } + + public String getValue() { + return value; + } + + public String getPv() { + return pv; + } + + public void setPv(String pv) { + this.pv = pv; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public Node getEditor(Widget widget) { + if (editorUi != null) { + return editorUi; + } + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("WritePVAction.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(ActionInfo.class).newInstance(this); + } catch (Exception e) { + Logger.getLogger(WritePVAction.class.getName()).log(Level.SEVERE, "Failed to construct WritePVActionController", e); + } + return null; + }); + + try { + editorUi = fxmlLoader.load(); + writePVActionController = fxmlLoader.getController(); + return editorUi; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void revert() { + writePVActionController.setDescription(description); + writePVActionController.setPvName(pv); + writePVActionController.setValue(value); + } + + @Override + public ActionInfo commit() { + description = writePVActionController.getDescription(); + value = writePVActionController.getValue(); + pv = writePVActionController.getPvName(); + return this; + } +} diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVActionController.java b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVActionController.java new file mode 100644 index 0000000000..fdbf17510b --- /dev/null +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVActionController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.csstudio.display.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import org.csstudio.display.builder.model.ActionControllerBase; +import org.csstudio.display.builder.model.spi.ActionInfo; +import org.phoebus.ui.autocomplete.PVAutocompleteMenu; + +/** + * FXML Controller for the write PV action editor. + */ +public class WritePVActionController extends ActionControllerBase { + + private final WritePVAction writePVActionInfo; + + @FXML + private TextField pvName; + @FXML + private TextField pvValue; + + private final StringProperty pvNameProperty = new SimpleStringProperty(); + private final StringProperty pvValueProperty = new SimpleStringProperty(); + + /** + * @param actionInfo {@link ActionInfo} + */ + public WritePVActionController(ActionInfo actionInfo) { + this.writePVActionInfo = (WritePVAction) actionInfo; + descriptionProperty.set(actionInfo.getDescription()); + } + + /** + * Init + */ + @FXML + public void initialize() { + super.initialize(); + + pvNameProperty.setValue(writePVActionInfo.getPV()); + pvValueProperty.setValue(writePVActionInfo.getValue()); + pvName.textProperty().bindBidirectional(pvNameProperty); + pvValue.textProperty().bindBidirectional(pvValueProperty); + + PVAutocompleteMenu.INSTANCE.attachField(pvName); + } + + public String getPvName(){ + return pvNameProperty.get(); + } + + public String getValue(){ + return pvValueProperty.get(); + } + + public void setValue(String value){ + pvValueProperty.set(value); + } + + public void setPvName(String pvName){ + pvNameProperty.set(pvName); + } +} diff --git a/app/display/actions/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.ActionInfo b/app/display/actions/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.ActionInfo new file mode 100644 index 0000000000..252c3b1bc6 --- /dev/null +++ b/app/display/actions/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.ActionInfo @@ -0,0 +1,6 @@ +org.csstudio.display.actions.OpenDisplayAction +org.csstudio.display.actions.WritePVAction +org.csstudio.display.actions.ExecuteScriptAction +org.csstudio.display.actions.ExecuteCommandAction +org.csstudio.display.actions.OpenFileAction +org.csstudio.display.actions.OpenWebPageAction diff --git a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/actionsdialog/ExecuteCommandActionDetails.fxml b/app/display/actions/src/main/resources/org/csstudio/display/actions/ExecuteCommandAction.fxml similarity index 53% rename from app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/actionsdialog/ExecuteCommandActionDetails.fxml rename to app/display/actions/src/main/resources/org/csstudio/display/actions/ExecuteCommandAction.fxml index 9c41f6a9e5..a05068e874 100644 --- a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/actionsdialog/ExecuteCommandActionDetails.fxml +++ b/app/display/actions/src/main/resources/org/csstudio/display/actions/ExecuteCommandAction.fxml @@ -4,26 +4,12 @@ + fx:controller="org.csstudio.display.actions.ExecuteCommandActionController">