diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts index cedfd683..f4f2e671 100644 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ b/arduino-ide-extension/src/browser/arduino-commands.ts @@ -24,20 +24,10 @@ export namespace ArduinoCommands { id: 'arduino-toggle-compile-for-debug' }; - export const SHOW_OPEN_CONTEXT_MENU: Command = { - id: 'arduino-show-open-context-menu', - label: 'Open Sketch', - category - }; - export const OPEN_FILE_NAVIGATOR: Command = { id: 'arduino-open-file-navigator' }; - export const OPEN_SKETCH: Command = { - id: 'arduino-open-file' - }; - /** * Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...) */ @@ -45,23 +35,6 @@ export namespace ArduinoCommands { id: 'arduino-open-sketch-files' }; - export const SAVE_SKETCH: Command = { - id: 'arduino-save-sketch' - }; - - export const SAVE_SKETCH_AS: Command = { - id: 'arduino-save-sketch-as' - }; - - export const NEW_SKETCH: Command = { - id: 'arduino-new-sketch', - label: 'New Sketch', - category - }; - export const NEW_SKETCH_TOOLBAR: Command = { - id: 'arduino-new-sketch-toolbar' - }; - export const OPEN_BOARDS_DIALOG: Command = { id: 'arduino-open-boards-dialog' }; diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 8c038b94..5805df8c 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -1,28 +1,24 @@ import * as React from 'react'; -import * as dateFormat from 'dateformat'; -import { remote } from 'electron'; import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { MessageService } from '@theia/core/lib/common/message-service'; import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { BoardsService, BoardsServiceClient, CoreService, Sketch, SketchesService, ToolOutputServiceClient } from '../common/protocol'; +import { BoardsService, BoardsServiceClient, CoreService, SketchesService, ToolOutputServiceClient } from '../common/protocol'; import { ArduinoCommands } from './arduino-commands'; import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; -import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath, notEmpty } from '@theia/core'; +import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; import { - ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution, + ContextMenuRenderer, StatusBar, StatusBarAlignment, FrontendApplicationContribution, FrontendApplication, KeybindingContribution, KeybindingRegistry, OpenerService, open } from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; -import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; -import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; import { MaybePromise } from '@theia/core/lib/common/types'; @@ -47,6 +43,7 @@ import { ConfigService } from '../common/protocol/config-service'; import { BoardsConfigStore } from './boards/boards-config-store'; import { MainMenuManager } from './menu/main-menu-manager'; import { FileSystemExt } from '../common/protocol/filesystem-ext'; +import { OpenSketch } from './contributions/open-sketch'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -209,24 +206,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut tooltip: 'Upload', priority: 2 }); - registry.registerItem({ - id: ArduinoCommands.NEW_SKETCH.id, - command: ArduinoCommands.NEW_SKETCH_TOOLBAR.id, - tooltip: 'New', - priority: 4 // Note: priority 3 was reserved by debug. - }); - registry.registerItem({ - id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id, - command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id, - tooltip: 'Open', - priority: 5 - }); - registry.registerItem({ - id: ArduinoCommands.SAVE_SKETCH.id, - command: ArduinoCommands.SAVE_SKETCH.id, - tooltip: 'Save', - priority: 6 - }); registry.registerItem({ id: BoardsToolBarItem.TOOLBAR_ID, render: () => ArduinoToolbar.is(widget) && widget.side === 'left', - execute: async (widget: Widget, target: EventTarget) => { - if (this.wsSketchCount) { - const el = (target as HTMLElement).parentElement; - if (el) { - this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, { - x: el.getBoundingClientRect().left, - y: el.getBoundingClientRect().top + el.offsetHeight - }); - } - } else { - this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id); - } - } - }); - registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, { execute: () => this.doOpenFile() }); - registry.registerCommand(ArduinoCommands.OPEN_SKETCH, { - execute: async (sketch: Sketch) => { - this.workspaceService.open(new URI(sketch.uri)); - } - }); - registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, { execute: async (uri: string) => { this.openSketchFiles(uri); } }); - registry.registerCommand(ArduinoCommands.SAVE_SKETCH, { - isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', - execute: (sketch: Sketch) => { - registry.executeCommand(CommonCommands.SAVE_ALL.id); - } - }); - - registry.registerCommand(ArduinoCommands.SAVE_SKETCH_AS, { - execute: async ({ execOnlyIfTemp }: { execOnlyIfTemp: boolean } = { execOnlyIfTemp: false }) => { - const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty); - if (!sketches.length) { - return; - } - if (sketches.length > 1) { - console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`); - } - const sketch = sketches[0]; - const isTemp = await this.sketchService.isTemp(sketch); - if (!isTemp && !!execOnlyIfTemp) { - return; - } - - // If target does not exist, propose a `directories.user`/${sketch.name} path - // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss} - const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri); - const exists = await this.fileSystem.exists(sketchDirUri.resolve(sketch.name).toString()); - const defaultUri = exists - ? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString()) - : sketchDirUri.resolve(sketch.name); - const defaultPath = await this.fileSystem.getFsPath(defaultUri.toString())!; - const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath }); - if (!filePath || canceled) { - return; - } - const destinationUri = await this.fileSystemExt.getUri(filePath); - if (!destinationUri) { - return; - } - const workspaceUri = await this.sketchService.copy(sketch, { destinationUri }); - if (workspaceUri) { - this.workspaceService.open(new URI(workspaceUri)); - } - } - }); - - registry.registerCommand(ArduinoCommands.NEW_SKETCH, { - execute: async () => { - try { - const sketch = await this.sketchService.createNewSketch(); - this.workspaceService.open(new URI(sketch.uri)); - } catch (e) { - await this.messageService.error(e.toString()); - } - } - }); - registry.registerCommand(ArduinoCommands.NEW_SKETCH_TOOLBAR, { - isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', - execute: async () => { - return registry.executeCommand(ArduinoCommands.NEW_SKETCH.id); - } - }); - registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { execute: async () => { const boardsConfig = await this.boardsConfigDialog.open(); @@ -516,17 +410,18 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut CommonCommands.TOGGLE_MAXIMIZED, FileNavigatorCommands.REVEAL_IN_NAVIGATOR ]) { - registry.unregisterMenuAction(command); + if (command) { } + // registry.unregisterMenuAction(command); } - registry.unregisterMenuAction(FileSystemCommands.UPLOAD); - registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD); + // registry.unregisterMenuAction(FileSystemCommands.UPLOAD); + // registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD); - registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER); - registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE); - registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE); - registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS); - registry.unregisterMenuAction(WorkspaceCommands.CLOSE); + // registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER); + // registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE); + // registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE); + // registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS); + // registry.unregisterMenuAction(WorkspaceCommands.CLOSE); registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION)); registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO)); @@ -549,10 +444,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut label: 'Upload', order: '3' }); - registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, { - commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id, - label: 'Open...' - }); registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools'); @@ -561,18 +452,10 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut label: 'Advanced Mode' }); - registry.registerMenuAction([...CommonMenus.FILE, '0_new_sketch'], { - commandId: ArduinoCommands.NEW_SKETCH.id - }); - registry.registerMenuAction([...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_cli'], { commandId: ArduinoCommands.OPEN_CLI_CONFIG.id }); - registry.registerMenuAction(CommonMenus.FILE_SAVE, { - commandId: ArduinoCommands.SAVE_SKETCH_AS.id, - label: 'Save As...' - }); } protected getMenuId(menuPath: string[]): string { @@ -591,14 +474,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut command: ArduinoCommands.UPLOAD.id, keybinding: 'CtrlCmd+Alt+U' }); - keybindings.registerKeybinding({ - command: ArduinoCommands.NEW_SKETCH.id, - keybinding: 'CtrlCmd+N' - }); - keybindings.registerKeybinding({ - command: ArduinoCommands.SAVE_SKETCH_AS.id, - keybinding: 'CtrlCmd+Shift+S' - }); } protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise { @@ -609,7 +484,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut id: 'openSketch' + sketch.name } this.commandRegistry.registerCommand(command, { - execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch) + execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) }); registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, { diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index f0b57c8f..0988fae2 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -79,6 +79,12 @@ import { ILogger } from '@theia/core'; import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext'; import { WorkspaceFrontendContribution, FileMenuContribution } from '@theia/workspace/lib/browser'; import { ArduinoWorkspaceFrontendContribution, ArduinoFileMenuContribution } from './customization/arduino-workspace-frontend-contribution'; +import { Contribution } from './contributions/contribution'; +import { NewSketch } from './contributions/new-sketch'; +import { OpenSketch } from './contributions/open-sketch'; +import { CloseSketch } from './contributions/close-sketch'; +import { SaveAsSketch } from './contributions/save-as-sketch'; +import { SaveSketch } from './contributions/save-sketch'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -298,4 +304,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un // File-system extension bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope(); + + Contribution.configure(bind, NewSketch); + Contribution.configure(bind, OpenSketch); + Contribution.configure(bind, CloseSketch); + Contribution.configure(bind, SaveSketch); + Contribution.configure(bind, SaveAsSketch); }); diff --git a/arduino-ide-extension/src/browser/contributions/close-sketch.ts b/arduino-ide-extension/src/browser/contributions/close-sketch.ts new file mode 100644 index 00000000..751ab02a --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/close-sketch.ts @@ -0,0 +1,49 @@ +import { injectable } from 'inversify'; +import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution'; +import { SaveAsSketch } from './save-as-sketch'; + +@injectable() +export class CloseSketch extends SketchContribution { + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, { + execute: async () => { + const sketch = await this.getCurrentSketch(); + if (!sketch) { + return; + } + const isTemp = await this.sketchService.isTemp(sketch); + if (isTemp) { + await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { openAfterMove: false, execOnlyIfTemp: true }); + await this.commandService.executeCommand(WorkspaceCommands.CLOSE.id); + } + } + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { + commandId: CloseSketch.Commands.CLOSE_SKETCH.id, + label: 'Close', + order: '5' + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: CloseSketch.Commands.CLOSE_SKETCH.id, + keybinding: 'CtrlCmd+W' // TODO: Windows binding? + }); + } + +} + +export namespace CloseSketch { + export namespace Commands { + export const CLOSE_SKETCH: Command = { + id: 'arduino-close-sketch' + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts new file mode 100644 index 00000000..73c693b0 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -0,0 +1,81 @@ +import { inject, injectable, interfaces } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { notEmpty } from '@theia/core/lib/common/objects'; +import { FileSystem } from '@theia/filesystem/lib/common'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu'; +import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command'; +import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol'; + +export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch }; + +@injectable() +export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution { + + @inject(ILogger) + protected readonly logger: ILogger; + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(CommandService) + protected readonly commandService: CommandService; + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + registerCommands(registry: CommandRegistry): void { + } + + registerMenus(registry: MenuModelRegistry): void { + } + + registerKeybindings(registry: KeybindingRegistry): void { + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + } + +} + +@injectable() +export abstract class SketchContribution extends Contribution { + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + @inject(FileSystemExt) + protected readonly fileSystemExt: FileSystemExt; + + @inject(ConfigService) + protected readonly configService: ConfigService; + + @inject(SketchesService) + protected readonly sketchService: SketchesService; + + protected async getCurrentSketch(): Promise { + const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty); + if (!sketches.length) { + return; + } + if (sketches.length > 1) { + console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`); + } + return sketches[0]; + } + +} + +export namespace Contribution { + export function configure(bind: interfaces.Bind, serviceIdentifier: interfaces.ServiceIdentifier): void { + bind(serviceIdentifier).toSelf().inSingletonScope(); + bind(CommandContribution).toService(serviceIdentifier); + bind(MenuContribution).toService(serviceIdentifier); + bind(KeybindingContribution).toService(serviceIdentifier); + bind(TabBarToolbarContribution).toService(serviceIdentifier); + } +} diff --git a/arduino-ide-extension/src/browser/contributions/new-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-sketch.ts new file mode 100644 index 00000000..694e950f --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/new-sketch.ts @@ -0,0 +1,63 @@ +import { injectable } from 'inversify'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; +import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; + +@injectable() +export class NewSketch extends SketchContribution { + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(NewSketch.Commands.NEW_SKETCH, { + execute: () => this.newSketch() + }); + registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, { + isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id) + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { + commandId: NewSketch.Commands.NEW_SKETCH.id, + label: 'New', + order: '0' + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: NewSketch.Commands.NEW_SKETCH.id, + keybinding: 'CtrlCmd+N' + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, + command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, + tooltip: 'New', + priority: 4 + }); + } + + async newSketch(): Promise { + try { + const sketch = await this.sketchService.createNewSketch(); + this.workspaceService.open(new URI(sketch.uri)); + } catch (e) { + await this.messageService.error(e.toString()); + } + } + +} + +export namespace NewSketch { + export namespace Commands { + export const NEW_SKETCH: Command = { + id: 'arduino-new-sketch' + }; + export const NEW_SKETCH__TOOLBAR: Command = { + id: 'arduino-new-sketch--toolbar' + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-sketch.ts new file mode 100644 index 00000000..ced0fbd1 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/open-sketch.ts @@ -0,0 +1,134 @@ +import { inject, injectable } from 'inversify'; +import { remote } from 'electron'; +import { Disposable } from '@theia/languages/lib/browser'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; +import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; + +@injectable() +export class OpenSketch extends SketchContribution { + + @inject(MenuModelRegistry) + protected readonly menuRegistry: MenuModelRegistry; + + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { + execute: arg => Sketch.is(arg) ? this.openSketch(arg) : this.openSketch() + }); + registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, { + isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + execute: async (_: Widget, target: EventTarget) => { + const sketches = await this.sketchService.getSketches(); + if (!sketches.length) { + this.openSketch(); + } else { + if (!(target instanceof HTMLElement)) { + return; + } + const toDisposeOnClose = new DisposableCollection(); + this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, { + commandId: OpenSketch.Commands.OPEN_SKETCH.id, + label: 'Open...' + }); + toDisposeOnClose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH))); + for (const sketch of sketches) { + const command = { id: `arduino-open-sketch--${sketch.uri}` }; + const handler = { execute: () => this.openSketch(sketch) }; + toDisposeOnClose.push(registry.registerCommand(command, handler)); + this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, { + commandId: command.id, + label: sketch.name + }); + toDisposeOnClose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))); + } + const { parentElement } = target; + if (parentElement) { + const options = { + menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT, + anchor: { + x: parentElement.getBoundingClientRect().left, + y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight + }, + onHide: () => toDisposeOnClose.dispose() + } + this.contextMenuRenderer.render(options); + } + } + } + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { + commandId: OpenSketch.Commands.OPEN_SKETCH.id, + label: 'Open...', + order: '1' + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: OpenSketch.Commands.OPEN_SKETCH.id, + keybinding: 'CtrlCmd+O' + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id, + command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id, + tooltip: 'Open', + priority: 5 + }); + } + + async openSketch(toOpen: MaybePromise = this.selectSketch()): Promise { + const sketch = await toOpen; + if (sketch) { + this.workspaceService.open(new URI(sketch.uri)); + } + } + + protected async selectSketch(): Promise { + const config = await this.configService.getConfiguration(); + const defaultPath = await this.fileSystem.getFsPath(config.sketchDirUri); + const { filePaths } = await remote.dialog.showOpenDialog({ + defaultPath, + properties: ['createDirectory', 'openFile'], + filters: [ + { + name: 'Sketch', + extensions: ['ino'] + } + ] + }); + if (!filePaths.length) { + return undefined; + } + if (filePaths.length > 1) { + this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`); + } + // TODO: validate sketch file name against the sketch folder. Move the file if required. + const sketchFilePath = filePaths[0]; + const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath); + return this.sketchService.getSketchFolder(sketchFileUri); + } + +} + +export namespace OpenSketch { + export namespace Commands { + export const OPEN_SKETCH: Command = { + id: 'arduino-open-sketch' + }; + export const OPEN_SKETCH__TOOLBAR: Command = { + id: 'arduino-open-sketch--toolbar' + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts new file mode 100644 index 00000000..37922508 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -0,0 +1,82 @@ +import { injectable } from 'inversify'; +import { remote } from 'electron'; +import * as dateFormat from 'dateformat'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution'; + +@injectable() +export class SaveAsSketch extends SketchContribution { + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, { + execute: args => this.saveAs(args) + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { + commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + label: 'Save As...', + order: '7' + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + keybinding: 'CtrlCmd+Shift+S' + }); + } + + async saveAs({ execOnlyIfTemp, openAfterMove }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise { + const sketch = await this.getCurrentSketch(); + if (!sketch) { + return; + } + + const isTemp = await this.sketchService.isTemp(sketch); + if (!isTemp && !!execOnlyIfTemp) { + return; + } + + // If target does not exist, propose a `directories.user`/${sketch.name} path + // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss} + const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri); + const exists = await this.fileSystem.exists(sketchDirUri.resolve(sketch.name).toString()); + const defaultUri = exists + ? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString()) + : sketchDirUri.resolve(sketch.name); + const defaultPath = await this.fileSystem.getFsPath(defaultUri.toString())!; + const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath }); + if (!filePath || canceled) { + return; + } + const destinationUri = await this.fileSystemExt.getUri(filePath); + if (!destinationUri) { + return; + } + const workspaceUri = await this.sketchService.copy(sketch, { destinationUri }); + if (workspaceUri && openAfterMove) { + this.workspaceService.open(new URI(workspaceUri)); + } + } + +} + +export namespace SaveAsSketch { + export namespace Commands { + export const SAVE_AS_SKETCH: Command = { + id: 'arduino-save-as-sketch' + }; + } + export interface Options { + readonly execOnlyIfTemp?: boolean; + readonly openAfterMove?: boolean; + } + export namespace Options { + export const DEFAULT: Options = { + execOnlyIfTemp: false, + openAfterMove: true + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/save-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-sketch.ts new file mode 100644 index 00000000..ed4f56d1 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/save-sketch.ts @@ -0,0 +1,59 @@ +import { injectable } from 'inversify'; +import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; +import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; + +@injectable() +export class SaveSketch extends SketchContribution { + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, { + execute: () => this.saveSketch() + }); + registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, { + isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + execute: () => registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id) + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { + commandId: SaveSketch.Commands.SAVE_SKETCH.id, + label: 'Save', + order: '6' + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: SaveSketch.Commands.SAVE_SKETCH.id, + keybinding: 'CtrlCmd+S' + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, + command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, + tooltip: 'Save', + priority: 6 + }); + } + + async saveSketch(): Promise { + return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id); + } + +} + +export namespace SaveSketch { + export namespace Commands { + export const SAVE_SKETCH: Command = { + id: 'arduino-save-sketch' + }; + export const SAVE_SKETCH__TOOLBAR: Command = { + id: 'arduino-save-sketch--toolbar' + }; + } +} diff --git a/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts index 1c2c23e6..4dc4bcb7 100644 --- a/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts @@ -3,8 +3,8 @@ import { injectable, inject } from 'inversify'; import { CommandService } from '@theia/core/lib/common/command'; import { ApplicationShell, Widget } from '@theia/core/lib/browser'; import { EditorMode } from '../editor-mode'; -import { ArduinoCommands } from '../arduino-commands'; import { EditorWidget } from '@theia/editor/lib/browser'; +import { SaveAsSketch } from '../contributions/save-as-sketch'; @injectable() export class ArduinoApplicationShell extends ApplicationShell { @@ -28,7 +28,7 @@ export class ArduinoApplicationShell extends ApplicationShell { async save(): Promise { await super.save(); - await this.commandService.executeCommand(ArduinoCommands.SAVE_SKETCH_AS.id, { execOnlyIfTemp: true }); + await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: true, openAfterMove: true }); } } diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts index fadb78a4..d703c0f6 100644 --- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts +++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts @@ -1,6 +1,15 @@ import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; export namespace ArduinoMenus { + + // Main menu export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch']; export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print']; + + // `Open...` context menu + export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context']; + export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [...OPEN_SKETCH__CONTEXT, '0_open']; + export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [...OPEN_SKETCH__CONTEXT, '1_recent']; + export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [...OPEN_SKETCH__CONTEXT, '2_examples']; + } diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index b2dcf1b1..702baf7e 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -58,12 +58,12 @@ mask-position: 156px -4px; } -.arduino-new-sketch-icon { +.arduino-new-sketch--toolbar-icon { -webkit-mask-position: 124px -4px; mask-position: 124px -4px; } -.arduino-show-open-context-menu-icon { +.arduino-open-sketch--toolbar-icon { -webkit-mask-position: 92px -4px; mask-position: 92px -4px; } diff --git a/arduino-ide-extension/src/common/protocol/index.ts b/arduino-ide-extension/src/common/protocol/index.ts index 83c20492..306b3f9f 100644 --- a/arduino-ide-extension/src/common/protocol/index.ts +++ b/arduino-ide-extension/src/common/protocol/index.ts @@ -3,6 +3,7 @@ export * from './arduino-daemon'; export * from './boards-service'; export * from './config-service'; export * from './core-service'; +export * from './filesystem-ext'; export * from './installable'; export * from './library-service'; export * from './monitor-service'; diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 138bc4f4..9a092b1a 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -42,3 +42,8 @@ export interface Sketch { readonly name: string; readonly uri: string; } +export namespace Sketch { + export function is(arg: any): arg is Sketch { + return !!arg && 'name' in arg && 'uri' in arg && typeof arg.name === 'string' && typeof arg.uri === 'string'; + } +}