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 createActionColumn() { TableColumn 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 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 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 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 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 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 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(); } }