fix: toolbar buttons clickable, add node delete
Fixes:
- SVG mousedown handler now ignores clicks outside SVG (toolbar works)
- Right-click node -> Destroy Node to delete virtual devices
- POST /api/destroy-node {node_id} endpoint (uses pw-cli destroy)
Node context menu shows node name and destroy button.
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
let hoveredPort = $state<number | null>(null);
|
let hoveredPort = $state<number | null>(null);
|
||||||
let selectedEdge = $state<string | null>(null);
|
let selectedEdge = $state<string | null>(null);
|
||||||
let contextMenu = $state<{ x: number; y: number; linkId: number; outputPortId: number; inputPortId: number; pinned: boolean } | null>(null);
|
let contextMenu = $state<{ x: number; y: number; linkId: number; outputPortId: number; inputPortId: number; pinned: boolean } | null>(null);
|
||||||
|
let nodeContextMenu = $state<{ x: number; y: number; nodeId: number; nodeName: string } | null>(null);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
let showAudio = $state(true);
|
let showAudio = $state(true);
|
||||||
@@ -234,6 +235,9 @@
|
|||||||
|
|
||||||
// Mouse handlers
|
// Mouse handlers
|
||||||
function onMouseDown(e: MouseEvent) {
|
function onMouseDown(e: MouseEvent) {
|
||||||
|
// Ignore clicks outside the SVG (toolbar, dialogs, etc.)
|
||||||
|
if (svgEl && !svgEl.contains(e.target as Node)) return;
|
||||||
|
|
||||||
contextMenu = null;
|
contextMenu = null;
|
||||||
if (e.button === 2) return;
|
if (e.button === 2) return;
|
||||||
const pt = svgPoint(e);
|
const pt = svgPoint(e);
|
||||||
@@ -352,6 +356,8 @@
|
|||||||
function onContextMenu(e: MouseEvent) {
|
function onContextMenu(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
|
// Right-click on edge
|
||||||
if (target.classList.contains('edge-path')) {
|
if (target.classList.contains('edge-path')) {
|
||||||
const edgeId = target.dataset.edgeId;
|
const edgeId = target.dataset.edgeId;
|
||||||
const link = $links.find(l => String(l.id) === edgeId);
|
const link = $links.find(l => String(l.id) === edgeId);
|
||||||
@@ -365,7 +371,40 @@
|
|||||||
pinned: $patchbay.pinned_connections.includes(link.id),
|
pinned: $patchbay.pinned_connections.includes(link.id),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right-click on node
|
||||||
|
const nodeGroup = target.closest('.node-group') as HTMLElement;
|
||||||
|
if (nodeGroup) {
|
||||||
|
const nodeId = Number(nodeGroup.dataset.nodeId);
|
||||||
|
const nd = $nodes.find(n => n.id === nodeId);
|
||||||
|
if (nd) {
|
||||||
|
nodeContextMenu = { x: e.clientX, y: e.clientY, nodeId, nodeName: nd.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadNodeModule() {
|
||||||
|
if (!nodeContextMenu) return;
|
||||||
|
// Try to find and unload the module that created this node
|
||||||
|
// Use pactl to unload by looking up the module
|
||||||
|
fetch('/api/load-module', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ module: 'module-null-sink', args: '' }),
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Actually, just use the destroy approach via pactl
|
||||||
|
// We need to find the module ID. Let's use a different endpoint.
|
||||||
|
// For now, use pw-cli to destroy the node
|
||||||
|
const name = nodeContextMenu.nodeName;
|
||||||
|
fetch('/api/destroy-node', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ node_id: nodeContextMenu.nodeId }),
|
||||||
|
}).catch(() => {});
|
||||||
|
nodeContextMenu = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKey(e: KeyboardEvent) {
|
function onKey(e: KeyboardEvent) {
|
||||||
@@ -379,7 +418,7 @@
|
|||||||
onDestroy(() => { destroyGraph(); });
|
onDestroy(() => { destroyGraph(); });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={onKey} onclick={() => { contextMenu = null; showVirtualMenu = false; }} />
|
<svelte:window onkeydown={onKey} onclick={() => { contextMenu = null; nodeContextMenu = null; showVirtualMenu = false; }} />
|
||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -584,6 +623,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if nodeContextMenu}
|
||||||
|
<div class="ctx" style="left:{nodeContextMenu.x}px;top:{nodeContextMenu.y}px" role="menu">
|
||||||
|
<div class="ctx-title">{nodeContextMenu.nodeName}</div>
|
||||||
|
<button onclick={() => {
|
||||||
|
fetch('/api/destroy-node', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ node_id: nodeContextMenu!.nodeId }),
|
||||||
|
}).catch(() => {});
|
||||||
|
nodeContextMenu = null;
|
||||||
|
}}>Destroy Node</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Hide Nodes Dialog -->
|
<!-- Hide Nodes Dialog -->
|
||||||
{#if showHideDialog}
|
{#if showHideDialog}
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
@@ -749,6 +802,11 @@
|
|||||||
font-size: 12px; cursor: pointer; text-align: left; font-family: monospace;
|
font-size: 12px; cursor: pointer; text-align: left; font-family: monospace;
|
||||||
}
|
}
|
||||||
.ctx button:hover { background: #444; }
|
.ctx button:hover { background: #444; }
|
||||||
|
.ctx-title {
|
||||||
|
padding: 4px 16px; font-size: 9px; color: #666;
|
||||||
|
border-bottom: 1px solid #444; font-family: monospace;
|
||||||
|
max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dialogs */
|
/* Dialogs */
|
||||||
.dialog {
|
.dialog {
|
||||||
|
|||||||
@@ -450,6 +450,37 @@ void WebServer::setupRoutes() {
|
|||||||
m_http.Options("/api/create-loopback", cors_handler);
|
m_http.Options("/api/create-loopback", cors_handler);
|
||||||
m_http.Options("/api/unload-module", cors_handler);
|
m_http.Options("/api/unload-module", cors_handler);
|
||||||
m_http.Options("/api/load-module", cors_handler);
|
m_http.Options("/api/load-module", cors_handler);
|
||||||
|
m_http.Options("/api/destroy-node", cors_handler);
|
||||||
|
|
||||||
|
// Destroy node: POST /api/destroy-node {"node_id":N}
|
||||||
|
m_http.Post("/api/destroy-node", [this](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
uint32_t node_id = 0;
|
||||||
|
if (sscanf(req.body.c_str(), "{\"node_id\":%u}", &node_id) == 1) {
|
||||||
|
// Find the module that owns this node and unload it
|
||||||
|
// First try to destroy via registry (works for pw-cli created nodes)
|
||||||
|
auto snap = m_engine.snapshot();
|
||||||
|
bool found = false;
|
||||||
|
for (auto &n : snap.nodes) {
|
||||||
|
if (n.id == node_id) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
// Use pw-cli to destroy
|
||||||
|
std::string cmd = "pw-cli destroy " + std::to_string(node_id) + " 2>/dev/null";
|
||||||
|
int ret = system(cmd.c_str());
|
||||||
|
(void)ret;
|
||||||
|
sleep(1);
|
||||||
|
broadcastGraph();
|
||||||
|
}
|
||||||
|
res.set_content(found ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"node not found\"}", "application/json");
|
||||||
|
} else {
|
||||||
|
res.status = 400;
|
||||||
|
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
||||||
|
}
|
||||||
|
res.set_header("Access-Control-Allow-Origin", "*");
|
||||||
|
});
|
||||||
|
|
||||||
// Helper: extract a string value from simple JSON
|
// Helper: extract a string value from simple JSON
|
||||||
auto extractStr = [](const std::string &body, const std::string &key) -> std::string {
|
auto extractStr = [](const std::string &body, const std::string &key) -> std::string {
|
||||||
|
|||||||
Reference in New Issue
Block a user