From 57b6aeacdf9d6b1691b8b1fa8f447c0dbf06ce95 Mon Sep 17 00:00:00 2001 From: David Mosbach Date: Sat, 27 May 2023 23:14:57 +0200 Subject: [PATCH] added context menus with quick actions --- editor.css | 37 ++++++++++++++++++ editor.html | 59 ++++++++++++++++++++++++++++- editor.js | 107 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 180 insertions(+), 23 deletions(-) diff --git a/editor.css b/editor.css index a91e487..8009d50 100644 --- a/editor.css +++ b/editor.css @@ -76,4 +76,41 @@ body { hyphens: auto; word-wrap: break-word; height: 80%; +} + +.contextmenu { + display: none; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); + border-radius: 5px; + line-height: 2; + position: absolute; + z-index: 15; +} + +.contextmenu div { + padding: 5px; + background-color: rgb(230, 230, 230); + opacity: 0.95; + transition: background-color 100ms ease-out 50ms; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.contextmenu .menutop { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.contextmenu .menubottom { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} + +.contextmenu div:hover { + background-color: rgb(240, 240, 240); +} + +.contextmenu div:active { + background-color: rgb(250, 250, 250); } \ No newline at end of file diff --git a/editor.html b/editor.html index e892785..7d0cf98 100644 --- a/editor.html +++ b/editor.html @@ -35,7 +35,64 @@
- + +
+ + +
+
+ + + + +
+
+ +
diff --git a/editor.js b/editor.js index 274fb61..6f5e384 100644 --- a/editor.js +++ b/editor.js @@ -123,7 +123,8 @@ var highlightedSources = []; var highlightedTargets = []; function selectActor() { - // console.log(selectedActor.value); + contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; + rightSelection = null; highlightedSources = []; highlightedTargets = []; selectedViewer.value = NO_VIEWER; @@ -136,6 +137,8 @@ function selectActor() { } function selectViewer() { + contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; + rightSelection = null; highlightedSources = []; highlightedTargets = []; selectedActor.value = NO_ACTOR; @@ -153,8 +156,13 @@ const selfLoopCurvMin = 0.5; // Minimum curvature of a self loop. const curvatureMinMax = 0.2; // Minimum/maximum curvature (1 +/- x) of overlapping edges. var selection = null; // The currently selected node/edge. +var rightSelection = null; // The currently right clicked node/edge. const sidePanel = document.getElementById('sidepanel'); const sideContent = document.getElementById('sidecontent'); +//Context menus +const contextMenuBg = document.getElementById('ctmenubg'); //Click on background +const contextMenuSt = document.getElementById('ctmenust'); //Click on state +const contextMenuEd = document.getElementById('ctmenued'); //Click on edge const edgeColourDefault = '#999999ff'; const edgeColourSelected = '#000000ff'; @@ -254,6 +262,8 @@ function generatePanelContent(selection) { * @param {*} item The node or edge to select. */ function select(item) { + contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; + rightSelection = null; selection = selection === item ? null : item; if (selection === item) { while (sideContent.firstChild) @@ -268,7 +278,11 @@ function select(item) { sidePanel.style.display = 'none'; } console.log(item); - // TODO +} + + +function rightSelect() { + select(rightSelection); } @@ -296,17 +310,33 @@ function connect(source, target) { /** - * Adds a new state to the workflow. - * @param {*} x The x coordinate on the canvas. - * @param {*} y The y coordinate on the canvas. - * @returns The new state. + * Adds a new state to the workflow and auto-selects it. */ -function addState(x, y) { - let nodeId = stateIdCounter ++; +function addState() { + var nodeId = stateIdCounter ++; + var x = newStateCoords.x; + var y = newStateCoords.y; state = {id: nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5}; workflow.states.push(state); updateGraph(); - return state; + select(state); +} + +function addEdge() { + //TODO +} + +function removeRightSelection() { + if (rightSelection) { + if (rightSelection.actionData) removeAction(rightSelection); + else removeState(rightSelection); + if (selection === rightSelection) { + selection = null; + sidePanel.style.display = 'none'; + } + rightSelection = null; + contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; + } } @@ -328,6 +358,8 @@ function removeState(state) { .filter(edge => edge.source === state || edge.target === state) .forEach(edge => removeAction(edge)); workflow.states.splice(workflow.states.indexOf(state), 1); + var abbreviation = state.stateData && state.stateData.abbreviation; + abbreviation && stateAbbreviations.splice(stateAbbreviations.indexOf(abbreviation), 1); } @@ -340,18 +372,19 @@ function getNodeColour(node) { var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) || highlightedSources.includes(node.id) || highlightedTargets.includes(node.id) var alpha = standard ? 'ff' : '55'; + var isSelected = selection === node || rightSelection === node; if (node.stateData && node.stateData.final !== 'False' && node.stateData.final !== '') { if (node.stateData.final === 'True' || node.stateData.final === 'ok') { - return (selection === node ? '#a4eb34' : '#7fad36') + alpha; + return (isSelected ? '#a4eb34' : '#7fad36') + alpha; } else if (node.stateData.final === 'not-ok') { - return (selection === node ? '#f77474' : '#f25050') + alpha; + return (isSelected ? '#f77474' : '#f25050') + alpha; } else { //console.log(node.stateData.final); } } else if (node.name === '@@INIT') { - return (selection === node ? '#e8cd84' : '#d1ad4b') + alpha; + return (isSelected ? '#e8cd84' : '#d1ad4b') + alpha; } else { - return (selection === node ? '#5fbad9' : '#4496b3') + alpha; + return (isSelected ? '#5fbad9' : '#4496b3') + alpha; } } @@ -364,12 +397,13 @@ function isHighlightedEdge(edge) { } function getEdgeColour(edge) { + var isSelected = selection === edge || rightSelection === edge; if (isHighlightedEdge(edge)) { - return selection === edge ? edgeColourHighlightSelected : edgeColourHighlightDefault; + return isSelected ? edgeColourHighlightSelected : edgeColourHighlightDefault; } else if (selectedActor.value !== NO_ACTOR) { - return selection === edge ? edgeColourSubtleSelected : edgeColourSubtleDefault; + return isSelected ? edgeColourSubtleSelected : edgeColourSubtleDefault; } else { - return selection === edge ? edgeColourSelected : edgeColourDefault; + return isSelected ? edgeColourSelected : edgeColourDefault; } } @@ -393,6 +427,20 @@ workflow.states.forEach(state => { state.stateData.abbreviation = labelString; }); + +/** + * + * @param {*} event + * @param {HTMLElement} menu + */ +function openContextMenu(x, y, menu) { + menu.style.top = y - 25; + menu.style.left = x + 20; + menu.style.display = 'block'; +} + +var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state + const Graph = ForceGraph() (document.getElementById('graph')) .linkDirectionalArrowLength(6) @@ -505,13 +553,28 @@ const Graph = ForceGraph() node.fy = node.y; }) .onNodeClick((node, _) => select(node)) - .onNodeRightClick((node, _) => removeState(node)) + .onNodeRightClick((node, event) => { + openContextMenu(event.layerX, event.layerY, contextMenuSt); + contextMenuBg.style.display = contextMenuEd.style.display = 'none'; + rightSelection = node; + }) .onLinkClick((edge, _) => select(edge)) - .onLinkRightClick((edge, _) => removeAction(edge)) - .onBackgroundClick(event => { - var coords = Graph.screen2GraphCoords(event.layerX, event.layerY); - var newState = addState(coords.x, coords.y); - selection = newState; + .onLinkRightClick((edge, event) => { + openContextMenu(event.layerX, event.layerY, contextMenuEd); + contextMenuBg.style.display = contextMenuSt.style.display = 'none'; + rightSelection = edge; + }) + .onBackgroundClick(_ => { + contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; + sidePanel.style.display = 'none'; + rightSelection = null; + selection = null; + }) + .onBackgroundRightClick(event => { + newStateCoords = Graph.screen2GraphCoords(event.layerX, event.layerY); + openContextMenu(event.layerX, event.layerY, contextMenuBg); + contextMenuEd.style.display = contextMenuSt.style.display = 'none'; + rightSelection = null; }) .autoPauseRedraw(false);