diff --git a/e2e/playwright/lib/electron-helpers.ts b/e2e/playwright/lib/electron-helpers.ts new file mode 100644 index 00000000000..50ac555c92e --- /dev/null +++ b/e2e/playwright/lib/electron-helpers.ts @@ -0,0 +1,7 @@ +export const throwError = (message: string): never => { + throw new Error(message) +} + +export const throwTronAppMissing = () => { + throwError('tronApp is missing') +} diff --git a/e2e/playwright/native-file-menu.spec.ts b/e2e/playwright/native-file-menu.spec.ts index fbd19a0a2f1..bd8cf3c959a 100644 --- a/e2e/playwright/native-file-menu.spec.ts +++ b/e2e/playwright/native-file-menu.spec.ts @@ -1,3 +1,4 @@ +import { throwTronAppMissing } from '@e2e/playwright/lib/electron-helpers' import { expect, test } from '@e2e/playwright/zoo-test' /** @@ -7,7 +8,7 @@ import { expect, test } from '@e2e/playwright/zoo-test' test.describe('Native file menu', { tag: ['@electron'] }, () => { test.describe('Home page', () => { test.describe('File role', () => { - test('File.Create project', async ({ tronApp, cmdBar, page }) => { + test('Home.File.Create project', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -27,7 +28,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expectedArgument = 'untitled' expect(actualArgument).toBe(expectedArgument) }) - test('File.Open project', async ({ tronApp, cmdBar, page }) => { + test('Home.File.Open project', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -47,7 +48,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expected = 'Open project' expect(actual).toBe(expected) }) - test('File.Preferences.User settings', async ({ + test('Home.File.Preferences.User settings', async ({ tronApp, cmdBar, page, @@ -71,7 +72,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { ) await expect(actualText).toBeVisible() }) - test('File.Preferences.Keybindings', async ({ + test('Home.File.Preferences.Keybindings', async ({ tronApp, cmdBar, page, @@ -93,7 +94,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const enterSketchMode = settings.locator('#enter-sketch-mode') await expect(enterSketchMode).toBeVisible() }) - test('File.Preferences.User default units', async ({ + test('Home.File.Preferences.User default units', async ({ tronApp, cmdBar, page, @@ -114,7 +115,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const defaultUnit = settings.locator('#defaultUnit') await expect(defaultUnit).toBeVisible() }) - test('File.Preferences.Theme', async ({ tronApp, cmdBar, page }) => { + test('Home.File.Preferences.Theme', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -135,7 +136,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expected = 'Settings · app · theme' expect(actual).toBe(expected) }) - test('File.Preferences.Theme color', async ({ + test('Home.File.Preferences.Theme color', async ({ tronApp, cmdBar, page, @@ -156,7 +157,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const defaultUnit = settings.locator('#themeColor') await expect(defaultUnit).toBeVisible() }) - test('File.Preferences.Sign out', async ({ tronApp, cmdBar, page }) => { + test('Home.File.Preferences.Sign out', async ({ + tronApp, + cmdBar, + page, + }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -175,7 +180,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { }) test.describe('Edit role', () => { - test('Edit.Rename project', async ({ tronApp, cmdBar, page }) => { + test('Home.Edit.Rename project', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -194,7 +199,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expected = 'Rename project' expect(actual).toBe(expected) }) - test('Edit.Delete project', async ({ tronApp, cmdBar, page }) => { + test('Home.Edit.Delete project', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -213,7 +218,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expected = 'Delete project' expect(actual).toBe(expected) }) - test('Edit.Change project directory', async ({ + test('Home.Edit.Change project directory', async ({ tronApp, cmdBar, page, @@ -236,7 +241,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { }) }) test.describe('View role', () => { - test('View.Command Palette...', async ({ tronApp, cmdBar, page }) => { + test('Home.View.Command Palette...', async ({ + tronApp, + cmdBar, + page, + }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -254,7 +263,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { }) }) test.describe('Help role', () => { - test('Help.Show all commands', async ({ tronApp, cmdBar, page }) => { + test('Home.Help.Show all commands', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -270,7 +279,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search') await expect(actual).toBeVisible() }) - test('Help.KCL code samples', async ({ tronApp, cmdBar, page }) => { + test('Home.Help.KCL code samples', async ({ tronApp, cmdBar, page }) => { if (!tronApp) fail() // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run @@ -282,7 +291,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { if (!menu) fail() }) }) - test('Help.Refresh and report a bug', async ({ + test('Home.Help.Refresh and report a bug', async ({ tronApp, cmdBar, page, @@ -305,8 +314,1846 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { ) await expect(actual).toBeVisible() }) - test('Help.Reset onboarding', async ({ tronApp, cmdBar, page }) => { + test('Home.Help.Reset onboarding', async ({ tronApp, cmdBar, page }) => { + if (!tronApp) fail() + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) fail() + const menu = app.applicationMenu.getMenuItemById( + 'Help.Reset onboarding' + ) + if (!menu) fail() + menu.click() + }) + + const actual = page.getByText( + `This is a hardware design tool that lets you edit visually, with code, or both. It's powered by the KittyCAD Design API, the first API created for anyone to build hardware design tools.` + ) + await expect(actual).toBeVisible() + }) + }) + }) + test.describe('Modeling page', () => { + test.describe('File Role', () => { + test('Modeling.File.Create project', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { if (!tronApp) fail() + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) fail() + const newProject = + app.applicationMenu.getMenuItemById('File.New project') + if (!newProject) fail() + newProject.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actualArgument = await cmdBar.cmdBarElement + .getByTestId('cmd-bar-arg-value') + .inputValue() + const expectedArgument = 'untitled' + expect(actualArgument).toBe(expectedArgument) + }) + test('Modeling.File.Open project', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = + app.applicationMenu.getMenuItemById('File.Open project') + if (!openProject) { + throw new Error('File.Open project') + } + openProject.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Open project' + expect(actual).toBe(expected) + }) + test('Modeling.File.Load a sample model', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = app.applicationMenu.getMenuItemById( + 'File.Load a sample model' + ) + if (!openProject) { + throw new Error('File.Load a sample model') + } + openProject.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Open sample' + expect(actual).toBe(expected) + }) + test('Modeling.File.Export current part', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = app.applicationMenu.getMenuItemById( + 'File.Export current part' + ) + if (!openProject) { + throw new Error('File.Export current part') + } + openProject.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Export' + expect(actual).toBe(expected) + }) + test('Modeling.File.Share current part (via Zoo link)', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = app.applicationMenu.getMenuItemById( + 'File.Share current part (via Zoo link)' + ) + if (!openProject) { + throw new Error('File.Share current part (via Zoo link)') + } + openProject.click() + }) + + const textToCheck = + 'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!' + // Check if text appears anywhere in the page + const isTextVisible = page.getByText(textToCheck) + + await expect(isTextVisible).toBeVisible({ timeout: 10000 }) + }) + test('Modeling.File.Preferences.Project settings', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = app.applicationMenu.getMenuItemById( + 'File.Preferences.Project settings' + ) + if (!openProject) { + throw new Error('File.Preferences.Project settings') + } + openProject.click() + }) + + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + // You are viewing the user tab + const actualText = settings.getByText( + 'The hue of the primary theme color for the app' + ) + await expect(actualText).toBeVisible() + }) + test('Modeling.File.Preferences.User settings', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const userSettings = app.applicationMenu.getMenuItemById( + 'File.Preferences.User settings' + ) + if (!userSettings) { + throw new Error('File.Preferences.User settings') + } + userSettings.click() + }) + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + // You are viewing the user tab + const actualText = settings.getByText( + 'The overall appearance of the app' + ) + await expect(actualText).toBeVisible() + }) + test('Modeling.File.Preferences.Keybindings', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const keybindings = app.applicationMenu.getMenuItemById( + 'File.Preferences.Keybindings' + ) + if (!keybindings) { + throw new Error('File.Preferences.Keybindings') + } + keybindings.click() + }) + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + // You are viewing the keybindings tab + const enterSketchMode = settings.locator('#enter-sketch-mode') + await expect(enterSketchMode).toBeVisible() + }) + test('Modeling.File.Preferences.User default units', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'File.Preferences.User default units' + ) + if (!menu) { + throw new Error('File.Preferences.User default units') + } + menu.click() + }) + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + const defaultUnit = settings.locator('#defaultUnit') + await expect(defaultUnit).toBeVisible() + }) + test('Modeling.File.Preferences.Theme', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'File.Preferences.Theme' + ) + if (!menu) { + throw new Error('File.Preferences.Theme') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Settings · app · theme' + expect(actual).toBe(expected) + }) + test('Modeling.File.Preferences.Theme color', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'File.Preferences.Theme color' + ) + if (!menu) { + throw new Error('File.Preferences.Theme color') + } + menu.click() + }) + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + const defaultUnit = settings.locator('#themeColor') + await expect(defaultUnit).toBeVisible() + }) + test('Modeling.File.Preferences.Sign out', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById('File.Sign out') + if (!menu) { + throw new Error('File.Sign out') + } + // FIXME: Add back when you can actually sign out + // menu.click() + }) + // FIXME: When signing out during E2E the page is not bound correctly. + // It cannot find the button + // const signIn = page.getByTestId('sign-in-button') + // await expect(signIn).toBeVisible() + }) + }) + test.describe('Edit role', () => { + test('Modeling.Edit.Modify with Zoo Text-To-CAD', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Edit.Modify with Zoo Text-To-CAD' + ) + if (!menu) { + throw new Error('Edit.Modify with Zoo Text-To-CAD') + } + menu.click() + }) + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Prompt-to-edit' + expect(actual).toBe(expected) + }) + test('Modeling.Edit.Edit parameter', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Edit.Edit parameter' + ) + if (!menu) { + throw new Error('Edit.Edit parameter') + } + menu.click() + }) + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Edit parameter' + expect(actual).toBe(expected) + }) + test('Modeling.Edit.Format code', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById('Edit.Format code') + if (!menu) { + throw new Error('Edit.Format code') + } + // NO OP: Do not test that the code mirror will actually format the code. + // The format code happens, there is no UI. + // The actual business logic to test this feature should be in another E2E test. + // menu.click() + }) + }) + test('Modeling.Edit.Rename project', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Edit.Rename project' + ) + if (!menu) { + throw new Error('Edit.Rename project') + } + menu.click() + }) + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Rename project' + expect(actual).toBe(expected) + }) + test('Modeling.Edit.Delete project', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Edit.Delete project' + ) + if (!menu) { + throw new Error('Edit.Delete project') + } + menu.click() + }) + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Delete project' + expect(actual).toBe(expected) + }) + test('Modeling.Edit.Change project directory', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Edit.Change project directory' + ) + if (!menu) { + throw new Error('Edit.Change project directory') + } + menu.click() + }) + const settings = page.getByTestId('settings-dialog-panel') + await expect(settings).toBeVisible() + const projectDirectory = settings.locator('#projectDirectory') + await expect(projectDirectory).toBeVisible() + }) + }) + test.describe('View role', () => { + test('Modeling.View.Command Palette...', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Command Palette...' + ) + if (!menu) { + throw new Error('View.Command Palette...') + } + menu.click() + }) + // Check the placeholder project name exists + const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search') + await expect(actual).toBeVisible() + }) + test('Modeling.View.Orthographic view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Orthographic view' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const textToCheck = + 'Set camera projection to "orthographic" as a user default' + // Check if text appears anywhere in the page + const isTextVisible = page.getByText(textToCheck) + + await expect(isTextVisible).toBeVisible({ timeout: 10000 }) + }) + test('Modeling.View.Perspective view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Perspective view' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const textToCheck = + 'Set camera projection to "perspective" as a user default' + // Check if text appears anywhere in the page + const isTextVisible = page.getByText(textToCheck) + + await expect(isTextVisible).toBeVisible({ timeout: 10000 }) + }) + test('Modeling.View.Standard views.Right view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Right view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + + // TODO: Make all of these screenshot E2E tests. + // Wait for camera to move + // await page.waitForTimeout(5000) + + // const locator = page.getByTestId('gizmo').locator('canvas') + // const image = await locator.screenshot({ path: 'Modeling.View.Standard-views.Right-view.png' }); + // expect(image).toMatchSnapshot('Modeling.View.Standard-views.Right-view') + }) + test('Modeling.View.Standard views.Back view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Back view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Top view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Top view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Left view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Left view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Front view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Front view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Bottom view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Bottom view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Reset view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Reset view' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Center view on selection', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Center view on selection' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Standard views.Refresh', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Standard views.Refresh' + ) + if (!menu) { + throw new Error('menu missing') + } + // menu.click() + }) + }) + test('Modeling.View.Named views.Create named view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Named views.Create named view' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Create named view' + expect(actual).toBe(expected) + }) + test('Modeling.View.Named views.Load named view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Named views.Load named view' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Load named view' + expect(actual).toBe(expected) + }) + test('Modeling.View.Named views.Delete named view', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Named views.Delete named view' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Delete named view' + expect(actual).toBe(expected) + }) + test('Modeling.View.Panes.Feature tree', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Panes.Feature tree' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const button = page.getByTestId('feature-tree-pane-button') + const isPressed = await button.getAttribute('aria-pressed') + expect(isPressed).toBe('true') + }) + test('Modeling.View.Panes.KCL code', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Panes.KCL code' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const button = page.getByTestId('code-pane-button') + const isPressed = await button.getAttribute('aria-pressed') + expect(isPressed).toBe('true') + }) + test('Modeling.View.Panes.Project files', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Panes.Project files' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const button = page.getByTestId('files-pane-button') + const isPressed = await button.getAttribute('aria-pressed') + expect(isPressed).toBe('true') + }) + test('Modeling.View.Panes.Variables', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'View.Panes.Variables' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const button = page.getByTestId('variables-pane-button') + const isPressed = await button.getAttribute('aria-pressed') + expect(isPressed).toBe('true') + }) + test('Modeling.View.Panes.Logs', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById('View.Panes.Logs') + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + + const button = page.getByTestId('logs-pane-button') + const isPressed = await button.getAttribute('aria-pressed') + expect(isPressed).toBe('true') + }) + }) + test.describe('Design role', () => { + // TODO Start sketch + test('Modeling.Design.Create an offset plane', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create an offset plane' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Offset plane' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Create a helix', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create a helix' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Helix' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Create a parameter', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create a parameter' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Create parameter' + expect(actual).toBe(expected) + }) + + test('Modeling.Design.Create an additive feature.Extrude', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create an additive feature.Extrude' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Extrude' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Create an additive feature.Revolve', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create an additive feature.Revolve' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Revolve' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Create an additive feature.Sweep', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create an additive feature.Sweep' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Sweep' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Create an additive feature.Loft', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create an additive feature.Loft' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Loft' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Apply modification feature.Fillet', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Apply modification feature.Fillet' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Fillet' + expect(actual).toBe(expected) + }) + test('Modeling.Design.Apply modification feature.Chamfer', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Apply modification feature.Chamfer' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Chamfer' + expect(actual).toBe(expected) + }) + + test('Modeling.Design.Apply modification feature.Shell', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Apply modification feature.Shell' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Shell' + expect(actual).toBe(expected) + }) + + test('Modeling.Design.Create with Zoo Text-To-CAD', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Create with Zoo Text-To-CAD' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Text-to-CAD' + expect(actual).toBe(expected) + }) + + test('Modeling.Design.Modify with Zoo Text-To-CAD', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const menu = app.applicationMenu.getMenuItemById( + 'Design.Modify with Zoo Text-To-CAD' + ) + if (!menu) { + throw new Error('menu missing') + } + menu.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Prompt-to-edit' + expect(actual).toBe(expected) + }) + }) + test.describe('Help role', () => { + test('Modeling.Help.Show all commands', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) fail() + const menu = app.applicationMenu.getMenuItemById( + 'Help.Show all commands' + ) + if (!menu) fail() + menu.click() + }) + // Check the placeholder project name exists + const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search') + await expect(actual).toBeVisible() + }) + test('Modeling.Help.KCL code samples', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) fail() + const menu = app.applicationMenu.getMenuItemById( + 'Help.KCL code samples' + ) + if (!menu) fail() + }) + }) + test('Modeling.Help.Refresh and report a bug', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + toolbar, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) fail() + const menu = app.applicationMenu.getMenuItemById( + 'Help.Refresh and report a bug' + ) + if (!menu) fail() + menu.click() + }) + // Core dump and refresh magic number timeout + await scene.waitForExecutionDone() + await expect(toolbar.startSketchBtn).toBeVisible() + }) + test('Modeling.Help.Reset onboarding', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + // Run electron snippet to find the Menu! await page.waitForTimeout(100) // wait for createModelingPageMenu() to run await tronApp.electron.evaluate(async ({ app }) => { diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index ed77e695774..0d5b7671827 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -11,6 +11,8 @@ import type { } from 'xstate' import { fromPromise } from 'xstate' +import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' +import { useMenuListener } from '@src/hooks/useMenu' import { newKclFile } from '@src/lang/project' import { createNamedViewsCommand } from '@src/lib/commandBarConfigs/namedViewsConfig' import { createRouteCommands } from '@src/lib/commandBarConfigs/routeCommandConfig' @@ -34,6 +36,7 @@ import { type IndexLoaderData } from '@src/lib/types' import { useSettings, useToken } from '@src/machines/appMachine' import { commandBarActor } from '@src/machines/commandBarMachine' import { fileMachine } from '@src/machines/fileMachine' +import { modelingMenuCallbackMostActions } from '@src/menu/register' type MachineContext = { state: StateFrom @@ -60,6 +63,14 @@ export const FileMachineProvider = ({ [] ) + const filePath = useAbsoluteFilePath() + // Only create the native file menus on desktop + useEffect(() => { + if (isDesktop()) { + window.electron.createModelingPageMenu().catch(reportRejection) + } + }, []) + // Only create the native file menus on desktop useEffect(() => { if (isDesktop()) { @@ -414,6 +425,15 @@ export const FileMachineProvider = ({ } ) + const cb = modelingMenuCallbackMostActions( + settings, + navigate, + filePath, + project, + token + ) + useMenuListener(cb) + const kclCommandMemo = useMemo( () => kclCommands({ diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 6736926642b..846353c83ef 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -19,6 +19,7 @@ import type { import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Plane } from '@rust/kcl-lib/bindings/Plane' +import { useAppState } from '@src/AppState' import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls' import { SEGMENT_BODIES, @@ -26,6 +27,7 @@ import { } from '@src/clientSideScene/sceneConstants' import type { MachineManager } from '@src/components/MachineManagerProvider' import { MachineManagerContext } from '@src/components/MachineManagerProvider' +import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes' import { applyConstraintIntersect } from '@src/components/Toolbar/Intersect' import { applyConstraintAbsDistance } from '@src/components/Toolbar/SetAbsDistance' import { @@ -38,8 +40,14 @@ import { applyConstraintLength, } from '@src/components/Toolbar/setAngleLength' import { useFileContext } from '@src/hooks/useFileContext' +import { + useMenuListener, + useSketchModeMenuEnableDisable, +} from '@src/hooks/useMenu' +import { useNetworkContext } from '@src/hooks/useNetworkContext' import { useSetupEngineManager } from '@src/hooks/useSetupEngineManager' import useStateMachineCommands from '@src/hooks/useStateMachineCommands' +import { useKclContext } from '@src/lang/KclProvider' import { updateModelingState } from '@src/lang/modelingWorkflows' import { insertNamedConstant, @@ -111,6 +119,7 @@ import { modelingMachine, modelingMachineDefaultContext, } from '@src/machines/modelingMachine' +import type { WebContentSendPayload } from '@src/menu/channels' export const ModelingMachineContext = createContext( {} as { @@ -1756,6 +1765,142 @@ export const ModelingMachineProvider = ({ } ) + // Register file menu actions based off modeling send + const cb = (data: WebContentSendPayload) => { + const openPanes = modelingActor.getSnapshot().context.store.openPanes + if (data.menuLabel === 'View.Panes.Feature tree') { + const featureTree: SidebarType = 'feature-tree' + const alwaysAddFeatureTree: SidebarType[] = [ + ...new Set([...openPanes, featureTree]), + ] + modelingSend({ + type: 'Set context', + data: { + openPanes: alwaysAddFeatureTree, + }, + }) + } else if (data.menuLabel === 'View.Panes.KCL code') { + const code: SidebarType = 'code' + const alwaysAddCode: SidebarType[] = [...new Set([...openPanes, code])] + modelingSend({ + type: 'Set context', + data: { + openPanes: alwaysAddCode, + }, + }) + } else if (data.menuLabel === 'View.Panes.Project files') { + const projectFiles: SidebarType = 'files' + const alwaysAddProjectFiles: SidebarType[] = [ + ...new Set([...openPanes, projectFiles]), + ] + modelingSend({ + type: 'Set context', + data: { + openPanes: alwaysAddProjectFiles, + }, + }) + } else if (data.menuLabel === 'View.Panes.Variables') { + const variables: SidebarType = 'variables' + const alwaysAddVariables: SidebarType[] = [ + ...new Set([...openPanes, variables]), + ] + modelingSend({ + type: 'Set context', + data: { + openPanes: alwaysAddVariables, + }, + }) + } else if (data.menuLabel === 'View.Panes.Logs') { + const logs: SidebarType = 'logs' + const alwaysAddLogs: SidebarType[] = [...new Set([...openPanes, logs])] + modelingSend({ + type: 'Set context', + data: { + openPanes: alwaysAddLogs, + }, + }) + } else if (data.menuLabel === 'Design.Start sketch') { + modelingSend({ + type: 'Enter sketch', + data: { forceNewSketch: true }, + }) + } + } + useMenuListener(cb) + + const { overallState } = useNetworkContext() + const { isExecuting } = useKclContext() + const { isStreamReady } = useAppState() + + // Assumes all commands are network commands + useSketchModeMenuEnableDisable( + modelingState.context.currentMode, + overallState, + isExecuting, + isStreamReady, + [ + { menuLabel: 'Edit.Modify with Zoo Text-To-CAD' }, + { menuLabel: 'View.Standard views' }, + { menuLabel: 'View.Named views' }, + { menuLabel: 'Design.Start sketch' }, + { + menuLabel: 'Design.Create an offset plane', + commandName: 'Offset plane', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create a helix', + commandName: 'Helix', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create an additive feature.Extrude', + commandName: 'Extrude', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create an additive feature.Revolve', + commandName: 'Revolve', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create an additive feature.Sweep', + commandName: 'Sweep', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create an additive feature.Loft', + commandName: 'Loft', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Apply modification feature.Fillet', + commandName: 'Fillet', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Apply modification feature.Chamfer', + commandName: 'Chamfer', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Apply modification feature.Shell', + commandName: 'Shell', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Create with Zoo Text-To-CAD', + commandName: 'Text-to-CAD', + groupId: 'modeling', + }, + { + menuLabel: 'Design.Modify with Zoo Text-To-CAD', + commandName: 'Prompt-to-edit', + groupId: 'modeling', + }, + ] + ) + // Add debug function to window object useEffect(() => { // @ts-ignore - we're intentionally adding this to window diff --git a/src/hooks/useMenu.ts b/src/hooks/useMenu.ts index 4842ae81edb..2e7813d6af4 100644 --- a/src/hooks/useMenu.ts +++ b/src/hooks/useMenu.ts @@ -1,7 +1,10 @@ -import { useEffect } from 'react' - +import { NetworkHealthState } from '@src/hooks/useNetworkStatus' import { isDesktop } from '@src/lib/isDesktop' -import type { WebContentSendPayload } from '@src/menu/channels' +import type { ToolbarModeName } from '@src/lib/toolbar' +import { reportRejection } from '@src/lib/trap' +import { useCommandBarState } from '@src/machines/commandBarMachine' +import type { MenuLabels, WebContentSendPayload } from '@src/menu/channels' +import { useEffect } from 'react' export function useMenuListener( callback: (data: WebContentSendPayload) => void @@ -23,3 +26,70 @@ export function useMenuListener( } }, []) } + +// Enable disable menu actions specifically based on if you are in the modeling mode of sketching or modeling. +// This is a similar behavior of the command bar which disables action if you are in sketch mode +export function useSketchModeMenuEnableDisable( + currentMode: ToolbarModeName, + overallState: NetworkHealthState, + isExecuting: boolean, + isStreamReady: boolean, + menus: { menuLabel: MenuLabels; commandName?: string; groupId?: string }[] +) { + const commandBarState = useCommandBarState() + const commands = commandBarState.context.commands + + useEffect(() => { + const onDesktop = isDesktop() + if (!onDesktop) { + // NO OP for web + return + } + + // Same exact logic as the command bar + const disableAllButtons = + (overallState !== NetworkHealthState.Ok && + overallState !== NetworkHealthState.Weak) || + isExecuting || + !isStreamReady + + // Enable or disable each menu based on the state of the application. + menus.forEach(({ menuLabel, commandName, groupId }) => { + // If someone goes wrong, disable all the buttons! Engine cannot take this request + if (disableAllButtons) { + window.electron.disableMenu(menuLabel).catch(reportRejection) + return + } + + if (commandName && groupId) { + // If your menu is tied to a command bar action, see if the command exists in the command bar + const foundCommand = commands.find((command) => { + return command.name === commandName && command.groupId === groupId + }) + if (!foundCommand) { + window.electron.disableMenu(menuLabel).catch(reportRejection) + } else { + if (currentMode === 'sketching') { + window.electron.disableMenu(menuLabel).catch(reportRejection) + } else if (currentMode === 'modeling') { + window.electron.enableMenu(menuLabel).catch(reportRejection) + } + } + } else { + // menu is not tied to a command bar, do the sketch mode check + if (currentMode === 'sketching') { + window.electron.disableMenu(menuLabel).catch(reportRejection) + } else if (currentMode === 'modeling') { + window.electron.enableMenu(menuLabel).catch(reportRejection) + } + } + }) + + return () => { + if (!onDesktop) { + // NO OP for web + return + } + } + }, [currentMode, commands]) +} diff --git a/src/menu.ts b/src/menu.ts index c3a47557a3a..c6275dc7287 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -1,15 +1,27 @@ +import { modelingDesignRole } from '@src/menu/designRole' +import { modelingEditRole, projectEditRole } from '@src/menu/editRole' +import { modelingFileRole, projectFileRole } from '@src/menu/fileRole' +import { helpRole } from '@src/menu/helpRole' +import type { ZooMenuItemConstructorOptions } from '@src/menu/roles' +import { modelingViewRole, projectViewRole } from '@src/menu/viewRole' import type { BrowserWindow } from 'electron' import { Menu, app } from 'electron' import os from 'node:os' -import { projectEditRole } from '@src/menu/editRole' -import { projectFileRole } from '@src/menu/fileRole' -import { helpRole } from '@src/menu/helpRole' -import type { ZooMenuItemConstructorOptions } from '@src/menu/roles' -import { projectViewRole } from '@src/menu/viewRole' - const isMac = os.platform() === 'darwin' +/** + * Gotcha + * If you call Menu.setApplicationMenu([,,,]) on Mac, it will turn into + * you need to create a new menu in the 0th index for the aka + * Menu.setApplicationMenu([,,,,]) + * If you do not do this, will not show up as file. It will be the and it contents live under that Menu + * The .setApplicationMenu does not tell you that the 0th index forces it to on Mac. + */ +function zooSetApplicationMenu(menu: Electron.Menu) { + Menu.setApplicationMenu(menu) +} + // Default electron menu. export function buildAndSetMenuForFallback(mainWindow: BrowserWindow) { const templateMac: ZooMenuItemConstructorOptions[] = [ @@ -121,22 +133,60 @@ export function buildAndSetMenuForFallback(mainWindow: BrowserWindow) { if (isMac) { const menu = Menu.buildFromTemplate(templateMac) - Menu.setApplicationMenu(menu) + zooSetApplicationMenu(menu) } else { const menu = Menu.buildFromTemplate(templateNotMac) - Menu.setApplicationMenu(menu) + zooSetApplicationMenu(menu) } } + +function appMenuMacOnly() { + let extraBits: ZooMenuItemConstructorOptions[] = [] + if (isMac) { + extraBits = [ + { + // @ts-ignore This is required for Mac's it will show the app name first. This is safe to ts-ignore, it is a string. + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }, + ] + } + return extraBits +} + // This will generate a new menu from the initial state // All state management from the previous menu is going to be lost. export function buildAndSetMenuForModelingPage(mainWindow: BrowserWindow) { - return buildAndSetMenuForFallback(mainWindow) + const template = [ + // Expand empty elements for environments that are not Mac + ...appMenuMacOnly(), + modelingFileRole(mainWindow), + modelingEditRole(mainWindow), + modelingViewRole(mainWindow), + modelingDesignRole(mainWindow), + // Help role is the same for all pages + helpRole(mainWindow), + ] + const menu = Menu.buildFromTemplate(template) + zooSetApplicationMenu(menu) } // This will generate a new menu from the initial state // All state management from the previous menu is going to be lost. export function buildAndSetMenuForProjectPage(mainWindow: BrowserWindow) { const template = [ + // Expand empty elements for environments that are not Mac + ...appMenuMacOnly(), projectFileRole(mainWindow), projectEditRole(mainWindow), projectViewRole(mainWindow), @@ -144,7 +194,7 @@ export function buildAndSetMenuForProjectPage(mainWindow: BrowserWindow) { helpRole(mainWindow), ] const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) + zooSetApplicationMenu(menu) } // Try to enable the menu based on the application menu diff --git a/src/menu/channels.ts b/src/menu/channels.ts index 84a96d394e2..39564033326 100644 --- a/src/menu/channels.ts +++ b/src/menu/channels.ts @@ -7,6 +7,12 @@ export type MenuLabels = | 'Help.Command Palette...' | 'Help.Refresh and report a bug' | 'Help.Reset onboarding' + | 'Edit.Rename project' + | 'Edit.Delete project' + | 'Edit.Change project directory' + | 'Edit.Modify with Zoo Text-To-CAD' + | 'Edit.Edit parameter' + | 'Edit.Format code' | 'File.New project' | 'File.Open project' | 'File.Import file from URL' @@ -16,10 +22,50 @@ export type MenuLabels = | 'File.Preferences.Theme' | 'File.Preferences.Theme color' | 'File.Sign out' - | 'Edit.Rename project' - | 'Edit.Delete project' - | 'Edit.Change project directory' + | 'File.Create new file' + | 'File.Create new folder' + | 'File.Load a sample model' + | 'File.Export current part' + | 'File.Share current part (via Zoo link)' + | 'File.Preferences.Project settings' + | 'Design.Start sketch' + | 'Design.Create an offset plane' + | 'Design.Create a helix' + | 'Design.Create a parameter' + | 'Design.Create an additive feature.Extrude' + | 'Design.Create an additive feature.Revolve' + | 'Design.Create an additive feature.Sweep' + | 'Design.Create an additive feature.Loft' + | 'Design.Apply modification feature.Fillet' + | 'Design.Apply modification feature.Chamfer' + | 'Design.Apply modification feature.Shell' + | 'Design.Create with Zoo Text-To-CAD' + | 'Design.Modify with Zoo Text-To-CAD' | 'View.Command Palette...' + | 'View.Orthographic view' + | 'View.Perspective view' + | 'View.Standard views.Right view' + | 'View.Standard views.Back view' + | 'View.Standard views.Top view' + | 'View.Standard views.Left view' + | 'View.Standard views.Front view' + | 'View.Standard views.Bottom view' + | 'View.Standard views.Reset view' + | 'View.Standard views.Center view on selection' + | 'View.Standard views.Refresh' + | 'View.Named views.Create named view' + | 'View.Named views.Load named view' + | 'View.Named views.Delete named view' + | 'View.Panes.Feature tree' + | 'View.Panes.KCL code' + | 'View.Panes.Project files' + | 'View.Panes.Variables' + | 'View.Panes.Logs' + | 'View.Panes.Debug' + | 'View.Standard views' + | 'View.Named views' + | 'Design.Create an additive feature' + | 'Design.Apply modification feature' export type WebContentSendPayload = { menuLabel: MenuLabels diff --git a/src/menu/designRole.ts b/src/menu/designRole.ts new file mode 100644 index 00000000000..c807d1bb575 --- /dev/null +++ b/src/menu/designRole.ts @@ -0,0 +1,145 @@ +import { typeSafeWebContentsSend } from '@src/menu/channels' +import type { ZooMenuItemConstructorOptions } from '@src/menu/roles' +import type { BrowserWindow } from 'electron' + +export const modelingDesignRole = ( + mainWindow: BrowserWindow +): ZooMenuItemConstructorOptions => { + return { + label: 'Design', + submenu: [ + { + label: 'Start sketch', + id: 'Design.Start sketch', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Start sketch', + }) + }, + }, + { type: 'separator' }, + { + label: 'Create an offset plane', + id: 'Design.Create an offset plane', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create an offset plane', + }) + }, + }, + { + label: 'Create a helix', + id: 'Design.Create a helix', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create a helix', + }) + }, + }, + { + label: 'Create a parameter', + id: 'Design.Create a parameter', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create a parameter', + }) + }, + }, + { type: 'separator' }, + { + label: 'Create an additive feature', + id: 'Design.Create an additive feature', + submenu: [ + { + label: 'Extrude', + id: 'Design.Create an additive feature.Extrude', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create an additive feature.Extrude', + }) + }, + }, + { + label: 'Revolve', + id: 'Design.Create an additive feature.Revolve', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create an additive feature.Revolve', + }) + }, + }, + { + label: 'Sweep', + id: 'Design.Create an additive feature.Sweep', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create an additive feature.Sweep', + }) + }, + }, + { + label: 'Loft', + id: 'Design.Create an additive feature.Loft', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create an additive feature.Loft', + }) + }, + }, + ], + }, + { + label: 'Apply modification feature', + id: 'Design.Apply modification feature', + submenu: [ + { + label: 'Fillet', + id: 'Design.Apply modification feature.Fillet', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Apply modification feature.Fillet', + }) + }, + }, + { + label: 'Chamfer', + id: 'Design.Apply modification feature.Chamfer', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Apply modification feature.Chamfer', + }) + }, + }, + { + label: 'Shell', + id: 'Design.Apply modification feature.Shell', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Apply modification feature.Shell', + }) + }, + }, + ], + }, + { type: 'separator' }, + { + label: 'Create with Zoo Text-To-CAD', + id: 'Design.Create with Zoo Text-To-CAD', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Create with Zoo Text-To-CAD', + }) + }, + }, + { + label: 'Modify with Zoo Text-To-CAD', + id: 'Design.Modify with Zoo Text-To-CAD', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Design.Modify with Zoo Text-To-CAD', + }) + }, + }, + ], + } +} diff --git a/src/menu/editRole.ts b/src/menu/editRole.ts index 1d2f42059cb..0b5389da41b 100644 --- a/src/menu/editRole.ts +++ b/src/menu/editRole.ts @@ -68,3 +68,94 @@ export const projectEditRole = ( ], } } + +export const modelingEditRole = ( + mainWindow: BrowserWindow +): ZooMenuItemConstructorOptions => { + let extraBits: ZooMenuItemConstructorOptions[] = [ + { role: 'delete' }, + { type: 'separator' }, + { role: 'selectAll' }, + ] + if (isMac) { + extraBits = [ + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }], + }, + ] + } + return { + label: 'Edit', + submenu: [ + { + label: 'Modify with Zoo Text-To-CAD', + id: 'Edit.Modify with Zoo Text-To-CAD', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Modify with Zoo Text-To-CAD', + }) + }, + }, + { + label: 'Edit parameter', + id: 'Edit.Edit parameter', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Edit parameter', + }) + }, + }, + { + label: 'Format code', + id: 'Edit.Format code', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Format code', + }) + }, + }, + { type: 'separator' }, + { + label: 'Rename project', + id: 'Edit.Rename project', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Rename project', + }) + }, + }, + { + label: 'Delete project', + id: 'Edit.Delete project', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Delete project', + }) + }, + }, + { type: 'separator' }, + { + label: 'Change project directory', + id: 'Edit.Change project directory', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'Edit.Change project directory', + }) + }, + }, + { type: 'separator' }, + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...extraBits, + ], + } +} diff --git a/src/menu/fileRole.ts b/src/menu/fileRole.ts index 4c2e0e63b6b..df9629a3788 100644 --- a/src/menu/fileRole.ts +++ b/src/menu/fileRole.ts @@ -100,3 +100,153 @@ export const projectFileRole = ( ], } } + +export const modelingFileRole = ( + mainWindow: BrowserWindow +): ZooMenuItemConstructorOptions => { + return { + label: 'File', + submenu: [ + // TODO: Once a safe command bar create new file and folder is implemented we can turn these on + // { + // label: 'Create new file', + // click: () => { + // typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + // menuLabel: 'File.Create new file', + // }) + // }, + // }, + // { + // label: 'Create new folder', + // click: () => { + // typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + // menuLabel: 'File.Create new folder', + // }) + // }, + // }, + { + label: 'New project', + id: 'File.New project', + accelerator: 'CommandOrControl+N', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.New project', + }) + }, + }, + { + label: 'Open project', + id: 'File.Open project', + accelerator: 'CommandOrControl+P', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Open project', + }) + }, + }, + // TODO https://www.electronjs.org/docs/latest/tutorial/recent-documents + // Appears to be only Windows and Mac OS specific. Linux does not have support + { type: 'separator' }, + { + label: 'Load a sample model', + id: 'File.Load a sample model', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Load a sample model', + }) + }, + }, + { type: 'separator' }, + { + label: 'Export current part', + id: 'File.Export current part', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Export current part', + }) + }, + }, + { + label: 'Share current part (via Zoo link)', + id: 'File.Share current part (via Zoo link)', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Share current part (via Zoo link)', + }) + }, + }, + { type: 'separator' }, + { + label: 'Preferences', + submenu: [ + { + label: 'Project settings', + id: 'File.Preferences.Project settings', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.Project settings', + }) + }, + }, + { + label: 'User settings', + id: 'File.Preferences.User settings', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.User settings', + }) + }, + }, + { + label: 'Keybindings', + id: 'File.Preferences.Keybindings', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.Keybindings', + }) + }, + }, + { + label: 'User default units', + id: 'File.Preferences.User default units', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.User default units', + }) + }, + }, + { + label: 'Theme', + id: 'File.Preferences.Theme', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.Theme', + }) + }, + }, + { + label: 'Theme color', + id: 'File.Preferences.Theme color', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Preferences.Theme color', + }) + }, + }, + ], + }, + { type: 'separator' }, + // Last in list + { + label: 'Sign out', + id: 'File.Sign out', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Sign out', + }) + }, + }, + isMac ? { role: 'close' } : { role: 'quit' }, + ], + } +} diff --git a/src/menu/register.ts b/src/menu/register.ts new file mode 100644 index 00000000000..8778c0db72a --- /dev/null +++ b/src/menu/register.ts @@ -0,0 +1,264 @@ +import { AxisNames } from '@src/lib/constants' +import { copyFileShareLink } from '@src/lib/links' +import { PATHS } from '@src/lib/paths' +import type { Project } from '@src/lib/project' +import type { SettingsType } from '@src/lib/settings/initialSettings' +import { + codeManager, + engineCommandManager, + sceneInfra, +} from '@src/lib/singletons' +import { reportRejection } from '@src/lib/trap' +import { uuidv4 } from '@src/lib/utils' +import { authActor, settingsActor } from '@src/machines/appMachine' +import { commandBarActor } from '@src/machines/commandBarMachine' +import type { WebContentSendPayload } from '@src/menu/channels' +import type { NavigateFunction } from 'react-router-dom' + +export function modelingMenuCallbackMostActions( + settings: SettingsType, + navigate: NavigateFunction, + filePath: string, + project: Project | undefined, + token: string | undefined +) { + // Menu listeners + const cb = (data: WebContentSendPayload) => { + if (data.menuLabel === 'File.New project') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'projects', + name: 'Create project', + argDefaultValues: { + name: settings.projects.defaultProjectName.current, + }, + }, + }) + } else if (data.menuLabel === 'File.Open project') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'projects', + name: 'Open project', + }, + }) + } else if (data.menuLabel === 'Edit.Rename project') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'projects', + name: 'Rename project', + }, + }) + } else if (data.menuLabel === 'Edit.Delete project') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'projects', + name: 'Delete project', + }, + }) + } else if (data.menuLabel === 'File.Preferences.User settings') { + navigate(filePath + PATHS.SETTINGS_USER) + } else if (data.menuLabel === 'File.Preferences.Keybindings') { + navigate(filePath + PATHS.SETTINGS_KEYBINDINGS) + } else if (data.menuLabel === 'Edit.Change project directory') { + navigate(filePath + PATHS.SETTINGS_USER + '#projectDirectory') + } else if (data.menuLabel === 'File.Preferences.Project settings') { + navigate(filePath + PATHS.SETTINGS_PROJECT) + } else if (data.menuLabel === 'File.Sign out') { + authActor.send({ type: 'Log out' }) + } else if ( + data.menuLabel === 'View.Command Palette...' || + data.menuLabel === 'Help.Command Palette...' + ) { + commandBarActor.send({ type: 'Open' }) + } else if (data.menuLabel === 'File.Preferences.Theme') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'settings', + name: 'app.theme', + }, + }) + } else if (data.menuLabel === 'File.Preferences.Theme color') { + navigate(filePath + PATHS.SETTINGS_USER + '#themeColor') + } else if (data.menuLabel === 'File.Share current part (via Zoo link)') { + copyFileShareLink({ + token: token ?? '', + code: codeManager.code, + name: project?.name || '', + }).catch(reportRejection) + } else if (data.menuLabel === 'File.Preferences.User default units') { + navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit') + } else if (data.menuLabel === 'File.Export current part') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'modeling', + name: 'Export', + }, + }) + } else if (data.menuLabel === 'File.Load a sample model') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'code', + name: 'open-kcl-example', + }, + }) + } else if (data.menuLabel === 'File.Create new file') { + // NO OP. A safe command bar create new file is not implemented yet. + } else if (data.menuLabel === 'Edit.Modify with Zoo Text-To-CAD') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Prompt-to-edit', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Edit.Edit parameter') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'event.parameter.edit', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Edit.Format code') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'format-code', groupId: 'code' }, + }) + } else if (data.menuLabel === 'View.Orthographic view') { + settingsActor.send({ + type: 'set.modeling.cameraProjection', + data: { + level: 'user', + value: 'orthographic', + }, + }) + } else if (data.menuLabel === 'View.Perspective view') { + settingsActor.send({ + type: 'set.modeling.cameraProjection', + data: { + level: 'user', + value: 'perspective', + }, + }) + } else if (data.menuLabel === 'View.Standard views.Right view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.X) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Back view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.Y) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Top view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.Z) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Left view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.NEG_X) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Front view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.NEG_Y) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Bottom view') { + sceneInfra.camControls + .updateCameraToAxis(AxisNames.NEG_Z) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Reset view') { + sceneInfra.camControls.resetCameraPosition().catch(reportRejection) + } else if ( + data.menuLabel === 'View.Standard views.Center view on selection' + ) { + // Gotcha: out of band from modelingMachineProvider, has no state or extra workflows. I am taking the function's logic and porting it here. + engineCommandManager + .sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_center_to_selection', + camera_movement: 'vantage', + }, + }) + .catch(reportRejection) + } else if (data.menuLabel === 'View.Standard views.Refresh') { + globalThis?.window?.location.reload() + } else if (data.menuLabel === 'View.Named views.Create named view') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Create named view', groupId: 'namedViews' }, + }) + } else if (data.menuLabel === 'View.Named views.Load named view') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Load named view', groupId: 'namedViews' }, + }) + } else if (data.menuLabel === 'View.Named views.Delete named view') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Delete named view', groupId: 'namedViews' }, + }) + } else if (data.menuLabel === 'Design.Create an offset plane') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Offset plane', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create a helix') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Helix', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create a parameter') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'event.parameter.create', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create an additive feature.Extrude') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Extrude', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create an additive feature.Revolve') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Revolve', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create an additive feature.Sweep') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Sweep', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create an additive feature.Loft') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Loft', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Apply modification feature.Fillet') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Fillet', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Apply modification feature.Chamfer') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Chamfer', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Apply modification feature.Shell') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Shell', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Create with Zoo Text-To-CAD') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Text-to-CAD', groupId: 'modeling' }, + }) + } else if (data.menuLabel === 'Design.Modify with Zoo Text-To-CAD') { + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Prompt-to-edit', groupId: 'modeling' }, + }) + } + } + return cb +} diff --git a/src/menu/roles.ts b/src/menu/roles.ts index b04daa4d7af..80430a1da7a 100644 --- a/src/menu/roles.ts +++ b/src/menu/roles.ts @@ -21,6 +21,12 @@ type FileRoleLabel = | 'Sign out' | 'Theme' | 'Theme color' + | 'Export current part' + | 'Create new file' + | 'Create new folder' + | 'Share current part (via Zoo link)' + | 'Project settings' + | 'Load a sample model' | 'User default units' type EditRoleLabel = @@ -28,6 +34,9 @@ type EditRoleLabel = | 'Delete project' | 'Change project directory' | 'Speech' + | 'Edit parameter' + | 'Modify with Zoo Text-To-CAD' + | 'Format code' type HelpRoleLabel = | 'Refresh and report a bug' @@ -42,7 +51,49 @@ type HelpRoleLabel = | 'Get started with Text-to-CAD' | 'Show all commands' -type ViewRoleLabel = 'Command Palette...' | 'Appearance' +type ViewRoleLabel = + | 'Command Palette...' + | 'Appearance' + | 'Panes' + | 'Feature tree' + | 'KCL code' + | 'Project files' + | 'Variables' + | 'Logs' + | 'Debug' + | 'Standard views' + | 'Orthographic view' + | 'Perspective view' + | 'Right view' + | 'Back view' + | 'Top view' + | 'Left view' + | 'Front view' + | 'Bottom view' + | 'Reset view' + | 'Center view on selection' + | 'Refresh' + | 'Named views' + | 'Create named view' + | 'Load named view' + | 'Delete named view' + +type DesignRoleLabel = + | 'Design' + | 'Create a parameter' + | 'Create with Zoo Text-To-CAD' + | 'Start sketch' + | 'Create an offset plane' + | 'Create a helix' + | 'Create an additive feature' + | 'Extrude' + | 'Revolve' + | 'Sweep' + | 'Loft' + | 'Apply modification feature' + | 'Fillet' + | 'Chamfer' + | 'Shell' // Only export the union of all the internal types since they are all labels // The internal types are only for readability within the file @@ -52,6 +103,7 @@ export type ZooLabel = | EditRoleLabel | HelpRoleLabel | ViewRoleLabel + | DesignRoleLabel // Extend the interface with additional custom properties export interface ZooMenuItemConstructorOptions diff --git a/src/menu/viewRole.ts b/src/menu/viewRole.ts index ea901b71696..ffd68309830 100644 --- a/src/menu/viewRole.ts +++ b/src/menu/viewRole.ts @@ -47,3 +47,244 @@ export const projectViewRole = ( ], } } + +export const modelingViewRole = ( + mainWindow: BrowserWindow +): ZooMenuItemConstructorOptions => { + let extraBits: ZooMenuItemConstructorOptions[] = [{ role: 'close' }] + if (isMac) { + extraBits = [ + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, + { role: 'window' }, + ] + } + return { + label: 'View', + submenu: [ + { + label: 'Command Palette...', + id: 'View.Command Palette...', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Command Palette...', + }) + }, + }, + { type: 'separator' }, + { + label: 'Orthographic view', + id: 'View.Orthographic view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Orthographic view', + }) + }, + }, + { + label: 'Perspective view', + id: 'View.Perspective view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Perspective view', + }) + }, + }, + { type: 'separator' }, + { + label: 'Standard views', + id: 'View.Standard views', + submenu: [ + { + label: 'Right view', + id: 'View.Standard views.Right view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Right view', + }) + }, + }, + { + label: 'Back view', + id: 'View.Standard views.Back view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Back view', + }) + }, + }, + + { + label: 'Top view', + id: 'View.Standard views.Top view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Top view', + }) + }, + }, + + { + label: 'Left view', + id: 'View.Standard views.Left view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Left view', + }) + }, + }, + + { + label: 'Front view', + id: 'View.Standard views.Front view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Front view', + }) + }, + }, + + { + label: 'Bottom view', + id: 'View.Standard views.Bottom view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Bottom view', + }) + }, + }, + { type: 'separator' }, + { + label: 'Reset view', + id: 'View.Standard views.Reset view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Reset view', + }) + }, + }, + + { + label: 'Center view on selection', + id: 'View.Standard views.Center view on selection', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Center view on selection', + }) + }, + }, + { + label: 'Refresh', + id: 'View.Standard views.Refresh', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Standard views.Refresh', + }) + }, + }, + ], + }, + { + label: 'Named views', + id: 'View.Named views', + submenu: [ + { + label: 'Create named view', + id: 'View.Named views.Create named view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Named views.Create named view', + }) + }, + }, + + { + label: 'Load named view', + id: 'View.Named views.Load named view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Named views.Load named view', + }) + }, + }, + + { + label: 'Delete named view', + id: 'View.Named views.Delete named view', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Named views.Delete named view', + }) + }, + }, + ], + }, + { type: 'separator' }, + { + label: 'Panes', + submenu: [ + { + label: 'Feature tree', + id: 'View.Panes.Feature tree', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Panes.Feature tree', + }) + }, + }, + { + label: 'KCL code', + id: 'View.Panes.KCL code', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Panes.KCL code', + }) + }, + }, + { + label: 'Project files', + id: 'View.Panes.Project files', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Panes.Project files', + }) + }, + }, + { + label: 'Variables', + id: 'View.Panes.Variables', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Panes.Variables', + }) + }, + }, + { + label: 'Logs', + id: 'View.Panes.Logs', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'View.Panes.Logs', + }) + }, + }, + ], + }, + { + label: 'Appearance', + submenu: [ + { role: 'togglefullscreen' }, + { type: 'separator' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { role: 'resetZoom' }, + ], + }, + { type: 'separator' }, + { role: 'minimize' }, + { role: 'zoom' }, + ...extraBits, + ], + } +}