Skip to content
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