opcua-service/main/java/de/opcua/app/ui/ActionButtonHelper.java
2026-05-11 19:40:18 +02:00

419 lines
18 KiB
Java

package de.opcua.app.controller;
import de.opcua.app.model.NodeAction;
import de.opcua.app.model.NodeRow;
import de.opcua.app.service.ActionService;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
/**
* COMPLETELY FIXED: All fields are now fully editable and functional
*/
public class ActionButtonHelper {
private final ActionService actionService;
public ActionButtonHelper(ActionService actionService) {
this.actionService = actionService;
}
public TableColumn<NodeRow, Void> createActionColumn() {
TableColumn<NodeRow, Void> actionCol = new TableColumn<>("Actions");
actionCol.setPrefWidth(120);
actionCol.setCellFactory(param -> new TableCell<>() {
private final Button btnActions = new Button("Actions");
private final HBox container = new HBox(5);
{
btnActions.setMaxWidth(Double.MAX_VALUE);
container.getChildren().add(btnActions);
container.setAlignment(Pos.CENTER);
btnActions.setOnAction(event -> {
NodeRow row = getTableView().getItems().get(getIndex());
if (row != null && row.isSelected()) {
showActionDialog(row);
}
});
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty || getIndex() >= getTableView().getItems().size()) {
setGraphic(null);
} else {
NodeRow row = getTableView().getItems().get(getIndex());
if (row != null && row.isSelected() && row.isReadable()) {
setGraphic(container);
} else {
setGraphic(null);
}
}
}
});
return actionCol;
}
private void showActionDialog(NodeRow nodeRow) {
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setTitle("Manage Actions");
dialog.setHeaderText("Actions for: " + nodeRow.getDisplayName() + "\nNode: " + nodeRow.getNodeId());
VBox mainBox = new VBox(10);
mainBox.setPadding(new Insets(10));
mainBox.setPrefWidth(700);
mainBox.setPrefHeight(400);
ListView<NodeAction> actionList = new ListView<>();
actionList.setPrefHeight(300);
actionList.getItems().addAll(actionService.getActions(nodeRow.getNodeId()));
VBox.setVgrow(actionList, Priority.ALWAYS);
actionList.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(NodeAction action, boolean empty) {
super.updateItem(action, empty);
if (empty || action == null) {
setGraphic(null);
} else {
HBox cell = new HBox(10);
cell.setAlignment(Pos.CENTER_LEFT);
cell.setPadding(new Insets(5));
CheckBox enabled = new CheckBox();
enabled.setSelected(action.isEnabled());
enabled.setOnAction(e -> actionService.setActionEnabled(
action.getNodeId(),
action.getActionName(),
enabled.isSelected()
));
Label label = new Label(action.getActionName() + " (" + action.getTriggerType() + ")");
label.setMinWidth(250);
HBox.setHgrow(label, Priority.ALWAYS);
Button btnEdit = new Button("Edit");
btnEdit.setOnAction(e -> {
editAction(action);
actionList.refresh();
});
Button btnTest = new Button("Test");
btnTest.setOnAction(e -> testAction(action, nodeRow));
Button btnDelete = new Button("Delete");
btnDelete.setOnAction(e -> {
actionService.removeAction(action.getNodeId(), action.getActionName());
actionList.getItems().remove(action);
});
cell.getChildren().addAll(enabled, label, btnEdit, btnTest, btnDelete);
setGraphic(cell);
}
}
});
HBox buttonBox = new HBox(10);
buttonBox.setAlignment(Pos.CENTER_LEFT);
Button addButton = new Button("Add New Action");
addButton.setOnAction(e -> {
NodeAction newAction = createNewAction(nodeRow);
if (newAction != null) {
actionService.addAction(newAction);
actionList.getItems().add(newAction);
}
});
buttonBox.getChildren().add(addButton);
mainBox.getChildren().addAll(actionList, buttonBox);
dialog.getDialogPane().setContent(mainBox);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.CLOSE);
dialog.setResizable(true);
dialog.showAndWait();
}
private NodeAction createNewAction(NodeRow nodeRow) {
Dialog<NodeAction> dialog = new Dialog<>();
dialog.setTitle("Create New Action");
dialog.setHeaderText("Add action for: " + nodeRow.getDisplayName());
dialog.setResizable(true);
// Main container
VBox mainContainer = new VBox(15);
mainContainer.setPadding(new Insets(20));
mainContainer.setPrefWidth(600);
// Name field
Label nameLabel = new Label("Action Name:");
nameLabel.setStyle("-fx-font-weight: bold;");
TextField nameField = new TextField();
nameField.setPromptText("e.g., Temperature Alarm");
nameField.setEditable(true); // ✅ EXPLICITLY EDITABLE
nameField.setDisable(false); // ✅ NOT DISABLED
// Trigger type
Label triggerLabel = new Label("Trigger Type:");
triggerLabel.setStyle("-fx-font-weight: bold;");
ComboBox<NodeAction.TriggerType> triggerCombo = new ComboBox<>();
triggerCombo.getItems().addAll(NodeAction.TriggerType.values());
triggerCombo.setValue(NodeAction.TriggerType.ON_CHANGE);
triggerCombo.setMaxWidth(Double.MAX_VALUE);
triggerCombo.setEditable(false); // ✅ ComboBox should not be text-editable
triggerCombo.setDisable(false); // ✅ But should be selectable
// Trigger value
Label valueLabel = new Label("Trigger Value (for ON_VALUE, ON_GREATER_THAN, ON_LESS_THAN):");
TextField triggerValueField = new TextField();
triggerValueField.setPromptText("e.g., 100");
triggerValueField.setEditable(true);
triggerValueField.setDisable(true); // Initially disabled
// Interval
Label intervalLabel = new Label("Interval in milliseconds (for ON_INTERVAL):");
TextField intervalField = new TextField("1000");
intervalField.setPromptText("e.g., 5000");
intervalField.setEditable(true);
intervalField.setDisable(true); // Initially disabled
// Script area
Label scriptLabel = new Label("JavaScript Code:");
scriptLabel.setStyle("-fx-font-weight: bold;");
TextArea scriptArea = new TextArea();
scriptArea.setPromptText(
"Example JavaScript:\n\n" +
"console.log('Value changed:', currentValue);\n" +
"console.log('Node:', nodeId);\n\n" +
"// Read from OPC UA\n" +
"var temp = opc.read('ns=2;s=Temperature');\n\n" +
"// Write to OPC UA\n" +
"opc.write('ns=2;s=Output', '123');\n\n" +
"// REST API call\n" +
"var response = rest.post(\n" +
" 'https://api.example.com/alert',\n" +
" JSON.stringify({value: currentValue})\n" +
");\n\n" +
"// Store data\n" +
"store.set('lastValue', currentValue);"
);
scriptArea.setPrefRowCount(12);
scriptArea.setWrapText(true);
scriptArea.setEditable(true); // ✅ EXPLICITLY EDITABLE
scriptArea.setDisable(false); // ✅ NOT DISABLED
VBox.setVgrow(scriptArea, Priority.ALWAYS);
// Dynamic field enabling
triggerCombo.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null) return;
boolean needsValue = newVal == NodeAction.TriggerType.ON_VALUE ||
newVal == NodeAction.TriggerType.ON_GREATER_THAN ||
newVal == NodeAction.TriggerType.ON_LESS_THAN;
triggerValueField.setDisable(!needsValue);
boolean needsInterval = newVal == NodeAction.TriggerType.ON_INTERVAL;
intervalField.setDisable(!needsInterval);
// Update script prompt based on trigger
updateScriptPrompt(scriptArea, newVal);
});
// Add all to container
mainContainer.getChildren().addAll(
nameLabel, nameField,
triggerLabel, triggerCombo,
valueLabel, triggerValueField,
intervalLabel, intervalField,
scriptLabel, scriptArea
);
dialog.getDialogPane().setContent(mainContainer);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
// Validation and conversion
dialog.setResultConverter(buttonType -> {
if (buttonType == ButtonType.OK) {
String name = nameField.getText();
if (name == null || name.trim().isEmpty()) {
showAlert("Validation Error", "Name is required", "Please enter a name for the action.");
return null;
}
String script = scriptArea.getText();
if (script == null || script.trim().isEmpty()) {
showAlert("Validation Error", "Script is required", "Please enter JavaScript code.");
return null;
}
NodeAction action = new NodeAction();
action.setNodeId(nodeRow.getNodeId());
action.setActionName(name.trim());
action.setTriggerType(triggerCombo.getValue());
action.setScript(script);
action.setTriggerValue(triggerValueField.getText());
try {
action.setIntervalMs(Integer.parseInt(intervalField.getText()));
} catch (NumberFormatException ex) {
action.setIntervalMs(1000);
}
return action;
}
return null;
});
// Set initial focus
javafx.application.Platform.runLater(() -> nameField.requestFocus());
return dialog.showAndWait().orElse(null);
}
private void editAction(NodeAction action) {
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setTitle("Edit Action");
dialog.setHeaderText("Edit: " + action.getActionName());
dialog.setResizable(true);
VBox mainContainer = new VBox(15);
mainContainer.setPadding(new Insets(20));
mainContainer.setPrefWidth(600);
Label nameLabel = new Label("Action Name:");
nameLabel.setStyle("-fx-font-weight: bold;");
TextField nameField = new TextField(action.getActionName());
nameField.setEditable(true);
nameField.setDisable(false);
Label triggerLabel = new Label("Trigger Type:");
triggerLabel.setStyle("-fx-font-weight: bold;");
ComboBox<NodeAction.TriggerType> triggerCombo = new ComboBox<>();
triggerCombo.getItems().addAll(NodeAction.TriggerType.values());
triggerCombo.setValue(action.getTriggerType());
triggerCombo.setMaxWidth(Double.MAX_VALUE);
triggerCombo.setDisable(false);
Label valueLabel = new Label("Trigger Value:");
TextField triggerValueField = new TextField(action.getTriggerValue() != null ? action.getTriggerValue() : "");
triggerValueField.setEditable(true);
Label intervalLabel = new Label("Interval (ms):");
TextField intervalField = new TextField(String.valueOf(action.getIntervalMs()));
intervalField.setEditable(true);
Label scriptLabel = new Label("JavaScript Code:");
scriptLabel.setStyle("-fx-font-weight: bold;");
TextArea scriptArea = new TextArea(action.getScript());
scriptArea.setPrefRowCount(12);
scriptArea.setWrapText(true);
scriptArea.setEditable(true);
scriptArea.setDisable(false);
VBox.setVgrow(scriptArea, Priority.ALWAYS);
// Dynamic enabling
triggerCombo.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null) return;
boolean needsValue = newVal == NodeAction.TriggerType.ON_VALUE ||
newVal == NodeAction.TriggerType.ON_GREATER_THAN ||
newVal == NodeAction.TriggerType.ON_LESS_THAN;
triggerValueField.setDisable(!needsValue);
boolean needsInterval = newVal == NodeAction.TriggerType.ON_INTERVAL;
intervalField.setDisable(!needsInterval);
});
// Set initial state
NodeAction.TriggerType type = action.getTriggerType();
boolean needsValue = type == NodeAction.TriggerType.ON_VALUE ||
type == NodeAction.TriggerType.ON_GREATER_THAN ||
type == NodeAction.TriggerType.ON_LESS_THAN;
triggerValueField.setDisable(!needsValue);
boolean needsInterval = type == NodeAction.TriggerType.ON_INTERVAL;
intervalField.setDisable(!needsInterval);
mainContainer.getChildren().addAll(
nameLabel, nameField,
triggerLabel, triggerCombo,
valueLabel, triggerValueField,
intervalLabel, intervalField,
scriptLabel, scriptArea
);
dialog.getDialogPane().setContent(mainContainer);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialog.setResultConverter(buttonType -> {
if (buttonType == ButtonType.OK) {
action.setActionName(nameField.getText());
action.setTriggerType(triggerCombo.getValue());
action.setScript(scriptArea.getText());
action.setTriggerValue(triggerValueField.getText());
try {
action.setIntervalMs(Integer.parseInt(intervalField.getText()));
} catch (NumberFormatException ex) {
// Keep existing
}
}
return buttonType;
});
dialog.showAndWait();
}
private void testAction(NodeAction action, NodeRow nodeRow) {
String currentValue = nodeRow.getValue();
if (currentValue == null || currentValue.isEmpty()) {
currentValue = "test-value";
}
actionService.testAction(action.getNodeId(), action.getActionName(), currentValue);
showAlert(
"Action Test",
"Testing: " + action.getActionName(),
"Action triggered with value: " + currentValue + "\n\n" +
"Check the log area below for script output (look for [JS] prefix)."
);
}
private void updateScriptPrompt(TextArea scriptArea, NodeAction.TriggerType type) {
String prompt = switch (type) {
case ON_CHANGE -> "Triggered on any value change\n\nconsole.log('Value changed to:', currentValue);";
case ON_EVEN -> "Triggered when value is even\n\nconsole.log('Even value:', currentValue);";
case ON_ODD -> "Triggered when value is odd\n\nconsole.log('Odd value:', currentValue);";
case ON_GREATER_THAN -> "Triggered when value > threshold\n\nconsole.log('ALARM! Value too high:', currentValue);";
case ON_LESS_THAN -> "Triggered when value < threshold\n\nconsole.log('WARNING! Value too low:', currentValue);";
case ON_VALUE -> "Triggered when value equals specific value\n\nconsole.log('Target value reached:', currentValue);";
case ON_TRUE -> "Triggered when value becomes truthy\n\nconsole.log('Activated:', currentValue);";
case ON_FALSE -> "Triggered when value becomes falsy\n\nconsole.log('Deactivated:', currentValue);";
case ON_INTERVAL -> "Triggered periodically\n\nvar value = opc.read(nodeId);\nconsole.log('Periodic check:', value);";
};
if (scriptArea.getText().isEmpty()) {
scriptArea.setPromptText(prompt);
}
}
private void showAlert(String title, String header, String content) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(header);
alert.setContentText(content);
alert.showAndWait();
}
}