From 1ca87a1170987eb57f31b4f73374e5e85390d321 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 15 May 2024 13:21:36 +0200 Subject: [PATCH 01/10] Proposal for actions re-factoring --- .../display/converter/edm/EdmConverter.java | 387 ++++----- .../Convert_activeExitButtonClass.java | 2 +- .../Convert_activeMessageButtonClass.java | 2 +- .../widgets/Convert_relatedDisplayClass.java | 68 +- .../edm/widgets/Convert_shellCmdClass.java | 2 +- .../translator/MessageButton2Model.java | 2 +- .../translator/RelatedDisplay2Model.java | 2 +- .../translator/ShellCommand2Model.java | 2 +- .../properties/ActionsPropertyBinding.java | 12 +- .../properties/PropertyPanelSection.java | 5 +- .../display/builder/model/Widget.java | 809 +++++++++--------- .../properties/CommonWidgetProperties.java | 28 +- .../properties/PluggableActionInfos.java | 75 ++ .../PluggableActionsWidgetProperty.java | 104 +++ .../model/spi/PluggableActionInfo.java | 110 +++ .../model/AllWidgetsAllProperties.java | 4 +- app/display/navigation/pom.xml | 5 + .../display/navigation/ProcessOPI.java | 32 +- .../javafx/actions/OpenDisplayAction.java | 313 +++++++ .../OpenDisplayActionDetailsController.java | 106 +-- .../javafx/actions/PluggableActionBase.java | 118 +++ .../javafx/actionsdialog/ActionsDialog.java | 7 +- .../ActionsDialogActionItem.java | 43 +- .../ActionsDialogController.java | 23 +- .../widgets/ActionButtonRepresentation.java | 374 ++++---- .../javafx/widgets/GroupRepresentation.java | 4 +- ...play.builder.model.spi.PluggableActionInfo | 1 + .../ExecuteCommandActionDetails.fxml | 0 .../ExecuteScriptActionDetails.fxml | 0 .../actions/OpenDisplayActionDetails.fxml | 64 ++ .../OpenFileActionDetails.fxml | 0 .../OpenWebPageActionDetails.fxml | 0 .../WritePVActionDetails.fxml | 0 .../OpenDisplayActionDetails.fxml | 67 -- .../javafx/ActionsDialogDemo.java | 18 +- .../representation/ToolkitListener.java | 3 + .../representation/ToolkitRepresentation.java | 17 + .../display/builder/runtime/ActionUtil.java | 35 + .../display/builder/runtime/RuntimeUtil.java | 8 + .../builder/runtime/WidgetRuntime.java | 14 +- .../OpenDisplayActionHandler.java | 158 ++++ .../runtime/app/ContextMenuSupport.java | 77 +- .../builder/runtime/spi/ActionHandler.java | 32 + ....display.builder.runtime.spi.ActionHandler | 1 + .../persistence/xml_map_parse_example.xml | 16 + 45 files changed, 2088 insertions(+), 1062 deletions(-) create mode 100644 app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionInfos.java create mode 100644 app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionsWidgetProperty.java create mode 100644 app/display/model/src/main/java/org/csstudio/display/builder/model/spi/PluggableActionInfo.java create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayAction.java rename app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/OpenDisplayActionDetailsController.java (59%) create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/PluggableActionBase.java create mode 100644 app/display/representation-javafx/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.PluggableActionInfo rename app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/ExecuteCommandActionDetails.fxml (100%) rename app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/ExecuteScriptActionDetails.fxml (100%) create mode 100644 app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayActionDetails.fxml rename app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/OpenFileActionDetails.fxml (100%) rename app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/OpenWebPageActionDetails.fxml (100%) rename app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/{actionsdialog => actions}/WritePVActionDetails.fxml (100%) delete mode 100644 app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/actionsdialog/OpenDisplayActionDetails.fxml create mode 100644 app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/actionhandlers/OpenDisplayActionHandler.java create mode 100644 app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/spi/ActionHandler.java create mode 100644 app/display/runtime/src/main/resources/META-INF/services/org.csstudio.display.builder.runtime.spi.ActionHandler create mode 100644 core/framework/src/test/resources/org/phoebus/framework/persistence/xml_map_parse_example.xml diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/EdmConverter.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/EdmConverter.java index 1d09a766b2..4e91eef132 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/EdmConverter.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/EdmConverter.java @@ -7,7 +7,22 @@ *******************************************************************************/ package org.csstudio.display.converter.edm; -import static org.csstudio.display.converter.edm.Converter.logger; +import org.csstudio.display.builder.editor.util.GeometryTools; +import org.csstudio.display.builder.model.ChildrenProperty; +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.NamedWidgetColors; +import org.csstudio.display.builder.model.properties.CommonWidgetProperties; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; +import org.csstudio.display.builder.model.widgets.ActionButtonWidget; +import org.csstudio.display.converter.edm.widgets.ConverterBase; +import org.csstudio.opibuilder.converter.model.EdmDisplay; +import org.csstudio.opibuilder.converter.model.EdmEntity; +import org.csstudio.opibuilder.converter.model.EdmWidget; +import org.csstudio.opibuilder.converter.parser.EdmDisplayParser; import java.awt.geom.Rectangle2D; import java.io.File; @@ -24,35 +39,20 @@ import java.util.logging.Level; import java.util.stream.Collectors; -import org.csstudio.display.builder.editor.util.GeometryTools; -import org.csstudio.display.builder.model.ChildrenProperty; -import org.csstudio.display.builder.model.DisplayModel; -import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.WidgetProperty; -import org.csstudio.display.builder.model.persist.ModelWriter; -import org.csstudio.display.builder.model.persist.NamedWidgetColors; -import org.csstudio.display.builder.model.properties.ActionInfo; -import org.csstudio.display.builder.model.properties.ActionInfos; -import org.csstudio.display.builder.model.properties.CommonWidgetProperties; -import org.csstudio.display.builder.model.widgets.ActionButtonWidget; -import org.csstudio.display.converter.edm.widgets.ConverterBase; -import org.csstudio.opibuilder.converter.model.EdmDisplay; -import org.csstudio.opibuilder.converter.model.EdmEntity; -import org.csstudio.opibuilder.converter.model.EdmWidget; -import org.csstudio.opibuilder.converter.parser.EdmDisplayParser; +import static org.csstudio.display.converter.edm.Converter.logger; -/** Convert one {@link EdmDisplay} to {@link DisplayModel} +/** + * Convert one {@link EdmDisplay} to {@link DisplayModel} * - *

Tracks the referenced displays for caller to potentially - * convert them as well, both 'included' displays from embedded - * screens or symbols, and 'linked' displays from buttons that - * open related displays. + *

Tracks the referenced displays for caller to potentially + * convert them as well, both 'included' displays from embedded + * screens or symbols, and 'linked' displays from buttons that + * open related displays. * - * @author Kay Kasemir + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class EdmConverter -{ +public class EdmConverter { private final AssetLocator asset_locator; private final DisplayModel model = new DisplayModel(); @@ -64,18 +64,18 @@ public class EdmConverter private final Set included_displays = new HashSet<>(); private final Set linked_displays = new HashSet<>(); - /** Parse EDL input and convert into {@link DisplayModel} + /** + * Parse EDL input and convert into {@link DisplayModel} * - *

Optional {@link AssetLocator} allows for example image - * widgets to locate their images, automatically downloading - * it from a http:/.. search path. + *

Optional {@link AssetLocator} allows for example image + * widgets to locate their images, automatically downloading + * it from a http:/.. search path. * - * @param input EDL input file - * @param asset_locator Optional {@link AssetLocator} or null - * @throws Exception on error + * @param input EDL input file + * @param asset_locator Optional {@link AssetLocator} or null + * @throws Exception on error */ - public EdmConverter(final File input, final AssetLocator asset_locator) throws Exception - { + public EdmConverter(final File input, final AssetLocator asset_locator) throws Exception { this.asset_locator = asset_locator; logger.log(Level.FINE, "Parsing EDM " + input); @@ -85,8 +85,8 @@ public EdmConverter(final File input, final AssetLocator asset_locator) throws E // Create Display Model final String name = input.getName() - .replace(".edl", "") - .replace('_', ' '); + .replace(".edl", "") + .replace('_', ' '); model.propName().setValue(name); model.propX().setValue(edm.getX()); model.propY().setValue(edm.getY()); @@ -99,8 +99,7 @@ public EdmConverter(final File input, final AssetLocator asset_locator) throws E model.propName().setValue(edm.getTitle()); model.propGridVisible().setValue(edm.isShowGrid()); - if (edm.getGridSize() > 0) - { + if (edm.getGridSize() > 0) { model.propGridStepX().setValue(edm.getGridSize()); model.propGridStepY().setValue(edm.getGridSize()); } @@ -111,89 +110,93 @@ public EdmConverter(final File input, final AssetLocator asset_locator) throws E correctChildWidgets(model); } - /** @return {@link DisplayModel} */ - public DisplayModel getDisplayModel() - { + /** + * @return {@link DisplayModel} + */ + public DisplayModel getDisplayModel() { return model; } - /** @param output File to write with Display Builder model - * @throws Exception on error + /** + * @param output File to write with Display Builder model + * @throws Exception on error */ - public void write(final File output) throws Exception - { + public void write(final File output) throws Exception { logger.log(Level.FINE, "Writing " + output); final ModelWriter writer = new ModelWriter(new FileOutputStream(output)); writer.writeModel(model); writer.close(); } - /** @return Number of next group */ - public int nextGroup() - { + /** + * @return Number of next group + */ + public int nextGroup() { return next_group.getAndIncrement(); } - /** Request download of asset. + /** + * Request download of asset. * - *

NOP when this converter doesn't have an {@link AssetLocator}. + *

NOP when this converter doesn't have an {@link AssetLocator}. * - * @param asset Asset of a widget, for example PNG file for Image widget - * @throws Exception + * @param asset Asset of a widget, for example PNG file for Image widget + * @throws Exception */ - public void downloadAsset(final String asset) throws Exception - { + public void downloadAsset(final String asset) throws Exception { if (asset_locator != null) asset_locator.locate(asset); } - /** @return Displays that were included by this display (embedded, symbol) */ - public Collection getIncludedDisplays() - { + /** + * @return Displays that were included by this display (embedded, symbol) + */ + public Collection getIncludedDisplays() { return included_displays.stream().sorted().collect(Collectors.toList()); } - /** @return Displays that were linked from this display (related display) */ - public Collection getLinkedDisplays() - { + /** + * @return Displays that were linked from this display (related display) + */ + public Collection getLinkedDisplays() { return linked_displays.stream().sorted().collect(Collectors.toList()); } - /** @param x X offset and - * @param y Y offset of widgets within currently handled container + /** + * @param x X offset and + * @param y Y offset of widgets within currently handled container */ - public void addPositionOffset(final int x, final int y) - { + public void addPositionOffset(final int x, final int y) { offset_x += x; offset_y += y; } - /** @return X offset of widgets within currently handled container */ - public int getOffsetX() - { + /** + * @return X offset of widgets within currently handled container + */ + public int getOffsetX() { return offset_x; } - /** @return Y offset of widgets within currently handled container */ - public int getOffsetY() - { + /** + * @return Y offset of widgets within currently handled container + */ + public int getOffsetY() { return offset_y; } - /** Convert one widget - * @param parent Parent - * @param edm EDM widget to convert + /** + * Convert one widget + * + * @param parent Parent + * @param edm EDM widget to convert */ - public void convertWidget(final Widget parent, final EdmEntity edm) - { - if (edm instanceof EdmWidget) - { - final EdmWidget w = (EdmWidget) edm; - if (w.getX() + w.getW() <= 0 || - w.getY() + w.getH() <= 0) - { + public void convertWidget(final Widget parent, final EdmEntity edm) { + if (edm instanceof EdmWidget w) { + if (w.getX() + w.getW() <= 0 || + w.getY() + w.getH() <= 0) { logger.log(Level.WARNING, "Skipping off-screen widget " + edm.getType() + - " @ " + w.getX() + "," + w.getY() + " sized " + w.getW() + " x " + w.getH()); + " @ " + w.getX() + "," + w.getY() + " sized " + w.getW() + " x " + w.getH()); return; } } @@ -201,94 +204,82 @@ public void convertWidget(final Widget parent, final EdmEntity edm) // Given an EDM Widget type like "activeXTextClass", // locate the matching "Convert_activeXTextClass" final Class clazz; - try - { + try { final String wc_name = ConverterBase.class.getPackageName() + - ".Convert_" + - edm.getType().replace(':', '_'); + ".Convert_" + + edm.getType().replace(':', '_'); clazz = Class.forName(wc_name); - } - catch (ClassNotFoundException ex) - { + } catch (ClassNotFoundException ex) { logger.log(Level.WARNING, "No converter for EDM " + edm.getType()); return; } - try - { - for (Constructor c : clazz.getConstructors()) - { // Look for suitable constructor + try { + for (Constructor c : clazz.getConstructors()) { // Look for suitable constructor final Class[] parms = c.getParameterTypes(); - if (parms.length == 3 && - parms[0] == EdmConverter.class && - parms[1] == Widget.class && - EdmWidget.class.isAssignableFrom(parms[2])) - { + if (parms.length == 3 && + parms[0] == EdmConverter.class && + parms[1] == Widget.class && + EdmWidget.class.isAssignableFrom(parms[2])) { // Simply constructing the converter will perform the conversion c.newInstance(this, parent, edm); return; } } throw new Exception(clazz.getSimpleName() + " lacks required constructor"); - } - catch (Exception ex) - { + } catch (Exception ex) { logger.log(Level.WARNING, "Cannot convert " + edm.getType(), ex); } } - /** Correct widget issues + /** + * Correct widget issues + * + *

Called after all widgets have been added to a parent * - *

Called after all widgets have been added to a parent - * @param parent + * @param parent */ - public void correctChildWidgets(final Widget parent) - { + public void correctChildWidgets(final Widget parent) { final ChildrenProperty children = ChildrenProperty.getChildren(parent); mergeButtons(children); fixCoveredButtons(children); raiseTransparentButtons(children); } - /** Merge action buttons that are overlapping into one button. - * EDM supports 'invisible' buttons which react to left or - * right mouse button to occupy the same space. - * In display builder, only the left button triggers an action. - * The right button opens the context menu. - * So merge actions from all overlapping buttons into one. + /** + * Merge action buttons that are overlapping into one button. + * EDM supports 'invisible' buttons which react to left or + * right mouse button to occupy the same space. + * In display builder, only the left button triggers an action. + * The right button opens the context menu. + * So merge actions from all overlapping buttons into one. * - * @param children Child widgets to correct + * @param children Child widgets to correct */ - private void mergeButtons(final ChildrenProperty children) - { + private void mergeButtons(final ChildrenProperty children) { // Start with the topmost button, i.e. end of list final List copy = new ArrayList<>(children.getValue()); - for (int i=copy.size()-1; i>=0; --i) - { + for (int i = copy.size() - 1; i >= 0; --i) { final Widget widget = copy.get(i); - if (! (widget instanceof ActionButtonWidget)) + if (!(widget instanceof ActionButtonWidget bw)) continue; - final ActionButtonWidget bw = (ActionButtonWidget) widget; // Look for buttons below that occupy roughly the same space - for (int o=i-1; o>=0; --o) - { + for (int o = i - 1; o >= 0; --o) { final Widget other = copy.get(o); - if (! (other instanceof ActionButtonWidget && doWidgetsOverlap(widget, other))) + if (!(other instanceof ActionButtonWidget ob && doWidgetsOverlap(widget, other))) continue; - final ActionButtonWidget ob = (ActionButtonWidget) other; logger.log(Level.INFO, "Merging actions from overlapping " + widget + " and " + other + " into one:"); logger.log(Level.INFO, "1) " + widget.propActions().getValue()); logger.log(Level.INFO, "2) " + other.propActions().getValue()); - final List actions = new ArrayList<>(widget.propActions().getValue().getActions()); + final List actions = new ArrayList<>(widget.propActions().getValue().getActions()); actions.addAll(other.propActions().getValue().getActions()); - widget.propActions().setValue(new ActionInfos(actions)); + widget.propActions().setValue(new PluggableActionInfos(actions)); // When merging buttons, as soon as one button is visible. // the remaining (merged) button must be visible - if (! ob.propTransparent().getValue()) - { + if (!ob.propTransparent().getValue()) { bw.propTransparent().setValue(false); bw.propText().setValue(ob.propText().getValue()); bw.propForegroundColor().setValue(ob.propForegroundColor().getValue()); @@ -305,69 +296,67 @@ private void mergeButtons(final ChildrenProperty children) } } - /** Do two widgets overlap, no matter which one covers the other? - * @param widget - * @param other - * @return Do the widgets overlap by a considerable amount? + /** + * Do two widgets overlap, no matter which one covers the other? + * + * @param widget + * @param other + * @return Do the widgets overlap by a considerable amount? */ - private boolean doWidgetsOverlap(final Widget widget, final Widget other) - { + private boolean doWidgetsOverlap(final Widget widget, final Widget other) { final Rectangle2D w = new Rectangle2D.Double(widget.propX().getValue(), - widget.propY().getValue(), - widget.propWidth().getValue(), - widget.propHeight().getValue()); + widget.propY().getValue(), + widget.propWidth().getValue(), + widget.propHeight().getValue()); final Rectangle2D o = new Rectangle2D.Double(other.propX().getValue(), - other.propY().getValue(), - other.propWidth().getValue(), - other.propHeight().getValue()); + other.propY().getValue(), + other.propWidth().getValue(), + other.propHeight().getValue()); final Rectangle2D common = w.createIntersection(o); - if (common.getWidth() <= 0 || common.getHeight() <= 0) + if (common.getWidth() <= 0 || common.getHeight() <= 0) return false; final int overlap = (int) (common.getWidth() * common.getHeight()); final int avg_area = (int) (w.getWidth() * w.getHeight() + - o.getWidth() * o.getHeight()) / 2; + o.getWidth() * o.getHeight()) / 2; // Overlap by at least a 5th?? return overlap > avg_area / 5; } - /** Make covered buttons 'transparent'. - * EDM supports buttons in the back of rectangles etc. - * They're not visible, but they react to mouse clicks. - * In display builder, covered buttons are, well, covered. - * Make them 'transparent', and a follow-up operation then raises - * all transparent buttons to the top so they get mouse clicks. + /** + * Make covered buttons 'transparent'. + * EDM supports buttons in the back of rectangles etc. + * They're not visible, but they react to mouse clicks. + * In display builder, covered buttons are, well, covered. + * Make them 'transparent', and a follow-up operation then raises + * all transparent buttons to the top so they get mouse clicks. * - * @param children Child widgets to correct + * @param children Child widgets to correct */ - private void fixCoveredButtons(final ChildrenProperty children) - { + private void fixCoveredButtons(final ChildrenProperty children) { final List list = children.getValue(); // Start of list = lowest widget - for (int i=0; i> check = other.checkProperty(CommonWidgetProperties.propTransparent); - if (check.isPresent() && check.get().getValue()) + if (check.isPresent() && check.get().getValue()) continue; // .. or invisible widgets check = other.checkProperty(CommonWidgetProperties.propVisible); - if (check.isPresent() && !check.get().getValue()) + if (check.isPresent() && !check.get().getValue()) continue; // Does other widget cover this one? - if (! isWidgetCovered(bottom, other)) + if (!isWidgetCovered(bottom, other)) continue; logger.log(Level.INFO, bottom + " is covered by " + other + ". Making it 'transparent' so it'll be raised."); @@ -379,23 +368,24 @@ private void fixCoveredButtons(final ChildrenProperty children) } } - /** Does one widget cover the other? - * @param bottom - * @param top - * @return Does top widget cover the one at the bottom by a considerable amount? + /** + * Does one widget cover the other? + * + * @param bottom + * @param top + * @return Does top widget cover the one at the bottom by a considerable amount? */ - private boolean isWidgetCovered(final Widget bottom, final Widget top) - { + private boolean isWidgetCovered(final Widget bottom, final Widget top) { final Rectangle2D w = new Rectangle2D.Double(bottom.propX().getValue(), - bottom.propY().getValue(), - bottom.propWidth().getValue(), - bottom.propHeight().getValue()); + bottom.propY().getValue(), + bottom.propWidth().getValue(), + bottom.propHeight().getValue()); final Rectangle2D o = new Rectangle2D.Double(top.propX().getValue(), - top.propY().getValue(), - top.propWidth().getValue(), - top.propHeight().getValue()); + top.propY().getValue(), + top.propWidth().getValue(), + top.propHeight().getValue()); final Rectangle2D common = w.createIntersection(o); - if (common.getWidth() <= 0 || common.getHeight() <= 0) + if (common.getWidth() <= 0 || common.getHeight() <= 0) return false; final int overlap = (int) (common.getWidth() * common.getHeight()); @@ -404,24 +394,20 @@ private boolean isWidgetCovered(final Widget bottom, final Widget top) return overlap > bottom_area / 2; } - /** Move transparent buttons to front. - * In EDM, transparent buttons may be placed behind text etc. - * In display builder, normal widget order would - * then have text block mouse events from button. - * @param children Child widgets to correct + /** + * Move transparent buttons to front. + * In EDM, transparent buttons may be placed behind text etc. + * In display builder, normal widget order would + * then have text block mouse events from button. + * + * @param children Child widgets to correct */ - private void raiseTransparentButtons(final ChildrenProperty children) - { + private void raiseTransparentButtons(final ChildrenProperty children) { final List copy = new ArrayList<>(children.getValue()); - for (Widget widget : copy) - { - if (widget instanceof ActionButtonWidget) - { - final ActionButtonWidget b = (ActionButtonWidget) widget; - if (b.propTransparent().getValue()) - { - try - { + for (Widget widget : copy) { + if (widget instanceof ActionButtonWidget b) { + if (b.propTransparent().getValue()) { + try { // Get absolute widget coords and check if widget is inside a group final DisplayModel display = widget.getDisplayModel(); final javafx.geometry.Rectangle2D bounds = GeometryTools.getDisplayBounds(widget); @@ -431,18 +417,15 @@ private void raiseTransparentButtons(final ChildrenProperty children) // If it was a top-level widget, just add back to display if (top_level) logger.log(Level.INFO, "Raising transparent " + b); - else - { // If widget was in group, raising it within the group could still mean + else { // If widget was in group, raising it within the group could still mean // that group is covered by something else. // So place at absolute coords and then place in display, not original group logger.log(Level.INFO, "Raising transparent " + b + " and moving from group to top"); - widget.propX().setValue((int)bounds.getMinX()); - widget.propY().setValue((int)bounds.getMinY()); + widget.propX().setValue((int) bounds.getMinX()); + widget.propY().setValue((int) bounds.getMinY()); } display.runtimeChildren().addChild(widget); - } - catch (Exception ex) - { + } catch (Exception ex) { logger.log(Level.WARNING, "Failed to raise transparent " + b, ex); } } @@ -450,15 +433,17 @@ private void raiseTransparentButtons(final ChildrenProperty children) } } - /** @param included_display Register a display that's included by the currently converted file */ - public void addIncludedDisplay(final String included_display) - { + /** + * @param included_display Register a display that's included by the currently converted file + */ + public void addIncludedDisplay(final String included_display) { included_displays.add(included_display); } - /** @param linked_display Register a display that was linked from the currently converted file */ - public void addLinkedDisplay(final String linked_display) - { + /** + * @param linked_display Register a display that was linked from the currently converted file + */ + public void addLinkedDisplay(final String linked_display) { linked_displays.add(linked_display); } } diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeExitButtonClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeExitButtonClass.java index d82c8816ff..abb9f8e8c7 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeExitButtonClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeExitButtonClass.java @@ -38,7 +38,7 @@ public Convert_activeExitButtonClass(final EdmConverter converter, final Widget convertColor(t.getFgColor(), widget.propForegroundColor()); convertFont(t.getFont(), widget.propFont()); widget.propText().setValue(t.getLabel()); - widget.propActions().setValue(new ActionInfos(List.of(new ExecuteScriptActionInfo("close", script)))); + //widget.propActions().setValue(new ActionInfos(List.of(new ExecuteScriptActionInfo("close", script)))); } @Override diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java index c0fdb02853..8a7886dc80 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java @@ -134,7 +134,7 @@ else if (have_release_value) // Set the button's $(pv_name) macro to the PV name, and use that within the write-PV action b.propPVName().setValue(pv); - b.propActions().setValue(new ActionInfos(List.of(new WritePVActionInfo(desc, "$(pv_name)", value)))); + //b.propActions().setValue(new ActionInfos(List.of(new WritePVActionInfo(desc, "$(pv_name)", value)))); } if (mb.getPassword() != null) diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_relatedDisplayClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_relatedDisplayClass.java index 684b877f27..9e3540b1b8 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_relatedDisplayClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_relatedDisplayClass.java @@ -7,19 +7,12 @@ *******************************************************************************/ package org.csstudio.display.converter.edm.widgets; -import static org.csstudio.display.converter.edm.Converter.logger; - -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; - import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.persist.NamedWidgetColors; -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.OpenDisplayActionInfo.Target; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; import org.csstudio.display.builder.model.widgets.ActionButtonWidget; +import org.csstudio.display.builder.representation.javafx.actions.OpenDisplayAction; import org.csstudio.display.converter.edm.EdmConverter; import org.csstudio.opibuilder.converter.model.EdmBoolean; import org.csstudio.opibuilder.converter.model.EdmString; @@ -27,30 +20,33 @@ import org.csstudio.opibuilder.converter.model.Edm_relatedDisplayClass; import org.phoebus.framework.macros.Macros; -/** Convert an EDM widget into Display Builder counterpart - * @author Kay Kasemir - * @author Matevz, Lei Hu, Xihui Chen et al - Original logic in Opi_.. converter +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import static org.csstudio.display.converter.edm.Converter.logger; + +/** + * Convert an EDM widget into Display Builder counterpart + * + * @author Kay Kasemir + * @author Matevz, Lei Hu, Xihui Chen et al - Original logic in Opi_.. converter */ @SuppressWarnings("nls") -public class Convert_relatedDisplayClass extends ConverterBase -{ - public Convert_relatedDisplayClass(final EdmConverter converter, final Widget parent, final Edm_relatedDisplayClass t) - { +public class Convert_relatedDisplayClass extends ConverterBase { + public Convert_relatedDisplayClass(final EdmConverter converter, final Widget parent, final Edm_relatedDisplayClass t) { super(converter, parent, t); - if (t.isInvisible()) - { + if (t.isInvisible()) { widget.propBackgroundColor().setValue(NamedWidgetColors.TRANSPARENT); widget.propTransparent().setValue(true); - } - else + } else convertColor(t.getBgColor(), widget.propBackgroundColor()); convertColor(t.getFgColor(), widget.propForegroundColor()); convertFont(t.getFont(), widget.propFont()); - List actions = new ArrayList<>(); - for (int i=0; i actions = new ArrayList<>(); + for (int i = 0; i < t.getNumDsps(); ++i) { final String is = Integer.toString(i); final EdmString menuLabel = t.getMenuLabel().getEdmAttributesMap().get(is); final String description = menuLabel != null ? menuLabel.get() : ""; @@ -62,31 +58,31 @@ public Convert_relatedDisplayClass(final EdmConverter converter, final Widget pa Macros macros = new Macros(); final EdmString symbols = t.getSymbols().getEdmAttributesMap().get(is); if (symbols != null) - try - { + try { macros = Macros.fromSimpleSpec(symbols.get()); - } - catch (Exception ex) - { + } catch (Exception ex) { logger.log(Level.WARNING, "Error in macros for related display " + path + " '" + symbols.get() + "'", ex); } final EdmBoolean closeDisplay = t.getCloseDisplay().getEdmAttributesMap().get(is); - final Target target = closeDisplay != null && closeDisplay.is() ? Target.REPLACE : Target.TAB; - - actions.add(new OpenDisplayActionInfo(description, path, macros, target)); + final OpenDisplayAction.Target target = closeDisplay != null && closeDisplay.is() ? OpenDisplayAction.Target.REPLACE : OpenDisplayAction.Target.TAB.TAB; + OpenDisplayAction openDisplayAction = new OpenDisplayAction(); + openDisplayAction.setFile(path); + openDisplayAction.setTarget(target); + openDisplayAction.setMacros(macros); + openDisplayAction.setDescription(description); + actions.add(openDisplayAction); } - widget.propActions().setValue(new ActionInfos(actions)); + widget.propActions().setValue(new PluggableActionInfos(actions)); - if (t.getButtonLabel() != null && !t.isInvisible()) + if (t.getButtonLabel() != null && !t.isInvisible()) widget.propText().setValue(t.getButtonLabel()); else widget.propText().setValue(""); } @Override - protected ActionButtonWidget createWidget(final EdmWidget edm) - { + protected ActionButtonWidget createWidget(final EdmWidget edm) { return new ActionButtonWidget(); } } diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_shellCmdClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_shellCmdClass.java index 2c947937a6..3c87965aca 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_shellCmdClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_shellCmdClass.java @@ -79,7 +79,7 @@ public Convert_shellCmdClass(final EdmConverter converter, final Widget parent, else actions.add(new ExecuteCommandActionInfo(description, command)); } - widget.propActions().setValue(new ActionInfos(actions)); + //widget.propActions().setValue(new ActionInfos(actions)); if (t.getButtonLabel() != null && !t.isInvisible()) widget.propText().setValue(t.getButtonLabel()); diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/MessageButton2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/MessageButton2Model.java index 1d6b2f6065..20b319f570 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/MessageButton2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/MessageButton2Model.java @@ -83,7 +83,7 @@ else if (!press_msg.isEmpty() && !release_msg.isEmpty()) } actions.add(new WritePVActionInfo("Write", messageButtonWidget.getAdlControl().getChan(), message)); - widgetModel.propActions().setValue(new ActionInfos(actions)); + //widgetModel.propActions().setValue(new ActionInfos(actions)); } } diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java index 5ddb0f613d..101f81431d 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java @@ -68,7 +68,7 @@ public void processWidget(ADLWidget adlWidget) throws Exception { actions.add(action); } - widgetModel.propActions().setValue(new ActionInfos(actions)); + //widgetModel.propActions().setValue(new ActionInfos(actions)); } String label = rdWidget.getLabel(); diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/ShellCommand2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/ShellCommand2Model.java index c70fdfc13c..27f6299d06 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/ShellCommand2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/ShellCommand2Model.java @@ -36,7 +36,7 @@ public void processWidget(ADLWidget adlWidget) throws Exception { } widgetModel.propText().setValue(commandWidget.getLabel()); - widgetModel.propActions().setValue(new ActionInfos(actions)); + //widgetModel.propActions().setValue(new ActionInfos(actions)); } @Override diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ActionsPropertyBinding.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ActionsPropertyBinding.java index a743f0b02c..5516471564 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ActionsPropertyBinding.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ActionsPropertyBinding.java @@ -15,6 +15,8 @@ import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.properties.ActionInfos; import org.csstudio.display.builder.model.properties.ActionsWidgetProperty; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; +import org.csstudio.display.builder.model.properties.PluggableActionsWidgetProperty; import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; import org.phoebus.ui.undo.UndoableActionManager; @@ -27,10 +29,10 @@ */ @SuppressWarnings("nls") public class ActionsPropertyBinding - extends WidgetPropertyBinding + extends WidgetPropertyBinding { /** Update property panel field as model changes */ - private final WidgetPropertyListener model_listener = (p, o, n) -> + private final WidgetPropertyListener model_listener = (p, o, n) -> { jfx_node.setText(widget_property.toString()); }; @@ -61,7 +63,7 @@ public class ActionsPropertyBinding dialog.setHeaderText(buf.toString()); } - final Optional result = dialog.showAndWait(); + final Optional result = dialog.showAndWait(); if (result.isPresent()) { undo.execute(new SetWidgetPropertyAction<>(widget_property, result.get())); @@ -70,7 +72,7 @@ public class ActionsPropertyBinding final String path = widget_property.getPath(); for (Widget w : other) { - final ActionsWidgetProperty other_prop = (ActionsWidgetProperty) w.getProperty(path); + final PluggableActionsWidgetProperty other_prop = (PluggableActionsWidgetProperty) w.getProperty(path); undo.execute(new SetWidgetPropertyAction<>(other_prop, result.get())); } } @@ -84,7 +86,7 @@ public class ActionsPropertyBinding */ public ActionsPropertyBinding(final UndoableActionManager undo, final Button field, - final ActionsWidgetProperty widget_property, + final PluggableActionsWidgetProperty widget_property, final List other) { super(undo, field, widget_property, other); diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java index d5fde2634c..edd9c396de 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java @@ -41,6 +41,7 @@ import org.csstudio.display.builder.model.properties.FontWidgetProperty; import org.csstudio.display.builder.model.properties.MacrosWidgetProperty; import org.csstudio.display.builder.model.properties.PVNameWidgetProperty; +import org.csstudio.display.builder.model.properties.PluggableActionsWidgetProperty; import org.csstudio.display.builder.model.properties.PointsWidgetProperty; import org.csstudio.display.builder.model.properties.RulesWidgetProperty; import org.csstudio.display.builder.model.properties.ScriptsWidgetProperty; @@ -641,9 +642,9 @@ else if (property instanceof MacrosWidgetProperty) field = macros_field; } - else if (property instanceof ActionsWidgetProperty) + else if (property instanceof PluggableActionsWidgetProperty) { - final ActionsWidgetProperty actions_prop = (ActionsWidgetProperty) property; + final PluggableActionsWidgetProperty actions_prop = (PluggableActionsWidgetProperty) property; final Button actions_field = new Button(); actions_field.setMnemonicParsing(false); actions_field.setMaxWidth(Double.MAX_VALUE); diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java index d5fddc0031..74b53623be 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java @@ -7,17 +7,13 @@ *******************************************************************************/ package org.csstudio.display.builder.model; -import static org.csstudio.display.builder.model.ModelPlugin.logger; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propActions; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHeight; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propName; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propRules; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propScripts; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propType; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propWidgetClass; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propWidth; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propX; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propY; +import org.csstudio.display.builder.model.macros.MacroOrPropertyProvider; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; +import org.csstudio.display.builder.model.properties.ScriptInfo; +import org.csstudio.display.builder.model.rules.RuleInfo; +import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; +import org.phoebus.framework.macros.MacroValueProvider; +import org.phoebus.framework.macros.Macros; import java.util.ArrayList; import java.util.Collections; @@ -31,56 +27,63 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.csstudio.display.builder.model.macros.MacroOrPropertyProvider; -import org.csstudio.display.builder.model.properties.ActionInfos; -import org.csstudio.display.builder.model.properties.ScriptInfo; -import org.csstudio.display.builder.model.rules.RuleInfo; -import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; -import org.phoebus.framework.macros.MacroValueProvider; -import org.phoebus.framework.macros.Macros; +import static org.csstudio.display.builder.model.ModelPlugin.logger; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propActions; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHeight; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propName; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propRules; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propScripts; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propType; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propWidgetClass; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propWidth; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propX; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propY; -/** Base class for all widgets. +/** + * Base class for all widgets. * - *

A Widget has properties, supporting read access, subscription - * and for most properties also write access. + *

A Widget has properties, supporting read access, subscription + * and for most properties also write access. * - *

Properties can be accessed in a most generic way based on the - * property name: - *

+ * 

Properties can be accessed in a most generic way based on the + * property name: + *

  *  getPropertyValue("text")
  *  setPropertyValue("text", "Hello")
  *  getPropertyValue("x")
  *  setPropertyValue("x", 60)
  *  
* - *

While this is ideal for access from scripts, - * Java code that deals with a specific widget can access - * properties in the type-safe way: - *

+ * 

While this is ideal for access from scripts, + * Java code that deals with a specific widget can access + * properties in the type-safe way: + *

  *  LabelWidget label;
  *  label.positionX().getValue();
  *  label.positionX().setValue(60);
  *  
* - *

Widgets are part of a hierarchy. - * Their parent is either the {@link DisplayModel} or another - * widget with a {@link ChildrenProperty} + *

Widgets are part of a hierarchy. + * Their parent is either the {@link DisplayModel} or another + * widget with a {@link ChildrenProperty} * - * @author Kay Kasemir + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class Widget -{ - /** Initial version for all Display Builder widgets. +public class Widget { + /** + * Initial version for all Display Builder widgets. * - *

Legacy used 1.0.0 for most widgets, - * so 2.0.0 indicates an update. - * Selected legacy widgets had incremented to a higher version, - * which needs to be handled for each such widget. + *

Legacy used 1.0.0 for most widgets, + * so 2.0.0 indicates an update. + * Selected legacy widgets had incremented to a higher version, + * which needs to be handled for each such widget. */ public static final Version BASE_WIDGET_VERSION = new Version(2, 0, 0); - /** Typical version of legacy BOY display widgets */ + /** + * Typical version of legacy BOY display widgets + */ public static final Version TYPICAL_LEGACY_WIDGET_VERSION = new Version(1, 0, 0); // These user data keys are reserved for internal use @@ -97,38 +100,46 @@ public class Widget // They all start with an underscore to indicate that they // are meant to be private, not to be used as API. - /** Reserved widget user data key for storing the representation. + /** + * Reserved widget user data key for storing the representation. * - *

The WidgetRepresentation for each {@link Widget} - * is stored under this key. + *

The WidgetRepresentation for each {@link Widget} + * is stored under this key. */ public static final String USER_DATA_REPRESENTATION = "_representation"; - /** Reserved user data key for Widget that has 'children', i.e. is a parent, - * to store the toolkit parent item + /** + * Reserved user data key for Widget that has 'children', i.e. is a parent, + * to store the toolkit parent item */ public static final String USER_DATA_TOOLKIT_PARENT = "_toolkit_parent"; - /** Reserved widget user data key for storing the runtime. + /** + * Reserved widget user data key for storing the runtime. * - *

The WidgetRuntime for each {@link Widget} - * is stored under this key. + *

The WidgetRuntime for each {@link Widget} + * is stored under this key. */ public static final String USER_DATA_RUNTIME = "_runtime"; - /** Reserved widget user data key for storing script support. + /** + * Reserved widget user data key for storing script support. * - *

ScriptSupport is attached to the top-level root - * of the widget tree, i.e. the {@link DisplayModel} - * that is obtained by traversing up via {@link EmbeddedDisplayWidget}s - * to the top-level model. + *

ScriptSupport is attached to the top-level root + * of the widget tree, i.e. the {@link DisplayModel} + * that is obtained by traversing up via {@link EmbeddedDisplayWidget}s + * to the top-level model. */ public static final String USER_DATA_SCRIPT_SUPPORT = "_script_support"; - /** Parent widget */ + /** + * Parent widget + */ private volatile Widget parent = null; - /** All properties, ordered by category, then sequence of definition */ + /** + * All properties, ordered by category, then sequence of definition + */ protected final Set> properties; // Design decision: @@ -147,45 +158,54 @@ public class Widget // The API for widget users would remain the same: // getProperties(), getProperty(), getPropertyValue(), setPropertyValue() - /** Map of property names to properties */ + /** + * Map of property names to properties + */ // Map is final, all properties are collected in widget constructor. // Values of properties can change, but the list of properties itself // is thread safe protected final Map> property_map; // Actual properties - private WidgetProperty type; - private WidgetProperty name; - private WidgetProperty widget_class; - private WidgetProperty x; - private WidgetProperty y; - private WidgetProperty width; - private WidgetProperty height; - private WidgetProperty actions; - private WidgetProperty> rules; - private WidgetProperty> scripts; - - /** Map of user data */ + private final WidgetProperty type; + private final WidgetProperty name; + private final WidgetProperty widget_class; + private final WidgetProperty x; + private final WidgetProperty y; + private final WidgetProperty width; + private final WidgetProperty height; + //private WidgetProperty actions; + private final WidgetProperty actions; + private final WidgetProperty> rules; + private final WidgetProperty> scripts; + + /** + * Map of user data + */ protected final Map user_data = new ConcurrentHashMap<>(4); // Reserve room for "representation", "runtime" - /** Loaded without errors? */ + /** + * Loaded without errors? + */ protected volatile Boolean clean; - /** Widget constructor. - * @param type Widget type + /** + * Widget constructor. + * + * @param type Widget type */ - public Widget(final String type) - { + public Widget(final String type) { this(type, 100, 20); } - /** Widget constructor. - * @param type Widget type - * @param default_width Default width - * @param default_height .. and height + /** + * Widget constructor. + * + * @param type Widget type + * @param default_width Default width + * @param default_height .. and height */ - public Widget(final String type, final int default_width, final int default_height) - { + public Widget(final String type, final int default_width, final int default_height) { // Collect properties final List> prelim_properties = new ArrayList<>(); @@ -197,7 +217,7 @@ public Widget(final String type, final int default_width, final int default_heig prelim_properties.add(y = propY.createProperty(this, 0)); prelim_properties.add(width = propWidth.createProperty(this, default_width)); prelim_properties.add(height = propHeight.createProperty(this, default_height)); - prelim_properties.add(actions = propActions.createProperty(this, ActionInfos.EMPTY)); + prelim_properties.add(actions = propActions.createProperty(this, PluggableActionInfos.EMPTY)); prelim_properties.add(rules = propRules.createProperty(this, Collections.emptyList())); prelim_properties.add(scripts = propScripts.createProperty(this, Collections.emptyList())); @@ -226,90 +246,96 @@ public Widget(final String type, final int default_width, final int default_heig Collectors.toMap(WidgetProperty::getName, Function.identity())); } - /** Unique runtime identifier of a widget + /** + * Unique runtime identifier of a widget * - *

At runtime, this ID can be used to construct - * PVs that are unique and specific to this instance - * of a widget. - * Even if the same display is opened multiple times - * within the same JVM, the widget is very likely - * to receive a new, unique identifier. + *

At runtime, this ID can be used to construct + * PVs that are unique and specific to this instance + * of a widget. + * Even if the same display is opened multiple times + * within the same JVM, the widget is very likely + * to receive a new, unique identifier. * - * @return Unique Runtime Identifier for widget + * @return Unique Runtime Identifier for widget */ - public final String getID() - { // Base on ID hash code + public final String getID() { // Base on ID hash code final int id = System.identityHashCode(this); return "WD" + Integer.toHexString(id); } - /** Get widget version - * @return Widget version number + /** + * Get widget version + * + * @return Widget version number */ - public Version getVersion() - { + public Version getVersion() { return BASE_WIDGET_VERSION; } - /** @return Widget Type */ - public final String getType() - { + /** + * @return Widget Type + */ + public final String getType() { return type.getValue(); } - /** @return Widget Name */ - public final String getName() - { + /** + * @return Widget Name + */ + public final String getName() { return name.getValue(); } - /** @return Widget class to use for updating properties that use the class */ - public final String getWidgetClass() - { + /** + * @return Widget class to use for updating properties that use the class + */ + public final String getWidgetClass() { return widget_class.getValue(); } - /** @return Parent widget in Widget tree */ - public final Optional getParent() - { + /** + * @return Parent widget in Widget tree + */ + public final Optional getParent() { return Optional.ofNullable(parent); } - /** Invoked by the parent widget - * @param parent Parent widget + /** + * Invoked by the parent widget + * + * @param parent Parent widget */ - protected void setParent(final Widget parent) - { + protected void setParent(final Widget parent) { if (parent == this) throw new IllegalArgumentException(); this.parent = parent; } - /** Locate display model, i.e. root of widget tree + /** + * Locate display model, i.e. root of widget tree * - *

Note that for embedded displays, this would - * return the embedded model, not the top-level - * model of the window. - * Compare getTopDisplayModel() + *

Note that for embedded displays, this would + * return the embedded model, not the top-level + * model of the window. + * Compare getTopDisplayModel() * - * @return {@link DisplayModel} for widget - * @throws Exception if widget is not part of a model + * @return {@link DisplayModel} for widget + * @throws Exception if widget is not part of a model */ - public final DisplayModel getDisplayModel() throws Exception - { + public final DisplayModel getDisplayModel() throws Exception { final DisplayModel model = checkDisplayModel(); if (model == null) throw new Exception("Missing DisplayModel for " + this); return model; } - /** Locate display model, i.e. root of widget tree + /** + * Locate display model, i.e. root of widget tree * - * @return {@link DisplayModel} for widget or null - * @see #getDisplayModel() version that throws exception + * @return {@link DisplayModel} for widget or null + * @see #getDisplayModel() version that throws exception */ - public final DisplayModel checkDisplayModel() - { + public final DisplayModel checkDisplayModel() { Widget candidate = this; while (candidate.getParent().isPresent()) candidate = candidate.getParent().get(); @@ -318,57 +344,55 @@ public final DisplayModel checkDisplayModel() return null; } - /** Locate top display model. + /** + * Locate top display model. * - *

For embedded displays, getDisplayModel - * only provides the embedded model. - * This method traverses up via the {@link EmbeddedDisplayWidget} - * to the top-level display model. + *

For embedded displays, getDisplayModel + * only provides the embedded model. + * This method traverses up via the {@link EmbeddedDisplayWidget} + * to the top-level display model. * - * @return Top-level {@link DisplayModel} for widget - * @throws Exception if widget is not part of a model + * @return Top-level {@link DisplayModel} for widget + * @throws Exception if widget is not part of a model */ - public final DisplayModel getTopDisplayModel() throws Exception - { + public final DisplayModel getTopDisplayModel() throws Exception { DisplayModel model = getDisplayModel(); - while (true) - { - final Widget embedder = model.getUserData(DisplayModel.USER_DATA_EMBEDDING_WIDGET); - if (embedder == null) - return model; - model = embedder.getTopDisplayModel(); - } + while (true) { + final Widget embedder = model.getUserData(DisplayModel.USER_DATA_EMBEDDING_WIDGET); + if (embedder == null) + return model; + model = embedder.getTopDisplayModel(); + } } - /** Set result of widget configurator - * @param configurator WidgetConfigurator + /** + * Set result of widget configurator + * + * @param configurator WidgetConfigurator */ - public final void setConfiguratorResult(final WidgetConfigurator configurator) - { + public final void setConfiguratorResult(final WidgetConfigurator configurator) { if (this.clean != null) throw new RuntimeException("Cannot change cleanliness of Widget"); // Only set it if not clean; DisplayModel needs to update it when all the children are loaded - if (configurator.isClean() == false) + if (!configurator.isClean()) this.clean = Boolean.valueOf(configurator.isClean()); } - /** @return true if this widget was loaded without errors, - * false if there were errors + /** + * @return true if this widget was loaded without errors, + * false if there were errors */ - public boolean isClean() - { + public boolean isClean() { Boolean safe = clean; - if (safe != null && ! safe.booleanValue()) + if (safe != null && !safe.booleanValue()) return false; java.util.Optional> child_dm_prop = checkProperty(EmbeddedDisplayWidget.runtimeModel.getName()); - if (child_dm_prop.isPresent()) - { + if (child_dm_prop.isPresent()) { final DisplayModel child_dm = child_dm_prop.get().getValue(); - if (child_dm != null && child_dm.isClean() == false) - { + if (child_dm != null && !child_dm.isClean()) { clean = Boolean.valueOf(false); return false; } @@ -377,15 +401,15 @@ public boolean isClean() return true; } - /** Called on construction to define widget's properties. + /** + * Called on construction to define widget's properties. * - *

Mandatory properties have already been defined. - * Derived class overrides to add its own properties. + *

Mandatory properties have already been defined. + * Derived class overrides to add its own properties. * - * @param properties List to which properties must be added + * @param properties List to which properties must be added */ - protected void defineProperties(final List> properties) - { + protected void defineProperties(final List> properties) { // Derived class should invoke // super.defineProperties(properties) // and may then add its own properties. @@ -396,424 +420,423 @@ protected void defineProperties(final List> properties) // but are useful in IDE when dealing with // known widget type - /** @return 'name' property */ - public final WidgetProperty propName() - { + /** + * @return 'name' property + */ + public final WidgetProperty propName() { return name; } - /** @return 'class' property */ - public final WidgetProperty propClass() - { + /** + * @return 'class' property + */ + public final WidgetProperty propClass() { return widget_class; } - /** @return 'x' property */ - public final WidgetProperty propX() - { + /** + * @return 'x' property + */ + public final WidgetProperty propX() { return x; } - /** @return 'y' property */ - public final WidgetProperty propY() - { + /** + * @return 'y' property + */ + public final WidgetProperty propY() { return y; } - /** @return 'width' property */ - public final WidgetProperty propWidth() - { + /** + * @return 'width' property + */ + public final WidgetProperty propWidth() { return width; } - /** @return 'height' property */ - public final WidgetProperty propHeight() - { + /** + * @return 'height' property + */ + public final WidgetProperty propHeight() { return height; } - /** @return 'actions' property */ + /** + * @return 'actions' property + */ + /* public final WidgetProperty propActions() { return actions; } - /** @return 'rules' property */ - public final WidgetProperty> propRules() - { + */ + public final WidgetProperty propActions() { + return actions; + } + + /** + * @return 'rules' property + */ + public final WidgetProperty> propRules() { return rules; } - /** @return 'scripts' property */ - public final WidgetProperty> propScripts() - { + /** + * @return 'scripts' property + */ + public final WidgetProperty> propScripts() { return scripts; } - /** Obtain configurator. + /** + * Obtain configurator. * - *

While typically using the default {@link WidgetConfigurator}, - * widget may provide a different configurator for reading older - * persisted date. - * @param persisted_version Version of the persisted data. - * @return Widget configurator for that version - * @throws Exception if persisted version cannot be handled + *

While typically using the default {@link WidgetConfigurator}, + * widget may provide a different configurator for reading older + * persisted date. + * + * @param persisted_version Version of the persisted data. + * @return Widget configurator for that version + * @throws Exception if persisted version cannot be handled */ public WidgetConfigurator getConfigurator(final Version persisted_version) - throws Exception - { + throws Exception { // if (persisted_version.getMajor() < 1) // throw new Exception("Can only handle version 1.0.0 and higher"); return new WidgetConfigurator(persisted_version); } - /** Get all properties of the widget. + /** + * Get all properties of the widget. + * + *

Properties are ordered by category and sequence of definition. * - *

Properties are ordered by category and sequence of definition. - * @return Unmodifiable set + * @return Unmodifiable set */ - public final Set> getProperties() - { + public final Set> getProperties() { return properties; } - /** Helper for obtaining the complete property name 'paths' + /** + * Helper for obtaining the complete property name 'paths' * - *

For a scalar property, this method simply returns that property name. + *

For a scalar property, this method simply returns that property name. * - *

For arrays or structures, it returns names for each array resp. structure element. + *

For arrays or structures, it returns names for each array resp. structure element. * - * @param property The widget property. - * @return List of property names + * @param property The widget property. + * @return List of property names */ - public static final List expandPropertyNames(final WidgetProperty property) - { + public static final List expandPropertyNames(final WidgetProperty property) { final List names = new ArrayList<>(); doAddPropertyNames(names, property.getName(), property); return names; } - private static final void doAddPropertyNames(final List names, String path, final WidgetProperty property) - { - if (property instanceof ArrayWidgetProperty) - { - final ArrayWidgetProperty array = (ArrayWidgetProperty) property; - for (int i=0; i names, String path, final WidgetProperty property) { + if (property instanceof ArrayWidgetProperty array) { + for (int i = 0; i < array.size(); ++i) doAddPropertyNames(names, path + "[" + i + "]", array.getElement(i)); - } - else if (property instanceof StructuredWidgetProperty) - { - final StructuredWidgetProperty struct = (StructuredWidgetProperty) property; - for (int i=0; i item = struct.getElement(i); doAddPropertyNames(names, path + "." + item.getName(), item); } - } - else + } else names.add(path); } - /** Check if widget has a given property. + /** + * Check if widget has a given property. * - *

This is called by code that needs to - * test if a widget has a certain property. + *

This is called by code that needs to + * test if a widget has a certain property. * - *

Only checks for direct properties of - * the widget, neither mapping legacy property names - * nor allowing for complex property paths. + *

Only checks for direct properties of + * the widget, neither mapping legacy property names + * nor allowing for complex property paths. * - * @param name Property name - * @param Type of the property's value. - * @return Optional {@link WidgetProperty} - * @see #getProperty(String) + * @param name Property name + * @param Type of the property's value. + * @return Optional {@link WidgetProperty} + * @see #getProperty(String) */ - public final Optional> checkProperty(final String name) - { - @SuppressWarnings("unchecked") - final WidgetProperty property = (WidgetProperty) property_map.get(name); + public final Optional> checkProperty(final String name) { + @SuppressWarnings("unchecked") final WidgetProperty property = (WidgetProperty) property_map.get(name); return Optional.ofNullable(property); } - /** Check if widget has a given property. - * @param property_description Property descriptor - * @param Type of the property's value. - * @return Optional {@link WidgetProperty} - * @see #checkProperty(WidgetPropertyDescriptor) + /** + * Check if widget has a given property. + * + * @param property_description Property descriptor + * @param Type of the property's value. + * @return Optional {@link WidgetProperty} + * @see #checkProperty(WidgetPropertyDescriptor) */ - public final Optional> checkProperty(final WidgetPropertyDescriptor property_description) - { + public final Optional> checkProperty(final WidgetPropertyDescriptor property_description) { return checkProperty(property_description.getName()); } - /** Get widget property. + /** + * Get widget property. * - *

Property access based on property description allows - * type-safe access. + *

Property access based on property description allows + * type-safe access. * - * @param property_description Property description - * @param Type of the property's value. - * @return {@link WidgetProperty} - * @throws IllegalArgumentException if property is unknown - * @see #checkProperty(WidgetPropertyDescriptor) + * @param property_description Property description + * @param Type of the property's value. + * @return {@link WidgetProperty} + * @throws IllegalArgumentException if property is unknown + * @see #checkProperty(WidgetPropertyDescriptor) */ @SuppressWarnings("unchecked") - public final WidgetProperty getProperty(final WidgetPropertyDescriptor property_description) - { + public final WidgetProperty getProperty(final WidgetPropertyDescriptor property_description) { final WidgetProperty property = getProperty(property_description.getName()); - return (WidgetProperty)property; + return (WidgetProperty) property; } - /** Get widget property. + /** + * Get widget property. * - *

Meant for rules, scripts and similar code - * which does not know the exact widget type - * and thus fetches properties by name. + *

Meant for rules, scripts and similar code + * which does not know the exact widget type + * and thus fetches properties by name. * - *

Supports access to complex properties by path name, - * for example "y_axes[1].minimum" to get the minimum - * property of the second Y axis of a plot. + *

Supports access to complex properties by path name, + * for example "y_axes[1].minimum" to get the minimum + * property of the second Y axis of a plot. * - *

To allow use of legacy scripts and rules, - * the widget implementation may override to - * handle deprecated property names. + *

To allow use of legacy scripts and rules, + * the widget implementation may override to + * handle deprecated property names. * - *

Caller presumes that the widget actually - * has the requested property, otherwise throwing Exception. + *

Caller presumes that the widget actually + * has the requested property, otherwise throwing Exception. * - * @param name Property name - * @return {@link WidgetProperty} - * @throws IllegalArgumentException if property is unknown - * @see #checkProperty(String) - * @throws IllegalArgumentException if path includes invalid elements, - * IndexOutOfBoundsException for access to array beyond size + * @param name Property name + * @return {@link WidgetProperty} + * @throws IllegalArgumentException if property is unknown + * @throws IllegalArgumentException if path includes invalid elements, + * IndexOutOfBoundsException for access to array beyond size + * @see #checkProperty(String) */ - public WidgetProperty getProperty(final String name) throws IllegalArgumentException, IndexOutOfBoundsException - { // Is name a path "struct_prop.array_prop[2].element" ? - if (name.indexOf('.') >=0 || name.indexOf('[') >= 0) + public WidgetProperty getProperty(final String name) throws IllegalArgumentException, IndexOutOfBoundsException { // Is name a path "struct_prop.array_prop[2].element" ? + if (name.indexOf('.') >= 0 || name.indexOf('[') >= 0) return getPropertyByPath(name, false); // Plain property name final WidgetProperty property = property_map.get(name); if (property == null) - throw new IllegalArgumentException(toString() + " has no '" + name + "' property"); + throw new IllegalArgumentException(this + " has no '" + name + "' property"); return property; } - /** Get property via path - * @param path_name "struct_prop.array_prop[2].element" - * @param create_elements Create missing array elements? - * @return Property for "element" - * @throws IllegalArgumentException if path includes invalid elements, - * IndexOutOfBoundsException for access to array beyond size + /** + * Get property via path + * + * @param path_name "struct_prop.array_prop[2].element" + * @param create_elements Create missing array elements? + * @return Property for "element" + * @throws IllegalArgumentException if path includes invalid elements, + * IndexOutOfBoundsException for access to array beyond size */ @SuppressWarnings("rawtypes") - public WidgetProperty getPropertyByPath(final String path_name, final boolean create_elements) throws IllegalArgumentException, IndexOutOfBoundsException - { + public WidgetProperty getPropertyByPath(final String path_name, final boolean create_elements) throws IllegalArgumentException, IndexOutOfBoundsException { final String[] path = path_name.split("\\."); WidgetProperty property = null; - for (String item : path) - { // Does item refer to array element? + for (String item : path) { // Does item refer to array element? final String name; final int index; final int braces = item.indexOf('['); - if (braces >= 0) - { - if (! item.endsWith("]")) + if (braces >= 0) { + if (!item.endsWith("]")) throw new IllegalArgumentException("Missing ']' for end of array element"); name = item.substring(0, braces); - index = Integer.parseInt(item.substring(braces+1, item.length() - 1)); - } - else - { + index = Integer.parseInt(item.substring(braces + 1, item.length() - 1)); + } else { name = item; index = -1; } // Get property for the 'name'. // For first item, from widget. Later descent into structure. - if (property == null) - { + if (property == null) { property = property_map.get(name); if (property == null) throw new IllegalArgumentException("Cannot locate '" + name + "' for '" + path_name + "'"); - } - else if (property instanceof StructuredWidgetProperty) - property = ((StructuredWidgetProperty)property).getElement(name); + } else if (property instanceof StructuredWidgetProperty) + property = ((StructuredWidgetProperty) property).getElement(name); else throw new IllegalArgumentException("Cannot locate '" + name + "' for '" + path_name + "'"); // Fetch individual array element? if (index >= 0) - if (property instanceof ArrayWidgetProperty) - { - final ArrayWidgetProperty array = (ArrayWidgetProperty)property; + if (property instanceof ArrayWidgetProperty array) { // Add array elements? - if (create_elements) - { + if (create_elements) { while (array.size() <= index) array.addElement(); - } - else - if (array.size() < index) - throw new IndexOutOfBoundsException("'" + name + "' of '" + path_name + - "' has only " + array.size() + " elements"); + } else if (array.size() < index) + throw new IndexOutOfBoundsException("'" + name + "' of '" + path_name + + "' has only " + array.size() + " elements"); property = array.getElement(index); - } - else + } else throw new IllegalArgumentException("'" + name + "' of '" + path_name + "' it not an array"); } return property; } - /** Get widget property value. + /** + * Get widget property value. * - *

Property access based on property description allows - * type-safe access. + *

Property access based on property description allows + * type-safe access. * - * @param property_description Property description - * @param Type of the property's value. - * @return Value of the property - * @throws IllegalArgumentException if property is unknown + * @param property_description Property description + * @param Type of the property's value. + * @return Value of the property + * @throws IllegalArgumentException if property is unknown */ - public final PT getPropertyValue(final WidgetPropertyDescriptor property_description) - { + public final PT getPropertyValue(final WidgetPropertyDescriptor property_description) { return getProperty(property_description).getValue(); } - /** Get widget property value. + /** + * Get widget property value. * - *

Property access based on property name returns generic - * WidgetProperty without known type. - * Data is cast to the receiver type, but that cast may fail - * if actual data type differs. - * - * @param name Property name, may also be path like "struct_prop.array_prop[2].element" - * @param Data is cast to the receiver's type - * @return Value of the property - * @throws IllegalArgumentException if property is unknown - * @throws IndexOutOfBoundsException for array access beyond last element + *

Property access based on property name returns generic + * WidgetProperty without known type. + * Data is cast to the receiver type, but that cast may fail + * if actual data type differs. * + * @param name Property name, may also be path like "struct_prop.array_prop[2].element" + * @param Data is cast to the receiver's type + * @return Value of the property + * @throws IllegalArgumentException if property is unknown + * @throws IndexOutOfBoundsException for array access beyond last element */ @SuppressWarnings("unchecked") - public final TYPE getPropertyValue(final String name) - { + public final TYPE getPropertyValue(final String name) { return (TYPE) getProperty(name).getValue(); } - /** Set widget property value. + /** + * Set widget property value. * - *

Property access based on property description allows - * type-safe access. + *

Property access based on property description allows + * type-safe access. * - * @param property_description Property description - * @param Type of the property's value. - * @param value New value of the property - * @throws IllegalArgumentException if property is unknown + * @param property_description Property description + * @param Type of the property's value. + * @param value New value of the property + * @throws IllegalArgumentException if property is unknown */ public final void setPropertyValue(final WidgetPropertyDescriptor property_description, - final PT value) - { + final PT value) { getProperty(property_description).setValue(value); } - /** Set widget property value. + /** + * Set widget property value. * - *

Property access based on property name returns generic - * WidgetProperty without known type. + *

Property access based on property name returns generic + * WidgetProperty without known type. * - * @param name Property name - * @param value New value of the property - * @throws IllegalArgumentException if property is unknown - * @throws Exception if value is unsuitable for this property + * @param name Property name + * @param value New value of the property + * @throws IllegalArgumentException if property is unknown + * @throws Exception if value is unsuitable for this property */ public final void setPropertyValue(final String name, - final Object value) throws Exception - { + final Object value) throws Exception { getProperty(name).setValueFromObject(value); } - /** Expand macros for this widget and potential child widgets - * @param input Input that should be used to expand macros + /** + * Expand macros for this widget and potential child widgets + * + * @param input Input that should be used to expand macros */ - public void expandMacros(final Macros input) - { + public void expandMacros(final Macros input) { // Plain widget has no macros to expand } - /** Determine effective macros. + /** + * Determine effective macros. * - *

Default implementation requests macros - * from parent. + *

Default implementation requests macros + * from parent. * - *

Macros will be null while - * the widget is loaded until it is included in a model. + *

Macros will be null while + * the widget is loaded until it is included in a model. * - * @return {@link Macros} + * @return {@link Macros} */ - public Macros getEffectiveMacros() - { + public Macros getEffectiveMacros() { final Optional the_parent = getParent(); - if (! the_parent.isPresent()) + if (!the_parent.isPresent()) return null; return the_parent.get().getEffectiveMacros(); } - /** @return Macro provider for effective macros, falling back to properties */ - public MacroValueProvider getMacrosOrProperties() - { + /** + * @return Macro provider for effective macros, falling back to properties + */ + public MacroValueProvider getMacrosOrProperties() { return new MacroOrPropertyProvider(this); } - /** Set user data + /** + * Set user data * - *

User code can attach arbitrary data to a widget. - * This data is _not_ persisted with the model, - * and there is no change notification. + *

User code can attach arbitrary data to a widget. + * This data is _not_ persisted with the model, + * and there is no change notification. * - *

User code should avoid using reserved keys - * which start with an underscore "_...". + *

User code should avoid using reserved keys + * which start with an underscore "_...". * - * @param key Key - * @param data Data + * @param key Key + * @param data Data */ - public final void setUserData(final String key, final Object data) - { + public final void setUserData(final String key, final Object data) { user_data.put(key, data); } - /** @param key Key - * @param Data is cast to the receiver's type - * @return User data associated with key, or null - * @see #setUserData(String, Object) + /** + * @param key Key + * @param Data is cast to the receiver's type + * @return User data associated with key, or null + * @see #setUserData(String, Object) */ @SuppressWarnings("unchecked") - public final TYPE getUserData(final String key) - { - if (key == null) - { // Debug gimmick: + public final TYPE getUserData(final String key) { + if (key == null) { // Debug gimmick: // null is not supported as valid key, // but triggers dump of all user properties logger.info(this + " user data: " + user_data.entrySet()); return null; } final Object data = user_data.get(key); - return (TYPE)data; + return (TYPE) data; } - /** Remove a user data entry - * @param key Key for which to remove user data - * @param User data is cast to the receiver's type - * @return User data associated with key that has been removed, or null + /** + * Remove a user data entry + * + * @param key Key for which to remove user data + * @param User data is cast to the receiver's type + * @return User data associated with key that has been removed, or null */ @SuppressWarnings("unchecked") - public final TYPE clearUserData(final String key) - { - return (TYPE)user_data.remove(key); + public final TYPE clearUserData(final String key) { + return (TYPE) user_data.remove(key); } @Override - public String toString() - { + public String toString() { // Show name's specification, not value, because otherwise // a plain debug printout can trigger macro resolution for the name - return "Widget '" + ((MacroizedWidgetProperty)name).getSpecification() + "' (" + getType() + ")"; + return "Widget '" + ((MacroizedWidgetProperty) name).getSpecification() + "' (" + getType() + ")"; } } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/CommonWidgetProperties.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/CommonWidgetProperties.java index d12e181069..f0ce80039a 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/CommonWidgetProperties.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/CommonWidgetProperties.java @@ -17,6 +17,7 @@ import org.csstudio.display.builder.model.WidgetPropertyCategory; import org.csstudio.display.builder.model.WidgetPropertyDescriptor; import org.csstudio.display.builder.model.rules.RuleInfo; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; import org.csstudio.display.builder.model.widgets.ActionButtonWidget; import org.epics.vtype.Alarm; import org.epics.vtype.Display; @@ -620,6 +621,7 @@ public WidgetProperty createProperty(final Widget widget, final String d newIntegerPropertyDescriptor(WidgetPropertyCategory.WIDGET, "bit", Messages.WidgetProperties_Bit); /** 'actions' property: Actions that user can invoke */ + /* public static final WidgetPropertyDescriptor propActions = new WidgetPropertyDescriptor<>( WidgetPropertyCategory.BEHAVIOR, "actions", Messages.WidgetProperties_Actions) @@ -642,7 +644,31 @@ public WidgetPropertyCategory getCategory() } }; } - }; + };*/ + + public static final WidgetPropertyDescriptor propActions = + new WidgetPropertyDescriptor<>( + WidgetPropertyCategory.BEHAVIOR, "actions", Messages.WidgetProperties_Actions) + { + @Override + public WidgetProperty createProperty(final Widget widget, final PluggableActionInfos actions) + { + return new PluggableActionsWidgetProperty(this, widget, actions) + { + @Override + public WidgetPropertyCategory getCategory() + { + // For action button, show "actions" as top-level property. + // This violates the consistent order of properties, + // but for an action button the actions are THE property + // which should not be listed prominently, not somewhere down the list. + if (widget instanceof ActionButtonWidget) + return WidgetPropertyCategory.WIDGET; + return super.getCategory(); + } + }; + } + }; /** 'scripts' property: Scripts to execute */ public static final WidgetPropertyDescriptor> propScripts = diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionInfos.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionInfos.java new file mode 100644 index 0000000000..de27aaa4e3 --- /dev/null +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionInfos.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2017 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.builder.model.properties; + +import org.csstudio.display.builder.model.Messages; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; + +import java.text.MessageFormat; +import java.util.Collections; +import java.util.List; + +/** + * Information about {@link PluggableActionInfo}s + * + */ +public class PluggableActionInfos +{ + /** Empty widget actions */ + public static final PluggableActionInfos EMPTY = new PluggableActionInfos(Collections.emptyList()); + + final private List actions; + final private boolean executeAsOne; + + /** @param actions List of action infos */ + public PluggableActionInfos(final List actions) + { + this(actions, false); + } + + /** @param actions List of action infos + * @param executeAsOne Execute all in order? + */ + public PluggableActionInfos(final List actions, final boolean executeAsOne) + { + this.actions = Collections.unmodifiableList(actions); + this.executeAsOne = executeAsOne; + } + + /** @return List of actions */ + public List getActions() + { + return actions; + } + + /** @return Should all actions on list be executed as one, or are they separate actions? */ + public boolean isExecutedAsOne() + { + return executeAsOne; + } + + /** @param actions Actions to represent + * @return String representation + */ + public static String toString(final List actions) + { + if (actions.isEmpty()) + return MessageFormat.format(Messages.Actions_N_Fmt, 0); + if (actions.size() == 1) + return actions.get(0).getDescription().isEmpty() ? + MessageFormat.format(Messages.Actions_N_Fmt, 1) : + actions.get(0).getDescription(); + return MessageFormat.format(Messages.Actions_N_Fmt, actions.size()); + } + + @Override + public String toString() + { + return toString(actions); + } +} diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionsWidgetProperty.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionsWidgetProperty.java new file mode 100644 index 0000000000..f1c10c44c4 --- /dev/null +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/PluggableActionsWidgetProperty.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2023 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.builder.model.properties; + +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyDescriptor; +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.spi.PluggableActionInfo; +import org.phoebus.framework.persistence.XMLUtil; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; + +/** + * Widget property that describes actions. + */ +@SuppressWarnings("nls") +public class PluggableActionsWidgetProperty extends WidgetProperty { + + /** + * Constructor + * + * @param descriptor Property descriptor + * @param widget Widget that holds the property and handles listeners + * @param defaultValue Default and initial value + */ + public PluggableActionsWidgetProperty( + final WidgetPropertyDescriptor descriptor, + final Widget widget, + final PluggableActionInfos defaultValue) { + super(descriptor, widget, defaultValue); + } + + /** + * @param value Must be ActionInfos or ActionInfo array(!), not List + */ + @Override + public void setValueFromObject(final Object value) throws Exception { + if (value instanceof PluggableActionInfos) + setValue((PluggableActionInfos) value); + else if (value instanceof PluggableActionInfo[]) + setValue(new PluggableActionInfos(Arrays.asList((PluggableActionInfo[]) value))); + else if ((value instanceof Collection) && + ((Collection) value).isEmpty()) + setValue(PluggableActionInfos.EMPTY); + else + throw new Exception("Need PluggableActionInfos or PluggableActionInfo[], got " + value); + } + + @Override + public void writeToXML(final ModelWriter modelWriter, final XMLStreamWriter writer) throws Exception { + if (value.isExecutedAsOne()) + writer.writeAttribute(XMLTags.EXECUTE_AS_ONE, Boolean.TRUE.toString()); + for (final PluggableActionInfo info : value.getActions()) { + info.writeToXML(modelWriter, writer); + } + } + + @Override + public void readFromXML(final ModelReader modelReader, final Element propertyXml) throws Exception { + final boolean executeAsOne = + Boolean.parseBoolean(propertyXml.getAttribute(XMLTags.EXECUTE_AS_ONE)) || + Boolean.parseBoolean(propertyXml.getAttribute("hook_all")); // Legacy files + + final List actions = new ArrayList<>(); + for (final Element actionXml : XMLUtil.getChildElements(propertyXml, XMLTags.ACTION)) { + final String type = actionXml.getAttribute(XMLTags.TYPE); + + PluggableActionInfo pluggableActionInfo; + // Get all implementations of PluggableActionInfo to check if anyone matches the type id. + ServiceLoader loader = ServiceLoader.load(PluggableActionInfo.class); + Optional> optionalPluggableActionInfo = + loader.stream().filter(p -> p.get().matchesLegacyAction(type)).findFirst(); + if (optionalPluggableActionInfo.isPresent()) { + pluggableActionInfo = optionalPluggableActionInfo.get().get(); + } else { + throw new RuntimeException("No action implementation matching type '" + type + "' found."); + } + + pluggableActionInfo.readFromXML(modelReader, actionXml); + actions.add(pluggableActionInfo); + } + setValue(new PluggableActionInfos(actions, executeAsOne)); + } + + @Override + public String toString() { + return PluggableActionInfos.toString(value.getActions()); + } +} diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/spi/PluggableActionInfo.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/spi/PluggableActionInfo.java new file mode 100644 index 0000000000..81082bd9b2 --- /dev/null +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/spi/PluggableActionInfo.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.csstudio.display.builder.model.spi; + +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.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.w3c.dom.Element; + +import javax.xml.stream.XMLStreamWriter; +import java.util.List; + +public interface PluggableActionInfo { + + /** + * @param actionId Legacy action id, e.g. open_display + * @return true if the input string is implemented by the {@link PluggableActionInfo}. + */ + default boolean matchesLegacyAction(String actionId) { + return false; + } + + /** + * The type of action, which is either a fully qualified class name, or a legacy identifier + * string like 'open_display'. + * + * @return The action 'type'. + */ + String getType(); + + /** + * Image shown in drop-down in editor and runtime. + * + * @return An {@link Image} representing the action. + */ + Image getImage(); + + /** + * @return Default or user-defined description string. + */ + String getDescription(); + + /** + * @param description User-defined string, potentially overriding default. + */ + void setDescription(String description); + + /** + * @return The editor UI for the action + */ + Node getEditor(Widget widget); + + /** + * Reads implementation specific XML. + * + * @param modelReader A {@link ModelReader} + * @param actionXml The {@link Element} holding the <action> tag data. + * @throws Exception On failure + */ + void readFromXML(final ModelReader modelReader, final Element actionXml) throws Exception; + + + /** + * Writes implementation specific XML starting from the <action> node. + * + * @param modelWriter A {@link ModelWriter} + * @param writer A {@link XMLStreamWriter} + * @throws Exception On failure + */ + void writeToXML(final ModelWriter modelWriter, final XMLStreamWriter writer) throws Exception; + + /** + * Used to define action behavior if it depends on key modifiers, e.g. open display in specific target. + * + * @param event The {@link MouseEvent} holding information on potential modifier keys. + */ + default void setModifiers(MouseEvent event) { + } + + void execute(Widget sourceWidget, Object... arguments); + + /** + * @return A {@link List} of {@link MenuItem}s for the widget's context menu. + * Defaults to null. + */ + default List getContextMenuItems(Widget widget) { + return null; + } +} diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java index 12d1493dd2..0ed5c180c1 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java @@ -48,8 +48,8 @@ public static void main(String[] args) throws Exception final Macros macros = new Macros(); macros.add("S", "Test"); macros.add("N", "2"); - button.propActions().setValue(new ActionInfos(Arrays.asList( - new OpenDisplayActionInfo("Display", "other.opi", macros, Target.REPLACE)))); + //button.propActions().setValue(new ActionInfos(Arrays.asList( + // new OpenDisplayActionInfo("Display", "other.opi", macros, Target.REPLACE)))); } model.runtimeChildren().addChild(widget); diff --git a/app/display/navigation/pom.xml b/app/display/navigation/pom.xml index 487f1c9e64..7d310a210b 100644 --- a/app/display/navigation/pom.xml +++ b/app/display/navigation/pom.xml @@ -30,6 +30,11 @@ app-display-model 4.7.4-SNAPSHOT + + org.phoebus + app-display-representation-javafx + 4.7.4-SNAPSHOT + org.junit.jupiter junit-jupiter diff --git a/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java b/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java index 9df2d3f871..4c15157b36 100644 --- a/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java +++ b/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java @@ -4,10 +4,10 @@ import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.persist.ModelReader; -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.PluggableActionInfos; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; import org.csstudio.display.builder.model.util.ModelResourceUtil; +import org.csstudio.display.builder.representation.javafx.actions.OpenDisplayAction; import org.phoebus.framework.macros.MacroHandler; import org.phoebus.framework.macros.MacroOrSystemProvider; import org.phoebus.framework.macros.Macros; @@ -31,12 +31,11 @@ public class ProcessOPI { private final File rootFile; private final Set allLinkedFiles; + /** - * * @param rootFile */ - public ProcessOPI(File rootFile) - { + public ProcessOPI(File rootFile) { this.rootFile = rootFile; this.allLinkedFiles = new HashSet<>(); } @@ -45,8 +44,7 @@ public ProcessOPI(File rootFile) * Gets All the files linked to via the rootFile * This call should be made on a separate thread since it may take some time to process all the linked files */ - public Set process() - { + public Set process() { getExtensionByStringHandling(this.rootFile.getName()).ifPresentOrElse(ext -> { if (!ext.equalsIgnoreCase("bob") && !ext.equalsIgnoreCase("opi")) { throw new UnsupportedOperationException("File extension " + ext + " is not supported. The supported extensions are .bob and .opi."); @@ -59,12 +57,11 @@ public Set process() return this.allLinkedFiles; } - private synchronized void getAllLinkedFiles(File file) - { + private synchronized void getAllLinkedFiles(File file) { System.out.println("Calculating linked files for " + file.getName()); Set linkedFiles = getLinkedFiles(file); linkedFiles.stream().forEach(f -> { - if (allLinkedFiles.contains(f) || f.equals(rootFile)){ + if (allLinkedFiles.contains(f) || f.equals(rootFile)) { // Already handled skip it } else { // Find all the linked files for this file @@ -74,25 +71,24 @@ private synchronized void getAllLinkedFiles(File file) }); } - public static synchronized Set getLinkedFiles(File file) - { + public static synchronized Set getLinkedFiles(File file) { Set result = new HashSet<>(); try { ModelReader reader = new ModelReader(new FileInputStream(file)); DisplayModel model = reader.readModel(); List children = model.getChildren(); - List actionsInfos = new ArrayList<>(); + List actionsInfos = new ArrayList<>(); children.stream().forEach(widget -> { // Find all the action properties - WidgetProperty actions = widget.propActions(); - Set openActions = actions.getValue().getActions().stream().filter(actionInfo -> { - return actionInfo.getType().equals(ActionInfo.ActionType.OPEN_DISPLAY); + WidgetProperty actions = widget.propActions(); + Set openActions = actions.getValue().getActions().stream().filter(actionInfo -> { + return actionInfo.getType().equalsIgnoreCase("open_display"); }).collect(Collectors.toSet()); // Resolve the complete valid path for each of the open display actions openActions.stream().forEach(openAction -> { // Path to resolve, after expanding macros of source widget and action - OpenDisplayActionInfo action = (OpenDisplayActionInfo) openAction; + OpenDisplayAction action = (OpenDisplayAction) openAction; try { // Path to resolve, after expanding macros of action in environment of source widget final Macros macros = action.getMacros(); // Not copying, just using action's macros diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayAction.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayAction.java new file mode 100644 index 0000000000..c1b546127d --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayAction.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.csstudio.display.builder.representation.javafx.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.Messages; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.macros.MacroHandler; +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.spi.PluggableActionInfo; +import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialogActionItem; +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.Optional; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OpenDisplayAction extends PluggableActionBase { + + /** + * Default {@link Target} is new tab. + */ + private Target target = Target.TAB; + + private Macros macros; + + private String pane; + + private String file; + + private static final String OPEN_DISPLAY = "open_display"; + + 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; + } + } + + public OpenDisplayAction() { + this.description = Messages.ActionOpenDisplay; + this.type = OPEN_DISPLAY; + } + + public Target getTarget() { + return target; + } + + public void setTarget(Target target) { + this.target = target; + } + + public Macros getMacros() { + return macros == null ? new Macros() : macros; + } + + public void setMacros(Macros macros) { + this.macros = macros; + } + + public String getPane() { + return pane; + } + + public void setPane(String pane) { + this.pane = pane; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + @Override + public boolean matchesLegacyAction(String actionId) { + return actionId.equalsIgnoreCase(OPEN_DISPLAY) || actionId.equalsIgnoreCase("OPEN_OPI_IN_VIEW"); + } + + @Override + public Node getEditor(Widget widget) { + ResourceBundle resourceBundle = NLS.getMessages(org.csstudio.display.builder.representation.javafx.Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(resourceBundle); + fxmlLoader.setLocation(this.getClass().getResource("OpenDisplayActionDetails.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(Widget.class, PluggableActionInfo.class).newInstance(widget, OpenDisplayAction.this); + } catch (Exception e) { + Logger.getLogger(ActionsDialogActionItem.class.getName()).log(Level.SEVERE, "Failed to construct OpenDisplayActionDetailsController", e); + } + return null; + }); + + try { + return fxmlLoader.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + public void readFromXML(ModelReader modelReader, Element actionXml) throws Exception { + String description = XMLUtil.getChildString(actionXml, XMLTags.DESCRIPTION).orElse(""); + if (!description.isEmpty()) { + this.description = description; + } + + 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.valueOf(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.writeStartElement(XMLTags.ACTION); + writer.writeAttribute(XMLTags.TYPE, OPEN_DISPLAY); + + writeDescriptionToXML(writer); + + writer.writeStartElement(XMLTags.FILE); + writer.writeCharacters(file); + writer.writeEndElement(); + if (!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(); + } + 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 void execute(Widget sourceWidget, Object... arguments) { + + + } + + @Override + public List getContextMenuItems(Widget widget){ + List items = new ArrayList<>(); + String desc; + try + { + desc = MacroHandler.replace(widget.getEffectiveMacros(), description); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot expand macros in action description '" + description + "'", ex); + desc = description; + } + items.add(createMenuItem(widget, desc)); + + // 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,desc + " (" + target + ")")); + } + + return items; + } + + private OpenDisplayAction.Target modeToTargetConvert(int mode) { + switch (mode) { + // 0 - REPLACE + case 0: + return OpenDisplayAction.Target.REPLACE; + // 7 - NEW_WINDOW + // 8 - NEW_SHELL + case 7: + case 8: + return OpenDisplayAction.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: + return OpenDisplayAction.Target.TAB; + } + } + + @Override + public String toString() { + if (getDescription().isEmpty()) + return "Open " + file; + else + return getDescription(); + } + + @Override + public Image getImage(){ + if(this.image == null){ + this.image = ImageCache.getImage(OpenDisplayAction.class, "/icons/open_display.png"); + } + return this.image; + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/OpenDisplayActionDetailsController.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayActionDetailsController.java similarity index 59% rename from app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/OpenDisplayActionDetailsController.java rename to app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayActionDetailsController.java index dc945e00c0..fc64df9877 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/OpenDisplayActionDetailsController.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/OpenDisplayActionDetailsController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 European Spallation Source ERIC. + * Copyright (C) 2023 European Spallation Source ERIC. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -14,36 +14,29 @@ * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * */ -package org.csstudio.display.builder.representation.javafx.actionsdialog; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.properties.ActionInfo; -import org.csstudio.display.builder.model.properties.OpenDisplayActionInfo; -import org.csstudio.display.builder.model.properties.OpenDisplayActionInfo.Target; -import org.csstudio.display.builder.representation.javafx.FilenameSupport; -import org.csstudio.display.builder.representation.javafx.MacrosTable; +package org.csstudio.display.builder.representation.javafx.actions; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; -import javafx.scene.control.Toggle; 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.Widget; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; +import org.csstudio.display.builder.representation.javafx.FilenameSupport; +import org.csstudio.display.builder.representation.javafx.MacrosTable; -/** FXML Controller */ -public class OpenDisplayActionDetailsController implements ActionDetailsController{ +import java.util.logging.Level; +import java.util.logging.Logger; +public class OpenDisplayActionDetailsController { @FXML private RadioButton replaceRadioButton; @FXML @@ -61,90 +54,85 @@ public class OpenDisplayActionDetailsController implements ActionDetailsControll private MacrosTable macrosTable; - private OpenDisplayActionInfo openDisplayActionInfo; + private final OpenDisplayAction openDisplayActionInfo; - private StringProperty paneProperty = new SimpleStringProperty(); - private StringProperty displayPathProperty = new SimpleStringProperty(); - private StringProperty descriptionProperty = new SimpleStringProperty(); + private final StringProperty paneProperty = new SimpleStringProperty(); + private final StringProperty displayPathProperty = new SimpleStringProperty(); + private final StringProperty descriptionProperty = new SimpleStringProperty(); - private OpenDisplayActionInfo.Target target; + private OpenDisplayAction.Target target; - private Widget widget; + private final Widget widget; - /** @param widget Widget - * @param actionInfo ActionInfo + /** + * @param widget Widget + * @param actionInfo ActionInfo */ - public OpenDisplayActionDetailsController(Widget widget, ActionInfo actionInfo){ + public OpenDisplayActionDetailsController(Widget widget, PluggableActionInfo actionInfo) { this.widget = widget; - this.openDisplayActionInfo = (OpenDisplayActionInfo)actionInfo; + this.openDisplayActionInfo = (OpenDisplayAction) actionInfo; } - /** Init */ + /** + * Init + */ @FXML - public void initialize(){ - replaceRadioButton.setUserData(Target.REPLACE); - newTabRadioButton.setUserData(Target.TAB); - newWindowRadioButton.setUserData(Target.WINDOW); + public void initialize() { + 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); - toggleGroup.selectedToggleProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observableValue, Toggle toggle, Toggle t1) { - target = (Target)t1.getUserData(); - } + toggleGroup.selectedToggleProperty().addListener((observableValue, toggle, t1) -> { + target = (OpenDisplayAction.Target) t1.getUserData(); + openDisplayActionInfo.setTarget(target); }); + target = openDisplayActionInfo.getTarget(); /* * Standalone is a deprecated name for Window */ - if (target == Target.STANDALONE) - target = Target.WINDOW; + if (target == OpenDisplayAction.Target.STANDALONE) { + target = OpenDisplayAction.Target.WINDOW; + } + toggleGroup.selectToggle(toggleGroup.getToggles().stream() .filter(t -> t.getUserData().equals(target)).findFirst().get()); descriptionProperty.setValue(openDisplayActionInfo.getDescription()); description.textProperty().bindBidirectional(descriptionProperty); + descriptionProperty.addListener((obs, o, n) -> openDisplayActionInfo.setDescription(n)); paneProperty.setValue(openDisplayActionInfo.getPane()); pane.textProperty().bindBidirectional(paneProperty); pane.disableProperty().bind(newTabRadioButton.selectedProperty().not()); + paneProperty.addListener((obs, o, n) -> openDisplayActionInfo.setPane(n)); displayPathProperty.setValue(openDisplayActionInfo.getFile()); displayPath.textProperty().bindBidirectional(displayPathProperty); + displayPathProperty.addListener((obs, o, n) -> openDisplayActionInfo.setFile(n)); macrosTable = new MacrosTable(openDisplayActionInfo.getMacros()); macrosTablePlaceholder.getChildren().add(macrosTable.getNode()); + macrosTable.addListener(observable -> openDisplayActionInfo.setMacros(macrosTable.getMacros())); GridPane.setHgrow(macrosTable.getNode(), Priority.ALWAYS); VBox.setVgrow(macrosTable.getNode(), Priority.ALWAYS); } - /** Prompt for filename */ + /** + * Prompt for filename + */ @FXML - public void selectDisplayPath(){ + public void selectDisplayPath() { try { - final String path = FilenameSupport.promptForRelativePath(widget, displayPathProperty.get()); - if (path != null){ - displayPathProperty.setValue(path); + final String path = FilenameSupport.promptForRelativePath(widget, openDisplayActionInfo.getFile()); + if (path != null) { + displayPathProperty.set(path); } } catch (Exception e) { Logger.getLogger(OpenDisplayActionDetailsController.class.getName()) .log(Level.WARNING, "Cannot prompt for filename", e); } } - - /** - * - * @return A new {@link ActionInfo} object as the fields in the class are read-only. Values are taken - * from the observables in this controller. - */ - @Override - public ActionInfo getActionInfo(){ - return new OpenDisplayActionInfo( - descriptionProperty.get(), - displayPathProperty.get(), - macrosTable.getMacros(), - target, - paneProperty.get()); - } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/PluggableActionBase.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/PluggableActionBase.java new file mode 100644 index 0000000000..35663f0ddd --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actions/PluggableActionBase.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.csstudio.display.builder.representation.javafx.actions; + +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.control.MenuItem; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.macros.MacroHandler; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.XMLTags; +import org.csstudio.display.builder.model.properties.CommonWidgetProperties; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; +import org.csstudio.display.builder.model.widgets.ActionButtonWidget; +import org.csstudio.display.builder.representation.javafx.widgets.ActionButtonRepresentation; + +import javax.xml.stream.XMLStreamWriter; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class PluggableActionBase implements PluggableActionInfo { + + protected String description; + protected Image image; + protected String type; + + private static final Logger logger = Logger.getLogger(PluggableActionBase.class.getName()); + + @Override + public void setDescription(String description){ + this.description = description; + } + + @Override + public String getDescription(){ + return description; + } + + @Override + public Image getImage(){ + return image; + } + + @Override + public String getType(){ + return type; + } + + /** + * Writes the description tag if non-empty, as it is common for all {@link PluggableActionInfo}s. + * @param writer A {@link XMLStreamWriter} + * @throws Exception upon failure... + */ + protected void writeDescriptionToXML(final XMLStreamWriter writer) throws Exception { + if (!description.isEmpty()) { + writer.writeStartElement(XMLTags.DESCRIPTION); + writer.writeCharacters(description); + writer.writeEndElement(); + } + } + + protected MenuItem createMenuItem(final Widget widget, final String description) { + // Expand macros in action description + String desc; + try + { + desc = MacroHandler.replace(widget.getEffectiveMacros(), description); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot expand macros in action description '" + description + "'", ex); + desc = description; + } + + final ImageView icon = new ImageView(getImage()); + final MenuItem item = new MenuItem(desc, icon); + + final Optional> enabled_prop = widget.checkProperty(CommonWidgetProperties.propEnabled); + if (enabled_prop.isPresent() && !enabled_prop.get().getValue()) + { + item.setDisable(true); + return item; + } + + /* + if (widget instanceof ActionButtonWidget) + { + ActionButtonRepresentation button = (ActionButtonRepresentation) widget.getUserData(Widget.USER_DATA_REPRESENTATION); + if (button != null) { + item.setOnAction(event -> button.handleContextMenuAction(info)); + return item; + } + } + + */ + return item; + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialog.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialog.java index c1d2c5f5e8..d5b3626219 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialog.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialog.java @@ -25,6 +25,7 @@ import javafx.scene.layout.GridPane; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.properties.ActionInfos; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; import org.csstudio.display.builder.representation.javafx.JFXRepresentation; import org.csstudio.display.builder.representation.javafx.Messages; import org.phoebus.framework.nls.NLS; @@ -37,9 +38,9 @@ import java.util.logging.Logger; /** - * Dialog for editing {@link org.csstudio.display.builder.model.properties.ActionInfo} list + * Dialog for editing {@link org.csstudio.display.builder.model.properties.PluggableActionInfos} list */ -public class ActionsDialog extends Dialog { +public class ActionsDialog extends Dialog { private final Widget widget; @@ -52,7 +53,7 @@ public class ActionsDialog extends Dialog { * @param initialActions Initial list of actions * @param owner Node that started this dialog */ - public ActionsDialog(final Widget widget, final ActionInfos initialActions, final Node owner) { + public ActionsDialog(final Widget widget, final PluggableActionInfos initialActions, final Node owner) { this.widget = widget; setTitle(Messages.ActionsDialog_Title); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogActionItem.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogActionItem.java index ec5b05f695..b57930e93d 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogActionItem.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogActionItem.java @@ -25,6 +25,7 @@ import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.properties.ActionInfo; import org.csstudio.display.builder.model.properties.ActionInfo.ActionType; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; import org.csstudio.display.builder.representation.javafx.Messages; import org.phoebus.framework.nls.NLS; @@ -38,10 +39,11 @@ public class ActionsDialogActionItem { private Node actionInfoEditor; - private ActionDetailsController controller; + //private ActionDetailsController controller; private String description; - private ActionType actionType; - + //private ActionType actionType; + private PluggableActionInfo pluggableActionInfo; + private Widget widget; /** * Constructor. * @@ -50,25 +52,18 @@ public class ActionsDialogActionItem { * @param widget Widget * @param actionInfo ActionInfo */ - public ActionsDialogActionItem(Widget widget, ActionInfo actionInfo){ + public ActionsDialogActionItem(Widget widget, PluggableActionInfo actionInfo){ this.description = actionInfo.getDescription(); - this.actionType = actionInfo.getType(); + this.pluggableActionInfo = actionInfo; + this.widget = widget; + //this.actionType = actionInfo.getType(); + /* ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader fxmlLoader = new FXMLLoader(); fxmlLoader.setResources(resourceBundle); try { switch(actionInfo.getType()){ - case OPEN_DISPLAY: - fxmlLoader.setLocation(this.getClass().getResource("OpenDisplayActionDetails.fxml")); - fxmlLoader.setControllerFactory(clazz -> { - try { - return clazz.getConstructor(Widget.class, ActionInfo.class).newInstance(widget, actionInfo); - } catch (Exception e) { - Logger.getLogger(ActionsDialogActionItem.class.getName()).log(Level.SEVERE, "Failed to construct OpenDisplayActionDetailsController", e); - } - return null; - }); - break; + case WRITE_PV: fxmlLoader.setLocation(this.getClass().getResource("WritePVActionDetails.fxml")); fxmlLoader.setControllerFactory(clazz -> { @@ -125,6 +120,7 @@ public ActionsDialogActionItem(Widget widget, ActionInfo actionInfo){ }); break; } + this.actionInfoEditor = fxmlLoader.load(); this.controller = fxmlLoader.getController(); this.actionInfoEditor.setVisible(false); @@ -132,10 +128,15 @@ public ActionsDialogActionItem(Widget widget, ActionInfo actionInfo){ Logger.getLogger(ActionsDialogActionItem.class.getName()) .log(Level.WARNING, String.format("Unable to create editor for action type \"%s\"", actionInfo.getType()), e); } + + */ } /** @return Node for action info */ public Node getActionInfoEditor(){ + if(actionInfoEditor == null){ + actionInfoEditor = pluggableActionInfo.getEditor(widget); + } return actionInfoEditor; } @@ -144,15 +145,25 @@ public String getDescription(){ return description; } + public PluggableActionInfo getPluggableActionInfo(){ + return pluggableActionInfo; + } + /** @return ActionType */ + /* public ActionType getActionType(){ return actionType; } + */ + /** * @return The {@link ActionInfo} object maintained by the controller for the editor component. */ + /* public ActionInfo getActionInfo(){ return controller.getActionInfo(); } + + */ } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogController.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogController.java index ab40880518..dde9e601b7 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogController.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/actionsdialog/ActionsDialogController.java @@ -20,6 +20,7 @@ import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.stream.Collectors; @@ -27,6 +28,8 @@ import org.csstudio.display.builder.model.properties.ActionInfo; import org.csstudio.display.builder.model.properties.ActionInfo.ActionType; import org.csstudio.display.builder.model.properties.ActionInfos; +import org.csstudio.display.builder.model.properties.PluggableActionInfos; +import org.csstudio.display.builder.model.spi.PluggableActionInfo; import org.csstudio.display.builder.representation.javafx.JFXUtil; import javafx.beans.binding.Bindings; @@ -96,7 +99,7 @@ protected void updateItem(final ActionsDialogActionItem actionsDialogActionItem, else { setText(actionsDialogActionItem.getDescription()); - setGraphic(new ImageView(new Image(actionsDialogActionItem.getActionType().getIconURL().toExternalForm()))); + setGraphic(new ImageView(actionsDialogActionItem.getPluggableActionInfo().getImage())); } } catch (Exception ex) @@ -115,15 +118,17 @@ public void initialize() { upButton.setGraphic(JFXUtil.getIcon("up.png")); downButton.setGraphic(JFXUtil.getIcon("down.png")); - for (ActionType type : ActionType.values()) + ServiceLoader pluggableActionInfos = ServiceLoader.load(PluggableActionInfo.class); + + for (PluggableActionInfo actionInfo : pluggableActionInfos) { - final ImageView icon = new ImageView(new Image(type.getIconURL().toExternalForm())); - final MenuItem item = new MenuItem(type.toString(), icon); + final ImageView icon = new ImageView(actionInfo.getImage()); + final MenuItem item = new MenuItem(actionInfo.toString(), icon); item.setOnAction(event -> { - final ActionInfo action = ActionInfo.createAction(type); + //final ActionInfo action = ActionInfo.createAction(type); ActionsDialogActionItem actionsDialogActionItem = - new ActionsDialogActionItem(widget, action); + new ActionsDialogActionItem(widget, actionInfo); actionList.add(actionsDialogActionItem); actionsListView.setItems(actionList); detailsPane.getChildren().add(actionsDialogActionItem.getActionInfoEditor()); @@ -166,7 +171,7 @@ public void initialize() { * {@link StackPane} and the top most item in the action list is selected. * @param actionInfos ActionInfos */ - public void setActionInfos(ActionInfos actionInfos){ + public void setActionInfos(PluggableActionInfos actionInfos){ if(actionInfos == null || actionInfos.getActions() == null || actionInfos.getActions().isEmpty()){ return; } @@ -183,8 +188,8 @@ public void setActionInfos(ActionInfos actionInfos){ } /** @return ActionInfos */ - public ActionInfos getActionInfos(){ - return new ActionInfos(actionList.stream().map(a -> a.getActionInfo()).collect(Collectors.toList()), + public PluggableActionInfos getActionInfos(){ + return new PluggableActionInfos(actionList.stream().map(a -> a.getPluggableActionInfo()).collect(Collectors.toList()), executeAll.get()); } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java index 3815762371..e17399df8a 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java @@ -7,23 +7,34 @@ *******************************************************************************/ package org.csstudio.display.builder.representation.javafx.widgets; -import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger; - -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.geometry.Pos; +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.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.text.TextAlignment; +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.PluggableActionInfos; 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.spi.PluggableActionInfo; import org.csstudio.display.builder.model.widgets.ActionButtonWidget; import org.csstudio.display.builder.representation.javafx.Cursors; import org.csstudio.display.builder.representation.javafx.JFXUtil; @@ -35,33 +46,20 @@ import org.phoebus.ui.vtype.FormatOption; import org.phoebus.ui.vtype.FormatOptionHandler; -import javafx.application.Platform; -import javafx.geometry.Dimension2D; -import javafx.geometry.Pos; -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.CornerRadii; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.scene.text.TextAlignment; -import javafx.scene.transform.Rotate; -import javafx.scene.transform.Translate; +import java.text.MessageFormat; +import java.util.List; +import java.util.logging.Level; -/** Creates JavaFX item for model widget - * @author Megan Grodowitz - * @author Kay Kasemir +import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger; + +/** + * Creates JavaFX item for model widget + * + * @author Megan Grodowitz + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class ActionButtonRepresentation extends RegionBaseRepresentation -{ +public class ActionButtonRepresentation extends RegionBaseRepresentation { // Uses a Button if there is only one action, // otherwise a MenuButton so that user can select the specific action. // @@ -84,11 +82,12 @@ public class ActionButtonRepresentation extends RegionBaseRepresentationUsed to optimize: - * If there never was a rotation, don't even _clear()_ it - * to keep the Node's nodeTransformation == null + *

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; @@ -98,76 +97,74 @@ public class ActionButtonRepresentation extends RegionBaseRepresentationIf not, we don't have to disable the button if the PV is readonly and/or disconnected */ private volatile boolean is_writePV = false; - private volatile boolean is_openDisplay = 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 volatile Pos pos; + @Override - protected boolean isFilteringEditModeClicks() - { + protected boolean isFilteringEditModeClicks() { return true; } @Override - public Pane createJFXNode() throws Exception - { + public Pane createJFXNode() throws Exception { updateColors(); base = makeBaseButton(); - pane = new Pane(); + Pane 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) - { + /** + * @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) - boolean middle_click = event.isMiddleButtonDown() && is_openDisplay && !is_writePV; - if (event.isShortcutDown() || middle_click) - target_modifier = Optional.of(OpenDisplayActionInfo.Target.TAB); - else if (event.isShiftDown()) - target_modifier = Optional.of(OpenDisplayActionInfo.Target.WINDOW); - else - target_modifier = Optional.empty(); + for (final PluggableActionInfo action : model_widget.propActions().getValue().getActions()) { + action.setModifiers(event); + } - // 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() }); + StringBuilder stringBuilder = new StringBuilder(); + if (event.isShiftDown()) { + stringBuilder.append("shift, "); + } + if (event.isShortcutDown()) { + stringBuilder.append("shortcut, "); + } + if (event.isMiddleButtonDown()) { + stringBuilder.append("middle button"); + } + + if (event.isShortcutDown() || event.isMiddleButtonDown() || event.isShiftDown()) { + logger.log(Level.FINE, "{0} modifiers: {1}", new Object[]{model_widget, stringBuilder.toString()}); base.arm(); } + + } - /** Create base, either single-action button - * or menu for selecting one out of N actions + /** + * 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(); + private ButtonBase makeBaseButton() { + final PluggableActionInfos actions = model_widget.propActions().getValue(); final ButtonBase result; boolean has_non_writePVAction = false; + /* for (final ActionInfo action: actions.getActions()) { if (action instanceof WritePVActionInfo) @@ -178,26 +175,24 @@ private ButtonBase makeBaseButton() is_openDisplay = true; } - if (actions.isExecutedAsOne() || actions.getActions().size() < 2) - { + */ + + if (actions.isExecutedAsOne() || actions.getActions().size() < 2) { final Button button = new Button(); - button.setOnAction(event -> confirm(() -> handleActions(actions.getActions()))); + button.setOnAction(event -> confirm(() -> handleActions(actions.getActions()))); result = button; - } - else - { + } else { // If there is at least one non-WritePVActionInfo then is_writePV should be false - is_writePV = ! has_non_writePVAction; + is_writePV = !has_non_writePVAction; final MenuButton button = new MenuButton(); - for (final ActionInfo action : actions.getActions()) - { + for (final PluggableActionInfo action : actions.getActions()) { final MenuItem item = new MenuItem(makeActionText(action), - new ImageView(new Image(action.getType().getIconURL().toExternalForm())) - ); + new ImageView(action.getImage()) + ); item.getStyleClass().add("action_button_item"); - item.setOnAction(event -> confirm(() -> handleAction(action))); + item.setOnAction(event -> confirm(() -> handleAction(action))); button.getItems().add(item); } result = button; @@ -206,7 +201,7 @@ private ButtonBase makeBaseButton() result.setStyle(background); // In edit mode, show dashed border for transparent/invisible widget - if (toolkit.isEditMode() && model_widget.propTransparent().getValue()) + if (toolkit.isEditMode() && model_widget.propTransparent().getValue()) result.setBorder(new Border(new BorderStroke(Color.BLACK, GroupRepresentation.EDIT_NONE_DASHED, CornerRadii.EMPTY, GroupRepresentation.EDIT_NONE_BORDER))); result.getStyleClass().add("action_button"); result.setMnemonicParsing(false); @@ -217,7 +212,7 @@ private ButtonBase makeBaseButton() // Monitor keys that modify the OpenDisplayActionInfo.Target. // Use filter to capture event that's otherwise already handled. - if (! toolkit.isEditMode()) + if (!toolkit.isEditMode()) result.addEventFilter(MouseEvent.MOUSE_PRESSED, this::checkModifiers); // Need to attach TT to the specific button, not the common jfx_node Pane @@ -229,13 +224,13 @@ private ButtonBase makeBaseButton() return result; } - /** Called by ContextMenuSupport when an action menu is selected - * @param action Action to perform + /** + * Called by ContextMenuSupport when an action menu is selected + * + * @param action Action to perform */ - public void handleContextMenuAction(ActionInfo action) - { - if (action instanceof WritePVActionInfo && ! writable) - { + public void handleContextMenuAction(PluggableActionInfo action) { + if (action instanceof WritePVActionInfo && !writable) { logger.log(Level.FINE, "{0} ignoring WritePVActionInfo because of readonly PV", model_widget); return; } @@ -243,99 +238,105 @@ public void handleContextMenuAction(ActionInfo action) confirm(() -> toolkit.fireAction(model_widget, action)); } - private void confirm(final Runnable action) - { + private void confirm(final Runnable action) { Platform.runLater(() -> { // If confirmation is requested.. - if (model_widget.propConfirmDialog().getValue()) - { + 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 (password.length() > 0) { if (toolkit.showPasswordDialog(model_widget, message, password) == null) return; - } - else - if (! toolkit.showConfirmationDialog(model_widget, message)) - return; + } else if (!toolkit.showConfirmationDialog(model_widget, message)) + return; } action.run(); }); } - /** @return Should 'label' show the PV's current value? */ - private boolean isLabelValue() - { - final StringWidgetProperty text_prop = (StringWidgetProperty)model_widget.propText(); + /** + * @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() - { + 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(); + final StringWidgetProperty text_prop = (StringWidgetProperty) model_widget.propText(); if (isLabelValue()) return FormatOptionHandler.format(model_widget.runtimePropValue().getValue(), FormatOption.DEFAULT, -1, true); - else if ("$(actions)".equals(text_prop.getSpecification())) - { - final List actions = model_widget.propActions().getValue().getActions(); + 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 (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 + } else return text_prop.getValue(); } - private String makeActionText(final ActionInfo action) - { + private String makeActionText(final ActionInfo action) { String action_str = action.getDescription(); if (action_str.isEmpty()) action_str = action.toString(); String expanded; - try - { + 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; } - catch (final Exception ex) - { + return expanded; + } + + private String makeActionText(final PluggableActionInfo 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) + /** + * @param actions Actions that the user invoked + */ + private void handleActions(final List actions) { + for (PluggableActionInfo action : actions) handleAction(action); } - /** @param action Action that the user invoked */ - private void handleAction(ActionInfo action) - { + /** + * @param action Action that the user invoked + */ + private void handleAction(PluggableActionInfo action) { // Keyboard presses are not suppressed so check if the widget is enabled - if (! enabled) + 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); @@ -347,12 +348,13 @@ private void handleAction(ActionInfo action) 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() - { + protected void registerListeners() { updateColors(); super.registerListeners(); pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), @@ -373,16 +375,15 @@ protected void registerListeners() model_widget.propVerticalAlignment().addUntypedPropertyListener(buttonChangedListener); model_widget.propActions().addUntypedPropertyListener(buttonChangedListener); - if (! toolkit.isEditMode() && isLabelValue()) + if (!toolkit.isEditMode() && isLabelValue()) model_widget.runtimePropValue().addUntypedPropertyListener(representationChangedListener); enablementChanged(null, null, null); } @Override - protected void unregisterListeners() - { - if (! toolkit.isEditMode() && isLabelValue()) + protected void unregisterListeners() { + if (!toolkit.isEditMode() && isLabelValue()) model_widget.runtimePropValue().removePropertyListener(representationChangedListener); model_widget.propWidth().removePropertyListener(representationChangedListener); model_widget.propHeight().removePropertyListener(representationChangedListener); @@ -401,34 +402,36 @@ protected void unregisterListeners() } @Override - protected void attachTooltip() - { + 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) - { - pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + /** + * Complete button needs to be updated + */ + private void buttonChanged(final WidgetProperty property, final Object old_value, final Object new_value) { + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), model_widget.propVerticalAlignment().getValue()); 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) - { + /** + * 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); } - /** enabled or pv_writable changed */ - private void enablementChanged(final WidgetProperty property, final Boolean old_value, final Boolean new_value) - { - enabled = model_widget.propEnabled().getValue(); + /** + * 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) @@ -438,8 +441,7 @@ private void enablementChanged(final WidgetProperty property, final Boo } - private void updateColors() - { + 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 @@ -449,24 +451,20 @@ private void updateColors() } @Override - public void updateChanges() - { + public void updateChanges() { super.updateChanges(); - if (dirty_actionls.checkAndClear()) - { + if (dirty_actionls.checkAndClear()) { base = makeBaseButton(); jfx_node.getChildren().setAll(base); } - if (dirty_representation.checkAndClear()) - { + 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) - { + 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_"); @@ -476,7 +474,7 @@ public void updateChanges() final RotationStep rotation = model_widget.propRotationStep().getValue(); final int width = model_widget.propWidth().getValue(), - height = model_widget.propHeight().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. @@ -484,43 +482,41 @@ 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; } base.setAlignment(pos); base.setTextAlignment(TextAlignment.values()[model_widget.propHorizontalAlignment().getValue().ordinal()]); } - if (dirty_enablement.checkAndClear()) - { + 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 - if(!toolkit.isEditMode()){ + if (!toolkit.isEditMode()) { jfx_node.setCursor(enabled ? Cursor.HAND : Cursors.NO_WRITE); } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java index 8b89c85d41..a3bf9c02c7 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java @@ -40,8 +40,8 @@ public class GroupRepresentation extends JFXBaseRepresentation + + + + + + + + + + + + + + + + + + + + +