This is an automated email from the ASF dual-hosted git repository.

tiagobento pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new c31e2744ceb kie-issues#793: The keyboard shortcuts panel doesn't show 
the new DMN Editor keyboard shortcuts (#2279)
c31e2744ceb is described below

commit c31e2744ceb1d622d495987f9bb3c80bbb73beeb
Author: Luiz João Motta <[email protected]>
AuthorDate: Mon May 6 14:01:51 2024 -0300

    kie-issues#793: The keyboard shortcuts panel doesn't show the new DMN 
Editor keyboard shortcuts (#2279)
---
 packages/dmn-editor-envelope/package.json          |   1 +
 .../dmn-editor-envelope/src/DmnEditorFactory.tsx   |   1 +
 packages/dmn-editor-envelope/src/DmnEditorRoot.tsx | 177 ++++++++
 packages/dmn-editor/src/DmnEditor.css              |   1 +
 packages/dmn-editor/src/DmnEditor.tsx              |  12 +-
 .../src/commands/CommandsContextProvider.tsx       |  91 +++++
 packages/dmn-editor/src/diagram/Diagram.tsx        | 411 +------------------
 .../dmn-editor/src/diagram/DiagramCommands.tsx     | 449 +++++++++++++++++++++
 .../envelope/DefaultKeyboardShortcutsService.ts    |   1 +
 pnpm-lock.yaml                                     |   3 +
 10 files changed, 737 insertions(+), 410 deletions(-)

diff --git a/packages/dmn-editor-envelope/package.json 
b/packages/dmn-editor-envelope/package.json
index 026f7e09739..44963a5078a 100644
--- a/packages/dmn-editor-envelope/package.json
+++ b/packages/dmn-editor-envelope/package.json
@@ -27,6 +27,7 @@
     "@kie-tools-core/editor": "workspace:*",
     "@kie-tools-core/envelope": "workspace:*",
     "@kie-tools-core/envelope-bus": "workspace:*",
+    "@kie-tools-core/keyboard-shortcuts": "workspace:*",
     "@kie-tools-core/notifications": "workspace:*",
     "@kie-tools-core/react-hooks": "workspace:*",
     "@kie-tools-core/workspace": "workspace:*",
diff --git a/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx 
b/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
index b5012e4cb2f..411287583be 100644
--- a/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
+++ b/packages/dmn-editor-envelope/src/DmnEditorFactory.tsx
@@ -150,6 +150,7 @@ function DmnEditorRootWrapper({
         onOpenFileFromNormalizedPosixPathRelativeToTheWorkspaceRoot
       }
       workspaceRootAbsolutePosixPath={workspaceRootAbsolutePosixPath}
+      keyboardShortcutsService={envelopeContext?.services.keyboardShortcuts}
     />
   );
 }
diff --git a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx 
b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
index 50907d890a0..3b06bd42875 100644
--- a/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
+++ b/packages/dmn-editor-envelope/src/DmnEditorRoot.tsx
@@ -41,6 +41,7 @@ import {
   imperativePromiseHandle,
   PromiseImperativeHandle,
 } from "@kie-tools-core/react-hooks/dist/useImperativePromiseHandler";
+import { KeyboardShortcutsService } from 
"@kie-tools-core/keyboard-shortcuts/dist/envelope/KeyboardShortcutsService";
 
 export const EXTERNAL_MODELS_SEARCH_GLOB_PATTERN = "**/*.{dmn,pmml}";
 
@@ -60,6 +61,7 @@ export type DmnEditorRootProps = {
   onRequestWorkspaceFileContent: 
WorkspaceChannelApi["kogitoWorkspace_resourceContentRequest"];
   onOpenFileFromNormalizedPosixPathRelativeToTheWorkspaceRoot: 
WorkspaceChannelApi["kogitoWorkspace_openFile"];
   workspaceRootAbsolutePosixPath: string;
+  keyboardShortcutsService: KeyboardShortcutsService | undefined;
 };
 
 export type DmnEditorRootState = {
@@ -70,6 +72,8 @@ export type DmnEditorRootState = {
   externalModelsByNamespace: DmnEditor.ExternalModelsIndex;
   readonly: boolean;
   externalModelsManagerDoneBootstraping: boolean;
+  keyboardShortcutsRegisterIds: number[];
+  keyboardShortcutsRegistred: boolean;
 };
 
 export class DmnEditorRoot extends React.Component<DmnEditorRootProps, 
DmnEditorRootState> {
@@ -89,6 +93,8 @@ export class DmnEditorRoot extends 
React.Component<DmnEditorRootProps, DmnEditor
       openFilenormalizedPosixPathRelativeToTheWorkspaceRoot: undefined,
       readonly: true,
       externalModelsManagerDoneBootstraping: false,
+      keyboardShortcutsRegisterIds: [],
+      keyboardShortcutsRegistred: false,
     };
   }
 
@@ -271,6 +277,177 @@ export class DmnEditorRoot extends 
React.Component<DmnEditorRootProps, DmnEditor
     );
   };
 
+  public componentDidUpdate(
+    prevProps: Readonly<DmnEditorRootProps>,
+    prevState: Readonly<DmnEditorRootState>,
+    snapshot?: any
+  ): void {
+    if (this.props.keyboardShortcutsService === undefined || 
this.state.keyboardShortcutsRegistred === true) {
+      return;
+    }
+
+    const commands = this.dmnEditorRef.current?.getCommands();
+    if (commands === undefined) {
+      return;
+    }
+    const cancelAction = 
this.props.keyboardShortcutsService.registerKeyPress("Escape", "Edit | 
Unselect", async () =>
+      commands.cancelAction()
+    );
+    const deleteSelectionBackspace = 
this.props.keyboardShortcutsService.registerKeyPress(
+      "Backspace",
+      "Edit | Delete selection",
+      async () => {}
+    );
+    const deleteSelectionDelete = 
this.props.keyboardShortcutsService.registerKeyPress(
+      "Delete",
+      "Edit | Delete selection",
+      async () => {}
+    );
+    const selectAll = this.props.keyboardShortcutsService?.registerKeyPress(
+      "A",
+      "Edit | Select/Deselect all",
+      async () => commands.selectAll()
+    );
+    const createGroup = this.props.keyboardShortcutsService?.registerKeyPress(
+      "G",
+      "Edit | Create group wrapping selection",
+      async () => {
+        console.log(" KEY GROUP PRESSED, ", commands);
+        return commands.createGroup();
+      }
+    );
+    const hideFromDrd = 
this.props.keyboardShortcutsService?.registerKeyPress("X", "Edit | Hide from 
DRD", async () =>
+      commands.hideFromDrd()
+    );
+    const copy = 
this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+C", "Edit | Copy 
nodes", async () =>
+      commands.copy()
+    );
+    const cut = 
this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+X", "Edit | Cut 
nodes", async () =>
+      commands.cut()
+    );
+    const paste = 
this.props.keyboardShortcutsService?.registerKeyPress("Ctrl+V", "Edit | Paste 
nodes", async () =>
+      commands.paste()
+    );
+    const togglePropertiesPanel = 
this.props.keyboardShortcutsService?.registerKeyPress(
+      "I",
+      "Misc | Open/Close properties panel",
+      async () => commands.togglePropertiesPanel()
+    );
+    const toggleHierarchyHighlight = 
this.props.keyboardShortcutsService?.registerKeyPress(
+      "H",
+      "Misc | Toggle hierarchy highlights",
+      async () => commands.toggleHierarchyHighlight()
+    );
+    const moveUp = this.props.keyboardShortcutsService.registerKeyPress(
+      "Up",
+      "Move | Move selection up",
+      async () => {}
+    );
+    const moveDown = this.props.keyboardShortcutsService.registerKeyPress(
+      "Down",
+      "Move | Move selection down",
+      async () => {}
+    );
+    const moveLeft = this.props.keyboardShortcutsService.registerKeyPress(
+      "Left",
+      "Move | Move selection left",
+      async () => {}
+    );
+    const moveRight = this.props.keyboardShortcutsService.registerKeyPress(
+      "Right",
+      "Move | Move selection right",
+      async () => {}
+    );
+    const bigMoveUp = this.props.keyboardShortcutsService.registerKeyPress(
+      "Shift + Up",
+      "Move | Move selection up a big distance",
+      async () => {}
+    );
+    const bigMoveDown = this.props.keyboardShortcutsService.registerKeyPress(
+      "Shift + Down",
+      "Move | Move selection down a big distance",
+      async () => {}
+    );
+    const bigMoveLeft = this.props.keyboardShortcutsService.registerKeyPress(
+      "Shift + Left",
+      "Move | Move selection left a big distance",
+      async () => {}
+    );
+    const bigMoveRight = this.props.keyboardShortcutsService.registerKeyPress(
+      "Shift + Right",
+      "Move | Move selection right a big distance",
+      async () => {}
+    );
+    const focusOnBounds = 
this.props.keyboardShortcutsService?.registerKeyPress(
+      "B",
+      "Navigate | Focus on selection",
+      async () => commands.focusOnSelection()
+    );
+    const resetPosition = 
this.props.keyboardShortcutsService?.registerKeyPress(
+      "Space",
+      "Navigate | Reset position to origin",
+      async () => commands.resetPosition()
+    );
+    const pan = this.props.keyboardShortcutsService?.registerKeyDownThenUp(
+      "Alt",
+      "Navigate | Hold and drag to Pan",
+      async () => commands.panDown(),
+      async () => commands.panUp()
+    );
+    const zoom = this.props.keyboardShortcutsService?.registerKeyPress(
+      "Ctrl",
+      "Navigate | Hold and scroll to zoom in/out",
+      async () => {}
+    );
+    const navigateHorizontally = 
this.props.keyboardShortcutsService?.registerKeyPress(
+      "Shift",
+      "Navigate | Hold and scroll to navigate horizontally",
+      async () => {}
+    );
+
+    this.setState((prev) => ({
+      ...prev,
+      keyboardShortcutsRegistred: true,
+      keyboardShortcutsRegisterIds: [
+        bigMoveDown,
+        bigMoveLeft,
+        bigMoveRight,
+        bigMoveUp,
+        cancelAction,
+        copy,
+        createGroup,
+        cut,
+        deleteSelectionBackspace,
+        deleteSelectionDelete,
+        focusOnBounds,
+        hideFromDrd,
+        moveDown,
+        moveLeft,
+        moveRight,
+        moveUp,
+        navigateHorizontally,
+        pan,
+        paste,
+        resetPosition,
+        selectAll,
+        toggleHierarchyHighlight,
+        togglePropertiesPanel,
+        zoom,
+      ],
+    }));
+  }
+
+  public componentWillUnmount() {
+    const keyboardShortcuts = this.dmnEditorRef.current?.getCommands();
+    if (keyboardShortcuts === undefined) {
+      return;
+    }
+
+    this.state.keyboardShortcutsRegisterIds.forEach((id) => {
+      this.props.keyboardShortcutsService?.deregister(id);
+    });
+  }
+
   public render() {
     return (
       <>
diff --git a/packages/dmn-editor/src/DmnEditor.css 
b/packages/dmn-editor/src/DmnEditor.css
index 1011c465840..7187edd7719 100644
--- a/packages/dmn-editor/src/DmnEditor.css
+++ b/packages/dmn-editor/src/DmnEditor.css
@@ -71,6 +71,7 @@
   z-index: 1;
 }
 .kie-dmn-editor--input-data-node {
+  outline: none;
   width: 100%;
   height: 100%;
 }
diff --git a/packages/dmn-editor/src/DmnEditor.tsx 
b/packages/dmn-editor/src/DmnEditor.tsx
index 58b6f73ce4d..571d18ec019 100644
--- a/packages/dmn-editor/src/DmnEditor.tsx
+++ b/packages/dmn-editor/src/DmnEditor.tsx
@@ -57,6 +57,7 @@ import { INITIAL_COMPUTED_CACHE } from 
"./store/computed/initial";
 
 import "@kie-tools/dmn-marshaller/dist/kie-extensions"; // This is here 
because of the KIE Extension for DMN.
 import "./DmnEditor.css"; // Leave it for last, as this overrides some of the 
PF and RF styles.
+import { Commands, CommandsContextProvider, useCommands } from 
"./commands/CommandsContextProvider";
 
 const ON_MODEL_CHANGE_DEBOUNCE_TIME_IN_MS = 500;
 
@@ -65,6 +66,7 @@ const SVG_PADDING = 20;
 export type DmnEditorRef = {
   reset: (mode: DmnLatestModel) => void;
   getDiagramSvg: () => Promise<string | undefined>;
+  getCommands: () => Commands;
 };
 
 export type EvaluationResults = Record<string, any>;
@@ -171,14 +173,13 @@ export const DmnEditorInternal = ({
   const navigationTab = useDmnEditorStore((s) => s.navigation.tab);
   const dmn = useDmnEditorStore((s) => s.dmn);
   const isDiagramEditingInProgress = useDmnEditorStore((s) => 
s.computed(s).isDiagramEditingInProgress());
-
   const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { commandsRef } = useCommands();
 
   const { dmnModelBeforeEditingRef, dmnEditorRootElementRef } = useDmnEditor();
   const { externalModelsByNamespace } = useExternalModels();
 
   // Refs
-
   const diagramRef = useRef<DiagramRef>(null);
   const diagramContainerRef = useRef<HTMLDivElement>(null);
   const beeContainerRef = useRef<HTMLDivElement>(null);
@@ -233,8 +234,9 @@ export const DmnEditorInternal = ({
 
         return new XMLSerializer().serializeToString(svg);
       },
+      getCommands: () => commandsRef.current,
     }),
-    [dmnEditorStoreApi, externalModelsByNamespace]
+    [dmnEditorStoreApi, externalModelsByNamespace, commandsRef]
   );
 
   // Make sure the DMN Editor reacts to props changing.
@@ -407,7 +409,9 @@ export const DmnEditor = React.forwardRef((props: 
DmnEditorProps, ref: React.Ref
       <ErrorBoundary FallbackComponent={DmnEditorErrorFallback} 
onReset={resetState}>
         <DmnEditorExternalModelsContextProvider {...props}>
           <DmnEditorStoreApiContext.Provider value={storeRef.current}>
-            <DmnEditorInternal forwardRef={ref} {...props} />
+            <CommandsContextProvider>
+              <DmnEditorInternal forwardRef={ref} {...props} />
+            </CommandsContextProvider>
           </DmnEditorStoreApiContext.Provider>
         </DmnEditorExternalModelsContextProvider>
       </ErrorBoundary>
diff --git a/packages/dmn-editor/src/commands/CommandsContextProvider.tsx 
b/packages/dmn-editor/src/commands/CommandsContextProvider.tsx
new file mode 100644
index 00000000000..ab664a26388
--- /dev/null
+++ b/packages/dmn-editor/src/commands/CommandsContextProvider.tsx
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as React from "react";
+import { useContext, useRef } from "react";
+
+export interface Commands {
+  hideFromDrd: () => void;
+  toggleHierarchyHighlight: () => void;
+  togglePropertiesPanel: () => void;
+  createGroup: () => void;
+  selectAll: () => void;
+  panDown: () => void;
+  panUp: () => void;
+  paste: () => void;
+  copy: () => void;
+  cut: () => void;
+  cancelAction: () => void;
+  focusOnSelection: () => void;
+  resetPosition: () => void;
+}
+
+const CommandsContext = React.createContext<{
+  commandsRef: React.MutableRefObject<Commands>;
+}>({} as any);
+
+export function useCommands() {
+  return useContext(CommandsContext);
+}
+
+export function CommandsContextProvider(props: React.PropsWithChildren<{}>) {
+  const commandsRef = useRef<Commands>({
+    hideFromDrd: () => {
+      throw new Error("DMN EDITOR: hideFromDrd command not implemented.");
+    },
+    toggleHierarchyHighlight: () => {
+      throw new Error("DMN EDITOR: toggleHierarchyHighlight command not 
implemented.");
+    },
+    togglePropertiesPanel: () => {
+      throw new Error("DMN EDITOR: togglePropertiesPanel command not 
implemented.");
+    },
+    createGroup: () => {
+      throw new Error("DMN EDITOR: createGroup command not implemented.");
+    },
+    selectAll: () => {
+      throw new Error("DMN EDITOR: selectAll command not implemented.");
+    },
+    panDown: () => {
+      throw new Error("DMN EDITOR: panDown command not implemented.");
+    },
+    panUp: () => {
+      throw new Error("DMN EDITOR: panUp command not implemented.");
+    },
+    paste: () => {
+      throw new Error("DMN EDITOR: paste command not implemented.");
+    },
+    copy: () => {
+      throw new Error("DMN EDITOR: copy command not implemented.");
+    },
+    cut: () => {
+      throw new Error("DMN EDITOR: cut command not implemented.");
+    },
+    cancelAction: () => {
+      throw new Error("DMN EDITOR: cancelAction command not implemented.");
+    },
+    focusOnSelection: () => {
+      throw new Error("DMN EDITOR: focusOnSelection command not implemented.");
+    },
+    resetPosition: () => {
+      throw new Error("DMN EDITOR: resetPosition command not implemented.");
+    },
+  });
+
+  return <CommandsContext.Provider value={{ commandsRef 
}}>{props.children}</CommandsContext.Provider>;
+}
diff --git a/packages/dmn-editor/src/diagram/Diagram.tsx 
b/packages/dmn-editor/src/diagram/Diagram.tsx
index bc3393e9ba7..57f591c6f9e 100644
--- a/packages/dmn-editor/src/diagram/Diagram.tsx
+++ b/packages/dmn-editor/src/diagram/Diagram.tsx
@@ -49,28 +49,19 @@ import { useDmnEditor } from "../DmnEditorContext";
 import { AutolayoutButton } from "../autolayout/AutolayoutButton";
 import { getDefaultColumnWidth } from 
"../boxedExpressions/getDefaultColumnWidth";
 import { getDefaultBoxedExpression } from 
"../boxedExpressions/getDefaultBoxedExpression";
-import {
-  DMN_EDITOR_DIAGRAM_CLIPBOARD_MIME_TYPE,
-  DmnEditorDiagramClipboard,
-  buildClipboardFromDiagram,
-  getClipboard,
-} from "../clipboard/Clipboard";
 import {
   ExternalNode,
   MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS,
 } from "../externalNodes/ExternalNodesPanel";
-import { getNewDmnIdRandomizer } from "../idRandomizer/dmnIdRandomizer";
 import { NodeNature, nodeNatures } from "../mutations/NodeNature";
 import { addConnectedNode } from "../mutations/addConnectedNode";
 import { addDecisionToDecisionService } from 
"../mutations/addDecisionToDecisionService";
 import { addEdge } from "../mutations/addEdge";
-import { addOrGetDrd } from "../mutations/addOrGetDrd";
 import { addShape } from "../mutations/addShape";
 import { addStandaloneNode } from "../mutations/addStandaloneNode";
 import { deleteDecisionFromDecisionService } from 
"../mutations/deleteDecisionFromDecisionService";
 import { EdgeDeletionMode, deleteEdge } from "../mutations/deleteEdge";
 import { NodeDeletionMode, canRemoveNodeFromDrdOnly, deleteNode } from 
"../mutations/deleteNode";
-import { repopulateInputDataAndDecisionsOnAllDecisionServices } from 
"../mutations/repopulateInputDataAndDecisionsOnDecisionService";
 import { repositionNode } from "../mutations/repositionNode";
 import { resizeNode } from "../mutations/resizeNode";
 import { updateExpression } from "../mutations/updateExpression";
@@ -99,12 +90,12 @@ import {
 } from "./edges/Edges";
 import { buildHierarchy } from "./graph/graph";
 import {
-  CONTAINER_NODES_DESIRABLE_PADDING,
-  getBounds,
   getDmnBoundsCenterPoint,
   getContainmentRelationship,
   getHandlePosition,
   getNodeTypeFromDmnObject,
+  getBounds,
+  CONTAINER_NODES_DESIRABLE_PADDING,
 } from "./maths/DmnMaths";
 import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from "./nodes/DefaultSizes";
 import { NODE_TYPES } from "./nodes/NodeTypes";
@@ -126,6 +117,7 @@ import {
   getDecisionServicePropertiesRelativeToThisDmn,
 } from "../mutations/addExistingDecisionServiceToDrd";
 import { updateExpressionWidths } from "../mutations/updateExpressionWidths";
+import { DiagramCommands } from "./DiagramCommands";
 
 const isFirefox = typeof (window as any).InstallTrigger !== "undefined"; // 
See 
https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
 
@@ -133,7 +125,7 @@ const PAN_ON_DRAG = [1, 2];
 
 const FIT_VIEW_OPTIONS: RF.FitViewOptions = { maxZoom: 1, minZoom: 0.1, 
duration: 400 };
 
-const DEFAULT_VIEWPORT = { x: 100, y: 100, zoom: 1 };
+export const DEFAULT_VIEWPORT = { x: 100, y: 100, zoom: 1 };
 
 const DELETE_NODE_KEY_CODES = ["Backspace", "Delete"];
 
@@ -167,7 +159,6 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
     const { externalModelsByNamespace } = useExternalModels();
     const snapGrid = useDmnEditorStore((s) => s.diagram.snapGrid);
     const thisDmn = useDmnEditorStore((s) => s.dmn);
-
     const { dmnModelBeforeEditingRef } = useDmnEditor();
 
     // State
@@ -177,7 +168,6 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
     >(undefined);
 
     // Refs
-
     React.useImperativeHandle(
       ref,
       () => ({
@@ -1181,8 +1171,7 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
             <SelectionStatus />
             <Palette pulse={isEmptyStateShowing} />
             <TopRightCornerPanels />
-            <PanWhenAltPressed />
-            <KeyboardShortcuts />
+            <DiagramCommands />
             {!isFirefox && <RF.Background />}
             <RF.Controls fitViewOptions={FIT_VIEW_OPTIONS} 
position={"bottom-right"} />
             <SetConnectionToReactFlowStore />
@@ -1487,393 +1476,3 @@ export function SelectionStatus() {
     </>
   );
 }
-
-export function KeyboardShortcuts(props: {}) {
-  const rfStoreApi = RF.useStoreApi();
-  const dmnEditorStoreApi = useDmnEditorStoreApi();
-  const { externalModelsByNamespace } = useExternalModels();
-
-  const rf = RF.useReactFlow<DmnDiagramNodeData, DmnDiagramEdgeData>();
-
-  // Reset position to origin
-  const space = RF.useKeyPress(["Space"]);
-  useEffect(() => {
-    if (!space) {
-      return;
-    }
-
-    rf.setViewport(DEFAULT_VIEWPORT, { duration: 200 });
-  }, [rf, space]);
-
-  // Focus on node bounds
-  const b = RF.useKeyPress(["b"]);
-  useEffect(() => {
-    if (!b) {
-      return;
-    }
-
-    const selectedNodes = rf.getNodes().filter((s) => s.selected);
-    if (selectedNodes.length <= 0) {
-      return;
-    }
-
-    const bounds = getBounds({
-      nodes: selectedNodes,
-      padding: 100,
-    });
-
-    rf.fitBounds(
-      {
-        x: bounds["@_x"],
-        y: bounds["@_y"],
-        width: bounds["@_width"],
-        height: bounds["@_height"],
-      },
-      { duration: 200 }
-    );
-  }, [b, rf]);
-
-  // Cancel action
-  const esc = RF.useKeyPress(["Escape"]);
-  useEffect(() => {
-    if (!esc) {
-      return;
-    }
-
-    rfStoreApi.setState((rfState) => {
-      if (rfState.connectionNodeId) {
-        console.debug("DMN DIAGRAM: Esc pressed. Cancelling connection.");
-        rfState.cancelConnection();
-        dmnEditorStoreApi.setState((state) => {
-          state.diagram.ongoingConnection = undefined;
-        });
-      } else {
-        (document.activeElement as any)?.blur?.();
-      }
-
-      return rfState;
-    });
-  }, [esc, dmnEditorStoreApi, rfStoreApi]);
-
-  // Cut
-  const cut = RF.useKeyPress(["Meta+x"]);
-  useEffect(() => {
-    if (!cut) {
-      return;
-    }
-    console.debug("DMN DIAGRAM: Cutting selected nodes...");
-
-    const { clipboard, copiedEdgesById, danglingEdgesById, copiedNodesById } = 
buildClipboardFromDiagram(
-      rfStoreApi.getState(),
-      dmnEditorStoreApi.getState()
-    );
-
-    navigator.clipboard.writeText(JSON.stringify(clipboard)).then(() => {
-      dmnEditorStoreApi.setState((state) => {
-        // Delete edges
-        [...copiedEdgesById.values(), 
...danglingEdgesById.values()].forEach((edge) => {
-          deleteEdge({
-            definitions: state.dmn.model.definitions,
-            drdIndex: state.diagram.drdIndex,
-            edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
-            mode: EdgeDeletionMode.FROM_DRG_AND_ALL_DRDS,
-          });
-          state.dispatch(state).diagram.setEdgeStatus(edge.id, {
-            selected: false,
-            draggingWaypoint: false,
-          });
-        });
-
-        // Delete nodes
-        rfStoreApi
-          .getState()
-          .getNodes()
-          .forEach((node: RF.Node<DmnDiagramNodeData>) => {
-            if (copiedNodesById.has(node.id)) {
-              deleteNode({
-                drgEdges: 
state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges,
-                definitions: state.dmn.model.definitions,
-                drdIndex: state.diagram.drdIndex,
-                dmnObjectNamespace: node.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
-                dmnObjectQName: node.data.dmnObjectQName,
-                dmnObjectId: node.data.dmnObject?.["@_id"],
-                nodeNature: nodeNatures[node.type as NodeType],
-                mode: NodeDeletionMode.FROM_DRG_AND_ALL_DRDS,
-                externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace)
-                  .dmns,
-              });
-              state.dispatch(state).diagram.setNodeStatus(node.id, {
-                selected: false,
-                dragging: false,
-                resizing: false,
-              });
-            }
-          });
-      });
-    });
-  }, [cut, dmnEditorStoreApi, rfStoreApi, externalModelsByNamespace]);
-
-  // Copy
-  const copy = RF.useKeyPress(["Meta+c"]);
-  useEffect(() => {
-    if (!copy) {
-      return;
-    }
-
-    console.debug("DMN DIAGRAM: Copying selected nodes...");
-
-    const { clipboard } = buildClipboardFromDiagram(rfStoreApi.getState(), 
dmnEditorStoreApi.getState());
-    navigator.clipboard.writeText(JSON.stringify(clipboard));
-  }, [copy, dmnEditorStoreApi, rfStoreApi]);
-
-  // Paste
-  const paste = RF.useKeyPress(["Meta+v"]);
-  useEffect(() => {
-    if (!paste) {
-      return;
-    }
-
-    console.debug("DMN DIAGRAM: Pasting nodes...");
-
-    navigator.clipboard.readText().then((text) => {
-      const clipboard = getClipboard<DmnEditorDiagramClipboard>(text, 
DMN_EDITOR_DIAGRAM_CLIPBOARD_MIME_TYPE);
-      if (!clipboard) {
-        return;
-      }
-
-      getNewDmnIdRandomizer()
-        .ack({
-          json: clipboard.drgElements,
-          type: "DMN15__tDefinitions",
-          attr: "drgElement",
-        })
-        .ack({
-          json: clipboard.artifacts,
-          type: "DMN15__tDefinitions",
-          attr: "artifact",
-        })
-        .ack({
-          json: clipboard.shapes,
-          type: "DMNDI15__DMNDiagram",
-          attr: "dmndi:DMNDiagramElement",
-          __$$element: "dmndi:DMNShape",
-        })
-        .ack({
-          json: clipboard.edges,
-          type: "DMNDI15__DMNDiagram",
-          attr: "dmndi:DMNDiagramElement",
-          __$$element: "dmndi:DMNEdge",
-        })
-        .ack<any>({
-          // This `any` argument ideally wouldn't be here, but the type of 
DMN's `meta` is not composed with KIE's `meta` in compile-time
-          json: clipboard.widths,
-          type: "KIE__tComponentsWidthsExtension",
-          attr: "kie:ComponentWidths",
-        })
-        .randomize();
-
-      dmnEditorStoreApi.setState((state) => {
-        state.dmn.model.definitions.drgElement ??= [];
-        state.dmn.model.definitions.drgElement.push(...clipboard.drgElements);
-        state.dmn.model.definitions.artifact ??= [];
-        state.dmn.model.definitions.artifact.push(...clipboard.artifacts);
-
-        const { diagramElements, widths } = addOrGetDrd({
-          definitions: state.dmn.model.definitions,
-          drdIndex: state.diagram.drdIndex,
-        });
-        diagramElements.push(...clipboard.shapes.map((s) => ({ ...s, 
__$$element: "dmndi:DMNShape" as const })));
-        diagramElements.push(...clipboard.edges.map((s) => ({ ...s, 
__$$element: "dmndi:DMNEdge" as const })));
-
-        widths.push(...clipboard.widths);
-
-        repopulateInputDataAndDecisionsOnAllDecisionServices({ definitions: 
state.dmn.model.definitions });
-
-        state.diagram._selectedNodes = [...clipboard.drgElements, 
...clipboard.artifacts].map((s) =>
-          buildXmlHref({ id: s["@_id"]! })
-        );
-
-        if (state.diagram._selectedNodes.length === 1) {
-          state.focus.consumableId = 
parseXmlHref(state.diagram._selectedNodes[0]).id;
-        }
-      });
-    });
-  }, [paste, dmnEditorStoreApi]);
-
-  // Select/deselect all
-  const selectAll = RF.useKeyPress(["a", "Meta+a"]);
-  useEffect(() => {
-    if (!selectAll) {
-      return;
-    }
-
-    const allNodeIds = rfStoreApi
-      .getState()
-      .getNodes()
-      .map((s) => s.id);
-
-    const allEdgeIds = rfStoreApi.getState().edges.map((s) => s.id);
-
-    dmnEditorStoreApi.setState((state) => {
-      const allSelectedNodesSet = new Set(state.diagram._selectedNodes);
-      const allSelectedEdgesSet = new Set(state.diagram._selectedEdges);
-
-      // If everything is selected, deselect everything.
-      if (
-        allNodeIds.every((id) => allSelectedNodesSet.has(id) && 
allEdgeIds.every((id) => allSelectedEdgesSet.has(id)))
-      ) {
-        state.diagram._selectedNodes = [];
-        state.diagram._selectedEdges = [];
-      } else {
-        state.diagram._selectedNodes = allNodeIds;
-        state.diagram._selectedEdges = allEdgeIds;
-      }
-    });
-  }, [selectAll, dmnEditorStoreApi, rfStoreApi]);
-
-  // Create group wrapping selection
-  const g = RF.useKeyPress(["g"]);
-  useEffect(() => {
-    if (!g) {
-      return;
-    }
-
-    const selectedNodes = rf.getNodes().filter((s) => s.selected);
-    if (selectedNodes.length <= 0) {
-      return;
-    }
-
-    dmnEditorStoreApi.setState((state) => {
-      if (state.diagram._selectedNodes.length <= 0) {
-        return;
-      }
-
-      const { href: newNodeId } = addStandaloneNode({
-        definitions: state.dmn.model.definitions,
-        drdIndex: state.diagram.drdIndex,
-        newNode: {
-          type: NODE_TYPES.group,
-          bounds: getBounds({
-            nodes: selectedNodes,
-            padding: CONTAINER_NODES_DESIRABLE_PADDING,
-          }),
-        },
-      });
-
-      state.dispatch(state).diagram.setNodeStatus(newNodeId, { selected: true 
});
-    });
-  }, [g, dmnEditorStoreApi, rf]);
-
-  // Toggle hierarchy highlights
-  const h = RF.useKeyPress(["h"]);
-  useEffect(() => {
-    if (!h) {
-      return;
-    }
-
-    dmnEditorStoreApi.setState((state) => {
-      state.diagram.overlays.enableNodeHierarchyHighlight = 
!state.diagram.overlays.enableNodeHierarchyHighlight;
-    });
-  }, [h, dmnEditorStoreApi]);
-
-  // Show Properties panel
-  const i = RF.useKeyPress(["i"]);
-  useEffect(() => {
-    if (!i) {
-      return;
-    }
-
-    dmnEditorStoreApi.setState((state) => {
-      state.diagram.propertiesPanel.isOpen = 
!state.diagram.propertiesPanel.isOpen;
-    });
-  }, [i, dmnEditorStoreApi]);
-
-  // Hide from DRD
-  const x = RF.useKeyPress(["x"]);
-  useEffect(() => {
-    if (!x) {
-      return;
-    }
-
-    const nodesById = rf
-      .getNodes()
-      .reduce((acc, s) => acc.set(s.id, s), new Map<string, 
RF.Node<DmnDiagramNodeData>>());
-
-    dmnEditorStoreApi.setState((state) => {
-      const selectedNodeIds = new Set(state.diagram._selectedNodes);
-      for (const edge of rf.getEdges()) {
-        if (
-          (selectedNodeIds.has(edge.source) &&
-            canRemoveNodeFromDrdOnly({
-              externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns,
-              definitions: state.dmn.model.definitions,
-              drdIndex: state.diagram.drdIndex,
-              dmnObjectNamespace:
-                nodesById.get(edge.source)!.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
-              dmnObjectId: 
nodesById.get(edge.source)!.data.dmnObject?.["@_id"],
-            })) ||
-          (selectedNodeIds.has(edge.target) &&
-            canRemoveNodeFromDrdOnly({
-              externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns,
-              definitions: state.dmn.model.definitions,
-              drdIndex: state.diagram.drdIndex,
-              dmnObjectNamespace:
-                nodesById.get(edge.target)!.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
-              dmnObjectId: 
nodesById.get(edge.target)!.data.dmnObject?.["@_id"],
-            }))
-        ) {
-          deleteEdge({
-            definitions: state.dmn.model.definitions,
-            drdIndex: state.diagram.drdIndex,
-            edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
-            mode: EdgeDeletionMode.FROM_CURRENT_DRD_ONLY,
-          });
-          state.dispatch(state).diagram.setEdgeStatus(edge.id, { selected: 
false, draggingWaypoint: false });
-        }
-      }
-
-      for (const node of rf.getNodes().filter((s) => s.selected)) {
-        // Prevent hiding artifact nodes from DRD;
-        if (nodeNatures[node.type as NodeType] === NodeNature.ARTIFACT) {
-          continue;
-        }
-        const { deletedDmnShapeOnCurrentDrd: deletedShape } = deleteNode({
-          drgEdges: [], // Deleting from DRD only.
-          definitions: state.dmn.model.definitions,
-          externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns,
-          drdIndex: state.diagram.drdIndex,
-          dmnObjectNamespace: node.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
-          dmnObjectQName: node.data.dmnObjectQName,
-          dmnObjectId: node.data.dmnObject?.["@_id"],
-          nodeNature: nodeNatures[node.type as NodeType],
-          mode: NodeDeletionMode.FROM_CURRENT_DRD_ONLY,
-        });
-
-        if (deletedShape) {
-          state.dispatch(state).diagram.setNodeStatus(node.id, {
-            selected: false,
-            dragging: false,
-            resizing: false,
-          });
-        }
-      }
-    });
-  }, [x, dmnEditorStoreApi, rf, externalModelsByNamespace]);
-
-  return <></>;
-}
-
-export function PanWhenAltPressed() {
-  const altPressed = RF.useKeyPress("Alt");
-  const rfStoreApi = RF.useStoreApi();
-
-  useEffect(() => {
-    rfStoreApi.setState({
-      nodesDraggable: !altPressed,
-      nodesConnectable: !altPressed,
-      elementsSelectable: !altPressed,
-    });
-  }, [altPressed, rfStoreApi]);
-
-  return <></>;
-}
diff --git a/packages/dmn-editor/src/diagram/DiagramCommands.tsx 
b/packages/dmn-editor/src/diagram/DiagramCommands.tsx
new file mode 100644
index 00000000000..9d073b1c98c
--- /dev/null
+++ b/packages/dmn-editor/src/diagram/DiagramCommands.tsx
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as RF from "reactflow";
+import * as React from "react";
+import { useEffect } from "react";
+import {
+  DMN_EDITOR_DIAGRAM_CLIPBOARD_MIME_TYPE,
+  DmnEditorDiagramClipboard,
+  buildClipboardFromDiagram,
+  getClipboard,
+} from "../clipboard/Clipboard";
+import { getNewDmnIdRandomizer } from "../idRandomizer/dmnIdRandomizer";
+import { NodeNature, nodeNatures } from "../mutations/NodeNature";
+import { addOrGetDrd } from "../mutations/addOrGetDrd";
+import { addStandaloneNode } from "../mutations/addStandaloneNode";
+import { EdgeDeletionMode, deleteEdge } from "../mutations/deleteEdge";
+import { NodeDeletionMode, canRemoveNodeFromDrdOnly, deleteNode } from 
"../mutations/deleteNode";
+import { repopulateInputDataAndDecisionsOnAllDecisionServices } from 
"../mutations/repopulateInputDataAndDecisionsOnDecisionService";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+import { DmnDiagramEdgeData } from "./edges/Edges";
+import { CONTAINER_NODES_DESIRABLE_PADDING, getBounds } from 
"./maths/DmnMaths";
+import { NODE_TYPES } from "./nodes/NodeTypes";
+import { DmnDiagramNodeData } from "./nodes/Nodes";
+import { useExternalModels } from 
"../includedModels/DmnEditorDependenciesContext";
+import { NodeType } from "./connections/graphStructure";
+import { buildXmlHref, parseXmlHref } from "../xml/xmlHrefs";
+import { DEFAULT_VIEWPORT } from "./Diagram";
+import { useCommands } from "../commands/CommandsContextProvider";
+
+export function DiagramCommands(props: {}) {
+  const rfStoreApi = RF.useStoreApi();
+  const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { commandsRef } = useCommands();
+  const { externalModelsByNamespace } = useExternalModels();
+  const rf = RF.useReactFlow<DmnDiagramNodeData, DmnDiagramEdgeData>();
+
+  // Cancel action
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.cancelAction = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Canceling action...");
+      rfStoreApi.setState((rfState) => {
+        if (rfState.connectionNodeId) {
+          rfState.cancelConnection();
+          dmnEditorStoreApi.setState((state) => {
+            state.diagram.ongoingConnection = undefined;
+          });
+        } else {
+          (document.activeElement as any)?.blur?.();
+        }
+
+        return rfState;
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef, rfStoreApi]);
+
+  // Reset position to origin
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.resetPosition = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Reseting position...");
+      rf.setViewport(DEFAULT_VIEWPORT, { duration: 200 });
+    };
+  }, [commandsRef, rf]);
+
+  // Focus on selection
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.focusOnSelection = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Focusing on selected bounds...");
+      const selectedNodes = rf.getNodes().filter((s) => s.selected);
+      if (selectedNodes.length <= 0) {
+        return;
+      }
+
+      const bounds = getBounds({
+        nodes: selectedNodes,
+        padding: 100,
+      });
+
+      rf.fitBounds(
+        {
+          x: bounds["@_x"],
+          y: bounds["@_y"],
+          width: bounds["@_width"],
+          height: bounds["@_height"],
+        },
+        { duration: 200 }
+      );
+    };
+  }, [commandsRef, rf]);
+
+  // Cut nodes
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.cut = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Cutting selected nodes...");
+      const { clipboard, copiedEdgesById, danglingEdgesById, copiedNodesById } 
= buildClipboardFromDiagram(
+        rfStoreApi.getState(),
+        dmnEditorStoreApi.getState()
+      );
+
+      navigator.clipboard.writeText(JSON.stringify(clipboard)).then(() => {
+        dmnEditorStoreApi.setState((state) => {
+          // Delete edges
+          [...copiedEdgesById.values(), 
...danglingEdgesById.values()].forEach((edge) => {
+            deleteEdge({
+              definitions: state.dmn.model.definitions,
+              drdIndex: state.diagram.drdIndex,
+              edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
+              mode: EdgeDeletionMode.FROM_DRG_AND_ALL_DRDS,
+            });
+            state.dispatch(state).diagram.setEdgeStatus(edge.id, {
+              selected: false,
+              draggingWaypoint: false,
+            });
+          });
+
+          // Delete nodes
+          rfStoreApi
+            .getState()
+            .getNodes()
+            .forEach((node: RF.Node<DmnDiagramNodeData>) => {
+              if (copiedNodesById.has(node.id)) {
+                deleteNode({
+                  drgEdges: 
state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges,
+                  definitions: state.dmn.model.definitions,
+                  drdIndex: state.diagram.drdIndex,
+                  dmnObjectNamespace: node.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
+                  dmnObjectQName: node.data.dmnObjectQName,
+                  dmnObjectId: node.data.dmnObject?.["@_id"],
+                  nodeNature: nodeNatures[node.type as NodeType],
+                  mode: NodeDeletionMode.FROM_DRG_AND_ALL_DRDS,
+                  externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace)
+                    .dmns,
+                });
+                state.dispatch(state).diagram.setNodeStatus(node.id, {
+                  selected: false,
+                  dragging: false,
+                  resizing: false,
+                });
+              }
+            });
+        });
+      });
+    };
+  }, [dmnEditorStoreApi, externalModelsByNamespace, commandsRef, rfStoreApi]);
+
+  // Copy nodes
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.copy = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Copying selected nodes...");
+      const { clipboard } = buildClipboardFromDiagram(rfStoreApi.getState(), 
dmnEditorStoreApi.getState());
+      navigator.clipboard.writeText(JSON.stringify(clipboard));
+    };
+  }, [dmnEditorStoreApi, commandsRef, rfStoreApi]);
+
+  // Paste nodes
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.paste = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Pasting nodes...");
+      navigator.clipboard.readText().then((text) => {
+        const clipboard = getClipboard<DmnEditorDiagramClipboard>(text, 
DMN_EDITOR_DIAGRAM_CLIPBOARD_MIME_TYPE);
+        if (!clipboard) {
+          return;
+        }
+
+        getNewDmnIdRandomizer()
+          .ack({
+            json: clipboard.drgElements,
+            type: "DMN15__tDefinitions",
+            attr: "drgElement",
+          })
+          .ack({
+            json: clipboard.artifacts,
+            type: "DMN15__tDefinitions",
+            attr: "artifact",
+          })
+          .ack({
+            json: clipboard.shapes,
+            type: "DMNDI15__DMNDiagram",
+            attr: "dmndi:DMNDiagramElement",
+            __$$element: "dmndi:DMNShape",
+          })
+          .ack({
+            json: clipboard.edges,
+            type: "DMNDI15__DMNDiagram",
+            attr: "dmndi:DMNDiagramElement",
+            __$$element: "dmndi:DMNEdge",
+          })
+          .ack<any>({
+            // This `any` argument ideally wouldn't be here, but the type of 
DMN's `meta` is not composed with KIE's `meta` in compile-time
+            json: clipboard.widths,
+            type: "KIE__tComponentsWidthsExtension",
+            attr: "kie:ComponentWidths",
+          })
+          .randomize();
+
+        dmnEditorStoreApi.setState((state) => {
+          state.dmn.model.definitions.drgElement ??= [];
+          
state.dmn.model.definitions.drgElement.push(...clipboard.drgElements);
+          state.dmn.model.definitions.artifact ??= [];
+          state.dmn.model.definitions.artifact.push(...clipboard.artifacts);
+
+          const { diagramElements, widths } = addOrGetDrd({
+            definitions: state.dmn.model.definitions,
+            drdIndex: state.diagram.drdIndex,
+          });
+          diagramElements.push(...clipboard.shapes.map((s) => ({ ...s, 
__$$element: "dmndi:DMNShape" as const })));
+          diagramElements.push(...clipboard.edges.map((s) => ({ ...s, 
__$$element: "dmndi:DMNEdge" as const })));
+
+          widths.push(...clipboard.widths);
+
+          repopulateInputDataAndDecisionsOnAllDecisionServices({ definitions: 
state.dmn.model.definitions });
+
+          state.diagram._selectedNodes = [...clipboard.drgElements, 
...clipboard.artifacts].map((s) =>
+            buildXmlHref({ id: s["@_id"]! })
+          );
+
+          if (state.diagram._selectedNodes.length === 1) {
+            state.focus.consumableId = 
parseXmlHref(state.diagram._selectedNodes[0]).id;
+          }
+        });
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef]);
+
+  // Select/deselect all nodes
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.selectAll = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Selecting/Deselecting nodes...");
+      const allNodeIds = rfStoreApi
+        .getState()
+        .getNodes()
+        .map((s) => s.id);
+
+      const allEdgeIds = rfStoreApi.getState().edges.map((s) => s.id);
+
+      dmnEditorStoreApi.setState((state) => {
+        const allSelectedNodesSet = new Set(state.diagram._selectedNodes);
+        const allSelectedEdgesSet = new Set(state.diagram._selectedEdges);
+
+        // If everything is selected, deselect everything.
+        if (
+          allNodeIds.every((id) => allSelectedNodesSet.has(id) && 
allEdgeIds.every((id) => allSelectedEdgesSet.has(id)))
+        ) {
+          state.diagram._selectedNodes = [];
+          state.diagram._selectedEdges = [];
+        } else {
+          state.diagram._selectedNodes = allNodeIds;
+          state.diagram._selectedEdges = allEdgeIds;
+        }
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef, rfStoreApi]);
+
+  // Create group wrapping selection
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.createGroup = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Grouping nodes...");
+      const selectedNodes = rf.getNodes().filter((s) => s.selected);
+      if (selectedNodes.length <= 0) {
+        return;
+      }
+
+      dmnEditorStoreApi.setState((state) => {
+        if (state.diagram._selectedNodes.length <= 0) {
+          return;
+        }
+
+        const { href: newNodeId } = addStandaloneNode({
+          definitions: state.dmn.model.definitions,
+          drdIndex: state.diagram.drdIndex,
+          newNode: {
+            type: NODE_TYPES.group,
+            bounds: getBounds({
+              nodes: selectedNodes,
+              padding: CONTAINER_NODES_DESIRABLE_PADDING,
+            }),
+          },
+        });
+
+        state.dispatch(state).diagram.setNodeStatus(newNodeId, { selected: 
true });
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef, rf]);
+
+  // Toggle hierarchy highlights
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.toggleHierarchyHighlight = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Toggle hierarchy highlights...");
+      dmnEditorStoreApi.setState((state) => {
+        state.diagram.overlays.enableNodeHierarchyHighlight = 
!state.diagram.overlays.enableNodeHierarchyHighlight;
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef]);
+
+  // Show Properties panel
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.togglePropertiesPanel = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Toggle properties panel...");
+      dmnEditorStoreApi.setState((state) => {
+        state.diagram.propertiesPanel.isOpen = 
!state.diagram.propertiesPanel.isOpen;
+      });
+    };
+  }, [dmnEditorStoreApi, commandsRef]);
+
+  // Hide from DRD
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.hideFromDrd = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Hide node from DRD...");
+      const nodesById = rf
+        .getNodes()
+        .reduce((acc, s) => acc.set(s.id, s), new Map<string, 
RF.Node<DmnDiagramNodeData>>());
+
+      dmnEditorStoreApi.setState((state) => {
+        const selectedNodeIds = new Set(state.diagram._selectedNodes);
+        for (const edge of rf.getEdges()) {
+          if (
+            (selectedNodeIds.has(edge.source) &&
+              canRemoveNodeFromDrdOnly({
+                externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace)
+                  .dmns,
+                definitions: state.dmn.model.definitions,
+                drdIndex: state.diagram.drdIndex,
+                dmnObjectNamespace:
+                  nodesById.get(edge.source)!.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
+                dmnObjectId: 
nodesById.get(edge.source)!.data.dmnObject?.["@_id"],
+              })) ||
+            (selectedNodeIds.has(edge.target) &&
+              canRemoveNodeFromDrdOnly({
+                externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace)
+                  .dmns,
+                definitions: state.dmn.model.definitions,
+                drdIndex: state.diagram.drdIndex,
+                dmnObjectNamespace:
+                  nodesById.get(edge.target)!.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
+                dmnObjectId: 
nodesById.get(edge.target)!.data.dmnObject?.["@_id"],
+              }))
+          ) {
+            deleteEdge({
+              definitions: state.dmn.model.definitions,
+              drdIndex: state.diagram.drdIndex,
+              edge: { id: edge.id, dmnObject: edge.data!.dmnObject },
+              mode: EdgeDeletionMode.FROM_CURRENT_DRD_ONLY,
+            });
+            state.dispatch(state).diagram.setEdgeStatus(edge.id, { selected: 
false, draggingWaypoint: false });
+          }
+        }
+
+        for (const node of rf.getNodes().filter((s) => s.selected)) {
+          // Prevent hiding artifact nodes from DRD;
+          if (nodeNatures[node.type as NodeType] === NodeNature.ARTIFACT) {
+            continue;
+          }
+          const { deletedDmnShapeOnCurrentDrd: deletedShape } = deleteNode({
+            drgEdges: [], // Deleting from DRD only.
+            definitions: state.dmn.model.definitions,
+            externalDmnsIndex: 
state.computed(state).getExternalModelTypesByNamespace(externalModelsByNamespace).dmns,
+            drdIndex: state.diagram.drdIndex,
+            dmnObjectNamespace: node.data.dmnObjectNamespace ?? 
state.dmn.model.definitions["@_namespace"],
+            dmnObjectQName: node.data.dmnObjectQName,
+            dmnObjectId: node.data.dmnObject?.["@_id"],
+            nodeNature: nodeNatures[node.type as NodeType],
+            mode: NodeDeletionMode.FROM_CURRENT_DRD_ONLY,
+          });
+
+          if (deletedShape) {
+            state.dispatch(state).diagram.setNodeStatus(node.id, {
+              selected: false,
+              dragging: false,
+              resizing: false,
+            });
+          }
+        }
+      });
+    };
+  }, [dmnEditorStoreApi, externalModelsByNamespace, commandsRef, rf]);
+
+  useEffect(() => {
+    if (!commandsRef.current) {
+      return;
+    }
+    commandsRef.current.panDown = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Panning down");
+      rfStoreApi.setState({
+        nodesDraggable: false,
+        nodesConnectable: false,
+        elementsSelectable: false,
+      });
+    };
+    commandsRef.current.panUp = async () => {
+      console.debug("DMN DIAGRAM: COMMANDS: Panning up");
+      rfStoreApi.setState({
+        nodesDraggable: true,
+        nodesConnectable: true,
+        elementsSelectable: true,
+      });
+    };
+  }, [commandsRef, rfStoreApi]);
+
+  return <></>;
+}
diff --git 
a/packages/keyboard-shortcuts/src/envelope/DefaultKeyboardShortcutsService.ts 
b/packages/keyboard-shortcuts/src/envelope/DefaultKeyboardShortcutsService.ts
index 164621f12d9..1a68e012b5d 100644
--- 
a/packages/keyboard-shortcuts/src/envelope/DefaultKeyboardShortcutsService.ts
+++ 
b/packages/keyboard-shortcuts/src/envelope/DefaultKeyboardShortcutsService.ts
@@ -52,6 +52,7 @@ const KEY_CODES = new Map<string, string>([
   ["esc", "Escape"],
   ["delete", "Delete"],
   ["backspace", "Backspace"],
+  ["space", "Space"],
   ["right", "ArrowRight"],
   ["left", "ArrowLeft"],
   ["up", "ArrowUp"],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a16f54497f3..d2526990c6a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3630,6 +3630,9 @@ importers:
       "@kie-tools-core/envelope-bus":
         specifier: workspace:*
         version: link:../envelope-bus
+      "@kie-tools-core/keyboard-shortcuts":
+        specifier: workspace:*
+        version: link:../keyboard-shortcuts
       "@kie-tools-core/notifications":
         specifier: workspace:*
         version: link:../notifications


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to