diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 4227122cb..f4353570a 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -10,10 +10,7 @@ import { MenuContribution, MenuModelRegistry, } from '@theia/core'; -import { - FrontendApplication, - FrontendApplicationContribution, -} from '@theia/core/lib/browser'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; @@ -77,7 +74,7 @@ export class ArduinoFrontendContribution } } - onStart(app: FrontendApplication): void { + onStart(): void { this.electronWindowPreferences.onPreferenceChanged((event) => { if (event.newValue !== event.oldValue) { switch (event.preferenceName) { @@ -98,8 +95,6 @@ export class ArduinoFrontendContribution webContents.setZoomLevel(zoomLevel); }) ); - // Removes the _Settings_ (cog) icon from the left sidebar - app.shell.leftPanelHandler.removeBottomMenu('settings-menu'); } registerToolbarItems(registry: TabBarToolbarRegistry): void { 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 4743f3f84..7dd6fc1b9 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -347,6 +347,9 @@ import { ConfigServiceClient } from './config/config-service-client'; import { ValidateSketch } from './contributions/validate-sketch'; import { RenameCloudSketch } from './contributions/rename-cloud-sketch'; import { CreateFeatures } from './create/create-features'; +import { Account } from './contributions/account'; +import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget'; +import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -734,6 +737,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, NewCloudSketch); Contribution.configure(bind, ValidateSketch); Contribution.configure(bind, RenameCloudSketch); + Contribution.configure(bind, Account); bindContributionProvider(bind, StartupTaskProvider); bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window @@ -1014,4 +1018,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }, })) .inSingletonScope(); + + bind(SidebarBottomMenuWidget).toSelf(); + rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget); }); diff --git a/arduino-ide-extension/src/browser/auth/authentication-client-service.ts b/arduino-ide-extension/src/browser/auth/authentication-client-service.ts index 4411a8eb5..ccdb99ffa 100644 --- a/arduino-ide-extension/src/browser/auth/authentication-client-service.ts +++ b/arduino-ide-extension/src/browser/auth/authentication-client-service.ts @@ -83,9 +83,13 @@ export class AuthenticationClientService registerCommands(registry: CommandRegistry): void { registry.registerCommand(CloudUserCommands.LOGIN, { execute: () => this.service.login(), + isEnabled: () => !this._session, + isVisible: () => !this._session, }); registry.registerCommand(CloudUserCommands.LOGOUT, { execute: () => this.service.logout(), + isEnabled: () => !!this._session, + isVisible: () => !!this._session, }); } diff --git a/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts b/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts index 165d5bda7..17ac787b9 100644 --- a/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts +++ b/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts @@ -1,5 +1,8 @@ import { Command } from '@theia/core/lib/common/command'; +export const LEARN_MORE_URL = + 'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync'; + export namespace CloudUserCommands { export const LOGIN = Command.toLocalizedCommand( { @@ -16,9 +19,4 @@ export namespace CloudUserCommands { }, 'arduino/cloud/signOut' ); - - export const OPEN_PROFILE_CONTEXT_MENU: Command = { - id: 'arduino-cloud-sketchbook--open-profile-menu', - label: 'Contextual menu', - }; } diff --git a/arduino-ide-extension/src/browser/contributions/account.ts b/arduino-ide-extension/src/browser/contributions/account.ts new file mode 100644 index 000000000..cb82229ad --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/account.ts @@ -0,0 +1,145 @@ +import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; +import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { MenuPath } from '@theia/core/lib/common/menu'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands'; +import { CreateFeatures } from '../create/create-features'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { + Command, + CommandRegistry, + Contribution, + MenuModelRegistry, +} from './contribution'; + +export const accountMenu: SidebarMenu = { + id: 'arduino-accounts-menu', + iconClass: 'codicon codicon-account', + title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'), + menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT, + order: 0, +}; + +@injectable() +export class Account extends Contribution { + @inject(WindowService) + private readonly windowService: WindowService; + @inject(CreateFeatures) + private readonly createFeatures: CreateFeatures; + + private readonly toDispose = new DisposableCollection(); + private app: FrontendApplication; + + override onStart(app: FrontendApplication): void { + this.app = app; + this.updateSidebarCommand(); + this.toDispose.push( + this.createFeatures.onDidChangeEnabled((enabled) => + this.updateSidebarCommand(enabled) + ) + ); + } + + onStop(): void { + this.toDispose.dispose(); + } + + override registerCommands(registry: CommandRegistry): void { + const openExternal = (url: string) => + this.windowService.openNewWindow(url, { external: true }); + registry.registerCommand(Account.Commands.LEARN_MORE, { + execute: () => openExternal(LEARN_MORE_URL), + isEnabled: () => !Boolean(this.createFeatures.session), + }); + registry.registerCommand(Account.Commands.GO_TO_PROFILE, { + execute: () => openExternal('https://id.arduino.cc/'), + isEnabled: () => Boolean(this.createFeatures.session), + }); + registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, { + execute: () => openExternal('https://create.arduino.cc/editor'), + isEnabled: () => Boolean(this.createFeatures.session), + }); + registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, { + execute: () => openExternal('https://create.arduino.cc/iot/'), + isEnabled: () => Boolean(this.createFeatures.session), + }); + } + + override registerMenus(registry: MenuModelRegistry): void { + const register = ( + menuPath: MenuPath, + ...commands: (Command | [command: Command, menuLabel: string])[] + ) => + commands.forEach((command, index) => { + const commandId = Array.isArray(command) ? command[0].id : command.id; + const label = Array.isArray(command) ? command[1] : command.label; + registry.registerMenuAction(menuPath, { + label, + commandId, + order: String(index), + }); + }); + + register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [ + CloudUserCommands.LOGIN, + nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'), + ]); + register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [ + Account.Commands.LEARN_MORE, + nls.localize('arduino/cloud/learnMore', 'Learn more'), + ]); + register( + ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP, + [ + Account.Commands.GO_TO_PROFILE, + nls.localize('arduino/account/goToProfile', 'Go to Profile'), + ], + [ + Account.Commands.GO_TO_CLOUD_EDITOR, + nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'), + ], + [ + Account.Commands.GO_TO_IOT_CLOUD, + nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'), + ] + ); + register( + ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP, + CloudUserCommands.LOGOUT + ); + } + + private updateSidebarCommand( + visible: boolean = this.preferences['arduino.cloud.enabled'] + ): void { + if (!this.app) { + return; + } + const handler = this.app.shell.leftPanelHandler; + if (visible) { + handler.addBottomMenu(accountMenu); + } else { + handler.removeBottomMenu(accountMenu.id); + } + } +} + +export namespace Account { + export namespace Commands { + export const GO_TO_PROFILE: Command = { + id: 'arduino-go-to-profile', + }; + export const GO_TO_CLOUD_EDITOR: Command = { + id: 'arduino-go-to-cloud-editor', + }; + export const GO_TO_IOT_CLOUD: Command = { + id: 'arduino-go-to-iot-cloud', + }; + export const LEARN_MORE: Command = { + id: 'arduino-learn-more', + }; + } +} diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts index 9ac323aa0..18b52b32c 100644 --- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts +++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts @@ -154,6 +154,25 @@ export namespace ArduinoMenus { '2_resources', ]; + // -- Account + export const ARDUINO_ACCOUNT__CONTEXT = ['arduino-account--context']; + export const ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP = [ + ...ARDUINO_ACCOUNT__CONTEXT, + '0_sign_in', + ]; + export const ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP = [ + ...ARDUINO_ACCOUNT__CONTEXT, + '1_learn_more', + ]; + export const ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP = [ + ...ARDUINO_ACCOUNT__CONTEXT, + '2_go_to', + ]; + export const ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP = [ + ...ARDUINO_ACCOUNT__CONTEXT, + '3_sign_out', + ]; + // -- ROOT SSL CERTIFICATES export const ROOT_CERTIFICATES__CONTEXT = [ 'arduino-root-certificates--context', diff --git a/arduino-ide-extension/src/browser/style/cloud-sketchbook.css b/arduino-ide-extension/src/browser/style/cloud-sketchbook.css index 8ed370329..f19159260 100644 --- a/arduino-ide-extension/src/browser/style/cloud-sketchbook.css +++ b/arduino-ide-extension/src/browser/style/cloud-sketchbook.css @@ -119,8 +119,8 @@ .account-icon { background: url("./account-icon.svg") center center no-repeat; - width: var(--theia-icon-size); - height: var(--theia-icon-size); + width: var(--theia-private-sidebar-icon-size); + height: var(--theia-private-sidebar-icon-size); border-radius: 50%; overflow: hidden; } diff --git a/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx b/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx new file mode 100644 index 000000000..308d77260 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/sidebar-bottom-menu-widget.tsx @@ -0,0 +1,83 @@ +import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; +import type { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget'; +import type { MenuPath } from '@theia/core/lib/common/menu'; +import { nls } from '@theia/core/lib/common/nls'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import * as React from '@theia/core/shared/react'; +import { accountMenu } from '../../contributions/account'; +import { CreateFeatures } from '../../create/create-features'; + +@injectable() +export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget { + @inject(CreateFeatures) + private readonly createFeatures: CreateFeatures; + + @postConstruct() + protected init(): void { + this.toDispose.push( + this.createFeatures.onDidChangeSession(() => this.update()) + ); + } + + protected override onClick( + e: React.MouseEvent, + menuPath: MenuPath + ): void { + const button = e.currentTarget.getBoundingClientRect(); + this.contextMenuRenderer.render({ + menuPath, + includeAnchorArg: false, + anchor: { + x: button.left + button.width, + // Bogus y coordinate? + // https://github.com/eclipse-theia/theia/discussions/12170 + y: button.top, + }, + }); + } + + protected override render(): React.ReactNode { + return ( + + {this.menus.map((menu) => this.renderMenu(menu))} + + ); + } + + private renderMenu(menu: SidebarMenu): React.ReactNode { + // Removes the _Settings_ (cog) icon from the left sidebar + if (menu.id === 'settings-menu') { + return undefined; + } + const arduinoAccount = menu.id === accountMenu.id; + const picture = + arduinoAccount && this.createFeatures.session?.account.picture; + const className = typeof picture === 'string' ? undefined : menu.iconClass; + return ( + this.onClick(e, menu.menuPath)} + onMouseDown={this.onMouseDown} + onMouseOut={this.onMouseOut} + > + {picture && ( +
+ {nls.localize( +
+ )} +
+ ); + } +} diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx index 960df9ec3..0b4d26a94 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx @@ -5,7 +5,7 @@ import { injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { UserStatus } from './cloud-user-status'; +import { CloudStatus } from './cloud-user-status'; import { nls } from '@theia/core/lib/common/nls'; import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget'; import { AuthenticationClientService } from '../../auth/authentication-client-service'; @@ -61,7 +61,7 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge onClick={this.onDidClickCreateNew} /> )} - { - this.toDisposeBeforeNewContextMenu.dispose(); - const container = arg.event.target; - if (!container) { - return; - } - - this.menuRegistry.registerMenuAction(CLOUD_USER__CONTEXT__MAIN_GROUP, { - commandId: CloudUserCommands.LOGOUT.id, - label: CloudUserCommands.LOGOUT.label, - }); - this.toDisposeBeforeNewContextMenu.push( - Disposable.create(() => - this.menuRegistry.unregisterMenuAction(CloudUserCommands.LOGOUT) - ) - ); - - const placeholder = new PlaceholderMenuNode( - CLOUD_USER__CONTEXT__USERNAME, - arg.username - ); - this.menuRegistry.registerMenuNode( - CLOUD_USER__CONTEXT__USERNAME, - placeholder - ); - this.toDisposeBeforeNewContextMenu.push( - Disposable.create(() => - this.menuRegistry.unregisterMenuNode(placeholder.id) - ) - ); - - const options: RenderContextMenuOptions = { - menuPath: CLOUD_USER__CONTEXT, - anchor: { - x: container.getBoundingClientRect().left, - y: - container.getBoundingClientRect().top - - 3.5 * container.offsetHeight, - }, - args: [arg], - }; - this.contextMenuRenderer.render(options); - }, - }); - this.registerMenus(this.menuRegistry); } } diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx index d2fb8a0fe..3f1ae430f 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx @@ -5,7 +5,10 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { AuthenticationClientService } from '../../auth/authentication-client-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { CloudSketchbookTree } from './cloud-sketchbook-tree'; -import { CloudUserCommands } from '../../auth/cloud-user-commands'; +import { + CloudUserCommands, + LEARN_MORE_URL, +} from '../../auth/cloud-user-commands'; import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget'; import { TreeNode } from '@theia/core/lib/browser/tree'; import { CompositeTreeNode } from '@theia/core/lib/browser'; @@ -13,9 +16,6 @@ import { shell } from '@theia/core/electron-shared/@electron/remote'; import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget'; import { nls } from '@theia/core/lib/common'; -const LEARN_MORE_URL = - 'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync'; - @injectable() export class CloudSketchbookTreeWidget extends SketchbookTreeWidget { @inject(AuthenticationClientService) diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx index 3611deca7..68a7da5eb 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx @@ -5,11 +5,9 @@ import { } from '@theia/core/lib/common/disposable'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { AuthenticationClientService } from '../../auth/authentication-client-service'; -import { CloudUserCommands } from '../../auth/cloud-user-commands'; -import { AuthenticationSessionAccountInformation } from '../../../common/protocol/authentication-service'; import { nls } from '@theia/core/lib/common'; -export class UserStatus extends React.Component< +export class CloudStatus extends React.Component< UserStatus.Props, UserStatus.State > { @@ -19,7 +17,6 @@ export class UserStatus extends React.Component< super(props); this.state = { status: this.status, - accountInfo: props.authenticationService.session?.account, refreshing: false, }; } @@ -29,9 +26,6 @@ export class UserStatus extends React.Component< window.addEventListener('online', statusListener); window.addEventListener('offline', statusListener); this.toDispose.pushAll([ - this.props.authenticationService.onSessionDidChange((session) => - this.setState({ accountInfo: session?.account }) - ), Disposable.create(() => window.removeEventListener('online', statusListener) ), @@ -73,34 +67,6 @@ export class UserStatus extends React.Component< onClick={this.onDidClickRefresh} /> -
-
{ - event.preventDefault(); - event.stopPropagation(); - this.props.model.commandRegistry.executeCommand( - CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU.id, - { - event: event.nativeEvent, - username: this.state.accountInfo?.label, - } - ); - }} - > - {this.state.accountInfo?.picture && ( - {nls.localize( - )} -
-
); } @@ -128,7 +94,6 @@ export namespace UserStatus { } export interface State { status: 'connected' | 'offline'; - accountInfo?: AuthenticationSessionAccountInformation; refreshing?: boolean; } } diff --git a/i18n/en.json b/i18n/en.json index 8fd1a3a1b..e02f7eeb4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4,6 +4,12 @@ "detail": "Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}", "label": "About {0}" }, + "account": { + "goToCloudEditor": "Go to Cloud Editor", + "goToIoTCloud": "Go to IoT Cloud", + "goToProfile": "Go to Profile", + "menuTitle": "Arduino Cloud" + }, "board": { "board": "Board{0}", "boardConfigDialogTitle": "Select Other Board and Port", @@ -83,7 +89,6 @@ "mouseError": "'Mouse' not found. Does your sketch include the line '#include '?" }, "cloud": { - "account": "Account", "chooseSketchVisibility": "Choose visibility of your Sketch:", "cloudSketchbook": "Cloud Sketchbook", "connected": "Connected",