Skip to content

Commit

Permalink
feat(menu): edit menu items now interact with flow (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiduzo authored Jan 12, 2025
1 parent acc1364 commit 0379d82
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 115 deletions.
162 changes: 88 additions & 74 deletions apps/electron-app/src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const appMenu: (MenuItemConstructorOptions | MenuItem)[] = isMac
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
{ role: 'editMenu' },
isMac ? { role: 'close' } : {},
],
},
Expand All @@ -35,77 +34,31 @@ export function createMenu(mainWindow: BrowserWindow) {
{
label: 'Insert node',
accelerator: isMac ? 'Cmd+K' : 'Ctrl+K',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'add-node' },
} satisfies MenuResponse);
},
},
{ type: 'separator' },
{
label: 'Undo',
accelerator: isMac ? 'Cmd+U' : 'Ctrl+U',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'undo' },
} satisfies MenuResponse);
},
},
{
label: 'Redo',
accelerator: isMac ? 'Cmd+Shift+U' : 'Ctrl+Shift+U',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'redo' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'add-node'),
},
{ type: 'separator' },
{
label: 'Save flow',
accelerator: isMac ? 'Cmd+S' : 'Ctrl+S',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'save-flow' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'save-flow'),
},
{
id: 'autosave',
label: 'Auto save',
type: 'checkbox',
checked: true,
click: menuItem => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'toggle-autosave', args: menuItem.checked },
} satisfies MenuResponse);
},
click: ({ checked }) => sendMessage(mainWindow, 'toggle-autosave', checked),
},
{ type: 'separator' },
{
label: 'New flow',
accelerator: isMac ? 'Cmd+N' : 'Ctrl+N',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'new-flow' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'new-flow'),
},
{
label: 'Export flow',
accelerator: isMac ? 'Cmd+E' : 'Ctrl+E',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'export-flow' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'export-flow'),
},
{
label: 'Import flow',
Expand All @@ -114,22 +67,86 @@ export function createMenu(mainWindow: BrowserWindow) {
const flow = await importFlow();
if (!flow) return;

mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'import-flow', args: flow },
} satisfies MenuResponse);
sendMessage(mainWindow, 'import-flow', flow);
},
},
{ type: 'separator' },
{
label: 'Fit flow in view',
accelerator: isMac ? 'Cmd+O' : 'Ctrl+O',
click: async () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'fit-flow' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'fit-flow'),
},
{ type: 'separator' },
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: isMac ? 'Cmd+Z' : 'Ctrl+Z',
click: () => {
mainWindow.webContents.undo();
sendMessage(mainWindow, 'undo');
},
},
{
label: 'Redo',
accelerator: isMac ? 'Cmd+Shift+Z' : 'Ctrl+Shift+Z',
click: () => {
mainWindow.webContents.redo();
sendMessage(mainWindow, 'redo');
},
},
{ type: 'separator' },
{
label: 'Cut',
accelerator: isMac ? 'Cmd+X' : 'Ctrl+X',
click: () => {
mainWindow.webContents.cut();
sendMessage(mainWindow, 'cut');
},
},
{
label: 'Copy',
accelerator: isMac ? 'Cmd+C' : 'Ctrl+C',
click: () => {
mainWindow.webContents.copy();
sendMessage(mainWindow, 'copy');
},
},
{
label: 'Paste',
accelerator: isMac ? 'Cmd+V' : 'Ctrl+V',
click: () => {
mainWindow.webContents.paste();
sendMessage(mainWindow, 'paste');
},
},
{ type: 'separator' },
{
label: 'Select all',
accelerator: isMac ? 'Cmd+A' : 'Ctrl+A',
click: () => {
mainWindow.webContents.selectAll();
sendMessage(mainWindow, 'select-all');
},
},
{
label: 'Deselect all',
accelerator: 'Escape',
click: () => {
sendMessage(mainWindow, 'deselect-all');
},
},
{ type: 'separator' },
{
label: 'Delete',
accelerator: isMac ? 'Backspace' : 'Backspace',
click: () => {
mainWindow.webContents.delete();
sendMessage(mainWindow, 'delete');
},
},
],
},
],
},
Expand All @@ -138,21 +155,11 @@ export function createMenu(mainWindow: BrowserWindow) {
submenu: [
{
label: 'Microcontroller settings',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'board-settings' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'board-settings'),
},
{
label: 'MQTT settings',
click: () => {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button: 'mqtt-settings' },
} satisfies MenuResponse);
},
click: () => sendMessage(mainWindow, 'mqtt-settings'),
},
],
},
Expand All @@ -175,3 +182,10 @@ export function createMenu(mainWindow: BrowserWindow) {
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
}

function sendMessage(mainWindow: BrowserWindow, button: string, args?: any) {
mainWindow.webContents.send('ipc-menu', {
success: true,
data: { button, args },
} satisfies MenuResponse);
}
110 changes: 104 additions & 6 deletions apps/electron-app/src/render/components/IpcMenuListener.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { useReactFlow, type Edge, type Node } from '@xyflow/react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useLocalStorage } from 'usehooks-ts';
import { FlowFile } from '../../common/types';
import { useSaveFlow } from '../hooks/useSaveFlow';
import { useReactFlowStore } from '../stores/react-flow';
import {
useDeselectAll,
useReactFlowStore,
useSelectAll,
useSelectedEdges,
useSelectNodes,
} from '../stores/react-flow';
import { MqttSettingsForm } from './forms/MqttSettingsForm';
import { AdvancedSettingsForm } from './forms/AdvancedSettingsForm';
import { useAppStore } from '../stores/app';
Expand All @@ -12,13 +18,29 @@ import { useShallow } from 'zustand/react/shallow';

export function IpcMenuListeners() {
const { getNodes, getEdges, fitView } = useReactFlow();
const { setEdges, setNodes, undo, redo } = useReactFlowStore();
const { setEdges, setNodes, undo, redo, onNodesChange, onEdgesChange } = useReactFlowStore();

const { saveNodesAndEdges, setAutoSave } = useSaveFlow();
const [, setLocalNodes] = useLocalStorage<Node[]>('nodes', []);
const [, setLocalEdges] = useLocalStorage<Edge[]>('edges', []);
const setOpen = useNewNodeStore(useShallow(state => state.setOpen));
const { settingsOpen, setSettingsOpen } = useAppStore();
const [copiedNodes, setCopiedNodes] = useState<Node[]>([]);
const selectAll = useSelectAll();
const deselectAll = useDeselectAll();
const selectedNodes = useSelectNodes();
const selectedEdges = useSelectedEdges();

function canTriggerAction() {
const selectedElement = window.document.activeElement as HTMLElement;

if (selectedElement === window.document.body) return true;
if (selectedElement.classList.contains('react-flow__node')) return true;
if (selectedElement.classList.contains('react-flow__edge')) return true;
if (selectedElement.classList.contains('react-flow__nodesselection-rect')) return true;

return false;
}

useEffect(() => {
return window.electron.ipcRenderer.on<{ button: string; args: any }>('ipc-menu', result => {
Expand Down Expand Up @@ -56,19 +78,95 @@ export function IpcMenuListeners() {
setNodes(nodes);
setEdges(edges);
break;
case 'fit-flow':
fitView({
duration: 400,
padding: 0.15,
nodes: selectedNodes().length > 0 ? selectedNodes() : undefined,
});
break;
case 'undo':
if (!canTriggerAction()) break;
undo();
break;
case 'redo':
if (!canTriggerAction()) break;
redo();
break;
case 'fit-flow':
fitView({ duration: 400, padding: 0.15 });
case 'select-all':
if (!canTriggerAction()) break;
selectAll();
break;
case 'deselect-all':
if (!canTriggerAction()) break;
deselectAll();
break;
case 'copy':
if (!canTriggerAction()) break;
setCopiedNodes(selectedNodes());
break;
case 'cut':
if (!canTriggerAction()) break;
setCopiedNodes(selectedNodes());
onNodesChange(
selectedNodes().map(node => ({
type: 'remove',
id: node.id,
})),
);
break;
case 'paste':
if (!canTriggerAction()) break;
deselectAll();
onNodesChange(
copiedNodes.map(node => ({
type: 'add',
item: {
...node,
id: Math.random().toString(36).substring(2, 8),
position: {
x: node.position.x + 20,
y: node.position.y + 20,
},
selected: true,
dragging: true,
},
})),
);
break;
case 'delete':
if (!canTriggerAction()) break;
onNodesChange(
selectedNodes().map(node => ({
type: 'remove',
id: node.id,
})),
);
onEdgesChange(
selectedEdges().map(edge => ({
type: 'remove',
id: edge.id,
})),
);
break;
default:
break;
}
});
}, [saveNodesAndEdges, setSettingsOpen, setOpen]);
}, [
saveNodesAndEdges,
setSettingsOpen,
setOpen,
undo,
redo,
selectAll,
deselectAll,
selectedNodes,
onNodesChange,
selectedEdges,
onEdgesChange,
copiedNodes,
]);

if (settingsOpen === 'mqtt-settings') return <MqttSettingsForm open />;
if (settingsOpen === 'board-settings') return <AdvancedSettingsForm open />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type Props = BaseNode<ButtonData>;
Button.defaultProps = {
data: {
group: 'hardware',
tags: ['digital', 'input'],
tags: ['input', 'digital'],
holdtime: 500,
isPulldown: false,
isPullup: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Gate.defaultProps = {
tags: ['control', 'transformation'],
label: 'Gate',
gate: 'and',
description: 'Combine and validate input signals using logic gates',
description: 'Validate signals using logic gates',
} satisfies Props['data'],
};

Expand Down
Loading

0 comments on commit 0379d82

Please sign in to comment.