Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI support for custom Python operations #640

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ project(":ui") {
testCompile group: 'org.testfx', name: 'testfx-core', version: '4.0.+'
testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.+'
testRuntime group: 'org.testfx', name: 'openjfx-monocle', version: '1.8.0_20'
compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.7-M2'
}

evaluationDependsOn(':core')
Expand Down
20 changes: 18 additions & 2 deletions core/src/main/java/edu/wpi/grip/core/Palette.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package edu.wpi.grip.core;

import edu.wpi.grip.core.events.OperationAddedEvent;
import edu.wpi.grip.core.events.OperationRemovedEvent;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;

import java.util.Collection;
import java.util.LinkedHashMap;
Expand All @@ -11,7 +14,6 @@

import javax.inject.Singleton;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
Expand All @@ -20,6 +22,7 @@
@Singleton
public class Palette {

@Inject private EventBus eventBus;
private final Map<String, OperationMetaData> operations = new LinkedHashMap<>();

@Subscribe
Expand All @@ -39,7 +42,20 @@ public void onOperationAdded(OperationAddedEvent event) {
* @throws IllegalArgumentException if the key is already in the {@link #operations} map.
*/
private void map(String key, OperationMetaData operation) {
checkArgument(!operations.containsKey(key), "Operation name or alias already exists: " + key);
if (operations.containsKey(key)) {
OperationDescription existing = operations.get(key).getDescription();
if (existing.category() == operation.getDescription().category()
&& existing.category() == OperationDescription.Category.CUSTOM) {
// It's a custom operation that can be changed at runtime, allow it
// (But first remove the existing operation)
operations.remove(key);
eventBus.post(new OperationRemovedEvent(existing));
} else {
// Not a custom operation, this should only happen if someone
// adds a new operation and uses an already-taken name
throw new IllegalArgumentException("Operation name or alias already exists: " + key);
}
}
operations.put(key, operation);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package edu.wpi.grip.core.events;

import edu.wpi.grip.core.OperationDescription;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* An event fired when an operation is removed from the palette.
*/
public class OperationRemovedEvent {

private final OperationDescription removedOperation;

public OperationRemovedEvent(OperationDescription removedOperation) {
this.removedOperation = checkNotNull(removedOperation);
}

public OperationDescription getRemovedOperation() {
return removedOperation;
}
}
105 changes: 29 additions & 76 deletions ui/src/main/java/edu/wpi/grip/ui/CustomOperationsListController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,67 @@

import edu.wpi.grip.core.OperationMetaData;
import edu.wpi.grip.core.events.OperationAddedEvent;
import edu.wpi.grip.core.operations.python.PythonOperationUtils;
import edu.wpi.grip.core.operations.python.PythonScriptFile;
import edu.wpi.grip.core.operations.python.PythonScriptOperation;
import edu.wpi.grip.core.sockets.InputSocket;
import edu.wpi.grip.core.sockets.OutputSocket;
import edu.wpi.grip.ui.annotations.ParametrizedController;
import edu.wpi.grip.ui.python.PythonEditorController;

import com.google.common.eventbus.EventBus;
import com.google.common.io.Files;
import com.google.inject.Inject;

import org.python.core.PyException;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextInputDialog;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

/**
* Controller for the custom operations list.
*/
@ParametrizedController(url = "CustomOperationsList.fxml")
public class CustomOperationsListController extends OperationListController {

@FXML private Button createNewButton;
@Inject private EventBus eventBus;
@Inject private InputSocket.Factory isf;
@Inject private OutputSocket.Factory osf;
private final Pattern namePattern = Pattern.compile("name *= *\"(.*)\" *");

@FXML
@SuppressWarnings("PMD.UnusedPrivateMethod")
private void createNewPythonOperation() {
Dialog<String> dialog = new TextInputDialog();
dialog.getDialogPane().setContent(new TextArea(PythonScriptFile.TEMPLATE));
dialog.setResultConverter(bt -> {
if (bt == ButtonType.OK) {
return ((TextArea) dialog.getDialogPane().getContent()).getText();
}
return null;
});
Optional<String> result = dialog.showAndWait();
if (result.isPresent()) {
String code = result.get();
String[] lines = code.split("\n");
String name = null;
// Find the name in the user code
for (String line : lines) {
Matcher m = namePattern.matcher(line);
if (m.matches()) {
name = m.group(1);
break;
}
}
if (name == null) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setContentText("A name must be specified");
a.showAndWait();
return;
} else if (name.isEmpty() || name.matches("[ \t]+")) {
// Empty names are not allowed
Alert a = new Alert(Alert.AlertType.ERROR);
a.setContentText("Name cannot be empty");
a.showAndWait();
return;
} else if (!name.matches("[a-zA-Z0-9_\\- ]+")) {
// Name can only contain (English) letters, numbers, underscores, dashes, and spaces
Alert a = new Alert(Alert.AlertType.ERROR);
a.setContentText("Name contains illegal characters");
a.showAndWait();
return;
}
File file = new File(PythonOperationUtils.DIRECTORY, name.replaceAll("[\\s]+", "_") + ".py");
if (file.exists()) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setContentText("A file for the custom operation \"" + name + "\" already exists");
a.showAndWait();
return;
}
try {
Files.write(code, file, Charset.defaultCharset());
} catch (IOException e) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setContentText("Could not save custom operation to " + file.getAbsolutePath());
a.showAndWait();
return;
}
try {
PythonEditorController editorController = loadEditor();
Stage stage = new Stage();
stage.setTitle("Python Script Editor");
stage.setScene(new Scene(editorController.getRoot()));
createNewButton.setDisable(true);
try {
stage.showAndWait();
String code = editorController.getScript();
if (code != null) {
PythonScriptFile script = PythonScriptFile.create(code);
eventBus.post(new OperationAddedEvent(new OperationMetaData(
PythonScriptOperation.descriptionFor(script),
() -> new PythonScriptOperation(isf, osf, script)
)));
} catch (PyException e) {
// Malformed script, alert the user
Alert a = new Alert(Alert.AlertType.ERROR);
a.setTitle("Error in python script");
a.setContentText("Error message: " + e.value.__getitem__(0).toString()); // wow
a.showAndWait();
}
} finally {
// make sure the button is re-enabled even if an exception gets thrown
createNewButton.setDisable(false);
}
}

private PythonEditorController loadEditor() {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/edu/wpi/grip/ui/python/PythonEditor.fxml"));
try {
loader.load();
return loader.getController();
} catch (IOException e) {
throw new RuntimeException("Couldn't load python editor", e);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugly

}
}

Expand Down
14 changes: 14 additions & 0 deletions ui/src/main/java/edu/wpi/grip/ui/OperationListController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package edu.wpi.grip.ui;

import edu.wpi.grip.core.OperationDescription;
import edu.wpi.grip.core.OperationMetaData;
import edu.wpi.grip.core.events.OperationAddedEvent;
import edu.wpi.grip.core.events.OperationRemovedEvent;
import edu.wpi.grip.ui.annotations.ParametrizedController;
import edu.wpi.grip.ui.util.ControllerMap;
import edu.wpi.grip.ui.util.SearchUtility;
Expand Down Expand Up @@ -91,6 +93,18 @@ public void onOperationAdded(OperationAddedEvent event) {
}
}

@Subscribe
public void onOperationRemoved(OperationRemovedEvent event) {
OperationDescription removedOperation = event.getRemovedOperation();
if (root.getUserData() == null || removedOperation.category() == root.getUserData()) {
PlatformImpl.runAndWait(() -> operationsMapManager.remove(operationsMapManager.keySet()
.stream()
.filter(c -> c.getOperationDescription().equals(removedOperation))
.findFirst()
.orElse(null)));
}
}

/**
* Remove all operations. Used for tests.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.controlsfx.control.SegmentedButton;

import java.util.function.Consumer;

import javafx.scene.Node;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.google.common.eventbus.Subscribe;

import java.util.List;

import javafx.application.Platform;
import javafx.geometry.Orientation;
import javafx.scene.control.CheckBox;
Expand Down
Loading