diff --git a/src/main/java/eu/mihosoft/monacofx/AbstractEditorAction.java b/src/main/java/eu/mihosoft/monacofx/AbstractEditorAction.java index a2b41f2..c2ebe0f 100644 --- a/src/main/java/eu/mihosoft/monacofx/AbstractEditorAction.java +++ b/src/main/java/eu/mihosoft/monacofx/AbstractEditorAction.java @@ -1,5 +1,7 @@ package eu.mihosoft.monacofx; +import java.util.Objects; + /** * This is a Call Back class where the action method can be implemented for a custom usage. * Using @see {@link MonacoFX#addContextMenuAction} the implementation of this class can be added to @@ -96,4 +98,19 @@ public String[] getKeyBindings() { public void setKeyBindings(String... keyBindings) { this.keyBindings = keyBindings; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbstractEditorAction that = (AbstractEditorAction) o; + + return Objects.equals(actionId, that.actionId); + } + + @Override + public int hashCode() { + return actionId != null ? actionId.hashCode() : 0; + } } diff --git a/src/main/java/eu/mihosoft/monacofx/CutAction.java b/src/main/java/eu/mihosoft/monacofx/CutAction.java index b7b9079..8a645f5 100644 --- a/src/main/java/eu/mihosoft/monacofx/CutAction.java +++ b/src/main/java/eu/mihosoft/monacofx/CutAction.java @@ -9,9 +9,11 @@ public CutAction() { setContextMenuOrder("3"); setContextMenuGroupId("9_cutcopypaste"); setVisibleOnReadonly(false); - setKeyBindings("monaco.KeyMod.Shift | monaco.KeyCode.Delete", + setKeyBindings( + "monaco.KeyMod.Shift | monaco.KeyCode.Delete", "monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyX"); - setRunScript("clipboardBridge.copy(editorView.getSelection());\n" + setRunScript( + "clipboardBridge.copy(editorView.getSelection());\n" + "document.execCommand('cut');\n" ); } diff --git a/src/main/java/eu/mihosoft/monacofx/MonacoFX.java b/src/main/java/eu/mihosoft/monacofx/MonacoFX.java index 35515e3..2b6f893 100755 --- a/src/main/java/eu/mihosoft/monacofx/MonacoFX.java +++ b/src/main/java/eu/mihosoft/monacofx/MonacoFX.java @@ -27,9 +27,9 @@ import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.value.ChangeListener; import javafx.concurrent.Worker; import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.input.KeyCode; @@ -44,6 +44,9 @@ import netscape.javascript.JSObject; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -64,11 +67,12 @@ public class MonacoFX extends Region { private boolean readOnly; private Worker.State workerState; - private Timeline oneSecondWonder; + + private Set addedActions; public MonacoFX() { view = new WebView(); - + addedActions = Collections.synchronizedSet(new HashSet<>()); getChildren().add(view); engine = view.getEngine(); String url = getClass().getResource(EDITOR_HTML_RESOURCE_LOCATION).toExternalForm(); @@ -113,19 +117,21 @@ public MonacoFX() { } }); engine.load(url); - waitForLoad(); - addClipboardFunctions(); + waitForSucceededWorkerState(); } /** - * wait a bit + * wait for succeeded state of the load worker */ - private void waitForLoad() { - oneSecondWonder = new Timeline(new KeyFrame(Duration.seconds(1), (ActionEvent event) -> { - if ( Worker.State.SUCCEEDED == workerState) { + private void waitForSucceededWorkerState() { + Timeline oneSecondWonder = new Timeline(); + EventHandler actionEventEventHandler = (ActionEvent event) -> { + if (Worker.State.SUCCEEDED == workerState) { oneSecondWonder.stop(); } - })); + }; + KeyFrame keyFrame = new KeyFrame(Duration.seconds(1), actionEventEventHandler); + oneSecondWonder.getKeyFrames().setAll(keyFrame); oneSecondWonder.setCycleCount(15); oneSecondWonder.play(); } @@ -135,10 +141,6 @@ public void reload() { setReadonly(isReadOnly()); } - private void addClipboardFunctions() { - addEventFilter(KeyEvent.KEY_PRESSED, event -> systemClipboardWrapper.handleCopyCutKeyEvent(event, (a) -> getSelectionObject(), readOnly)); - } - private Object getSelectionObject() { return engine.executeScript("editorView.getModel().getValueInRange(editorView.getSelection())"); } @@ -159,20 +161,6 @@ public void requestFocus() { }); } - protected void postConstruct() { - addPasteAction(); - addCutAction(); - } - - private void addPasteAction() { - final PasteAction pasteAction = new PasteAction(); - addContextMenuAction(pasteAction); - } - - private void addCutAction() { - final CutAction cutAction = new CutAction(); - addContextMenuAction(cutAction); - } @Override protected double computePrefWidth(double height) { return view.prefWidth(height); } @@ -208,10 +196,32 @@ public WebEngine getWebEngine() { * @param action {@link eu.mihosoft.monacofx.AbstractEditorAction} call back object as abstract action. */ public void addContextMenuAction(AbstractEditorAction action) { - executeJavaScriptLambda(action, param -> { - doAddContextMenuAction(action); - return null; - }); + if (!readOnly || action.isVisibleOnReadonly() ) { + executeJavaScriptLambda(action, param -> { + if (!addedActions.contains(action)) { + doAddContextMenuAction(action); + } + return null; + }); + } + } + + public void removeContextMenuAction(AbstractEditorAction action) { + if (addedActions.contains(action)) { + removeAction(action); + addedActions.remove(action); + } + } + + public void removeContextMenuActions() { + addedActions.forEach(this::removeAction); + addedActions.clear(); + } + private void removeAction(AbstractEditorAction action) { + String script = String.format("removeAction('%s');", action.getActionId()); + getWebEngine().executeScript(script); + JSObject window = (JSObject) getWebEngine().executeScript("window"); + window.removeMember(action.getName()); } public boolean isReadOnly() { @@ -247,7 +257,7 @@ private void doAddContextMenuAction(AbstractEditorAction action) { try { getWebEngine().executeScript( "editorView.addAction({\n" + - "id: \"" + action.getActionId() + actionName + "\",\n" + + "id: \"" + action.getActionId() + "\",\n" + "label: \"" + action.getLabel() + "\",\n" + "contextMenuGroupId: \"" + action.getContextMenuGroupId() + "\",\n" + "precondition: " + precondition + ",\n" + @@ -259,9 +269,11 @@ private void doAddContextMenuAction(AbstractEditorAction action) { "}\n" + "});" ); + addedActions.add(action); } catch (JSException exception) { LOGGER.log(Level.SEVERE, exception.getMessage()); } + } private void executeJavaScriptLambda(Object parameter , Callback callback) { @@ -269,39 +281,36 @@ private void executeJavaScriptLambda(Object parameter , Callback c if (Worker.State.SUCCEEDED == stateProperty.getValue()) { callback.call(parameter); } else { - ChangeListener stateChangeListener = (o, old, state) -> { - if (Worker.State.SUCCEEDED == state) { - AtomicBoolean jsDone = new AtomicBoolean(false); - AtomicInteger attempts = new AtomicInteger(); - Thread thread = new Thread(() -> { - while (!jsDone.get()) { - // check if JS execution is done. - Platform.runLater(() -> { - JSObject window = (JSObject) engine.executeScript("window"); - Object jsEditorObj = window.call("getEditorView"); - if (jsEditorObj instanceof JSObject) { - callback.call(parameter); - jsDone.set(true); - } - }); - if (attempts.getAndIncrement() > 10) { - throw new RuntimeException( - "Cannot initialize editor (JS execution not complete). Max number of attempts reached." - ); - } - if (!jsDone.get()) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + waitForSucceededWorkerState(); + AtomicBoolean jsDone = new AtomicBoolean(false); + AtomicInteger attempts = new AtomicInteger(); + Thread thread = new Thread(() -> { + while (!jsDone.get()) { + // check if JS execution is done. + Platform.runLater(() -> { + JSObject window = (JSObject) engine.executeScript("window"); + Object jsEditorObj = window.call("getEditorView"); + if (jsEditorObj instanceof JSObject) { + callback.call(parameter); + jsDone.set(true); } }); - thread.start(); + if (attempts.getAndIncrement() > 10) { + throw new RuntimeException( + "Cannot initialize editor (JS execution not complete). Max number of attempts reached." + ); + } + if (!jsDone.get()) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } - }; - stateProperty.addListener(stateChangeListener); + }); + thread.start(); + } } } diff --git a/src/main/java/eu/mihosoft/monacofx/PasteAction.java b/src/main/java/eu/mihosoft/monacofx/PasteAction.java index 04114b3..7bfebf4 100644 --- a/src/main/java/eu/mihosoft/monacofx/PasteAction.java +++ b/src/main/java/eu/mihosoft/monacofx/PasteAction.java @@ -11,7 +11,8 @@ public PasteAction() { setContextMenuOrder("3"); setContextMenuGroupId("9_cutcopypaste"); setVisibleOnReadonly(false); - setKeyBindings("monaco.KeyMod.Shift | monaco.KeyCode.Insert", + setKeyBindings( + "monaco.KeyMod.Shift | monaco.KeyCode.Insert", "monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyV"); setRunScript("let position = editor.getPosition();\n" + "let newPosition = clipboardBridge.paste(editor.getSelection(), position);\n" diff --git a/src/main/java/eu/mihosoft/monacofx/SystemClipboardWrapper.java b/src/main/java/eu/mihosoft/monacofx/SystemClipboardWrapper.java index 258b43f..03fab75 100644 --- a/src/main/java/eu/mihosoft/monacofx/SystemClipboardWrapper.java +++ b/src/main/java/eu/mihosoft/monacofx/SystemClipboardWrapper.java @@ -39,8 +39,8 @@ public class SystemClipboardWrapper { private final KeyCodeCombination KEY_CODE_CTRL_C = new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN); private final KeyCodeCombination KEY_CODE_CTRL_X = new KeyCodeCombination(KeyCode.X, KeyCombination.SHORTCUT_DOWN); - private final KeyCodeCombination KEY_CODE_CTRL_INSERT = new KeyCodeCombination(KeyCode.INSERT, KeyCombination.SHORTCUT_DOWN); - private final KeyCodeCombination KEY_CODE_CTRL_DELETE = new KeyCodeCombination(KeyCode.DELETE, KeyCombination.SHORTCUT_DOWN); + private final KeyCodeCombination KEY_CODE_SHIFT_INSERT = new KeyCodeCombination(KeyCode.INSERT, KeyCombination.SHIFT_DOWN); + private final KeyCodeCombination KEY_CODE_SHIFT_DELETE = new KeyCodeCombination(KeyCode.DELETE, KeyCombination.SHIFT_DOWN); /** * Puts the text into clipboard. @@ -66,13 +66,13 @@ public void handleCopyCutKeyEvent(KeyEvent event, Callback getSele if (event.getEventType().getName().equals("KEY_PRESSED") && KEY_CODE_CTRL_X.match(event) || KEY_CODE_CTRL_C.match(event) - || KEY_CODE_CTRL_DELETE.match(event) - || KEY_CODE_CTRL_INSERT.match(event)) { + || KEY_CODE_SHIFT_DELETE.match(event) + || KEY_CODE_SHIFT_INSERT.match(event)) { Object obj = getSelectionObjectCallBack.call(null); String selectedText = String.valueOf(obj); if ((readOnly && KEY_CODE_CTRL_X.match(event)) - || (readOnly && KEY_CODE_CTRL_INSERT.match(event)) - || (readOnly && KEY_CODE_CTRL_DELETE.match(event)) + || (readOnly && KEY_CODE_SHIFT_INSERT.match(event)) + || (readOnly && KEY_CODE_SHIFT_DELETE.match(event)) || selectedText.isEmpty() ) { event.consume(); diff --git a/src/main/resources/eu/mihosoft/monacofx/monaco-editor/index.html b/src/main/resources/eu/mihosoft/monacofx/monaco-editor/index.html index 7a858b0..cb75422 100644 --- a/src/main/resources/eu/mihosoft/monacofx/monaco-editor/index.html +++ b/src/main/resources/eu/mihosoft/monacofx/monaco-editor/index.html @@ -101,7 +101,7 @@ id: 'editor.action.clipboardCopyAction', label: 'Copy', contextMenuGroupId: '9_cutcopypaste', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Insert, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC ], run: () => { clipboardBridge.copy(editorView.getSelection()); }, @@ -204,6 +204,19 @@ return editorView; } + function removeAction(actionId) { + actionId = "vs.editor.ICodeEditor:1:" + actionId; + let menus = require('vs/platform/actions/common/actions').MenuRegistry._menuItems; + let contextMenuEntry = [...menus].find(entry => entry[0].id === "EditorContext"); + let contextMenuLinks = contextMenuEntry[1]; + let node = contextMenuLinks._first; + do { + if (actionId === node.element?.command?.id) { + contextMenuLinks._remove(node); + } + } while ((node = node.next)); + } +