diff --git a/media/options-widget.css b/media/options-widget.css index 43caac6..853279e 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -14,6 +14,39 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +.memory-options-widget { + margin-bottom: 8px; + padding-bottom: 4px; +} + +.memory-options-widget .title-container { + display: flex; + align-items: center; +} + +.memory-options-widget .title-container h1 { + flex-grow: 1; + font-size: 1.3em; + margin: 8px 0 4px; +} + +.memory-options-widget .title-container input { + margin: 5px 0 0; + flex-grow: 1; +} + +.memory-options-widget .edit-label-toggle { + position: absolute; + right: 24px; + top: 8px; + opacity: 0; + transition: opacity 0.2s; +} + +.memory-options-widget .title-container:hover .edit-label-toggle { + opacity: 1; +} + .core-options { display: flex; flex-direction: row; @@ -21,6 +54,7 @@ margin: 6px 0; flex-wrap: wrap; border-bottom: 1px solid var(--vscode-widget-border); + border-top: 1px solid var(--vscode-widget-border); } .form-options { diff --git a/src/common/messaging.ts b/src/common/messaging.ts index c2d295e..a8d9d8d 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -26,6 +26,7 @@ export const readyType: NotificationType = { method: 'ready' }; export const logMessageType: RequestType = { method: 'logMessage' }; export const setMemoryViewSettingsType: NotificationType> = { method: 'setMemoryViewSettings' }; export const resetMemoryViewSettingsType: NotificationType = { method: 'resetMemoryViewSettings' }; +export const setTitleType: NotificationType = { method: 'setTitle' }; export const setOptionsType: RequestType, void> = { method: 'setOptions' }; export const readMemoryType: RequestType = { method: 'readMemory' }; export const writeMemoryType: RequestType = { method: 'writeMemory' }; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 23be47f..2368ab2 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -30,6 +30,7 @@ import { getVariables, setMemoryViewSettingsType, resetMemoryViewSettingsType, + setTitleType, } from '../common/messaging'; import { MemoryProvider } from './memory-provider'; import { outputChannelLogger } from './logger'; @@ -62,6 +63,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected messenger: Messenger; protected refreshOnStop: RefreshEnum; + protected panelIndices: number = 1; + public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider) { this.messenger = new Messenger(); @@ -125,7 +128,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }; if (!panel) { - panel = vscode.window.createWebviewPanel(MemoryWebview.ViewType, 'Memory Inspector', vscode.ViewColumn.Active, options); + panel = vscode.window.createWebviewPanel(MemoryWebview.ViewType, `Memory ${this.panelIndices++}`, vscode.ViewColumn.Active, options); } else { panel.webview.options = options; } @@ -135,10 +138,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { // Sets up an event listener to listen for messages passed from the webview view context // and executes code based on the message that is received - const webviewParticipant = this.setWebviewMessageListener(panel, initialMemory); - - // initialize with configuration - this.setInitialSettings(webviewParticipant); + this.setWebviewMessageListener(panel, initialMemory); } protected getRefresh(): RefreshEnum { @@ -176,11 +176,12 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { `; } - protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: Partial): WebviewIdMessageParticipant { + protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: Partial): void { const participant = this.messenger.registerWebviewPanel(panel); const disposables = [ this.messenger.onNotification(readyType, () => { + this.setInitialSettings(participant, panel.title); this.refresh(participant, options); }, { sender: participant }), this.messenger.onRequest(setOptionsType, o => { @@ -190,7 +191,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), - this.messenger.onNotification(resetMemoryViewSettingsType, () => this.setInitialSettings(participant), { sender: participant }), + this.messenger.onNotification(resetMemoryViewSettingsType, () => this.setInitialSettings(participant, panel.title), { sender: participant }), + this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), this.memoryProvider.onDidStopDebug(() => { if (this.refreshOnStop === RefreshEnum.on) { @@ -204,19 +206,17 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { } }); panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); - - return participant; - } - - protected setInitialSettings(webviewParticipant: WebviewIdMessageParticipant): void { - this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, this.getMemoryViewSettings()); } protected async refresh(participant: WebviewIdMessageParticipant, options?: Partial): Promise { this.messenger.sendRequest(setOptionsType, participant, options); } - protected getMemoryViewSettings(): MemoryViewSettings { + protected setInitialSettings(webviewParticipant: WebviewIdMessageParticipant, title: string): void { + this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, this.getMemoryViewSettings(title)); + } + + protected getMemoryViewSettings(title: string): MemoryViewSettings { const memoryInspectorConfiguration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const wordsPerGroup = memoryInspectorConfiguration.get(manifest.CONFIG_WORDS_PER_GROUP, manifest.DEFAULT_WORDS_PER_GROUP); const groupsPerRow = memoryInspectorConfiguration.get(manifest.CONFIG_GROUPS_PER_ROW, manifest.DEFAULT_GROUPS_PER_ROW); @@ -224,7 +224,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { const visibleColumns = CONFIGURABLE_COLUMNS .filter(column => vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(column, false)) .map(columnId => columnId.replace('columns.', '')); - return { wordsPerGroup, groupsPerRow, scrollingBehavior, visibleColumns }; + return { title, wordsPerGroup, groupsPerRow, scrollingBehavior, visibleColumns }; } protected async readMemory(request: DebugProtocol.ReadMemoryArguments): Promise { diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index b09baea..83fb7a0 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -23,6 +23,7 @@ import { OptionsWidget } from './options-widget'; interface MemoryWidgetProps extends MemoryDisplayConfiguration { memory?: Memory; + title: string; decorations: Decoration[]; columns: ColumnStatus[]; memoryReference: string; @@ -34,6 +35,7 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { toggleColumn(id: string, active: boolean): void; updateMemoryDisplayConfiguration: (memoryArguments: Partial) => void; resetMemoryDisplayConfiguration: () => void; + updateTitle: (title: string) => void; fetchMemory(partialOptions?: Partial): Promise } @@ -56,6 +58,8 @@ export class MemoryWidget extends React.Component , Required { + title: string; updateRenderOptions: (options: Partial) => void; resetRenderOptions: () => void; + updateTitle: (title: string) => void; updateMemoryArguments: ( memoryArguments: Partial ) => void; @@ -40,6 +42,10 @@ export interface OptionsWidgetProps toggleColumn(id: string, isVisible: boolean): void; } +interface OptionsWidgetState { + isTitleEditing: boolean; +} + const enum InputId { Address = 'address', Offset = 'offset', @@ -57,9 +63,10 @@ interface OptionsForm { const allowedBytesPerGroup = [1, 2, 4, 8, 16]; const allowedGroupsPerRow = [1, 2, 4, 8, 16, 32]; -export class OptionsWidget extends React.Component { +export class OptionsWidget extends React.Component { protected formConfig: FormikConfig; protected extendedOptions = React.createRef(); + protected labelEditInput = React.createRef(); protected get optionsFormValues(): OptionsForm { return { @@ -80,6 +87,7 @@ export class OptionsWidget extends React.Component { this.props.refreshMemory(); }, }; + this.state = { isTitleEditing: false }; } protected validate = (values: OptionsForm) => { @@ -117,11 +125,43 @@ export class OptionsWidget extends React.Component { return errors; }; + componentDidUpdate(_: Readonly, prevState: Readonly): void { + if (!prevState.isTitleEditing && this.state.isTitleEditing) { + this.labelEditInput.current?.focus(); + this.labelEditInput.current?.select(); + } + } + override render(): React.ReactNode { this.formConfig.initialValues = this.optionsFormValues; + const isLabelEditing = this.state.isTitleEditing; return (
+
+ + {!isLabelEditing && ( +

{this.props.title}

+ )} + {!isLabelEditing && ( +
{formik => ( @@ -327,4 +367,34 @@ export class OptionsWidget extends React.Component { protected handleResetAdvancedOptions: MouseEventHandler | undefined = () => this.props.resetRenderOptions(); + protected enableTitleEditing = () => this.doEnableTitleEditing(); + protected doEnableTitleEditing(): void { + if (this.labelEditInput.current) { + this.labelEditInput.current.value = this.props.title; + } + this.setState({ isTitleEditing: true }); + } + + protected disableTitleEditing = () => this.doDisableTitleEditing(); + protected doDisableTitleEditing(): void { + this.setState({ isTitleEditing: false }); + } + + protected handleTitleEditingKeyDown: KeyboardEventHandler | undefined = event => this.doHandleTitleEditingKeyDown(event); + protected doHandleTitleEditingKeyDown(event: React.KeyboardEvent): void { + if (event.key === 'Enter' && this.labelEditInput.current) { + this.doConfirmEditedTitle(); + } else if (event.key === 'Escape') { + this.disableTitleEditing(); + } + } + + protected confirmEditedTitle: FocusEventHandler | undefined = () => this.doConfirmEditedTitle(); + protected doConfirmEditedTitle(): void { + if (this.state.isTitleEditing && this.labelEditInput.current) { + this.props.updateTitle(this.labelEditInput.current.value.trim()); + this.disableTitleEditing(); + } + } + } diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index cb95a7c..dc86aa2 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -22,6 +22,7 @@ import { logMessageType, setOptionsType, readMemoryType, + setTitleType, setMemoryViewSettingsType, resetMemoryViewSettingsType, } from '../common/messaging'; @@ -39,6 +40,7 @@ import { PrimeReactProvider } from 'primereact/api'; import 'primeflex/primeflex.css'; export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { + title: string; decorations: Decoration[]; columns: ColumnStatus[]; } @@ -59,6 +61,7 @@ class App extends React.Component<{}, MemoryAppState> { columnContributionService.register(new AsciiColumn()); decorationService.register(variableDecorator); this.state = { + title: 'Memory', memory: undefined, memoryReference: '', offset: 0, @@ -78,7 +81,7 @@ class App extends React.Component<{}, MemoryAppState> { const configurable = column.configurable; this.toggleColumn(id, !configurable || !!config.visibleColumns?.includes(id)); } - this.setState(prevState => ({ ...prevState, ...(config as MemoryDisplayConfiguration) })); + this.setState(prevState => ({ ...prevState, ...config, title: config.title ?? prevState.title, })); }); messenger.sendNotification(readyType, HOST_EXTENSION, undefined); } @@ -92,9 +95,11 @@ class App extends React.Component<{}, MemoryAppState> { memoryReference={this.state.memoryReference} offset={this.state.offset ?? 0} count={this.state.count} + title={this.state.title} updateMemoryArguments={this.updateMemoryState} updateMemoryDisplayConfiguration={this.updateMemoryDisplayConfiguration} resetMemoryDisplayConfiguration={this.resetMemoryDisplayConfiguration} + updateTitle={this.updateTitle} refreshMemory={this.refreshMemory} toggleColumn={this.toggleColumn} fetchMemory={this.fetchMemory} @@ -109,6 +114,10 @@ class App extends React.Component<{}, MemoryAppState> { protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected updateMemoryDisplayConfiguration = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected resetMemoryDisplayConfiguration = () => messenger.sendNotification(resetMemoryViewSettingsType, HOST_EXTENSION, undefined); + protected updateTitle = (title: string) => { + this.setState({ title }); + messenger.sendNotification(setTitleType, HOST_EXTENSION, title); + }; protected async setOptions(options?: Partial): Promise { messenger.sendRequest(logMessageType, HOST_EXTENSION, `Setting options: ${JSON.stringify(options)}`); diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 3a22024..0ae00c1 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -73,7 +73,9 @@ export interface FullNodeAttributes extends StylableNodeAttributes { } /** All settings related to memory view that can be specified for the webview from the extension "main". */ -export interface MemoryViewSettings extends ColumnVisibilityStatus, MemoryDisplayConfiguration { } +export interface MemoryViewSettings extends ColumnVisibilityStatus, MemoryDisplayConfiguration { + title: string +} /** The memory display configuration that can be specified for the memory widget. */ export interface MemoryDisplayConfiguration {