From 2313132d554b056244a1c27476e1281e9055b1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 16 Feb 2021 15:39:48 +0100 Subject: [PATCH 01/45] :lipstick: --- src/vs/base/browser/ui/list/list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 4f8e796f2066a..6ea7b64714247 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -15,7 +15,7 @@ export interface IListVirtualDelegate { } export interface IListRenderer { - templateId: string; + readonly templateId: string; renderTemplate(container: HTMLElement): TTemplateData; renderElement(element: T, index: number, templateData: TTemplateData, height: number | undefined): void; disposeElement?(element: T, index: number, templateData: TTemplateData, height: number | undefined): void; From ee9da16746d3cecde42899c53b95839166d64e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 16 Feb 2021 15:53:35 +0100 Subject: [PATCH 02/45] table: intro --- src/vs/base/browser/ui/table/table.css | 17 +++ src/vs/base/browser/ui/table/table.ts | 27 ++++ src/vs/base/browser/ui/table/tableWidget.ts | 159 ++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/vs/base/browser/ui/table/table.css create mode 100644 src/vs/base/browser/ui/table/table.ts create mode 100644 src/vs/base/browser/ui/table/tableWidget.ts diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css new file mode 100644 index 0000000000000..ed7a3dcc08e31 --- /dev/null +++ b/src/vs/base/browser/ui/table/table.css @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-table { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + width: 100%; + white-space: nowrap; +} + +.monaco-table > .monaco-list { + flex: 1; +} diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts new file mode 100644 index 0000000000000..cdf427d4efb79 --- /dev/null +++ b/src/vs/base/browser/ui/table/table.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IListRenderer } from 'vs/base/browser/ui/list/list'; + +export interface ITableColumn { + readonly label: string; + readonly weight: number; + readonly templateId: string; + project(row: TRow): TCell; +} + +export interface ITableVirtualDelegate { + readonly headerRowHeight: number; + getHeight(row: TRow): number; +} + +export interface ITableRenderer extends IListRenderer { } + +export class TableError extends Error { + + constructor(user: string, message: string) { + super(`TableError [${user}] ${message}`); + } +} diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts new file mode 100644 index 0000000000000..c69b43b32b85d --- /dev/null +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./table'; +import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; +import { ITableColumn, ITableRenderer, ITableVirtualDelegate, TableError } from 'vs/base/browser/ui/table/table'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { IThemable } from 'vs/base/common/styler'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; +import { Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; + +// TODO@joao +type TCell = any; + +export interface ITableOptions extends IListOptions { } +export interface ITableStyles extends IListStyles { } + +interface RowTemplateData { + readonly container: HTMLElement; + readonly cellContainers: HTMLElement[]; + readonly cellTemplateData: unknown[]; +} + +class TableListRenderer implements IListRenderer { + + static TemplateId = 'row'; + readonly templateId = TableListRenderer.TemplateId; + private renderers: ITableRenderer[]; + + constructor( + private columns: ITableColumn[], + renderers: ITableRenderer[] + ) { + const rendererMap = new Map(renderers.map(r => [r.templateId, r])); + + this.renderers = columns.map(column => { + const result = rendererMap.get(column.templateId); + + if (!result) { + throw new Error(`Table cell renderer for template id ${column.templateId} not found.`); + } + + return result; + }); + } + + renderTemplate(container: HTMLElement) { + const cellContainers: HTMLElement[] = []; + const cellTemplateData: unknown[] = []; + + for (let i = 0; i < this.columns.length; i++) { + const renderer = this.renderers[i]; + const cellContainer = append(container, $('.monaco-table-cell')); + + cellContainers.push(cellContainer); + cellTemplateData.push(renderer.renderTemplate(cellContainer)); + } + + return { container, cellContainers, cellTemplateData }; + } + + renderElement(element: TRow, index: number, templateData: RowTemplateData, height: number | undefined): void { + for (let i = 0; i < this.columns.length; i++) { + const column = this.columns[i]; + const cell = column.project(element); + const renderer = this.renderers[i]; + renderer.renderElement(cell, index, templateData.cellTemplateData[i], height); + } + } + + disposeElement(element: TRow, index: number, templateData: RowTemplateData, height: number | undefined): void { + for (let i = 0; i < this.columns.length; i++) { + const renderer = this.renderers[i]; + + if (renderer.disposeElement) { + const column = this.columns[i]; + const cell = column.project(element); + + renderer.disposeElement(cell, index, templateData.cellTemplateData[i], height); + } + } + } + + disposeTemplate(templateData: RowTemplateData): void { + for (let i = 0; i < this.columns.length; i++) { + const renderer = this.renderers[i]; + renderer.disposeTemplate(templateData.cellTemplateData[i]); + } + + clearNode(templateData.container); + } +} + +function asListVirtualDelegate(delegate: ITableVirtualDelegate): IListVirtualDelegate { + return { + getHeight(row) { return delegate.getHeight(row); }, + getTemplateId() { return TableListRenderer.TemplateId; }, + }; +} + +export class TableWidget implements ISpliceable, IThemable, IDisposable { + + private domNode: HTMLElement; + private splitview: SplitView; + private list: List; + + constructor( + private user: string, + container: HTMLElement, + private virtualDelegate: ITableVirtualDelegate, + columns: ITableColumn[], + renderers: ITableRenderer[], + private _options?: ITableOptions + ) { + this.domNode = append(container, $('.monaco-table')); + + this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL }); + this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; + + this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [new TableListRenderer(columns, renderers)], _options); + } + + splice(start: number, deleteCount: number, elements: TRow[] = []): void { + if (start < 0 || start > this.list.length) { + throw new TableError(this.user, `Invalid start index: ${start}`); + } + + if (deleteCount < 0) { + throw new TableError(this.user, `Invalid delete count: ${deleteCount}`); + } + + if (deleteCount === 0 && elements.length === 0) { + return; + } + + throw new Error('Method not implemented'); + } + + layout(height?: number, width?: number): void { + height = height ?? getContentHeight(this.domNode); + width = width ?? getContentWidth(this.domNode); + + this.splitview.layout(width); + this.list.layout(height - this.virtualDelegate.headerRowHeight, width); + } + + style(styles: ITableStyles): void { + this.list.style(styles); + } + + dispose(): void { + this.splitview.dispose(); + this.list.dispose(); + } +} From 3385cc7a5cce1f538e19cd3fa0a34a68ab073046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 16 Feb 2021 16:24:39 +0100 Subject: [PATCH 03/45] :lipstick: --- src/vs/base/browser/ui/splitview/splitview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 8bb8939127955..290a46c7f5d8b 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -200,7 +200,7 @@ export namespace Sizing { export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; } } -export interface ISplitViewDescriptor { +export interface ISplitViewDescriptor { size: number; views: { visible?: boolean; From 3d0245d5e4fbb200759b03a1bfab25f71fc7eb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 16 Feb 2021 16:34:34 +0100 Subject: [PATCH 04/45] more table progress --- src/vs/base/browser/ui/table/table.css | 24 ++++++++++++ src/vs/base/browser/ui/table/tableWidget.ts | 42 +++++++++++++-------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index ed7a3dcc08e31..8a401facf1bcf 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -15,3 +15,27 @@ .monaco-table > .monaco-list { flex: 1; } + +.monaco-table-tr { + display: flex; +} + +.monaco-table-th { + width: 100%; + height: 100%; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + border-right: 1px solid black; +} + +.monaco-table-th, +.monaco-table-td { + padding: 0 10px; +} + +.monaco-table-th[data-col-index="0"], +.monaco-table-td[data-col-index="0"] { + padding-left: 20px; +} diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index c69b43b32b85d..4ee2365421612 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -10,8 +10,9 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { IThemable } from 'vs/base/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; -import { Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { ISplitViewDescriptor, IView, LayoutPriority, Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Event } from 'vs/base/common/event'; // TODO@joao type TCell = any; @@ -49,12 +50,13 @@ class TableListRenderer implements IListRenderer { } renderTemplate(container: HTMLElement) { + const rowContainer = append(container, $('.monaco-table-tr')); const cellContainers: HTMLElement[] = []; const cellTemplateData: unknown[] = []; for (let i = 0; i < this.columns.length; i++) { const renderer = this.renderers[i]; - const cellContainer = append(container, $('.monaco-table-cell')); + const cellContainer = append(rowContainer, $('.monaco-table-td', { 'data-col-index': i })); cellContainers.push(cellContainer); cellTemplateData.push(renderer.renderTemplate(cellContainer)); @@ -102,6 +104,20 @@ function asListVirtualDelegate(delegate: ITableVirtualDelegate): ILi }; } +class ColumnHeader implements IView { + + readonly element: HTMLElement; + readonly minimumSize = 120; + readonly maximumSize = Number.POSITIVE_INFINITY; + readonly onDidChange = Event.None; + + constructor(column: ITableColumn, index: number) { + this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); + } + + layout(): void { } +} + export class TableWidget implements ISpliceable, IThemable, IDisposable { private domNode: HTMLElement; @@ -118,26 +134,20 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa ) { this.domNode = append(container, $('.monaco-table')); - this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL }); + const descriptor: ISplitViewDescriptor = { + size: columns.length, + views: columns.map((c, i) => ({ size: 1, view: new ColumnHeader(c, i) })) + }; + + this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, descriptor }); this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; + this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`; this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [new TableListRenderer(columns, renderers)], _options); } splice(start: number, deleteCount: number, elements: TRow[] = []): void { - if (start < 0 || start > this.list.length) { - throw new TableError(this.user, `Invalid start index: ${start}`); - } - - if (deleteCount < 0) { - throw new TableError(this.user, `Invalid delete count: ${deleteCount}`); - } - - if (deleteCount === 0 && elements.length === 0) { - return; - } - - throw new Error('Method not implemented'); + this.list.splice(start, deleteCount, elements); } layout(height?: number, width?: number): void { From f290c162de7aeba244edd58f9be4dc063e70ab1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 10:10:26 +0100 Subject: [PATCH 05/45] table: layout --- src/vs/base/browser/ui/table/table.css | 2 +- src/vs/base/browser/ui/table/tableWidget.ts | 54 +++++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 8a401facf1bcf..55b7be7a24225 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -26,12 +26,12 @@ font-weight: bold; overflow: hidden; text-overflow: ellipsis; - box-sizing: border-box; border-right: 1px solid black; } .monaco-table-th, .monaco-table-td { + box-sizing: border-box; padding: 0 10px; } diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 4ee2365421612..9de2714ecdb79 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -5,14 +5,14 @@ import 'vs/css!./table'; import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; -import { ITableColumn, ITableRenderer, ITableVirtualDelegate, TableError } from 'vs/base/browser/ui/table/table'; +import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { ISpliceable } from 'vs/base/common/sequence'; import { IThemable } from 'vs/base/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; -import { ISplitViewDescriptor, IView, LayoutPriority, Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; // TODO@joao type TCell = any; @@ -31,22 +31,24 @@ class TableListRenderer implements IListRenderer { static TemplateId = 'row'; readonly templateId = TableListRenderer.TemplateId; private renderers: ITableRenderer[]; + private renderedTemplates = new Set(); constructor( private columns: ITableColumn[], renderers: ITableRenderer[] ) { const rendererMap = new Map(renderers.map(r => [r.templateId, r])); + this.renderers = []; - this.renderers = columns.map(column => { - const result = rendererMap.get(column.templateId); + for (const column of columns) { + const renderer = rendererMap.get(column.templateId); - if (!result) { + if (!renderer) { throw new Error(`Table cell renderer for template id ${column.templateId} not found.`); } - return result; - }); + this.renderers.push(renderer); + } } renderTemplate(container: HTMLElement) { @@ -62,7 +64,10 @@ class TableListRenderer implements IListRenderer { cellTemplateData.push(renderer.renderTemplate(cellContainer)); } - return { container, cellContainers, cellTemplateData }; + const result = { container, cellContainers, cellTemplateData }; + this.renderedTemplates.add(result); + + return result; } renderElement(element: TRow, index: number, templateData: RowTemplateData, height: number | undefined): void { @@ -94,6 +99,13 @@ class TableListRenderer implements IListRenderer { } clearNode(templateData.container); + this.renderedTemplates.delete(templateData); + } + + layoutColumn(index: number, size: number): void { + for (const { cellContainers } of this.renderedTemplates) { + cellContainers[index].style.width = `${size}px`; + } } } @@ -111,11 +123,16 @@ class ColumnHeader implements IView { readonly maximumSize = Number.POSITIVE_INFINITY; readonly onDidChange = Event.None; - constructor(column: ITableColumn, index: number) { + private _onDidLayout = new Emitter<[number, number]>(); + readonly onDidLayout = this._onDidLayout.event; + + constructor(column: ITableColumn, private index: number) { this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); } - layout(): void { } + layout(size: number): void { + this._onDidLayout.fire([this.index, size]); + } } export class TableWidget implements ISpliceable, IThemable, IDisposable { @@ -123,27 +140,33 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa private domNode: HTMLElement; private splitview: SplitView; private list: List; + private columnLayoutDisposable: IDisposable; constructor( - private user: string, + user: string, container: HTMLElement, private virtualDelegate: ITableVirtualDelegate, columns: ITableColumn[], renderers: ITableRenderer[], - private _options?: ITableOptions + _options?: ITableOptions ) { this.domNode = append(container, $('.monaco-table')); + const headers = columns.map((c, i) => new ColumnHeader(c, i)); const descriptor: ISplitViewDescriptor = { size: columns.length, - views: columns.map((c, i) => ({ size: 1, view: new ColumnHeader(c, i) })) + views: headers.map(view => ({ size: 1, view })) }; this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, descriptor }); this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`; - this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [new TableListRenderer(columns, renderers)], _options); + const renderer = new TableListRenderer(columns, renderers); + this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [renderer], _options); + + this.columnLayoutDisposable = Event.any(...headers.map(h => h.onDidLayout)) + (([index, size]) => renderer.layoutColumn(index, size)); } splice(start: number, deleteCount: number, elements: TRow[] = []): void { @@ -165,5 +188,6 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa dispose(): void { this.splitview.dispose(); this.list.dispose(); + this.columnLayoutDisposable.dispose(); } } From bb999b90b45f4f52fd6a46e1bcb54e7646f6ffcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 10:17:59 +0100 Subject: [PATCH 06/45] table widget: fix overflow behavior --- src/vs/base/browser/ui/splitview/splitview.ts | 5 +++-- src/vs/base/browser/ui/table/table.css | 1 + src/vs/base/browser/ui/table/tableWidget.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 290a46c7f5d8b..8280a965ee479 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -33,6 +33,7 @@ export interface ISplitViewOptions { readonly inverseAltBehavior?: boolean; readonly proportionalLayout?: boolean; // default true, readonly descriptor?: ISplitViewDescriptor; + readonly scrollbarVisibility?: ScrollbarVisibility; } /** @@ -309,8 +310,8 @@ export class SplitView extends Disposable { this.scrollable = new Scrollable(125, scheduleAtNextAnimationFrame); this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, { - vertical: this.orientation === Orientation.VERTICAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden, - horizontal: this.orientation === Orientation.HORIZONTAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden + vertical: this.orientation === Orientation.VERTICAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden, + horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden }, this.scrollable)); this._register(this.scrollableElement.onScroll(e => { diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 55b7be7a24225..4696b0924481e 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -33,6 +33,7 @@ .monaco-table-td { box-sizing: border-box; padding: 0 10px; + flex-shrink: 0; } .monaco-table-th[data-col-index="0"], diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 9de2714ecdb79..b3bc3cddd5af0 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -13,6 +13,7 @@ import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Emitter, Event } from 'vs/base/common/event'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; // TODO@joao type TCell = any; @@ -158,7 +159,7 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa views: headers.map(view => ({ size: 1, view })) }; - this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, descriptor }); + this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, scrollbarVisibility: ScrollbarVisibility.Hidden, descriptor }); this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`; From fe02cd157d88a8cb05fd398717478a1fb8b9f58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 10:24:49 +0100 Subject: [PATCH 07/45] table: fix initial cell sizing --- src/vs/base/browser/ui/table/tableWidget.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index b3bc3cddd5af0..b944191306903 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -36,7 +36,8 @@ class TableListRenderer implements IListRenderer { constructor( private columns: ITableColumn[], - renderers: ITableRenderer[] + renderers: ITableRenderer[], + private getColumnSize: (index: number) => number ) { const rendererMap = new Map(renderers.map(r => [r.templateId, r])); this.renderers = []; @@ -61,6 +62,7 @@ class TableListRenderer implements IListRenderer { const renderer = this.renderers[i]; const cellContainer = append(rowContainer, $('.monaco-table-td', { 'data-col-index': i })); + cellContainer.style.width = `${this.getColumnSize(i)}px`; cellContainers.push(cellContainer); cellTemplateData.push(renderer.renderTemplate(cellContainer)); } @@ -163,7 +165,7 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`; - const renderer = new TableListRenderer(columns, renderers); + const renderer = new TableListRenderer(columns, renderers, i => this.splitview.getViewSize(i)); this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [renderer], _options); this.columnLayoutDisposable = Event.any(...headers.map(h => h.onDidLayout)) From d3f2e22c19bfe9ae5ca06ad4d177b32623713da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 10:30:58 +0100 Subject: [PATCH 08/45] table: css --- src/vs/base/browser/ui/table/table.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 4696b0924481e..8f41cfb61e5b4 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -26,7 +26,6 @@ font-weight: bold; overflow: hidden; text-overflow: ellipsis; - border-right: 1px solid black; } .monaco-table-th, From 037b4b161a174e58fdf07b062488be18ec68716b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 13:16:51 +0100 Subject: [PATCH 09/45] simplify ITunnelViewModel --- .../contrib/remote/browser/tunnelView.ts | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 02b75df99e9c0..43cfac0bb38a2 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -16,7 +16,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -61,30 +61,25 @@ class TunnelTreeVirtualDelegate implements IListVirtualDelegate { } export interface ITunnelViewModel { - onForwardedPortsChanged: Event; - readonly forwarded: TunnelItem[]; - readonly detected: TunnelItem[]; - readonly candidates: TunnelItem[]; + readonly onForwardedPortsChanged: Event; + readonly all: (ITunnelGroup | TunnelItem)[]; readonly input: TunnelItem; - groupsAndForwarded(): Promise; + isEmpty(): boolean; } -export class TunnelViewModel extends Disposable implements ITunnelViewModel { - private _onForwardedPortsChanged: Emitter = new Emitter(); - public onForwardedPortsChanged: Event = this._onForwardedPortsChanged.event; +export class TunnelViewModel implements ITunnelViewModel { + + readonly onForwardedPortsChanged: Event; private model: TunnelModel; private _input: TunnelItem; private _candidates: Map = new Map(); constructor( @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @IConfigurationService private readonly configurationService: IConfigurationService) { - super(); + @IConfigurationService private readonly configurationService: IConfigurationService + ) { this.model = remoteExplorerService.tunnelModel; - this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire())); - this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire())); - this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire())); - this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire())); + this.onForwardedPortsChanged = Event.any(this.model.onForwardPort, this.model.onClosePort, this.model.onPortName, this.model.onCandidatesChanged); this._input = { label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), wideLabel: nls.localize('remote.tunnelsView.add', "Forward a Port..."), @@ -98,10 +93,10 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }; } - async groupsAndForwarded(): Promise { + get all(): (ITunnelGroup | TunnelItem)[] { const groups: (ITunnelGroup | TunnelItem)[] = []; this._candidates = new Map(); - (await this.model.candidates).forEach(candidate => { + this.model.candidates.forEach(candidate => { this._candidates.set(makeAddress(candidate.host, candidate.port), candidate); }); if ((this.model.forwarded.size > 0) || this.remoteExplorerService.getEditableData(undefined)) { @@ -115,7 +110,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } if (!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) { - const candidates = await this.candidates; + const candidates = this.candidates; if (candidates.length > 0) { groups.push({ label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"), @@ -137,7 +132,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { } } - get forwarded(): TunnelItem[] { + private get forwarded(): TunnelItem[] { const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => { const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, tunnel); this.addProcessInfoFromCandidate(tunnelItem); @@ -155,7 +150,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { return forwarded; } - get detected(): TunnelItem[] { + private get detected(): TunnelItem[] { return Array.from(this.model.detected.values()).map(tunnel => { const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, tunnel, TunnelType.Detected, false); this.addProcessInfoFromCandidate(tunnelItem); @@ -163,7 +158,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } - get candidates(): TunnelItem[] { + private get candidates(): TunnelItem[] { const candidates: TunnelItem[] = []; this._candidates.forEach(value => { if (!mapHasAddressLocalhostOrAllInterfaces(this.model.forwarded, value.host, value.port) && @@ -178,8 +173,8 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { return this._input; } - dispose() { - super.dispose(); + isEmpty(): boolean { + return this.forwarded.length === 0 && this.candidates.length === 0 && this.detected.length === 0; } } @@ -391,7 +386,7 @@ class TunnelDataSource implements IAsyncDataSourceelement).items) { @@ -712,8 +707,7 @@ export class TunnelPanel extends ViewPane { } shouldShowWelcome(): boolean { - return (this.viewModel.forwarded.length === 0) && (this.viewModel.candidates.length === 0) && - (this.viewModel.detected.length === 0) && !this.isEditing; + return this.viewModel.isEmpty() && !this.isEditing; } focus(): void { From 589a0f268a363518b33dd9ec8f3e577042f8adc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 13:19:47 +0100 Subject: [PATCH 10/45] further TunnelViewModel simplification --- .../contrib/remote/browser/tunnelView.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 43cfac0bb38a2..38c5d7122eec0 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -71,26 +71,26 @@ export class TunnelViewModel implements ITunnelViewModel { readonly onForwardedPortsChanged: Event; private model: TunnelModel; - private _input: TunnelItem; private _candidates: Map = new Map(); + readonly input = { + label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), + wideLabel: nls.localize('remote.tunnelsView.add', "Forward a Port..."), + tunnelType: TunnelType.Add, + remoteHost: 'localhost', + remotePort: 0, + description: '', + wideDescription: '', + icon: undefined, + tooltip: '' + }; + constructor( @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IConfigurationService private readonly configurationService: IConfigurationService ) { this.model = remoteExplorerService.tunnelModel; this.onForwardedPortsChanged = Event.any(this.model.onForwardPort, this.model.onClosePort, this.model.onPortName, this.model.onCandidatesChanged); - this._input = { - label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - wideLabel: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - tunnelType: TunnelType.Add, - remoteHost: 'localhost', - remotePort: 0, - description: '', - wideDescription: '', - icon: undefined, - tooltip: '' - }; } get all(): (ITunnelGroup | TunnelItem)[] { @@ -120,7 +120,7 @@ export class TunnelViewModel implements ITunnelViewModel { } } if (groups.length === 0) { - groups.push(this._input); + groups.push(this.input); } return groups; } @@ -145,7 +145,7 @@ export class TunnelViewModel implements ITunnelViewModel { } }); if (this.remoteExplorerService.getEditableData(undefined)) { - forwarded.push(this._input); + forwarded.push(this.input); } return forwarded; } @@ -169,10 +169,6 @@ export class TunnelViewModel implements ITunnelViewModel { return candidates; } - get input(): TunnelItem { - return this._input; - } - isEmpty(): boolean { return this.forwarded.length === 0 && this.candidates.length === 0 && this.detected.length === 0; } From e8b4860729856b7c06f6285868d7f1942c2ea165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 13:35:24 +0100 Subject: [PATCH 11/45] splitview: getSashOrthogonalSize --- src/vs/base/browser/ui/splitview/splitview.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 8280a965ee479..40082a2270f69 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -34,6 +34,7 @@ export interface ISplitViewOptions { readonly proportionalLayout?: boolean; // default true, readonly descriptor?: ISplitViewDescriptor; readonly scrollbarVisibility?: ScrollbarVisibility; + readonly getSashOrthogonalSize?: () => number; } /** @@ -228,6 +229,7 @@ export class SplitView extends Disposable { private state: State = State.Idle; private inverseAltBehavior: boolean; private proportionalLayout: boolean; + private readonly getSashOrthogonalSize: { (): number } | undefined; private _onDidSashChange = this._register(new Emitter()); readonly onDidSashChange = this._onDidSashChange.event; @@ -299,6 +301,7 @@ export class SplitView extends Disposable { this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; this.inverseAltBehavior = !!options.inverseAltBehavior; this.proportionalLayout = types.isUndefined(options.proportionalLayout) ? true : !!options.proportionalLayout; + this.getSashOrthogonalSize = options.getSashOrthogonalSize; this.el = document.createElement('div'); this.el.classList.add('monaco-split-view2'); @@ -707,17 +710,11 @@ export class SplitView extends Disposable { // Add sash if (this.viewItems.length > 1) { + let opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash }; + const sash = this.orientation === Orientation.VERTICAL - ? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, { - orientation: Orientation.HORIZONTAL, - orthogonalStartSash: this.orthogonalStartSash, - orthogonalEndSash: this.orthogonalEndSash - }) - : new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, { - orientation: Orientation.VERTICAL, - orthogonalStartSash: this.orthogonalStartSash, - orthogonalEndSash: this.orthogonalEndSash - }); + ? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.HORIZONTAL }) + : new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.VERTICAL }); const sashEventMapper = this.orientation === Orientation.VERTICAL ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey }) From 4006dd2e4fb2206091571d0714ddd84b2783e188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 13:35:34 +0100 Subject: [PATCH 12/45] table: use getSashOrthogonalSize --- src/vs/base/browser/ui/table/tableWidget.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index b944191306903..8a805e9e27897 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -144,6 +144,7 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa private splitview: SplitView; private list: List; private columnLayoutDisposable: IDisposable; + private cachedHeight: number = 0; constructor( user: string, @@ -161,7 +162,13 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa views: headers.map(view => ({ size: 1, view })) }; - this.splitview = new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, scrollbarVisibility: ScrollbarVisibility.Hidden, descriptor }); + this.splitview = new SplitView(this.domNode, { + orientation: Orientation.HORIZONTAL, + scrollbarVisibility: ScrollbarVisibility.Hidden, + getSashOrthogonalSize: () => this.cachedHeight, + descriptor + }); + this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`; this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`; @@ -180,6 +187,7 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa height = height ?? getContentHeight(this.domNode); width = width ?? getContentWidth(this.domNode); + this.cachedHeight = height; this.splitview.layout(width); this.list.layout(height - this.virtualDelegate.headerRowHeight, width); } From 0ac9125128ea1567f5a583c930b05ccc1ca6995b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 14:18:48 +0100 Subject: [PATCH 13/45] table: styles, domFocus --- src/vs/base/browser/ui/table/table.css | 5 ++++- src/vs/base/browser/ui/table/tableWidget.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 8f41cfb61e5b4..603a701c7dca7 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -31,8 +31,11 @@ .monaco-table-th, .monaco-table-td { box-sizing: border-box; - padding: 0 10px; + padding-left: 10px; flex-shrink: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .monaco-table-th[data-col-index="0"], diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 8a805e9e27897..d356a36b80f69 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -196,6 +196,10 @@ export class TableWidget implements ISpliceable, IThemable, IDisposa this.list.style(styles); } + domFocus(): void { + this.list.domFocus(); + } + dispose(): void { this.splitview.dispose(); this.list.dispose(); From 0cf9477e966597574fde066b6d15699c90a64b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 14:21:47 +0100 Subject: [PATCH 14/45] tunnel view: start to adopt table --- .../contrib/remote/browser/tunnelView.ts | 322 ++++++++++-------- 1 file changed, 180 insertions(+), 142 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 38c5d7122eec0..a1407ff0d59c1 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -7,7 +7,6 @@ import 'vs/css!./media/tunnelView'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IViewDescriptor, IEditableData, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -46,23 +45,24 @@ import { forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, priva import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; +import { TableWidget } from 'vs/base/browser/ui/table/tableWidget'; +import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); export const PORT_AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; -class TunnelTreeVirtualDelegate implements IListVirtualDelegate { - getHeight(element: ITunnelItem): number { - return 22; - } +class TunnelTreeVirtualDelegate implements ITableVirtualDelegate { - getTemplateId(element: ITunnelItem): string { - return 'tunnelItemTemplate'; + readonly headerRowHeight: number = 22; + + getHeight(): number { + return 22; } } export interface ITunnelViewModel { readonly onForwardedPortsChanged: Event; - readonly all: (ITunnelGroup | TunnelItem)[]; + readonly all: TunnelItem[]; readonly input: TunnelItem; isEmpty(): boolean; } @@ -93,36 +93,36 @@ export class TunnelViewModel implements ITunnelViewModel { this.onForwardedPortsChanged = Event.any(this.model.onForwardPort, this.model.onClosePort, this.model.onPortName, this.model.onCandidatesChanged); } - get all(): (ITunnelGroup | TunnelItem)[] { - const groups: (ITunnelGroup | TunnelItem)[] = []; + get all(): TunnelItem[] { + const result: TunnelItem[] = []; this._candidates = new Map(); this.model.candidates.forEach(candidate => { this._candidates.set(makeAddress(candidate.host, candidate.port), candidate); }); if ((this.model.forwarded.size > 0) || this.remoteExplorerService.getEditableData(undefined)) { - groups.push(...this.forwarded); - } - if (this.model.detected.size > 0) { - groups.push({ - label: nls.localize('remote.tunnelsView.detected', "Static Ports"), - tunnelType: TunnelType.Detected, - items: this.detected - }); - } - if (!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) { - const candidates = this.candidates; - if (candidates.length > 0) { - groups.push({ - label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"), - tunnelType: TunnelType.Candidate, - items: candidates - }); - } + result.push(...this.forwarded); } - if (groups.length === 0) { - groups.push(this.input); + // if (this.model.detected.size > 0) { + // result.push({ + // label: nls.localize('remote.tunnelsView.detected', "Static Ports"), + // tunnelType: TunnelType.Detected, + // items: this.detected + // }); + // } + // if (!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) { + // const candidates = this.candidates; + // if (candidates.length > 0) { + // result.push({ + // label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"), + // tunnelType: TunnelType.Candidate, + // items: candidates + // }); + // } + // } + if (result.length === 0) { + result.push(this.input); } - return groups; + return result; } private addProcessInfoFromCandidate(tunnelItem: ITunnelItem) { @@ -392,6 +392,50 @@ class TunnelDataSource implements IAsyncDataSource { + readonly label: string = nls.localize('label', "Label"); + readonly weight: number = 1; + readonly templateId: string = 'string'; + project(row: ITunnelItem): string { + return row.label; + } +} + +class LocalAddressColumn implements ITableColumn { + readonly label: string = nls.localize('local address', "Local Address"); + readonly weight: number = 1; + readonly templateId: string = 'string'; + project(row: ITunnelItem): string { + return row.localAddress!; // TODO@joao TODO@alexr00 + } +} + +class RunningProcessColumn implements ITableColumn { + readonly label: string = nls.localize('process', "Running Process"); + readonly weight: number = 1; + readonly templateId: string = 'string'; + project(row: ITunnelItem): string { + return row.description!; // TODO@joao TODO@alexr00 + } +} + +class StringRenderer implements ITableRenderer { + + readonly templateId = 'string'; + + renderTemplate(container: HTMLElement): HTMLElement { + return container; + } + + renderElement(element: string, index: number, container: HTMLElement): void { + container.textContent = element; + } + + disposeTemplate(templateData: HTMLElement): void { + // noop + } +} + interface ITunnelGroup { tunnelType: TunnelType; label: string; @@ -477,10 +521,6 @@ class TunnelItem implements ITunnelItem { private static getDescription(item: TunnelItem, isWide: boolean) { const description: string[] = []; - if (item.name && item.localAddress) { - description.push(nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} \u2192 {1}", item.remotePort, isWide ? item.localAddress : TunnelItem.compactLongAddress(item.localAddress))); - } - if (item.runningProcess) { let processPid: string; if (item.pid && item.remoteExplorerService?.namedProcesses.has(item.pid)) { @@ -559,12 +599,12 @@ const TunnelViewSelectionContextKey = new RawContextKey const PortChangableContextKey = new RawContextKey('portChangable', false); const WebContextKey = new RawContextKey('isWeb', isWeb); -class TunnelDataTree extends WorkbenchAsyncDataTree { } - export class TunnelPanel extends ViewPane { + static readonly ID = TUNNEL_VIEW_ID; static readonly TITLE = nls.localize('remote.tunnel', "Ports"); - private tree!: TunnelDataTree; + + private table!: TableWidget; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelPrivacyContext: IContextKey; @@ -621,86 +661,84 @@ export class TunnelPanel extends ViewPane { super.renderBody(container); const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view')); - const treeContainer = dom.append(panelContainer, dom.$('.customview-tree')); - treeContainer.classList.add('ports-view'); - treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); + const widgetContainer = dom.append(panelContainer, dom.$('.customview-tree')); + widgetContainer.classList.add('ports-view'); + widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); - this.tree = this.instantiationService.createInstance(TunnelDataTree, + this.table = new TableWidget( 'RemoteTunnels', - treeContainer, + widgetContainer, new TunnelTreeVirtualDelegate(), - [renderer], - new TunnelDataSource(), + [new LabelColumn(), new LocalAddressColumn(), new RunningProcessColumn()], + [new StringRenderer()], { - collapseByDefault: (e: ITunnelItem | ITunnelGroup): boolean => { - return false; - }, - keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => { - return item.label; - } - }, - multipleSelectionSupport: false, - accessibilityProvider: { - getAriaLabel: (item: ITunnelItem | ITunnelGroup) => { - if (item instanceof TunnelItem) { - return item.tooltip; - } else { - return item.label; - } - }, - getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View") - } + // keyboardNavigationLabelProvider: { + // getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => { + // return item.label; + // } + // }, + // multipleSelectionSupport: false, + // accessibilityProvider: { + // getAriaLabel: (item: ITunnelItem | ITunnelGroup) => { + // if (item instanceof TunnelItem) { + // return item.tooltip; + // } else { + // return item.label; + // } + // }, + // getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View") + // } } ); + const actionRunner: ActionRunner = new ActionRunner(); renderer.actionRunner = actionRunner; - this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner))); - this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); - this._register(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements))); - this._register(this.tree.onDidFocus(() => this.tunnelViewFocusContext.set(true))); - this._register(this.tree.onDidBlur(() => this.tunnelViewFocusContext.set(false))); + // this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner))); + // this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); + // this._register(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements))); + // this._register(this.tree.onDidFocus(() => this.tunnelViewFocusContext.set(true))); + // this._register(this.tree.onDidBlur(() => this.tunnelViewFocusContext.set(false))); - this.tree.setInput(this.viewModel); + this.table.splice(0, 0, this.viewModel.all); this._register(this.viewModel.onForwardedPortsChanged(() => { this._onDidChangeViewWelcomeState.fire(); - this.tree.updateChildren(undefined, true); + this.table.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); })); - this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(e => { - if (e.element && (e.element.tunnelType === TunnelType.Add)) { - this.commandService.executeCommand(ForwardPortAction.INLINE_ID); - } - })); + // this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(e => { + // if (e.element && (e.element.tunnelType === TunnelType.Add)) { + // this.commandService.executeCommand(ForwardPortAction.INLINE_ID); + // } + // })); - this._register(this.remoteExplorerService.onDidChangeEditable(async e => { - this.isEditing = !!this.remoteExplorerService.getEditableData(e); - this._onDidChangeViewWelcomeState.fire(); + // this._register(this.remoteExplorerService.onDidChangeEditable(async e => { + // this.isEditing = !!this.remoteExplorerService.getEditableData(e); + // this._onDidChangeViewWelcomeState.fire(); - if (!this.isEditing) { - treeContainer.classList.remove('highlight'); - } + // if (!this.isEditing) { + // widgetContainer.classList.remove('highlight'); + // } - await this.tree.updateChildren(undefined, false); + // await this.tree.updateChildren(undefined, false); - if (this.isEditing) { - treeContainer.classList.add('highlight'); - if (!e) { - // When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view. - this.tree.reveal(this.viewModel.input); - } - } else { - this.tree.domFocus(); - } - })); + // if (this.isEditing) { + // widgetContainer.classList.add('highlight'); + // if (!e) { + // // When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view. + // this.tree.reveal(this.viewModel.input); + // } + // } else { + // this.tree.domFocus(); + // } + // })); } - private get contributedContextMenu(): IMenu { - const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService)); - return contributedContextMenu; - } + // private get contributedContextMenu(): IMenu { + // const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService)); + // return contributedContextMenu; + // } shouldShowWelcome(): boolean { return this.viewModel.isEmpty() && !this.isEditing; @@ -708,7 +746,7 @@ export class TunnelPanel extends ViewPane { focus(): void { super.focus(); - this.tree.domFocus(); + this.table.domFocus(); } private onFocusChanged(elements: ITunnelItem[]) { @@ -729,49 +767,49 @@ export class TunnelPanel extends ViewPane { } private onContextMenu(treeEvent: ITreeContextMenuEvent, actionRunner: ActionRunner): void { - if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) { - return; - } - const node: ITunnelItem | null = treeEvent.element; - const event: UIEvent = treeEvent.browserEvent; - - event.preventDefault(); - event.stopPropagation(); - - if (node) { - this.tree!.setFocus([node]); - this.tunnelTypeContext.set(node.tunnelType); - this.tunnelCloseableContext.set(!!node.closeable); - this.tunnelPrivacyContext.set(node.privacy); - this.portChangableContextKey.set(!!node.localPort); - } else { - this.tunnelTypeContext.set(TunnelType.Add); - this.tunnelCloseableContext.set(false); - this.tunnelPrivacyContext.set(undefined); - this.portChangableContextKey.set(false); - } - - const actions: IAction[] = []; - this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions)); - - this.contextMenuService.showContextMenu({ - getAnchor: () => treeEvent.anchor, - getActions: () => actions, - getActionViewItem: (action) => { - const keybinding = this.keybindingService.lookupKeybinding(action.id); - if (keybinding) { - return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - } - return undefined; - }, - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - this.tree!.domFocus(); - } - }, - getActionsContext: () => node, - actionRunner - }); + // if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) { + // return; + // } + // const node: ITunnelItem | null = treeEvent.element; + // const event: UIEvent = treeEvent.browserEvent; + + // event.preventDefault(); + // event.stopPropagation(); + + // if (node) { + // this.tree!.setFocus([node]); + // this.tunnelTypeContext.set(node.tunnelType); + // this.tunnelCloseableContext.set(!!node.closeable); + // this.tunnelPrivacyContext.set(node.privacy); + // this.portChangableContextKey.set(!!node.localPort); + // } else { + // this.tunnelTypeContext.set(TunnelType.Add); + // this.tunnelCloseableContext.set(false); + // this.tunnelPrivacyContext.set(undefined); + // this.portChangableContextKey.set(false); + // } + + // const actions: IAction[] = []; + // this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions)); + + // this.contextMenuService.showContextMenu({ + // getAnchor: () => treeEvent.anchor, + // getActions: () => actions, + // getActionViewItem: (action) => { + // const keybinding = this.keybindingService.lookupKeybinding(action.id); + // if (keybinding) { + // return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + // } + // return undefined; + // }, + // onHide: (wasCancelled?: boolean) => { + // if (wasCancelled) { + // this.tree!.domFocus(); + // } + // }, + // getActionsContext: () => node, + // actionRunner + // }); } private onMouseDblClick(e: ITreeMouseEvent): void { @@ -782,7 +820,7 @@ export class TunnelPanel extends ViewPane { protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); - this.tree.layout(height, width); + this.table.layout(height, width); } } From 5816c4ea2c9be52a8abf52ea5cd78d2c90b15889 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 17 Feb 2021 14:48:20 +0100 Subject: [PATCH 15/45] Improve tunnel label and process description --- .../contrib/remote/browser/tunnelView.ts | 59 ++++++++----------- .../remote/common/remoteExplorerService.ts | 2 +- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index a1407ff0d59c1..ec5fe8ad3e527 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -79,7 +79,7 @@ export class TunnelViewModel implements ITunnelViewModel { tunnelType: TunnelType.Add, remoteHost: 'localhost', remotePort: 0, - description: '', + processDescription: '', wideDescription: '', icon: undefined, tooltip: '' @@ -128,7 +128,7 @@ export class TunnelViewModel implements ITunnelViewModel { private addProcessInfoFromCandidate(tunnelItem: ITunnelItem) { const key = makeAddress(tunnelItem.remoteHost, tunnelItem.remotePort); if (this._candidates.has(key)) { - tunnelItem.description = this._candidates.get(key)!.detail; + tunnelItem.processDescription = this._candidates.get(key)!.detail; } } @@ -257,7 +257,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { readonly weight: number = 1; readonly templateId: string = 'string'; project(row: ITunnelItem): string { - return row.description!; // TODO@joao TODO@alexr00 + return row.processDescription ?? ''; } } @@ -473,22 +473,20 @@ class TunnelItem implements ITunnelItem { private remoteExplorerService?: IRemoteExplorerService ) { } - private static getLabel(name: string | undefined, localAddress: string | undefined, remotePort: number, isWide: boolean = false): string { + private static getLabel(name: string | undefined, remotePort: number): string { if (name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", name); - } else if (localAddress) { - return nls.localize('remote.tunnelsView.forwardedPortLabel1', "{0} \u2192 {1}", remotePort, isWide ? localAddress : TunnelItem.compactLongAddress(localAddress)); } else { return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}", remotePort); } } get label(): string { - return TunnelItem.getLabel(this.name, this.localAddress, this.remotePort); + return TunnelItem.getLabel(this.name, this.remotePort); } get wideLabel(): string { - return TunnelItem.getLabel(this.name, this.localAddress, this.remotePort, true); + return TunnelItem.getLabel(this.name, this.remotePort); } private static compactLongAddress(address: string): string { @@ -514,49 +512,40 @@ class TunnelItem implements ITunnelItem { return displayAddress; } - set description(description: string | undefined) { + set processDescription(description: string | undefined) { this.runningProcess = description; } - private static getDescription(item: TunnelItem, isWide: boolean) { - const description: string[] = []; - + private static getProcessDescription(item: TunnelItem, isWide: boolean) { + let description: string = ''; if (item.runningProcess) { - let processPid: string; if (item.pid && item.remoteExplorerService?.namedProcesses.has(item.pid)) { // This is a known process. Give it a friendly name. - processPid = item.remoteExplorerService.namedProcesses.get(item.pid)!; + description = item.remoteExplorerService.namedProcesses.get(item.pid)!; } else if (isWide) { - processPid = item.runningProcess.replace(/\0/g, ' ').trim(); + description = item.runningProcess.replace(/\0/g, ' ').trim(); } else { const nullIndex = item.runningProcess.indexOf('\0'); - processPid = item.runningProcess.substr(0, nullIndex > 0 ? nullIndex : item.runningProcess.length).trim(); - const spaceIndex = processPid.indexOf(' ', 110); - processPid = processPid.substr(0, spaceIndex > 0 ? spaceIndex : processPid.length); + description = item.runningProcess.substr(0, nullIndex > 0 ? nullIndex : item.runningProcess.length).trim(); + const spaceIndex = description.indexOf(' ', 110); + description = description.substr(0, spaceIndex > 0 ? spaceIndex : description.length); } if (item.pid) { - processPid += ` (${item.pid})`; + description += ` (${item.pid})`; } - description.push(processPid); - } - - if (item.source) { - description.push(item.source); - } - - if (description.length > 0) { - return description.join(' \u2022 '); + } else if (item.source) { + description = item.source; } - return undefined; + return description; } - get description(): string | undefined { - return TunnelItem.getDescription(this, false); + get processDescription(): string | undefined { + return TunnelItem.getProcessDescription(this, false); } get wideDescription(): string | undefined { - return TunnelItem.getDescription(this, true); + return TunnelItem.getProcessDescription(this, true); } get icon(): ThemeIcon | undefined { @@ -956,7 +945,7 @@ function makeTunnelPicks(tunnels: Tunnel[], remoteExplorerService: IRemoteExplor const item = TunnelItem.createFromTunnel(remoteExplorerService, forwarded); return { label: item.label, - description: item.description, + description: item.processDescription, tunnel: item }; }); @@ -1092,7 +1081,7 @@ namespace OpenPortInBrowserCommandPaletteAction { const tunnelItem = TunnelItem.createFromTunnel(remoteExplorerService, value[1]); return { label: tunnelItem.label, - description: tunnelItem.description, + description: tunnelItem.processDescription, tunnel: tunnelItem }; }); diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 1ced7e337bf2e..d909a8105dde9 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -46,7 +46,7 @@ export interface ITunnelItem { name?: string; closeable?: boolean; privacy?: TunnelPrivacy; - description?: string; + processDescription?: string; wideDescription?: string; readonly icon?: ThemeIcon; readonly label: string; From 8244f999659200220b96f3c4dcde8b5af713f180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 15:31:00 +0100 Subject: [PATCH 16/45] table: rename --- src/vs/base/browser/ui/table/tableWidget.ts | 2 +- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index d356a36b80f69..2cf34dbebd5e3 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -138,7 +138,7 @@ class ColumnHeader implements IView { } } -export class TableWidget implements ISpliceable, IThemable, IDisposable { +export class Table implements ISpliceable, IThemable, IDisposable { private domNode: HTMLElement; private splitview: SplitView; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index ec5fe8ad3e527..825ae17262226 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -45,7 +45,7 @@ import { forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, priva import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; -import { TableWidget } from 'vs/base/browser/ui/table/tableWidget'; +import { Table } from 'vs/base/browser/ui/table/tableWidget'; import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); @@ -593,7 +593,7 @@ export class TunnelPanel extends ViewPane { static readonly ID = TUNNEL_VIEW_ID; static readonly TITLE = nls.localize('remote.tunnel', "Ports"); - private table!: TableWidget; + private table!: Table; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelPrivacyContext: IContextKey; @@ -655,7 +655,7 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); - this.table = new TableWidget( + this.table = new Table( 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), From 03ad0f1f8d9823c084cadcabea81b5c62761bd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 15:54:29 +0100 Subject: [PATCH 17/45] workbench table --- src/vs/base/browser/ui/table/table.ts | 7 +- src/vs/base/browser/ui/table/tableWidget.ts | 102 +++++++++++- src/vs/platform/list/browser/listService.ts | 148 +++++++++++++++++- .../workbench/browser/actions/listCommands.ts | 45 +++--- 4 files changed, 264 insertions(+), 38 deletions(-) diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index cdf427d4efb79..cf5e085d9f7a6 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list'; export interface ITableColumn { readonly label: string; @@ -19,6 +19,11 @@ export interface ITableVirtualDelegate { export interface ITableRenderer extends IListRenderer { } +export interface ITableEvent extends IListEvent { } +export interface ITableMouseEvent extends IListMouseEvent { } +export interface ITableTouchEvent extends IListTouchEvent { } +export interface ITableGestureEvent extends IListGestureEvent { } + export class TableError extends Error { constructor(user: string, message: string) { diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 2cf34dbebd5e3..3140bcb243bc1 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./table'; -import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; -import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { IListOptions, IListOptionsUpdate, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; +import { ITableColumn, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableTouchEvent, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { ISpliceable } from 'vs/base/common/sequence'; import { IThemable } from 'vs/base/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -13,14 +13,11 @@ import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Emitter, Event } from 'vs/base/common/event'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; // TODO@joao type TCell = any; -export interface ITableOptions extends IListOptions { } -export interface ITableStyles extends IListStyles { } - interface RowTemplateData { readonly container: HTMLElement; readonly cellContainers: HTMLElement[]; @@ -138,14 +135,45 @@ class ColumnHeader implements IView { } } +export interface ITableOptions extends IListOptions { } +export interface ITableOptionsUpdate extends IListOptionsUpdate { } +export interface ITableStyles extends IListStyles { } + export class Table implements ISpliceable, IThemable, IDisposable { - private domNode: HTMLElement; + readonly domNode: HTMLElement; private splitview: SplitView; private list: List; private columnLayoutDisposable: IDisposable; private cachedHeight: number = 0; + get onDidChangeFocus(): Event> { return this.list.onDidChangeFocus; } + get onDidChangeSelection(): Event> { return this.list.onDidChangeSelection; } + + get onDidScroll(): Event { return this.list.onDidScroll; } + get onMouseClick(): Event> { return this.list.onMouseClick; } + get onMouseDblClick(): Event> { return this.list.onMouseDblClick; } + get onMouseMiddleClick(): Event> { return this.list.onMouseMiddleClick; } + get onPointer(): Event> { return this.list.onPointer; } + get onMouseUp(): Event> { return this.list.onMouseUp; } + get onMouseDown(): Event> { return this.list.onMouseDown; } + get onMouseOver(): Event> { return this.list.onMouseOver; } + get onMouseMove(): Event> { return this.list.onMouseMove; } + get onMouseOut(): Event> { return this.list.onMouseOut; } + get onTouchStart(): Event> { return this.list.onTouchStart; } + get onTap(): Event> { return this.list.onTap; } + + get onDidFocus(): Event { return this.list.onDidFocus; } + get onDidBlur(): Event { return this.list.onDidBlur; } + + get scrollTop(): number { return this.list.scrollTop; } + set scrollTop(scrollTop: number) { this.list.scrollTop = scrollTop; } + get scrollLeft(): number { return this.list.scrollLeft; } + set scrollLeft(scrollLeft: number) { this.list.scrollLeft = scrollLeft; } + get scrollHeight(): number { return this.list.scrollHeight; } + get renderHeight(): number { return this.list.renderHeight; } + get onDidDispose(): Event { return this.list.onDidDispose; } + constructor( user: string, container: HTMLElement, @@ -179,10 +207,22 @@ export class Table implements ISpliceable, IThemable, IDisposable { (([index, size]) => renderer.layoutColumn(index, size)); } + updateOptions(options: ITableOptionsUpdate): void { + this.list.updateOptions(options); + } + splice(start: number, deleteCount: number, elements: TRow[] = []): void { this.list.splice(start, deleteCount, elements); } + get length(): number { + return this.list.length; + } + + getHTMLElement(): HTMLElement { + return this.domNode; + } + layout(height?: number, width?: number): void { height = height ?? getContentHeight(this.domNode); width = width ?? getContentWidth(this.domNode); @@ -192,6 +232,10 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.list.layout(height - this.virtualDelegate.headerRowHeight, width); } + toggleKeyboardNavigation(): void { + this.list.toggleKeyboardNavigation(); + } + style(styles: ITableStyles): void { this.list.style(styles); } @@ -200,6 +244,50 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.list.domFocus(); } + setSelection(indexes: number[], browserEvent?: UIEvent): void { + this.list.setSelection(indexes, browserEvent); + } + + getSelection(): number[] { + return this.list.getSelection(); + } + + setFocus(indexes: number[], browserEvent?: UIEvent): void { + this.list.setFocus(indexes, browserEvent); + } + + focusNext(n = 1, loop = false, browserEvent?: UIEvent): void { + this.list.focusNext(n, loop, browserEvent); + } + + focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void { + this.list.focusPrevious(n, loop, browserEvent); + } + + focusNextPage(browserEvent?: UIEvent): void { + this.list.focusNextPage(browserEvent); + } + + focusPreviousPage(browserEvent?: UIEvent): void { + this.list.focusPreviousPage(browserEvent); + } + + focusFirst(browserEvent?: UIEvent): void { + this.list.focusFirst(browserEvent); + } + + focusLast(browserEvent?: UIEvent): void { + this.list.focusLast(browserEvent); + } + + getFocus(): number[] { + return this.list.getFocus(); + } + + reveal(index: number, relativeTop?: number): void { + this.list.reveal(index, relativeTop); + } + dispose(): void { this.splitview.dispose(); this.list.dispose(); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 752fdb0e4e173..5057a73bf4eae 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -26,9 +26,11 @@ import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeC import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; import { IKeyboardNavigationEventFilter, IAbstractTreeOptions, RenderIndentGuides, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ITableOptions, ITableOptionsUpdate, Table } from 'vs/base/browser/ui/table/tableWidget'; +import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; -export type ListWidget = List | PagedList | ObjectTree | DataTree | AsyncDataTree; -export type WorkbenchListWidget = WorkbenchList | WorkbenchPagedList | WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree; +export type ListWidget = List | PagedList | ObjectTree | DataTree | AsyncDataTree | Table; +export type WorkbenchListWidget = WorkbenchList | WorkbenchPagedList | WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree | WorkbenchTable; export const IListService = createDecorator('listService'); @@ -189,9 +191,7 @@ export interface IWorkbenchListOptionsUpdate extends IListOptionsUpdate { readonly overrideStyles?: IColorMapping; } -export interface IWorkbenchListOptions extends IWorkbenchListOptionsUpdate, IListOptions { - readonly accessibilityProvider: IListAccessibilityProvider; -} +export interface IWorkbenchListOptions extends IWorkbenchListOptionsUpdate, IListOptions { } export class WorkbenchList extends List { @@ -317,9 +317,7 @@ export class WorkbenchList extends List { } } -export interface IWorkbenchPagedListOptions extends IWorkbenchListOptionsUpdate, IPagedListOptions { - readonly accessibilityProvider: IListAccessibilityProvider; -} +export interface IWorkbenchPagedListOptions extends IWorkbenchListOptionsUpdate, IPagedListOptions { } export class WorkbenchPagedList extends PagedList { @@ -403,6 +401,140 @@ export class WorkbenchPagedList extends PagedList { } } +export interface IWorkbenchTableOptionsUpdate extends ITableOptionsUpdate { + readonly overrideStyles?: IColorMapping; +} + +export interface IWorkbenchTableOptions extends IWorkbenchTableOptionsUpdate, ITableOptions { } + +export class WorkbenchTable extends Table { + + readonly contextKeyService: IContextKeyService; + private readonly themeService: IThemeService; + + private listHasSelectionOrFocus: IContextKey; + private listDoubleSelection: IContextKey; + private listMultiSelection: IContextKey; + private horizontalScrolling: boolean | undefined; + + private _styler: IDisposable | undefined; + private _useAltAsMultipleSelectionModifier: boolean; + + private readonly disposables: DisposableStore; + + constructor( + user: string, + container: HTMLElement, + delegate: ITableVirtualDelegate, + columns: ITableColumn[], + renderers: ITableRenderer[], + options: IWorkbenchTableOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService + ) { + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); + const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); + + super(user, container, delegate, columns, renderers, + { + keyboardSupport: false, + ...computeStyles(themeService.getColorTheme(), defaultListStyles), + ...workbenchListOptions, + horizontalScrolling + } + ); + + this.disposables = new DisposableStore(); + this.disposables.add(workbenchListOptionsDisposable); + + this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.themeService = themeService; + + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + + this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); + this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); + this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); + this.horizontalScrolling = options.horizontalScrolling; + + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + + this.disposables.add(this.contextKeyService); + this.disposables.add((listService as ListService).register(this)); + + if (options.overrideStyles) { + this.updateStyles(options.overrideStyles); + } + + this.disposables.add(this.onDidChangeSelection(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.contextKeyService.bufferChangeEvents(() => { + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + this.listMultiSelection.set(selection.length > 1); + this.listDoubleSelection.set(selection.length === 2); + }); + })); + this.disposables.add(this.onDidChangeFocus(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + })); + this.disposables.add(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + } + + let options: IListOptionsUpdate = {}; + + if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) { + const horizontalScrolling = configurationService.getValue(horizontalScrollingKey); + options = { ...options, horizontalScrolling }; + } + if (e.affectsConfiguration(listSmoothScrolling)) { + const smoothScrolling = configurationService.getValue(listSmoothScrolling); + options = { ...options, smoothScrolling }; + } + if (Object.keys(options).length > 0) { + this.updateOptions(options); + } + })); + } + + updateOptions(options: IWorkbenchTableOptionsUpdate): void { + super.updateOptions(options); + + if (options.overrideStyles) { + this.updateStyles(options.overrideStyles); + } + } + + dispose(): void { + super.dispose(); + if (this._styler) { + this._styler.dispose(); + } + } + + private updateStyles(styles: IColorMapping): void { + if (this._styler) { + this._styler.dispose(); + } + + this._styler = attachListStyler(this, this.themeService, styles); + } + + get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } +} + export interface IOpenResourceOptions { editorOptions: IEditorOptions; sideBySide: boolean; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index b71489ac38e1f..8404499923304 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -7,7 +7,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus, getSelectionKeyboardEvent, WorkbenchListWidget } from 'vs/platform/list/browser/listService'; import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { range } from 'vs/base/common/arrays'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -16,6 +16,7 @@ import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Table } from 'vs/base/browser/ui/table/tableWidget'; function ensureDOMFocus(widget: ListWidget | undefined): void { // it can happen that one of the commands is executed while @@ -32,7 +33,7 @@ function focusDown(accessor: ServicesAccessor, arg2?: number, loop: boolean = fa const count = typeof arg2 === 'number' ? arg2 : 1; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.focusNext(count); @@ -71,10 +72,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor, arg2) => focusDown(accessor, arg2) }); -function expandMultiSelection(focused: List | PagedList | ObjectTree | DataTree | AsyncDataTree, previousFocus: unknown): void { +function expandMultiSelection(focused: WorkbenchListWidget, previousFocus: unknown): void { // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; const focus = list.getFocus() ? list.getFocus()[0] : undefined; @@ -118,7 +119,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List / Tree - if (focused instanceof List || focused instanceof PagedList || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { const list = focused; // Focus down first @@ -136,7 +137,7 @@ function focusUp(accessor: ServicesAccessor, arg2?: number, loop: boolean = fals const count = typeof arg2 === 'number' ? arg2 : 1; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.focusPrevious(count); @@ -184,7 +185,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List / Tree - if (focused instanceof List || focused instanceof PagedList || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table || focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { const list = focused; // Focus up first @@ -210,7 +211,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { + if (focused && !(focused instanceof List || focused instanceof PagedList || focused instanceof Table)) { if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { const tree = focused; const focusedElements = tree.getFocus(); @@ -245,10 +246,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow] }, handler: (accessor) => { - const focusedTree = accessor.get(IListService).lastFocusedList; + const focused = accessor.get(IListService).lastFocusedList; - if (focusedTree && !(focusedTree instanceof List || focusedTree instanceof PagedList)) { - focusedTree.collapseAll(); + if (focused && !(focused instanceof List || focused instanceof PagedList || focused instanceof Table)) { + focused.collapseAll(); } } }); @@ -261,7 +262,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor) => { const focused = accessor.get(IListService).lastFocusedList; - if (!focused || focused instanceof List || focused instanceof PagedList) { + if (!focused || focused instanceof List || focused instanceof PagedList || focused instanceof Table) { return; } @@ -291,7 +292,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // Tree only - if (focused && !(focused instanceof List || focused instanceof PagedList)) { + if (focused && !(focused instanceof List || focused instanceof PagedList || focused instanceof Table)) { if (focused instanceof ObjectTree || focused instanceof DataTree) { // TODO@Joao: instead of doing this here, just delegate to a tree method const tree = focused; @@ -355,7 +356,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { focused.focusPreviousPage(); } @@ -379,7 +380,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { focused.focusNextPage(); } @@ -414,7 +415,7 @@ function listFocusFirst(accessor: ServicesAccessor, options?: { fromFocused: boo const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.setFocus([0]); @@ -458,7 +459,7 @@ function listFocusLast(accessor: ServicesAccessor, options?: { fromFocused: bool const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.setFocus([list.length - 1]); @@ -487,7 +488,7 @@ function focusElement(accessor: ServicesAccessor, retainCurrentFocus: boolean): const focused = accessor.get(IListService).lastFocusedList; const fakeKeyboardEvent = getSelectionKeyboardEvent('keydown', retainCurrentFocus); // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.setSelection(list.getFocus(), fakeKeyboardEvent); } @@ -546,7 +547,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; const fakeKeyboardEvent = new KeyboardEvent('keydown'); list.setSelection(range(list.length), fakeKeyboardEvent); @@ -666,7 +667,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.setSelection([]); @@ -690,7 +691,7 @@ CommandsRegistry.registerCommand({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { const list = focused; list.toggleKeyboardNavigation(); } @@ -709,7 +710,7 @@ CommandsRegistry.registerCommand({ const focused = accessor.get(IListService).lastFocusedList; // List - if (focused instanceof List || focused instanceof PagedList) { + if (focused instanceof List || focused instanceof PagedList || focused instanceof Table) { // TODO@joao } From a16b41d9c49188004f65bef92908196d711fe907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 17 Feb 2021 15:59:10 +0100 Subject: [PATCH 18/45] adopt WorkbenchTable in tunnel view --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 825ae17262226..fbf11f1c3d9fe 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -45,8 +45,8 @@ import { forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, priva import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; -import { Table } from 'vs/base/browser/ui/table/tableWidget'; import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { WorkbenchTable } from 'vs/platform/list/browser/listService'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); export const PORT_AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; @@ -593,7 +593,7 @@ export class TunnelPanel extends ViewPane { static readonly ID = TUNNEL_VIEW_ID; static readonly TITLE = nls.localize('remote.tunnel', "Ports"); - private table!: Table; + private table!: WorkbenchTable; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelPrivacyContext: IContextKey; @@ -655,7 +655,7 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); - this.table = new Table( + this.table = this.instantiationService.createInstance(WorkbenchTable, 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), @@ -679,7 +679,7 @@ export class TunnelPanel extends ViewPane { // getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View") // } } - ); + ) as WorkbenchTable; const actionRunner: ActionRunner = new ActionRunner(); renderer.actionRunner = actionRunner; From bd5d97dbce82f81102f760754d0cd61bb6c5bf0f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 17 Feb 2021 18:14:52 +0100 Subject: [PATCH 19/45] Rendering for local address and label --- src/vs/platform/actions/common/actions.ts | 1 + .../remote/browser/media/tunnelView.css | 56 ++++++++ .../contrib/remote/browser/remoteIcons.ts | 1 + .../contrib/remote/browser/tunnelView.ts | 127 ++++++++++++++++-- 4 files changed, 174 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1e2d2df6c2f8c..89cbda97864b8 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -126,6 +126,7 @@ export class MenuId { static readonly TunnelContext = new MenuId('TunnelContext'); static readonly TunnelInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); + static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); static readonly ViewItemContext = new MenuId('ViewItemContext'); static readonly ViewContainerTitle = new MenuId('ViewContainerTitle'); static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); diff --git a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css index 55776ca9962c7..23d8c643df022 100644 --- a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css +++ b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css @@ -30,3 +30,59 @@ .pane-body.wide .ports-view .monaco-action-bar { margin-left: 10px; } + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell { + display: flex; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + flex-wrap: nowrap; +} + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .monaco-inputbox { + line-height: normal; + flex: 1; +} + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell > .ports-view-actionbar-cell-icon.codicon { + margin-top: 3px; +} + +.ports-view .monaco-list .monaco-list-row.selected .ports-view-actionbar-cell > .ports-view-actionbar-cell-icon.codicon { + color: currentColor !important; +} + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .ports-view-actionbar-cell-resourceLabel .monaco-icon-label-container > .monaco-icon-name-container { + flex: 1; +} + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .ports-view-actionbar-cell-resourceLabel::after { + padding-right: 0px; +} + +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .actions { + display: none; +} + +.ports-view .monaco-list .monaco-list-row:hover .ports-view-actionbar-cell .actions, +.ports-view .monaco-list .monaco-list-row.selected .ports-view-actionbar-cell .actions, +.ports-view .monaco-list .monaco-list-row.focused .ports-view-actionbar-cell .actions { + display: block; +} + +.ports-view .monaco-list .ports-view-actionbar-cell .actions .action-label { + width: 16px; + height: 100%; + background-size: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; +} + +.ports-view .monaco-list .ports-view-actionbar-cell .actions .action-label.codicon { + line-height: 22px; + height: 22px; +} + +.ports-view .monaco-list .ports-view-actionbar-cell .actions .action-label.codicon::before { + vertical-align: middle; +} diff --git a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts index e1ebf4235eb2b..586832c145d61 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts @@ -24,3 +24,4 @@ export const forwardPortIcon = registerIcon('ports-forward-icon', Codicon.plus, export const stopForwardIcon = registerIcon('ports-stop-forward-icon', Codicon.x, nls.localize('stopForwardIcon', 'Icon for the stop forwarding action.')); export const openBrowserIcon = registerIcon('ports-open-browser-icon', Codicon.globe, nls.localize('openBrowserIcon', 'Icon for the open browser action.')); export const openPreviewIcon = registerIcon('ports-open-preview-icon', Codicon.openPreview, nls.localize('openPreviewIcon', 'Icon for the open preview action.')); +export const copyAddressIcon = registerIcon('ports-copy-address-icon', Codicon.clippy, nls.localize('copyAddressIcon', 'Icon for the copy local address action.')); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index fbf11f1c3d9fe..e0f3597183291 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -41,12 +41,13 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, publicPortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; +import { copyAddressIcon, forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, publicPortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { Codicon } from 'vs/base/common/codicons'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); export const PORT_AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; @@ -392,21 +393,36 @@ class TunnelDataSource implements IAsyncDataSource { +class LabelColumn implements ITableColumn { readonly label: string = nls.localize('label', "Label"); readonly weight: number = 1; - readonly templateId: string = 'string'; - project(row: ITunnelItem): string { - return row.label; + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + const label = row.name ? `${row.name} (${row.remotePort})` : `${row.remotePort}`; + const icon = row.processDescription ? Codicon.circleFilled : Codicon.circleOutline; + const context: [string, any][] = + [ + ['view', TUNNEL_VIEW_ID], + ['tunnelType', row.tunnelType], + ['tunnelCloseable', row.closeable] + ]; + return { label, icon, tunnel: row, context }; } } -class LocalAddressColumn implements ITableColumn { +class LocalAddressColumn implements ITableColumn { readonly label: string = nls.localize('local address', "Local Address"); readonly weight: number = 1; - readonly templateId: string = 'string'; - project(row: ITunnelItem): string { - return row.localAddress!; // TODO@joao TODO@alexr00 + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + const context: [string, any][] = + [ + ['view', TUNNEL_VIEW_ID], + ['tunnelType', row.tunnelType], + ['tunnelCloseable', row.closeable] + ]; + const label = row.localAddress ?? ''; + return { label, menuId: MenuId.TunnelLocalAddressInline, context, tunnel: row }; } } @@ -436,6 +452,82 @@ class StringRenderer implements ITableRenderer { } } +interface IActionBarTemplateData { + elementDisposable: IDisposable; + container: HTMLElement; + label: IconLabel; + icon: HTMLElement; + actionBar: ActionBar; +} + +interface ActionBarCell { + label: string; + icon?: Codicon; + menuId?: MenuId; + context: [string, any][]; + tunnel: ITunnelItem; +} + +class ActionBarRenderer extends Disposable implements ITableRenderer { + readonly templateId = 'actionbar'; + private _actionRunner: ActionRunner | undefined; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { super(); } + + set actionRunner(actionRunner: ActionRunner) { + this._actionRunner = actionRunner; + } + + renderTemplate(container: HTMLElement): IActionBarTemplateData { + container.classList.add('ports-view-actionbar-cell'); + const icon = dom.append(container, dom.$('.ports-view-actionbar-cell-icon')); + const label = new IconLabel(container, { supportHighlights: true }); + const actionsContainer = dom.append(container, dom.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService), + respectOrientationForPreviousAndNextKey: true + }); + return { label, icon, actionBar, container, elementDisposable: Disposable.None }; + } + + renderElement(element: ActionBarCell, index: number, templateData: IActionBarTemplateData): void { + // reset + templateData.actionBar.clear(); + templateData.icon.className = 'ports-view-actionbar-cell-icon'; + templateData.icon.hidden = true; + + templateData.label.setLabel(element.label); + templateData.actionBar.context = element.tunnel; + const contextKeyService = this.contextKeyService.createOverlay(element.context); + const disposableStore = new DisposableStore(); + templateData.elementDisposable = disposableStore; + if (element.menuId) { + const menu = disposableStore.add(this.menuService.createMenu(element.menuId, contextKeyService)); + const actions: IAction[] = []; + disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions)); + if (actions) { + templateData.actionBar.push(actions, { icon: true, label: false }); + if (this._actionRunner) { + templateData.actionBar.actionRunner = this._actionRunner; + } + } + } + if (element.icon) { + templateData.icon.className = `ports-view-actionbar-cell-icon ${ThemeIcon.asClassName(element.icon)}`; + templateData.icon.hidden = false; + } + } + + disposeTemplate(templateData: IActionBarTemplateData): void { + templateData.actionBar.dispose(); + templateData.elementDisposable.dispose(); + } +} + interface ITunnelGroup { tunnelType: TunnelType; label: string; @@ -654,13 +746,15 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('ports-view'); widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); + const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, this.menuService); + const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); this.table = this.instantiationService.createInstance(WorkbenchTable, 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), [new LabelColumn(), new LocalAddressColumn(), new RunningProcessColumn()], - [new StringRenderer()], + [new StringRenderer(), actionBarRenderer], { // keyboardNavigationLabelProvider: { // getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => { @@ -682,7 +776,7 @@ export class TunnelPanel extends ViewPane { ) as WorkbenchTable; const actionRunner: ActionRunner = new ActionRunner(); - renderer.actionRunner = actionRunner; + actionBarRenderer.actionRunner = actionRunner; // this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner))); // this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); @@ -1422,3 +1516,14 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ }, when: TunnelCloseableContextKey })); + +MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ + group: '0_manage', + order: 0, + command: { + id: CopyAddressAction.INLINE_ID, + title: CopyAddressAction.INLINE_LABEL, + icon: copyAddressIcon + }, + when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected)) +})); From 3203a54e0bc3e973836a9fb9fb3b094386ee3dbe Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 09:41:00 +0100 Subject: [PATCH 20/45] More actions on cells --- src/vs/platform/actions/common/actions.ts | 2 +- .../contrib/remote/browser/tunnelView.ts | 52 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 89cbda97864b8..7b749b2417a3f 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -124,7 +124,7 @@ export class MenuId { static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); - static readonly TunnelInline = new MenuId('TunnelInline'); + static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); static readonly ViewItemContext = new MenuId('ViewItemContext'); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index e0f3597183291..70a6f178e49d0 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -271,7 +271,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { - readonly label: string = nls.localize('label', "Label"); +class PortColumn implements ITableColumn { + readonly label: string = nls.localize('port', "Port"); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -406,7 +406,7 @@ class LabelColumn implements ITableColumn { ['tunnelType', row.tunnelType], ['tunnelCloseable', row.closeable] ]; - return { label, icon, tunnel: row, context }; + return { label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline }; } } @@ -753,7 +753,7 @@ export class TunnelPanel extends ViewPane { 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), - [new LabelColumn(), new LocalAddressColumn(), new RunningProcessColumn()], + [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()], [new StringRenderer(), actionBarRenderer], { // keyboardNavigationLabelProvider: { @@ -1480,25 +1480,8 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ when: TunnelCloseableContextKey })); -MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ - order: 0, - command: { - id: OpenPortInBrowserAction.ID, - title: OpenPortInBrowserAction.LABEL, - icon: openBrowserIcon - }, - when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected)) -})); -MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ - order: 1, - command: { - id: OpenPortInPreviewAction.ID, - title: OpenPortInPreviewAction.LABEL, - icon: openPreviewIcon - }, - when: ContextKeyExpr.and(WebContextKey.negate(), ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))) -})); -MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ + +MenuRegistry.appendMenuItem(MenuId.TunnelPortInline, ({ order: 0, command: { id: ForwardPortAction.INLINE_ID, @@ -1507,7 +1490,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ }, when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate) })); -MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ +MenuRegistry.appendMenuItem(MenuId.TunnelPortInline, ({ order: 2, command: { id: ClosePortAction.INLINE_ID, @@ -1518,7 +1501,6 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ })); MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ - group: '0_manage', order: 0, command: { id: CopyAddressAction.INLINE_ID, @@ -1527,3 +1509,21 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ }, when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected)) })); +MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ + order: 1, + command: { + id: OpenPortInBrowserAction.ID, + title: OpenPortInBrowserAction.LABEL, + icon: openBrowserIcon + }, + when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected)) +})); +MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ + order: 2, + command: { + id: OpenPortInPreviewAction.ID, + title: OpenPortInPreviewAction.LABEL, + icon: openPreviewIcon + }, + when: ContextKeyExpr.and(WebContextKey.negate(), ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded), TunnelTypeContextKey.isEqualTo(TunnelType.Detected))) +})); From d15bdd6aeecbf01a6c709686cbc2581f6ba777e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 10:48:14 +0100 Subject: [PATCH 21/45] cleanup workbench lists --- src/vs/base/browser/ui/table/tableWidget.ts | 4 + src/vs/platform/list/browser/listService.ts | 104 ++++++++++++------ .../contrib/debug/browser/breakpointsView.ts | 19 ++-- .../extensions/browser/extensionsViews.ts | 12 +- .../files/browser/views/openEditorsView.ts | 9 +- 5 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 3140bcb243bc1..045771dd2b0ef 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -244,6 +244,10 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.list.domFocus(); } + getSelectedElements(): TRow[] { + return this.list.getSelectedElements(); + } + setSelection(indexes: number[], browserEvent?: UIEvent): void { this.list.setSelection(indexes, browserEvent); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 5057a73bf4eae..cd39b86801e3d 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -191,20 +191,20 @@ export interface IWorkbenchListOptionsUpdate extends IListOptionsUpdate { readonly overrideStyles?: IColorMapping; } -export interface IWorkbenchListOptions extends IWorkbenchListOptionsUpdate, IListOptions { } +export interface IWorkbenchListOptions extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IListOptions { } export class WorkbenchList extends List { readonly contextKeyService: IContextKeyService; private readonly themeService: IThemeService; - private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; private horizontalScrolling: boolean | undefined; - private _styler: IDisposable | undefined; private _useAltAsMultipleSelectionModifier: boolean; + private navigator: ListResourceNavigator; + get onDidOpen(): Event> { return this.navigator.onDidOpen; } constructor( user: string, @@ -287,6 +287,9 @@ export class WorkbenchList extends List { this.updateOptions(options); } })); + + this.navigator = new ListResourceNavigator(this, { configurationService, ...options }); + this.disposables.add(this.navigator); } updateOptions(options: IWorkbenchListOptionsUpdate): void { @@ -297,36 +300,33 @@ export class WorkbenchList extends List { } } - dispose(): void { - super.dispose(); - if (this._styler) { - this._styler.dispose(); - } - } - private updateStyles(styles: IColorMapping): void { - if (this._styler) { - this._styler.dispose(); - } - + this._styler?.dispose(); this._styler = attachListStyler(this, this.themeService, styles); } get useAltAsMultipleSelectionModifier(): boolean { return this._useAltAsMultipleSelectionModifier; } + + dispose(): void { + this._styler?.dispose(); + super.dispose(); + } } -export interface IWorkbenchPagedListOptions extends IWorkbenchListOptionsUpdate, IPagedListOptions { } +export interface IWorkbenchPagedListOptions extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IPagedListOptions { } export class WorkbenchPagedList extends PagedList { readonly contextKeyService: IContextKeyService; - + private readonly themeService: IThemeService; private readonly disposables: DisposableStore; - private _useAltAsMultipleSelectionModifier: boolean; private horizontalScrolling: boolean | undefined; + private _styler: IDisposable | undefined; + private navigator: ListResourceNavigator; + get onDidOpen(): Event> { return this.navigator.onDidOpen; } constructor( user: string, @@ -355,6 +355,8 @@ export class WorkbenchPagedList extends PagedList { this.disposables.add(workbenchListOptionsDisposable); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.themeService = themeService; + this.horizontalScrolling = options.horizontalScrolling; const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); @@ -365,6 +367,10 @@ export class WorkbenchPagedList extends PagedList { this.disposables.add(this.contextKeyService); this.disposables.add((listService as ListService).register(this)); + if (options.overrideStyles) { + this.updateStyles(options.overrideStyles); + } + if (options.overrideStyles) { this.disposables.add(attachListStyler(this, themeService, options.overrideStyles)); } @@ -388,6 +394,22 @@ export class WorkbenchPagedList extends PagedList { this.updateOptions(options); } })); + + this.navigator = new ListResourceNavigator(this, { configurationService, ...options }); + this.disposables.add(this.navigator); + } + + updateOptions(options: IWorkbenchListOptionsUpdate): void { + super.updateOptions(options); + + if (options.overrideStyles) { + this.updateStyles(options.overrideStyles); + } + } + + private updateStyles(styles: IColorMapping): void { + this._styler?.dispose(); + this._styler = attachListStyler(this, this.themeService, styles); } get useAltAsMultipleSelectionModifier(): boolean { @@ -395,9 +417,9 @@ export class WorkbenchPagedList extends PagedList { } dispose(): void { - super.dispose(); - + this._styler?.dispose(); this.disposables.dispose(); + super.dispose(); } } @@ -411,16 +433,15 @@ export class WorkbenchTable extends Table { readonly contextKeyService: IContextKeyService; private readonly themeService: IThemeService; - private listHasSelectionOrFocus: IContextKey; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; private horizontalScrolling: boolean | undefined; - private _styler: IDisposable | undefined; private _useAltAsMultipleSelectionModifier: boolean; - private readonly disposables: DisposableStore; + private navigator: TableResourceNavigator; + get onDidOpen(): Event> { return this.navigator.onDidOpen; } constructor( user: string, @@ -505,6 +526,9 @@ export class WorkbenchTable extends Table { this.updateOptions(options); } })); + + this.navigator = new TableResourceNavigator(this, { configurationService, ...options }); + this.disposables.add(this.navigator); } updateOptions(options: IWorkbenchTableOptionsUpdate): void { @@ -515,24 +539,20 @@ export class WorkbenchTable extends Table { } } - dispose(): void { - super.dispose(); - if (this._styler) { - this._styler.dispose(); - } - } - private updateStyles(styles: IColorMapping): void { - if (this._styler) { - this._styler.dispose(); - } - + this._styler?.dispose(); this._styler = attachListStyler(this, this.themeService, styles); } get useAltAsMultipleSelectionModifier(): boolean { return this._useAltAsMultipleSelectionModifier; } + + dispose(): void { + this._styler?.dispose(); + this.disposables.dispose(); + super.dispose(); + } } export interface IOpenResourceOptions { @@ -684,11 +704,11 @@ abstract class ResourceNavigator extends Disposable { abstract getSelectedElement(): T | undefined; } -export class ListResourceNavigator extends ResourceNavigator { +class ListResourceNavigator extends ResourceNavigator { constructor( protected readonly widget: List | PagedList, - options?: IResourceNavigatorOptions + options: IResourceNavigatorOptions ) { super(widget, options); } @@ -698,6 +718,20 @@ export class ListResourceNavigator extends ResourceNavigator { } } +class TableResourceNavigator extends ResourceNavigator { + + constructor( + protected readonly widget: Table, + options: IResourceNavigatorOptions + ) { + super(widget, options); + } + + getSelectedElement(): TRow | undefined { + return this.widget.getSelectedElements()[0]; + } +} + class TreeResourceNavigator extends ResourceNavigator { constructor( diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index b3a0a93f2b6bb..27291b2f7c700 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -19,7 +19,7 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { WorkbenchList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -111,7 +111,7 @@ export class BreakpointsView extends ViewPane { container.classList.add('debug-breakpoints'); const delegate = new BreakpointsDelegate(this); - this.list = >this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ + this.list = this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointSupportsCondition), new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.debugService), new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService), @@ -126,7 +126,7 @@ export class BreakpointsView extends ViewPane { overrideStyles: { listBackground: this.getBackgroundColor() } - }); + }) as WorkbenchList; CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.list.contextKeyService); @@ -142,8 +142,7 @@ export class BreakpointsView extends ViewPane { } }); - const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); - this._register(resourceNavigator.onDidOpen(async e => { + this._register(this.list.onDidOpen(async e => { if (!e.element) { return; } @@ -856,11 +855,11 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea startColumn: breakpoint.column || 1, endColumn: breakpoint.endColumn || Constants.MAX_SAFE_SMALL_INTEGER } : { - startLineNumber: breakpoint.lineNumber, - startColumn: breakpoint.column || 1, - endLineNumber: breakpoint.lineNumber, - endColumn: breakpoint.column || Constants.MAX_SAFE_SMALL_INTEGER - }; + startLineNumber: breakpoint.lineNumber, + startColumn: breakpoint.column || 1, + endLineNumber: breakpoint.lineNumber, + endColumn: breakpoint.column || Constants.MAX_SAFE_SMALL_INTEGER + }; return editorService.openEditor({ resource: breakpoint.uri, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index b984e11f93f55..e49254fae8497 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -26,7 +26,7 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; +import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; @@ -156,7 +156,7 @@ export class ExtensionsListView extends ViewPane { const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState); - this.list = this.instantiationService.createInstance>(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { + this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { multipleSelectionSupport: false, setRowLineHeight: false, horizontalScrolling: false, @@ -170,15 +170,15 @@ export class ExtensionsListView extends ViewPane { }, overrideStyles: { listBackground: SIDE_BAR_BACKGROUND - } - }); + }, + openOnSingleClick: true + }) as WorkbenchPagedList; this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); this._register(this.list.onDidChangeFocus(e => extensionsViewState.onFocusChange(coalesce(e.elements)), this)); this._register(this.list); this._register(extensionsViewState); - const resourceNavigator = this._register(new ListResourceNavigator(this.list, { openOnSingleClick: true })); - this._register(Event.debounce(Event.filter(resourceNavigator.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { + this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); })); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 9936ef5a7549a..bb751c3785cf5 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -21,7 +21,7 @@ import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platfo import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { WorkbenchList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -229,7 +229,7 @@ export class OpenEditorsView extends ViewPane { this.listLabels.clear(); } this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); - this.list = >this.instantiationService.createInstance(WorkbenchList, 'OpenEditors', container, delegate, [ + this.list = this.instantiationService.createInstance(WorkbenchList, 'OpenEditors', container, delegate, [ new EditorGroupRenderer(this.keybindingService, this.instantiationService), new OpenEditorRenderer(this.listLabels, this.instantiationService, this.keybindingService, this.configurationService) ], { @@ -239,7 +239,7 @@ export class OpenEditorsView extends ViewPane { listBackground: this.getBackgroundColor() }, accessibilityProvider: new OpenEditorsAccessibilityProvider() - }); + }) as WorkbenchList; this._register(this.list); this._register(this.listLabels); @@ -281,8 +281,7 @@ export class OpenEditorsView extends ViewPane { e.element.group.closeEditor(e.element.editor, { preserveFocus: true }); } })); - const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); - this._register(resourceNavigator.onDidOpen(e => { + this._register(this.list.onDidOpen(e => { if (!e.element) { return; } else if (e.element instanceof OpenEditor) { From 9752a1cb3e046d9c3e8ee0e0df2995aef9cebf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 10:53:39 +0100 Subject: [PATCH 22/45] reenable more tunnel view functionality --- src/vs/base/browser/ui/table/tableWidget.ts | 12 ++++ .../contrib/remote/browser/tunnelView.ts | 65 ++++++++++--------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 045771dd2b0ef..25d0b54387933 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -215,6 +215,18 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.list.splice(start, deleteCount, elements); } + rerender(): void { + this.list.rerender(); + } + + row(index: number): TRow { + return this.list.element(index); + } + + indexOf(element: TRow): number { + return this.list.indexOf(element); + } + get length(): number { return this.list.length; } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 70a6f178e49d0..bc71d0288ab23 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -45,7 +45,7 @@ import { copyAddressIcon, forwardPortIcon, openBrowserIcon, openPreviewIcon, por import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; -import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { ITableColumn, ITableMouseEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { Codicon } from 'vs/base/common/codicons'; @@ -778,44 +778,47 @@ export class TunnelPanel extends ViewPane { const actionRunner: ActionRunner = new ActionRunner(); actionBarRenderer.actionRunner = actionRunner; - // this._register(this.tree.onContextMenu(e => this.onContextMenu(e, actionRunner))); - // this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); - // this._register(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements))); - // this._register(this.tree.onDidFocus(() => this.tunnelViewFocusContext.set(true))); - // this._register(this.tree.onDidBlur(() => this.tunnelViewFocusContext.set(false))); + // this._register(this.table.onContextMenu(e => this.onContextMenu(e, actionRunner))); + this._register(this.table.onMouseDblClick(e => this.onMouseDblClick(e))); + this._register(this.table.onDidChangeFocus(e => this.onFocusChanged(e.elements))); + this._register(this.table.onDidFocus(() => this.tunnelViewFocusContext.set(true))); + this._register(this.table.onDidBlur(() => this.tunnelViewFocusContext.set(false))); - this.table.splice(0, 0, this.viewModel.all); + const rerender = () => this.table.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); + + rerender(); this._register(this.viewModel.onForwardedPortsChanged(() => { this._onDidChangeViewWelcomeState.fire(); - this.table.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); + rerender(); })); - // this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(e => { - // if (e.element && (e.element.tunnelType === TunnelType.Add)) { - // this.commandService.executeCommand(ForwardPortAction.INLINE_ID); - // } - // })); + // TODO@joao why the debounce? + this._register(Event.debounce(this.table.onDidOpen, (last, event) => event, 75, true)(e => { + if (e.element && (e.element.tunnelType === TunnelType.Add)) { + this.commandService.executeCommand(ForwardPortAction.INLINE_ID); + } + })); - // this._register(this.remoteExplorerService.onDidChangeEditable(async e => { - // this.isEditing = !!this.remoteExplorerService.getEditableData(e); - // this._onDidChangeViewWelcomeState.fire(); + this._register(this.remoteExplorerService.onDidChangeEditable(e => { + this.isEditing = !!this.remoteExplorerService.getEditableData(e); + this._onDidChangeViewWelcomeState.fire(); - // if (!this.isEditing) { - // widgetContainer.classList.remove('highlight'); - // } + if (!this.isEditing) { + widgetContainer.classList.remove('highlight'); + } - // await this.tree.updateChildren(undefined, false); + rerender(); - // if (this.isEditing) { - // widgetContainer.classList.add('highlight'); - // if (!e) { - // // When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view. - // this.tree.reveal(this.viewModel.input); - // } - // } else { - // this.tree.domFocus(); - // } - // })); + if (this.isEditing) { + widgetContainer.classList.add('highlight'); + if (!e) { + // When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view. + this.table.reveal(this.table.indexOf(this.viewModel.input)); + } + } else { + this.table.domFocus(); + } + })); } // private get contributedContextMenu(): IMenu { @@ -895,7 +898,7 @@ export class TunnelPanel extends ViewPane { // }); } - private onMouseDblClick(e: ITreeMouseEvent): void { + private onMouseDblClick(e: ITableMouseEvent): void { if (!e.element) { this.commandService.executeCommand(ForwardPortAction.INLINE_ID); } From 5d8db7c493f4aa0fa2f81ed319abbdaca1eec8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 10:55:27 +0100 Subject: [PATCH 23/45] reenable tunnel view list options --- .../contrib/remote/browser/tunnelView.ts | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index bc71d0288ab23..d32116374e806 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -748,7 +748,6 @@ export class TunnelPanel extends ViewPane { const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, this.menuService); - const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); this.table = this.instantiationService.createInstance(WorkbenchTable, 'RemoteTunnels', widgetContainer, @@ -756,22 +755,22 @@ export class TunnelPanel extends ViewPane { [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()], [new StringRenderer(), actionBarRenderer], { - // keyboardNavigationLabelProvider: { - // getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => { - // return item.label; - // } - // }, - // multipleSelectionSupport: false, - // accessibilityProvider: { - // getAriaLabel: (item: ITunnelItem | ITunnelGroup) => { - // if (item instanceof TunnelItem) { - // return item.tooltip; - // } else { - // return item.label; - // } - // }, - // getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View") - // } + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (item: ITunnelItem | ITunnelGroup) => { + return item.label; + } + }, + multipleSelectionSupport: false, + accessibilityProvider: { + getAriaLabel: (item: ITunnelItem | ITunnelGroup) => { + if (item instanceof TunnelItem) { + return item.tooltip; + } else { + return item.label; + } + }, + getWidgetAriaLabel: () => nls.localize('tunnelView', "Tunnel View") + } } ) as WorkbenchTable; From df97ef2288a9fc5fee79636832f3ed41e1536223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 10:59:01 +0100 Subject: [PATCH 24/45] tunnel view: enable context menu clicking --- src/vs/base/browser/ui/table/table.ts | 3 +- src/vs/base/browser/ui/table/tableWidget.ts | 3 +- .../contrib/remote/browser/tunnelView.ts | 97 +++++++++---------- 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index cf5e085d9f7a6..575a6fcfdb788 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list'; +import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list'; export interface ITableColumn { readonly label: string; @@ -23,6 +23,7 @@ export interface ITableEvent extends IListEvent { } export interface ITableMouseEvent extends IListMouseEvent { } export interface ITableTouchEvent extends IListTouchEvent { } export interface ITableGestureEvent extends IListGestureEvent { } +export interface ITableContextMenuEvent extends IListContextMenuEvent { } export class TableError extends Error { diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 25d0b54387933..b50861244332d 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -5,7 +5,7 @@ import 'vs/css!./table'; import { IListOptions, IListOptionsUpdate, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; -import { ITableColumn, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableTouchEvent, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableTouchEvent, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { ISpliceable } from 'vs/base/common/sequence'; import { IThemable } from 'vs/base/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -162,6 +162,7 @@ export class Table implements ISpliceable, IThemable, IDisposable { get onMouseOut(): Event> { return this.list.onMouseOut; } get onTouchStart(): Event> { return this.list.onTouchStart; } get onTap(): Event> { return this.list.onTap; } + get onContextMenu(): Event> { return this.list.onContextMenu; } get onDidFocus(): Event { return this.list.onDidFocus; } get onDidBlur(): Event { return this.list.onDidBlur; } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index d32116374e806..66125b497548c 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -45,7 +45,7 @@ import { copyAddressIcon, forwardPortIcon, openBrowserIcon, openPreviewIcon, por import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; -import { ITableColumn, ITableMouseEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { ITableColumn, ITableContextMenuEvent, ITableMouseEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { Codicon } from 'vs/base/common/codicons'; @@ -777,7 +777,7 @@ export class TunnelPanel extends ViewPane { const actionRunner: ActionRunner = new ActionRunner(); actionBarRenderer.actionRunner = actionRunner; - // this._register(this.table.onContextMenu(e => this.onContextMenu(e, actionRunner))); + this._register(this.table.onContextMenu(e => this.onContextMenu(e, actionRunner))); this._register(this.table.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.table.onDidChangeFocus(e => this.onFocusChanged(e.elements))); this._register(this.table.onDidFocus(() => this.tunnelViewFocusContext.set(true))); @@ -820,11 +820,6 @@ export class TunnelPanel extends ViewPane { })); } - // private get contributedContextMenu(): IMenu { - // const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService)); - // return contributedContextMenu; - // } - shouldShowWelcome(): boolean { return this.viewModel.isEmpty() && !this.isEditing; } @@ -851,50 +846,52 @@ export class TunnelPanel extends ViewPane { } } - private onContextMenu(treeEvent: ITreeContextMenuEvent, actionRunner: ActionRunner): void { - // if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) { - // return; - // } - // const node: ITunnelItem | null = treeEvent.element; - // const event: UIEvent = treeEvent.browserEvent; - - // event.preventDefault(); - // event.stopPropagation(); - - // if (node) { - // this.tree!.setFocus([node]); - // this.tunnelTypeContext.set(node.tunnelType); - // this.tunnelCloseableContext.set(!!node.closeable); - // this.tunnelPrivacyContext.set(node.privacy); - // this.portChangableContextKey.set(!!node.localPort); - // } else { - // this.tunnelTypeContext.set(TunnelType.Add); - // this.tunnelCloseableContext.set(false); - // this.tunnelPrivacyContext.set(undefined); - // this.portChangableContextKey.set(false); - // } + private onContextMenu(event: ITableContextMenuEvent, actionRunner: ActionRunner): void { + if ((event.element !== null) && !(event.element instanceof TunnelItem)) { + return; + } + + event.browserEvent.preventDefault(); + event.browserEvent.stopPropagation(); + + const node: ITunnelItem | null = event.element; + + if (node) { + this.table.setFocus([this.table.indexOf(node)]); + this.tunnelTypeContext.set(node.tunnelType); + this.tunnelCloseableContext.set(!!node.closeable); + this.tunnelPrivacyContext.set(node.privacy); + this.portChangableContextKey.set(!!node.localPort); + } else { + this.tunnelTypeContext.set(TunnelType.Add); + this.tunnelCloseableContext.set(false); + this.tunnelPrivacyContext.set(undefined); + this.portChangableContextKey.set(false); + } - // const actions: IAction[] = []; - // this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions)); - - // this.contextMenuService.showContextMenu({ - // getAnchor: () => treeEvent.anchor, - // getActions: () => actions, - // getActionViewItem: (action) => { - // const keybinding = this.keybindingService.lookupKeybinding(action.id); - // if (keybinding) { - // return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - // } - // return undefined; - // }, - // onHide: (wasCancelled?: boolean) => { - // if (wasCancelled) { - // this.tree!.domFocus(); - // } - // }, - // getActionsContext: () => node, - // actionRunner - // }); + const menu = this.menuService.createMenu(MenuId.TunnelContext, this.table.contextKeyService); + const actions: IAction[] = []; + this._register(createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, actions)); + menu.dispose(); + + this.contextMenuService.showContextMenu({ + getAnchor: () => event.anchor, + getActions: () => actions, + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.table.domFocus(); + } + }, + getActionsContext: () => node, + actionRunner + }); } private onMouseDblClick(e: ITableMouseEvent): void { From 7efe1382bbe227651cc06c0bcab053eb00a69b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 11:00:31 +0100 Subject: [PATCH 25/45] update comment --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 66125b497548c..8da1edb78a6d7 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -791,7 +791,7 @@ export class TunnelPanel extends ViewPane { rerender(); })); - // TODO@joao why the debounce? + // TODO@alexr00 Joao asks: why the debounce? this._register(Event.debounce(this.table.onDidOpen, (last, event) => event, 75, true)(e => { if (e.element && (e.element.tunnelType === TunnelType.Add)) { this.commandService.executeCommand(ForwardPortAction.INLINE_ID); From 6d7eefbae2d5adb44c2a174e79e72f4ddfd765bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 11:01:16 +0100 Subject: [PATCH 26/45] remove unused imports --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 8da1edb78a6d7..d7f3646edf520 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -16,14 +16,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Event } from 'vs/base/common/event'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { IMenuService, MenuId, IMenu, MenuRegistry, ILocalizedString, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuRegistry, ILocalizedString, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; From edab4b7bdc98390fe387ee3015f19a30a926578f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 11:06:47 +0100 Subject: [PATCH 27/45] privacy and source columns --- .../contrib/remote/browser/tunnelView.ts | 58 ++++++++++++++----- .../remote/common/remoteExplorerService.ts | 1 + 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index d7f3646edf520..df1a21087fcf5 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -82,7 +82,8 @@ export class TunnelViewModel implements ITunnelViewModel { processDescription: '', wideDescription: '', icon: undefined, - tooltip: '' + tooltip: '', + source: '' }; constructor( @@ -163,7 +164,7 @@ export class TunnelViewModel implements ITunnelViewModel { this._candidates.forEach(value => { if (!mapHasAddressLocalhostOrAllInterfaces(this.model.forwarded, value.host, value.port) && !mapHasAddressLocalhostOrAllInterfaces(this.model.detected, value.host, value.port)) { - candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, undefined, false, undefined, value.detail, undefined, value.pid)); + candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, '', undefined, undefined, false, undefined, value.detail, value.pid)); } }); return candidates; @@ -425,12 +426,44 @@ class LocalAddressColumn implements ITableColumn { } } -class RunningProcessColumn implements ITableColumn { +class RunningProcessColumn implements ITableColumn { readonly label: string = nls.localize('process', "Running Process"); readonly weight: number = 1; - readonly templateId: string = 'string'; - project(row: ITunnelItem): string { - return row.processDescription ?? ''; + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + return { label: row.processDescription ?? '', tunnel: row, context: [] }; + } +} + +class SourceColumn implements ITableColumn { + readonly label: string = nls.localize('source', "Source"); + readonly weight: number = 1; + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + const context: [string, any][] = + [ + ['view', TUNNEL_VIEW_ID], + ['tunnelType', row.tunnelType], + ['tunnelCloseable', row.closeable] + ]; + const label = row.source; + return { label, context, tunnel: row }; + } +} + +class PrivacyColumn implements ITableColumn { + readonly label: string = nls.localize('privacy', "Privacy"); + readonly weight: number = 1; + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + const context: [string, any][] = + [ + ['view', TUNNEL_VIEW_ID], + ['tunnelType', row.tunnelType], + ['tunnelCloseable', row.closeable] + ]; + const label = row.privacy === TunnelPrivacy.Public ? nls.localize('tunnel.privacyPublic', "Public") : nls.localize('tunnel.privacyPrivate', "Private"); + return { label, context, tunnel: row, icon: row.icon }; } } @@ -461,7 +494,7 @@ interface IActionBarTemplateData { interface ActionBarCell { label: string; - icon?: Codicon; + icon?: ThemeIcon; menuId?: MenuId; context: [string, any][]; tunnel: ITunnelItem; @@ -538,12 +571,12 @@ class TunnelItem implements ITunnelItem { return new TunnelItem(type, tunnel.remoteHost, tunnel.remotePort, + tunnel.source ?? (tunnel.restore ? nls.localize('tunnel.user', "User Forwarded") : nls.localize('tunnel.automatic', "Auto Forwarded")), tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, tunnel.name, tunnel.runningProcess, - tunnel.source, tunnel.pid, tunnel.privacy, remoteExplorerService); @@ -553,12 +586,12 @@ class TunnelItem implements ITunnelItem { public tunnelType: TunnelType, public remoteHost: string, public remotePort: number, + public source: string, public localAddress?: string, public localPort?: number, public closeable?: boolean, public name?: string, private runningProcess?: string, - private source?: string, private pid?: number, public privacy?: TunnelPrivacy, private remoteExplorerService?: IRemoteExplorerService @@ -624,8 +657,6 @@ class TunnelItem implements ITunnelItem { if (item.pid) { description += ` (${item.pid})`; } - } else if (item.source) { - description = item.source; } return description; @@ -641,9 +672,8 @@ class TunnelItem implements ITunnelItem { get icon(): ThemeIcon | undefined { switch (this.privacy) { - case TunnelPrivacy.Private: return privatePortIcon; case TunnelPrivacy.Public: return publicPortIcon; - default: return undefined; + default: return privatePortIcon; } } @@ -751,7 +781,7 @@ export class TunnelPanel extends ViewPane { 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), - [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()], + [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn(), new PrivacyColumn(), new SourceColumn()], [new StringRenderer(), actionBarRenderer], { keyboardNavigationLabelProvider: { diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index d909a8105dde9..856c23b0dd5e3 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -45,6 +45,7 @@ export interface ITunnelItem { localPort?: number; name?: string; closeable?: boolean; + source: string; privacy?: TunnelPrivacy; processDescription?: string; wideDescription?: string; From 49378fc9a5812d70ac7f6490a316d4efb21520cf Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 11:13:46 +0100 Subject: [PATCH 28/45] Use container in renderTemplate --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index df1a21087fcf5..7570dabe84b72 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -515,15 +515,15 @@ class ActionBarRenderer extends Disposable implements ITableRenderer Date: Thu, 18 Feb 2021 11:21:37 +0100 Subject: [PATCH 29/45] Hide privacy column --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 7570dabe84b72..a2140f27e477b 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -742,6 +742,7 @@ export class TunnelPanel extends ViewPane { @IThemeService themeService: IThemeService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @ITelemetryService telemetryService: ITelemetryService, + @ITunnelService private readonly tunnelService: ITunnelService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); @@ -776,12 +777,17 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, this.menuService); + const columns = [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; + if (this.tunnelService.canMakePublic) { + columns.push(new PrivacyColumn()); + } + columns.push(new SourceColumn()); this.table = this.instantiationService.createInstance(WorkbenchTable, 'RemoteTunnels', widgetContainer, new TunnelTreeVirtualDelegate(), - [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn(), new PrivacyColumn(), new SourceColumn()], + columns, [new StringRenderer(), actionBarRenderer], { keyboardNavigationLabelProvider: { From d5d0bf7b073b5142983005a4a463365dd7c21af0 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 11:30:57 +0100 Subject: [PATCH 30/45] Some clean up in naming --- .../contrib/remote/browser/tunnelView.ts | 58 ++++--------------- .../remote/common/remoteExplorerService.ts | 14 ++--- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index a2140f27e477b..fbbcbd480f830 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -257,9 +257,8 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer 0 ? nullIndex : item.runningProcess.length).trim(); - const spaceIndex = description.indexOf(' ', 110); - description = description.substr(0, spaceIndex > 0 ? spaceIndex : description.length); + description = this.runningProcess.replace(/\0/g, ' ').trim(); } - if (item.pid) { - description += ` (${item.pid})`; + if (this.pid) { + description += ` (${this.pid})`; } } return description; } - get processDescription(): string | undefined { - return TunnelItem.getProcessDescription(this, false); - } - - get wideDescription(): string | undefined { - return TunnelItem.getProcessDescription(this, true); - } - get icon(): ThemeIcon | undefined { switch (this.privacy) { case TunnelPrivacy.Public: return publicPortIcon; @@ -738,7 +701,6 @@ export class TunnelPanel extends ViewPane { @IQuickInputService protected quickInputService: IQuickInputService, @ICommandService protected commandService: ICommandService, @IMenuService private readonly menuService: IMenuService, - @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService themeService: IThemeService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @ITelemetryService telemetryService: ITelemetryService, diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 856c23b0dd5e3..1366434d65ca8 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -48,10 +48,8 @@ export interface ITunnelItem { source: string; privacy?: TunnelPrivacy; processDescription?: string; - wideDescription?: string; readonly icon?: ThemeIcon; readonly label: string; - readonly wideLabel: string; } export interface Tunnel { @@ -65,7 +63,7 @@ export interface Tunnel { runningProcess: string | undefined; pid: number | undefined; source?: string; - restore: boolean; + userForwarded: boolean; } export function makeAddress(host: string, port: number): string { @@ -279,7 +277,7 @@ export class TunnelModel extends Disposable { runningProcess: matchingCandidate?.detail, pid: matchingCandidate?.pid, privacy: this.makeTunnelPrivacy(tunnel.public), - restore: true + userForwarded: true }); this.remoteTunnels.set(key, tunnel); } @@ -300,7 +298,7 @@ export class TunnelModel extends Disposable { runningProcess: matchingCandidate?.detail, pid: matchingCandidate?.pid, privacy: this.makeTunnelPrivacy(tunnel.public), - restore: true + userForwarded: true }); } await this.storeForwarded(); @@ -366,7 +364,7 @@ export class TunnelModel extends Disposable { private async storeForwarded() { if (this.configurationService.getValue('remote.restoreForwardedPorts')) { - this.storageService.store(await this.getStorageKey(), JSON.stringify(Array.from(this.forwarded.values()).filter(value => value.restore)), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(await this.getStorageKey(), JSON.stringify(Array.from(this.forwarded.values()).filter(value => value.userForwarded)), StorageScope.GLOBAL, StorageTarget.USER); } } @@ -394,7 +392,7 @@ export class TunnelModel extends Disposable { pid: matchingCandidate?.pid, source, privacy: this.makeTunnelPrivacy(tunnel.public), - restore + userForwarded: restore }; const key = makeAddress(remote.host, remote.port); this.forwarded.set(key, newForward); @@ -451,7 +449,7 @@ export class TunnelModel extends Disposable { runningProcess: matchingCandidate?.detail, pid: matchingCandidate?.pid, privacy: TunnelPrivacy.ConstantPrivate, - restore: false + userForwarded: false }); }); } From 9e6e47cea1ec9713bab7dddb925d178490d7a390 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 13:07:45 +0100 Subject: [PATCH 31/45] Show detected ports and add input box --- .../contrib/remote/browser/tunnelView.ts | 171 +++++++++++++----- .../remote/common/remoteExplorerService.ts | 30 +-- 2 files changed, 141 insertions(+), 60 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index fbbcbd480f830..082f9b6d2cb29 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -24,7 +24,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, MenuRegistry, ILocalizedString, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -75,12 +75,10 @@ export class TunnelViewModel implements ITunnelViewModel { readonly input = { label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - wideLabel: nls.localize('remote.tunnelsView.add', "Forward a Port..."), tunnelType: TunnelType.Add, - remoteHost: 'localhost', + remoteHost: '', remotePort: 0, processDescription: '', - wideDescription: '', icon: undefined, tooltip: '', source: '' @@ -103,23 +101,10 @@ export class TunnelViewModel implements ITunnelViewModel { if ((this.model.forwarded.size > 0) || this.remoteExplorerService.getEditableData(undefined)) { result.push(...this.forwarded); } - // if (this.model.detected.size > 0) { - // result.push({ - // label: nls.localize('remote.tunnelsView.detected', "Static Ports"), - // tunnelType: TunnelType.Detected, - // items: this.detected - // }); - // } - // if (!this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) { - // const candidates = this.candidates; - // if (candidates.length > 0) { - // result.push({ - // label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"), - // tunnelType: TunnelType.Candidate, - // items: candidates - // }); - // } - // } + if (this.model.detected.size > 0) { + result.push(...this.detected); + } + if (result.length === 0) { result.push(this.input); } @@ -171,7 +156,9 @@ export class TunnelViewModel implements ITunnelViewModel { } isEmpty(): boolean { - return this.forwarded.length === 0 && this.candidates.length === 0 && this.detected.length === 0; + return (this.detected.length === 0) && + ((this.forwarded.length === 0) || (this.forwarded.length === 1 && + (this.forwarded[0].tunnelType === TunnelType.Add) && !this.remoteExplorerService.getEditableData(undefined))); } } @@ -405,7 +392,7 @@ class PortColumn implements ITableColumn { ['tunnelType', row.tunnelType], ['tunnelCloseable', row.closeable] ]; - return { label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline }; + return { label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline, editId: row.tunnelType === TunnelType.Add ? TunnelEditId.New : TunnelEditId.Label }; } } @@ -421,7 +408,7 @@ class LocalAddressColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.localAddress ?? ''; - return { label, menuId: MenuId.TunnelLocalAddressInline, context, tunnel: row }; + return { label, menuId: MenuId.TunnelLocalAddressInline, context, tunnel: row, editId: TunnelEditId.LocalPort }; } } @@ -430,7 +417,7 @@ class RunningProcessColumn implements ITableColumn { readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { - return { label: row.processDescription ?? '', tunnel: row, context: [] }; + return { label: row.processDescription ?? '', tunnel: row, context: [], editId: TunnelEditId.None }; } } @@ -446,7 +433,7 @@ class SourceColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.source; - return { label, context, tunnel: row }; + return { label, context, tunnel: row, editId: TunnelEditId.None }; } } @@ -462,7 +449,7 @@ class PrivacyColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.privacy === TunnelPrivacy.Public ? nls.localize('tunnel.privacyPublic', "Public") : nls.localize('tunnel.privacyPrivate', "Private"); - return { label, context, tunnel: row, icon: row.icon }; + return { label, context, tunnel: row, icon: row.icon, editId: TunnelEditId.None }; } } @@ -497,16 +484,21 @@ interface ActionBarCell { menuId?: MenuId; context: [string, any][]; tunnel: ITunnelItem; + editId: TunnelEditId; } class ActionBarRenderer extends Disposable implements ITableRenderer { readonly templateId = 'actionbar'; + private inputDone?: (success: boolean, finishEditing: boolean) => void; private _actionRunner: ActionRunner | undefined; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, + @IContextViewService private readonly contextViewService: IContextViewService, + @IThemeService private readonly themeService: IThemeService, + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService ) { super(); } set actionRunner(actionRunner: ActionRunner) { @@ -530,7 +522,25 @@ class ActionBarRenderer extends Disposable implements ITableRenderer { + const message = editableData.validationMessage(value); + if (!message) { + return null; + } + + return { + content: message.content, + formatContent: true, + type: message.severity === Severity.Error ? MessageType.ERROR : MessageType.INFO + }; + } + }, + placeholder: editableData.placeholder || '' + }); + const styler = attachInputBoxStyler(inputBox, this.themeService); + + inputBox.value = value; + inputBox.focus(); + inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 }); + + const done = once((success: boolean, finishEditing: boolean) => { + if (this.inputDone) { + this.inputDone = undefined; + } + inputBox.element.style.display = 'none'; + const inputValue = inputBox.value; + dispose(toDispose); + if (finishEditing) { + editableData.onFinish(inputValue, success); + } + }); + this.inputDone = done; + + const toDispose = [ + inputBox, + dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter)) { + if (inputBox.validate() !== MessageType.ERROR) { + done(true, true); + } else { + done(false, true); + } + } else if (e.equals(KeyCode.Escape)) { + done(false, true); + } + }), + dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => { + done(inputBox.validate() !== MessageType.ERROR, true); + }), + styler + ]; + + return toDisposable(() => { + done(false, false); + }); + } + disposeTemplate(templateData: IActionBarTemplateData): void { templateData.actionBar.dispose(); templateData.elementDisposable.dispose(); @@ -570,7 +648,8 @@ class TunnelItem implements ITunnelItem { return new TunnelItem(type, tunnel.remoteHost, tunnel.remotePort, - tunnel.source ?? (tunnel.userForwarded ? nls.localize('tunnel.user', "User Forwarded") : nls.localize('tunnel.automatic', "Auto Forwarded")), + tunnel.source ?? (tunnel.userForwarded ? nls.localize('tunnel.user', "User Forwarded") : + (type === TunnelType.Detected ? nls.localize('tunnel.staticallyForwarded', "Statically Forwarded") : nls.localize('tunnel.automatic', "Auto Forwarded"))), tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, @@ -596,22 +675,14 @@ class TunnelItem implements ITunnelItem { private remoteExplorerService?: IRemoteExplorerService ) { } - private static getLabel(name: string | undefined, remotePort: number): string { - if (name) { - return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", name); + get label(): string { + if (this.name) { + return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); } else { - return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}", remotePort); + return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}", this.remotePort); } } - get label(): string { - return TunnelItem.getLabel(this.name, this.remotePort); - } - - get wideLabel(): string { - return TunnelItem.getLabel(this.name, this.remotePort); - } - set processDescription(description: string | undefined) { this.runningProcess = description; } @@ -704,7 +775,8 @@ export class TunnelPanel extends ViewPane { @IThemeService themeService: IThemeService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @ITelemetryService telemetryService: ITelemetryService, - @ITunnelService private readonly tunnelService: ITunnelService + @ITunnelService private readonly tunnelService: ITunnelService, + @IContextViewService private readonly contextViewService: IContextViewService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); @@ -738,7 +810,8 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('ports-view'); widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); - const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, this.menuService); + const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, + this.menuService, this.contextViewService, this.themeService, this.remoteExplorerService); const columns = [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; if (this.tunnelService.canMakePublic) { columns.push(new PrivacyColumn()); @@ -796,7 +869,7 @@ export class TunnelPanel extends ViewPane { })); this._register(this.remoteExplorerService.onDidChangeEditable(e => { - this.isEditing = !!this.remoteExplorerService.getEditableData(e); + this.isEditing = !!this.remoteExplorerService.getEditableData(e?.tunnel, e?.editId); this._onDidChangeViewWelcomeState.fire(); if (!this.isEditing) { @@ -934,12 +1007,12 @@ namespace LabelTunnelAction { if (context instanceof TunnelItem) { return new Promise(resolve => { const remoteExplorerService = accessor.get(IRemoteExplorerService); - remoteExplorerService.setEditable(context, { + remoteExplorerService.setEditable(context, TunnelEditId.Label, { onFinish: async (value, success) => { + remoteExplorerService.setEditable(context, TunnelEditId.Label, null); if (success) { await remoteExplorerService.tunnelModel.name(context.remoteHost, context.remotePort, value); } - remoteExplorerService.setEditable(context, null); resolve(success ? { port: context.remotePort, label: value } : undefined); }, validationMessage: () => null, @@ -991,13 +1064,13 @@ export namespace ForwardPortAction { if (arg instanceof TunnelItem) { remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort)); } else { - remoteExplorerService.setEditable(undefined, { + remoteExplorerService.setEditable(undefined, TunnelEditId.New, { onFinish: async (value, success) => { + remoteExplorerService.setEditable(undefined, TunnelEditId.New, null); let parsed: { host: string, port: number } | undefined; if (success && (parsed = parseAddress(value))) { remoteExplorerService.forward({ host: parsed.host, port: parsed.port }, undefined, undefined, undefined, true).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port)); } - remoteExplorerService.setEditable(undefined, null); }, validationMessage: (value) => validateInput(value, tunnelService.canElevate), placeholder: forwardPrompt @@ -1256,9 +1329,9 @@ namespace ChangeLocalPortAction { const tunnelService = accessor.get(ITunnelService); const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName); if (context instanceof TunnelItem) { - remoteExplorerService.setEditable(context, { + remoteExplorerService.setEditable(context, TunnelEditId.LocalPort, { onFinish: async (value, success) => { - remoteExplorerService.setEditable(context, null); + remoteExplorerService.setEditable(context, TunnelEditId.LocalPort, null); if (success) { await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort }); const numberValue = Number(value); diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 1366434d65ca8..5c71435ecb23f 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -52,6 +52,13 @@ export interface ITunnelItem { readonly label: string; } +export enum TunnelEditId { + None = 0, + New = 1, + Label = 2, + LocalPort = 3 +} + export interface Tunnel { remoteHost: string; remotePort: number; @@ -545,9 +552,9 @@ export interface IRemoteExplorerService { onDidChangeTargetType: Event; targetType: string[]; readonly tunnelModel: TunnelModel; - onDidChangeEditable: Event; - setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void; - getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined; + onDidChangeEditable: Event<{ tunnel: ITunnelItem, editId: TunnelEditId } | undefined>; + setEditable(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData | null): void; + getEditableData(tunnelItem: ITunnelItem | undefined, editId?: TunnelEditId): IEditableData | undefined; forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean): Promise; close(remote: { host: string, port: number }): Promise; setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void; @@ -566,9 +573,9 @@ class RemoteExplorerService implements IRemoteExplorerService { private readonly _onDidChangeTargetType: Emitter = new Emitter(); public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _tunnelModel: TunnelModel; - private _editable: { tunnelItem: ITunnelItem | undefined, data: IEditableData } | undefined; - private readonly _onDidChangeEditable: Emitter = new Emitter(); - public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; + private _editable: { tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData } | undefined; + private readonly _onDidChangeEditable: Emitter<{ tunnel: ITunnelItem, editId: TunnelEditId } | undefined> = new Emitter(); + public readonly onDidChangeEditable: Event<{ tunnel: ITunnelItem, editId: TunnelEditId } | undefined> = this._onDidChangeEditable.event; private readonly _onEnabledPortsFeatures: Emitter = new Emitter(); public readonly onEnabledPortsFeatures: Event = this._onEnabledPortsFeatures.event; private _portsFeaturesEnabled: boolean = false; @@ -616,19 +623,20 @@ class RemoteExplorerService implements IRemoteExplorerService { this.tunnelModel.addEnvironmentTunnels(tunnelInformation?.environmentTunnels); } - setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void { + setEditable(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData | null): void { if (!data) { this._editable = undefined; } else { - this._editable = { tunnelItem, data }; + this._editable = { tunnelItem, data, editId }; } - this._onDidChangeEditable.fire(tunnelItem); + this._onDidChangeEditable.fire(tunnelItem ? { tunnel: tunnelItem, editId } : undefined); } - getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined { + getEditableData(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId): IEditableData | undefined { return (this._editable && ((!tunnelItem && (tunnelItem === this._editable.tunnelItem)) || - (tunnelItem && (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost)))) ? + (tunnelItem && (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost) + && (this._editable.editId === editId)))) ? this._editable.data : undefined; } From 9e61f455450c02202a924325eb372ed4151a11dd Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 13:26:58 +0100 Subject: [PATCH 32/45] Source -> Origin and added a menu --- src/vs/platform/actions/common/actions.ts | 1 + src/vs/workbench/api/common/menusExtensionPoint.ts | 5 +++++ src/vs/workbench/contrib/remote/browser/tunnelView.ts | 8 ++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 7b749b2417a3f..1f4a5a81567b7 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -127,6 +127,7 @@ export class MenuId { static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); + static readonly TunnelOriginInline = new MenuId('TunnelOriginInline'); static readonly ViewItemContext = new MenuId('ViewItemContext'); static readonly ViewContainerTitle = new MenuId('ViewContainerTitle'); static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 6360dadf550c8..81527dad3afab 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -186,6 +186,11 @@ const apiMenus: IAPIMenu[] = [ key: 'ports/item/context', id: MenuId.TunnelContext, description: localize('view.tunnelContext', "The Ports view item context menu") + }, + { + key: 'ports/item/origin/inline', + id: MenuId.TunnelOriginInline, + description: localize('view.tunnelOriginInline', "The Ports view item origin inline menu") } ]; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 082f9b6d2cb29..576acdc5ec235 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -421,8 +421,8 @@ class RunningProcessColumn implements ITableColumn { } } -class SourceColumn implements ITableColumn { - readonly label: string = nls.localize('source', "Source"); +class OriginColumn implements ITableColumn { + readonly label: string = nls.localize('origin', "Origin"); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -433,7 +433,7 @@ class SourceColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.source; - return { label, context, tunnel: row, editId: TunnelEditId.None }; + return { label, context, menuId: MenuId.TunnelOriginInline, tunnel: row, editId: TunnelEditId.None }; } } @@ -816,7 +816,7 @@ export class TunnelPanel extends ViewPane { if (this.tunnelService.canMakePublic) { columns.push(new PrivacyColumn()); } - columns.push(new SourceColumn()); + columns.push(new OriginColumn()); this.table = this.instantiationService.createInstance(WorkbenchTable, 'RemoteTunnels', From e1e5f3810c51d252034945bbc18cb8d77197fdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 15:07:00 +0100 Subject: [PATCH 33/45] table: fix weights --- src/vs/base/browser/ui/table/tableWidget.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index b50861244332d..d58945b202ccc 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -126,7 +126,7 @@ class ColumnHeader implements IView { private _onDidLayout = new Emitter<[number, number]>(); readonly onDidLayout = this._onDidLayout.event; - constructor(column: ITableColumn, private index: number) { + constructor(readonly column: ITableColumn, private index: number) { this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); } @@ -187,8 +187,8 @@ export class Table implements ISpliceable, IThemable, IDisposable { const headers = columns.map((c, i) => new ColumnHeader(c, i)); const descriptor: ISplitViewDescriptor = { - size: columns.length, - views: headers.map(view => ({ size: 1, view })) + size: headers.reduce((a, b) => a + b.column.weight, 0), + views: headers.map(view => ({ size: view.column.weight, view })) }; this.splitview = new SplitView(this.domNode, { From 200323caf6d185e4f6a4e2c2222ba84fbf16adbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 15:09:24 +0100 Subject: [PATCH 34/45] table: column header tooltip --- src/vs/base/browser/ui/table/table.ts | 1 + src/vs/base/browser/ui/table/tableWidget.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index 575a6fcfdb788..ff610a78a26f7 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -7,6 +7,7 @@ import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, export interface ITableColumn { readonly label: string; + readonly tooltip: string; readonly weight: number; readonly templateId: string; project(row: TRow): TCell; diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index d58945b202ccc..a4db96f6ef607 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -127,7 +127,7 @@ class ColumnHeader implements IView { readonly onDidLayout = this._onDidLayout.event; constructor(readonly column: ITableColumn, private index: number) { - this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); + this.element = $('.monaco-table-th', { 'data-col-index': index, title: column.tooltip }, column.label); } layout(size: number): void { From a65139f4bbb5c848301ef0ce6a9532fa8eb683d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 15:11:56 +0100 Subject: [PATCH 35/45] table: column size constraints --- src/vs/base/browser/ui/table/table.ts | 6 ++++++ src/vs/base/browser/ui/table/tableWidget.ts | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index ff610a78a26f7..c111537d5364d 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -4,12 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list'; +import { Event } from 'vs/base/common/event'; export interface ITableColumn { readonly label: string; readonly tooltip: string; readonly weight: number; readonly templateId: string; + + readonly minimumWidth?: number; + readonly maximumWidth?: number; + readonly onDidChangeWidthConstraints?: Event; + project(row: TRow): TCell; } diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index a4db96f6ef607..5b091fa5a594b 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -119,9 +119,10 @@ function asListVirtualDelegate(delegate: ITableVirtualDelegate): ILi class ColumnHeader implements IView { readonly element: HTMLElement; - readonly minimumSize = 120; - readonly maximumSize = Number.POSITIVE_INFINITY; - readonly onDidChange = Event.None; + + get minimumSize() { return this.column.minimumWidth ?? 120; } + get maximumSize() { return this.column.maximumWidth ?? Number.POSITIVE_INFINITY; } + get onDidChange() { return this.column.onDidChangeWidthConstraints ?? Event.None; } private _onDidLayout = new Emitter<[number, number]>(); readonly onDidLayout = this._onDidLayout.event; From c0f7c4cf9abeaa44d16388fb9dc5f69b6cbdacaf Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 15:13:31 +0100 Subject: [PATCH 36/45] Add tooltips to port cells and some cleanup --- .../contrib/remote/browser/tunnelView.ts | 277 +++--------------- 1 file changed, 37 insertions(+), 240 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 576acdc5ec235..65c2bfc0602d2 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -16,7 +16,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Event } from 'vs/base/common/event'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -80,7 +79,7 @@ export class TunnelViewModel implements ITunnelViewModel { remotePort: 0, processDescription: '', icon: undefined, - tooltip: '', + tooltipPostfix: '', source: '' }; @@ -162,223 +161,6 @@ export class TunnelViewModel implements ITunnelViewModel { } } -interface ITunnelTemplateData { - elementDisposable: IDisposable; - container: HTMLElement; - iconLabel: IconLabel; - icon: HTMLElement; - actionBar: ActionBar; -} - -class TunnelTreeRenderer extends Disposable implements ITreeRenderer { - static readonly ITEM_HEIGHT = 22; - static readonly TREE_TEMPLATE_ID = 'tunnelItemTemplate'; - - private inputDone?: (success: boolean, finishEditing: boolean) => void; - private _actionRunner: ActionRunner | undefined; - - constructor( - private readonly viewId: string, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextViewService private readonly contextViewService: IContextViewService, - @IThemeService private readonly themeService: IThemeService, - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService - ) { - super(); - } - - set actionRunner(actionRunner: ActionRunner) { - this._actionRunner = actionRunner; - } - - get templateId(): string { - return TunnelTreeRenderer.TREE_TEMPLATE_ID; - } - - renderTemplate(container: HTMLElement): ITunnelTemplateData { - container.classList.add('custom-view-tree-node-item'); - const icon = dom.append(container, dom.$('.custom-view-tree-node-item-icon')); - const iconLabel = new IconLabel(container, { supportHighlights: true }); - // dom.addClass(iconLabel.element, 'tunnel-view-label'); - const actionsContainer = dom.append(iconLabel.element, dom.$('.actions')); - const actionBar = new ActionBar(actionsContainer, { - actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService), - respectOrientationForPreviousAndNextKey: true - }); - - return { icon, iconLabel, actionBar, container, elementDisposable: Disposable.None }; - } - - private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem { - return !!((item).remotePort); - } - - renderElement(element: ITreeNode, index: number, templateData: ITunnelTemplateData): void { - templateData.elementDisposable.dispose(); - const node = element.element; - - // reset - templateData.actionBar.clear(); - templateData.icon.className = 'custom-view-tree-node-item-icon'; - templateData.icon.hidden = true; - - let editableData: IEditableData | undefined; - if (this.isTunnelItem(node)) { - editableData = this.remoteExplorerService.getEditableData(node); - if (editableData) { - templateData.iconLabel.element.style.display = 'none'; - this.renderInputBox(templateData.container, editableData); - } else { - templateData.iconLabel.element.style.display = 'flex'; - this.renderTunnel(node, templateData); - } - } else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) { - templateData.iconLabel.element.style.display = 'none'; - this.renderInputBox(templateData.container, editableData); - } else { - templateData.iconLabel.element.style.display = 'flex'; - templateData.iconLabel.setLabel(node.label); - } - } - - private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) { - const description = node.processDescription; - const label = node.label; - const tooltip = label + (description ? (' - ' + description) : ''); - templateData.iconLabel.setLabel(label, description, { title: node instanceof TunnelItem ? node.tooltip : tooltip, extraClasses: ['tunnel-view-label'] }); - - templateData.actionBar.context = node; - const contextKeyService = this.contextKeyService.createOverlay([ - ['view', this.viewId], - ['tunnelType', node.tunnelType], - ['tunnelCloseable', node.closeable], - ]); - const disposableStore = new DisposableStore(); - templateData.elementDisposable = disposableStore; - const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelPortInline, contextKeyService)); - const actions: IAction[] = []; - disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions)); - if (actions) { - templateData.actionBar.push(actions, { icon: true, label: false }); - if (this._actionRunner) { - templateData.actionBar.actionRunner = this._actionRunner; - } - } - if (node.icon) { - templateData.icon.className = `custom-view-tree-node-item-icon ${ThemeIcon.asClassName(node.icon)}`; - templateData.icon.hidden = false; - } else { - templateData.icon.className = 'custom-view-tree-node-item-icon'; - templateData.icon.hidden = true; - } - - menu.dispose(); - } - - private renderInputBox(container: HTMLElement, editableData: IEditableData): IDisposable { - // Required for FireFox. The blur event doesn't fire on FireFox when you just mash the "+" button to forward a port. - if (this.inputDone) { - this.inputDone(false, false); - this.inputDone = undefined; - } - const value = editableData.startingValue || ''; - const inputBox = new InputBox(container, this.contextViewService, { - ariaLabel: nls.localize('remote.tunnelsView.input', "Press Enter to confirm or Escape to cancel."), - validationOptions: { - validation: (value) => { - const message = editableData.validationMessage(value); - if (!message) { - return null; - } - - return { - content: message.content, - formatContent: true, - type: message.severity === Severity.Error ? MessageType.ERROR : MessageType.INFO - }; - } - }, - placeholder: editableData.placeholder || '' - }); - const styler = attachInputBoxStyler(inputBox, this.themeService); - - inputBox.value = value; - inputBox.focus(); - inputBox.select({ start: 0, end: editableData.startingValue ? editableData.startingValue.length : 0 }); - - const done = once((success: boolean, finishEditing: boolean) => { - if (this.inputDone) { - this.inputDone = undefined; - } - inputBox.element.style.display = 'none'; - const inputValue = inputBox.value; - dispose(toDispose); - if (finishEditing) { - editableData.onFinish(inputValue, success); - } - }); - this.inputDone = done; - - const toDispose = [ - inputBox, - dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { - if (e.equals(KeyCode.Enter)) { - if (inputBox.validate() !== MessageType.ERROR) { - done(true, true); - } else { - done(false, true); - } - } else if (e.equals(KeyCode.Escape)) { - done(false, true); - } - }), - dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => { - done(inputBox.validate() !== MessageType.ERROR, true); - }), - styler - ]; - - return toDisposable(() => { - done(false, false); - }); - } - - disposeElement(resource: ITreeNode, index: number, templateData: ITunnelTemplateData): void { - templateData.elementDisposable.dispose(); - } - - disposeTemplate(templateData: ITunnelTemplateData): void { - templateData.actionBar.dispose(); - templateData.elementDisposable.dispose(); - } -} - -class TunnelDataSource implements IAsyncDataSource { - hasChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) { - if (element instanceof TunnelViewModel) { - return true; - } else if (element instanceof TunnelItem) { - return false; - } else if ((element).items) { - return true; - } - return false; - } - - async getChildren(element: ITunnelViewModel | ITunnelItem | ITunnelGroup) { - if (element instanceof TunnelViewModel) { - return element.all; - } else if (element instanceof TunnelItem) { - return []; - } else if ((element).items) { - return (element).items!; - } - return []; - } -} - class PortColumn implements ITableColumn { readonly label: string = nls.localize('port', "Port"); readonly weight: number = 1; @@ -392,7 +174,16 @@ class PortColumn implements ITableColumn { ['tunnelType', row.tunnelType], ['tunnelCloseable', row.closeable] ]; - return { label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline, editId: row.tunnelType === TunnelType.Add ? TunnelEditId.New : TunnelEditId.Label }; + let tooltip: string; + if (row instanceof TunnelItem) { + tooltip = `${row.name ? nls.localize('remote.tunnel.tooltipName', "Port labeled {0}. ", row.name) : ''} ${row.tooltipPostfix}`; + } else { + tooltip = label; + } + return { + label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline, + editId: row.tunnelType === TunnelType.Add ? TunnelEditId.New : TunnelEditId.Label, tooltip + }; } } @@ -408,7 +199,13 @@ class LocalAddressColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.localAddress ?? ''; - return { label, menuId: MenuId.TunnelLocalAddressInline, context, tunnel: row, editId: TunnelEditId.LocalPort }; + let tooltip: string; + if (row instanceof TunnelItem) { + tooltip = row.tooltipPostfix; + } else { + tooltip = label; + } + return { label, menuId: MenuId.TunnelLocalAddressInline, context, tunnel: row, editId: TunnelEditId.LocalPort, tooltip }; } } @@ -417,7 +214,8 @@ class RunningProcessColumn implements ITableColumn { readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { - return { label: row.processDescription ?? '', tunnel: row, context: [], editId: TunnelEditId.None }; + const label = row.processDescription ?? ''; + return { label, tunnel: row, context: [], editId: TunnelEditId.None, tooltip: label }; } } @@ -433,7 +231,8 @@ class OriginColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.source; - return { label, context, menuId: MenuId.TunnelOriginInline, tunnel: row, editId: TunnelEditId.None }; + const tooltip = `${label}. ${row instanceof TunnelItem ? row.tooltipPostfix : ''}`; + return { label, context, menuId: MenuId.TunnelOriginInline, tunnel: row, editId: TunnelEditId.None, tooltip }; } } @@ -449,7 +248,14 @@ class PrivacyColumn implements ITableColumn { ['tunnelCloseable', row.closeable] ]; const label = row.privacy === TunnelPrivacy.Public ? nls.localize('tunnel.privacyPublic', "Public") : nls.localize('tunnel.privacyPrivate', "Private"); - return { label, context, tunnel: row, icon: row.icon, editId: TunnelEditId.None }; + let tooltip: string; + if (row instanceof TunnelItem) { + tooltip = `${row.privacy === TunnelPrivacy.Public ? nls.localize('remote.tunnel.tooltipPublic', "Accessible publicly. ") : + nls.localize('remote.tunnel.tooltipPrivate', "Only accessible from this machine. ")} ${row.tooltipPostfix}`; + } else { + tooltip = label; + } + return { label, context, tunnel: row, icon: row.icon, editId: TunnelEditId.None, tooltip }; } } @@ -481,6 +287,7 @@ interface IActionBarTemplateData { interface ActionBarCell { label: string; icon?: ThemeIcon; + tooltip: string; menuId?: MenuId; context: [string, any][]; tunnel: ITunnelItem; @@ -541,7 +348,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer { if (item instanceof TunnelItem) { - return item.tooltip; + return item.tooltipPostfix; } else { return item.label; } From 7e674bd5a7f092725579aa3916e703659d01b1b5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 15:19:52 +0100 Subject: [PATCH 37/45] Add port header tooltips --- .../contrib/remote/browser/tunnelView.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 65c2bfc0602d2..9028ea6994817 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -162,7 +162,8 @@ export class TunnelViewModel implements ITunnelViewModel { } class PortColumn implements ITableColumn { - readonly label: string = nls.localize('port', "Port"); + readonly label: string = nls.localize('tunnel.portColumn.label', "Port"); + readonly tooltip: string = nls.localize('tunnel.portColumn.tooltip', "The label and remote port number of the forwarded port."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -188,7 +189,8 @@ class PortColumn implements ITableColumn { } class LocalAddressColumn implements ITableColumn { - readonly label: string = nls.localize('local address', "Local Address"); + readonly label: string = nls.localize('tunnel.addressColumn.label', "Local Address"); + readonly tooltip: string = nls.localize('tunnel.addressColumn.tooltip', "The address that the forwarded port is available at locally."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -210,7 +212,8 @@ class LocalAddressColumn implements ITableColumn { } class RunningProcessColumn implements ITableColumn { - readonly label: string = nls.localize('process', "Running Process"); + readonly label: string = nls.localize('tunnel.processColumn.label', "Running Process"); + readonly tooltip: string = nls.localize('tunnel.processColumn.tooltip', "The command line of the process that is using the port."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -220,7 +223,8 @@ class RunningProcessColumn implements ITableColumn { } class OriginColumn implements ITableColumn { - readonly label: string = nls.localize('origin', "Origin"); + readonly label: string = nls.localize('tunnel.originColumn.label', "Origin"); + readonly tooltip: string = nls.localize('tunnel.originColumn.tooltip', "The source that a forwarded port originates from. Can be an extension, user forwarded, statically forwarded, or automatically forwarded."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { @@ -237,7 +241,8 @@ class OriginColumn implements ITableColumn { } class PrivacyColumn implements ITableColumn { - readonly label: string = nls.localize('privacy', "Privacy"); + readonly label: string = nls.localize('tunnel.privacyColumn.label', "Privacy"); + readonly tooltip: string = nls.localize('tunnel.privacyColumn.tooltip', "The availability of the forwarded port."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { From 590f7bc0896e8e3213144f227c24998485252c62 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 15:21:42 +0100 Subject: [PATCH 38/45] Use column weight in ports table --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 9028ea6994817..9bb9586e00fa9 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -214,7 +214,7 @@ class LocalAddressColumn implements ITableColumn { class RunningProcessColumn implements ITableColumn { readonly label: string = nls.localize('tunnel.processColumn.label', "Running Process"); readonly tooltip: string = nls.localize('tunnel.processColumn.tooltip', "The command line of the process that is using the port."); - readonly weight: number = 1; + readonly weight: number = 2; readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { const label = row.processDescription ?? ''; From ed8013813319d12f8ab810763ebd0af69462624d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 15:33:19 +0100 Subject: [PATCH 39/45] More clean up and fix icons --- .../contrib/remote/browser/remoteExplorer.ts | 3 +-- .../contrib/remote/browser/remoteIcons.ts | 2 ++ .../contrib/remote/browser/tunnelView.ts | 21 ++++--------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 89a5e8545f4aa..78c3e421517ee 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -47,7 +47,6 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IActivityService private readonly activityService: IActivityService, @IStatusbarService private readonly statusbarService: IStatusbarService, - @IConfigurationService private readonly configurationService: IConfigurationService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { super(); @@ -92,7 +91,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu if (this.environmentService.remoteAuthority && viewEnabled) { const viewContainer = await this.getViewContainer(); - const tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService, this.configurationService), this.environmentService); + const tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); if (viewContainer) { this.remoteExplorerService.enablePortsFeatures(); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts index 586832c145d61..b8a1498e2dd10 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts @@ -25,3 +25,5 @@ export const stopForwardIcon = registerIcon('ports-stop-forward-icon', Codicon.x export const openBrowserIcon = registerIcon('ports-open-browser-icon', Codicon.globe, nls.localize('openBrowserIcon', 'Icon for the open browser action.')); export const openPreviewIcon = registerIcon('ports-open-preview-icon', Codicon.openPreview, nls.localize('openPreviewIcon', 'Icon for the open preview action.')); export const copyAddressIcon = registerIcon('ports-copy-address-icon', Codicon.clippy, nls.localize('copyAddressIcon', 'Icon for the copy local address action.')); +export const forwardedPortWithoutProcessIcon = registerIcon('ports-forwarded-without-process-icon', Codicon.circleOutline, nls.localize('forwardedPortWithoutProcessIcon', 'Icon for forwarded ports that don\'t have a running process.')); +export const forwardedPortWithProcessIcon = registerIcon('ports-forwarded-with-process-icon', Codicon.circleFilled, nls.localize('forwardedPortWithProcessIcon', 'Icon for forwarded ports that do have a running process.')); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 9bb9586e00fa9..05a410862c3fa 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -23,7 +23,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, MenuRegistry, ILocalizedString, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -39,13 +39,12 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { copyAddressIcon, forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, publicPortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; +import { copyAddressIcon, forwardedPortWithoutProcessIcon, forwardedPortWithProcessIcon, forwardPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, publicPortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWeb } from 'vs/base/common/platform'; import { ITableColumn, ITableContextMenuEvent, ITableMouseEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; -import { Codicon } from 'vs/base/common/codicons'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); export const PORT_AUTO_FORWARD_SETTING = 'remote.autoForwardPorts'; @@ -84,8 +83,7 @@ export class TunnelViewModel implements ITunnelViewModel { }; constructor( - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService ) { this.model = remoteExplorerService.tunnelModel; this.onForwardedPortsChanged = Event.any(this.model.onForwardPort, this.model.onClosePort, this.model.onPortName, this.model.onCandidatesChanged); @@ -143,17 +141,6 @@ export class TunnelViewModel implements ITunnelViewModel { }); } - private get candidates(): TunnelItem[] { - const candidates: TunnelItem[] = []; - this._candidates.forEach(value => { - if (!mapHasAddressLocalhostOrAllInterfaces(this.model.forwarded, value.host, value.port) && - !mapHasAddressLocalhostOrAllInterfaces(this.model.detected, value.host, value.port)) { - candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, '', undefined, undefined, false, undefined, value.detail, value.pid)); - } - }); - return candidates; - } - isEmpty(): boolean { return (this.detected.length === 0) && ((this.forwarded.length === 0) || (this.forwarded.length === 1 && @@ -168,7 +155,7 @@ class PortColumn implements ITableColumn { readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { const label = row.name ? `${row.name} (${row.remotePort})` : `${row.remotePort}`; - const icon = row.processDescription ? Codicon.circleFilled : Codicon.circleOutline; + const icon = row.processDescription ? forwardedPortWithProcessIcon : forwardedPortWithoutProcessIcon; const context: [string, any][] = [ ['view', TUNNEL_VIEW_ID], From 777c51f046f79d7c8dbaeb50b687e6f7bf0ae832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 16:07:47 +0100 Subject: [PATCH 40/45] table: optional tooltip --- src/vs/base/browser/ui/table/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/table/table.ts b/src/vs/base/browser/ui/table/table.ts index c111537d5364d..2a9786bf42e22 100644 --- a/src/vs/base/browser/ui/table/table.ts +++ b/src/vs/base/browser/ui/table/table.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; export interface ITableColumn { readonly label: string; - readonly tooltip: string; + readonly tooltip?: string; readonly weight: number; readonly templateId: string; From 26dff8f2cca845ee23c7bb77dc79fbbe5f48cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 18 Feb 2021 16:15:25 +0100 Subject: [PATCH 41/45] table hover feedback --- src/vs/base/browser/ui/list/listWidget.ts | 12 +++++++++- src/vs/base/browser/ui/table/table.css | 22 +++++++++++++++++++ src/vs/base/browser/ui/table/tableWidget.ts | 19 ++++++++++++++-- src/vs/platform/theme/common/colorRegistry.ts | 1 + src/vs/platform/theme/common/styler.ts | 6 +++-- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d399ef27bd7c0..89518c8263b0d 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -831,6 +831,14 @@ export class DefaultStyleController implements IStyleController { content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); } + if (styles.tableColumnsBorder) { + content.push(` + .monaco-table:hover > .monaco-split-view2, + .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before { + border-color: ${styles.tableColumnsBorder}; + }`); + } + this.styleElement.textContent = content.join('\n'); } } @@ -886,6 +894,7 @@ export interface IListStyles { listFilterWidgetNoMatchesOutline?: Color; listMatchesShadow?: Color; treeIndentGuidesStroke?: Color; + tableColumnsBorder?: Color; } const defaultStyles: IListStyles = { @@ -897,7 +906,8 @@ const defaultStyles: IListStyles = { listInactiveSelectionBackground: Color.fromHex('#3F3F46'), listHoverBackground: Color.fromHex('#2A2D2E'), listDropBackground: Color.fromHex('#383B3D'), - treeIndentGuidesStroke: Color.fromHex('#a9a9a9') + treeIndentGuidesStroke: Color.fromHex('#a9a9a9'), + tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2) }; const DefaultOptions: IListOptions = { diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 603a701c7dca7..19ae63929ad71 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -12,6 +12,10 @@ white-space: nowrap; } +.monaco-table > .monaco-split-view2 { + border-bottom: 1px solid transparent; +} + .monaco-table > .monaco-list { flex: 1; } @@ -42,3 +46,21 @@ .monaco-table-td[data-col-index="0"] { padding-left: 20px; } + +.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before { + content: ""; + position: absolute; + left: calc(var(--sash-size) / 2); + width: 0; + border-left: 1px solid transparent; +} + +.monaco-table > .monaco-split-view2, +.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before { + transition: border-color 0.2s ease-out; +} +/* +.monaco-table:hover > .monaco-split-view2, +.monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before { + border-color: rgba(204, 204, 204, 0.2); +} */ diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 5b091fa5a594b..183200e2eb5c5 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -9,7 +9,7 @@ import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableGestureEvent, import { ISpliceable } from 'vs/base/common/sequence'; import { IThemable } from 'vs/base/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { $, append, clearNode, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; +import { $, append, clearNode, createStyleSheet, getContentHeight, getContentWidth } from 'vs/base/browser/dom'; import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Emitter, Event } from 'vs/base/common/event'; @@ -142,11 +142,15 @@ export interface ITableStyles extends IListStyles { } export class Table implements ISpliceable, IThemable, IDisposable { + private static InstanceCount = 0; + readonly domId = `table_id_${++Table.InstanceCount}`; + readonly domNode: HTMLElement; private splitview: SplitView; private list: List; private columnLayoutDisposable: IDisposable; private cachedHeight: number = 0; + private styleElement: HTMLStyleElement; get onDidChangeFocus(): Event> { return this.list.onDidChangeFocus; } get onDidChangeSelection(): Event> { return this.list.onDidChangeSelection; } @@ -184,7 +188,7 @@ export class Table implements ISpliceable, IThemable, IDisposable { renderers: ITableRenderer[], _options?: ITableOptions ) { - this.domNode = append(container, $('.monaco-table')); + this.domNode = append(container, $(`.monaco-table.${this.domId}`)); const headers = columns.map((c, i) => new ColumnHeader(c, i)); const descriptor: ISplitViewDescriptor = { @@ -207,6 +211,9 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.columnLayoutDisposable = Event.any(...headers.map(h => h.onDidLayout)) (([index, size]) => renderer.layoutColumn(index, size)); + + this.styleElement = createStyleSheet(this.domNode); + this.style({}); } updateOptions(options: ITableOptionsUpdate): void { @@ -251,6 +258,14 @@ export class Table implements ISpliceable, IThemable, IDisposable { } style(styles: ITableStyles): void { + const content: string[] = []; + + content.push(`.monaco-table.${this.domId} > .monaco-split-view2 .monaco-sash.vertical::before { + top: ${this.virtualDelegate.headerRowHeight + 1}px; + height: calc(100% - ${this.virtualDelegate.headerRowHeight}px); + }`); + + this.styleElement.textContent = content.join('\n'); this.list.style(styles); } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 58b90b849ffeb..c83eb434e484e 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -385,6 +385,7 @@ export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget. export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hc: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hc: contrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hc: '#a9a9a9' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); +export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', { dark: '#CCCCCC20', light: '#61616120', hc: null }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', { dark: '#8C8C8C', light: '#8E8E90', hc: '#A7A8A9' }, nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized. ")); /** diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 02c66cc4325b8..bf489c8d1eaa9 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground, treeIndentGuidesStroke, editorWidgetForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, ColorValue, resolveColorValue, textLinkForeground, problemsWarningIconForeground, problemsErrorIconForeground, problemsInfoIconForeground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, listFocusOutline, listInactiveFocusOutline, tableColumnsBorder } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { IThemable, styleFn } from 'vs/base/common/styler'; @@ -229,6 +229,7 @@ export interface IListStyleOverrides extends IStyleOverrides { listFilterWidgetNoMatchesOutline?: ColorIdentifier; listMatchesShadow?: ColorIdentifier; treeIndentGuidesStroke?: ColorIdentifier; + tableColumnsBorder?: ColorIdentifier; } export function attachListStyler(widget: IThemable, themeService: IThemeService, overrides?: IColorMapping): IDisposable { @@ -256,7 +257,8 @@ export const defaultListStyles: IColorMapping = { listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listMatchesShadow: widgetShadow, - treeIndentGuidesStroke + treeIndentGuidesStroke, + tableColumnsBorder }; export interface IButtonStyleOverrides extends IStyleOverrides { From 7b74c7787c63a7c1eabe042b414440f49222362f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 16:29:38 +0100 Subject: [PATCH 42/45] Fix hygiene issue in breakpoints view --- .../workbench/contrib/debug/browser/breakpointsView.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 27291b2f7c700..41840b2342cef 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -855,11 +855,11 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea startColumn: breakpoint.column || 1, endColumn: breakpoint.endColumn || Constants.MAX_SAFE_SMALL_INTEGER } : { - startLineNumber: breakpoint.lineNumber, - startColumn: breakpoint.column || 1, - endLineNumber: breakpoint.lineNumber, - endColumn: breakpoint.column || Constants.MAX_SAFE_SMALL_INTEGER - }; + startLineNumber: breakpoint.lineNumber, + startColumn: breakpoint.column || 1, + endLineNumber: breakpoint.lineNumber, + endColumn: breakpoint.column || Constants.MAX_SAFE_SMALL_INTEGER + }; return editorService.openEditor({ resource: breakpoint.uri, From ba97aa53374727d5d381f7a582901b483b8b6c8c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 17:40:37 +0100 Subject: [PATCH 43/45] Add padding-right to port cell icon --- src/vs/workbench/contrib/remote/browser/media/tunnelView.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css index 23d8c643df022..9279605592bf7 100644 --- a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css +++ b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css @@ -46,6 +46,7 @@ .ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell > .ports-view-actionbar-cell-icon.codicon { margin-top: 3px; + padding-right: 3px; } .ports-view .monaco-list .monaco-list-row.selected .ports-view-actionbar-cell > .ports-view-actionbar-cell-icon.codicon { From 50fe3228e4850cc8d3f8a7df060e01677bfaa1a5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 18 Feb 2021 17:59:18 +0100 Subject: [PATCH 44/45] Add tooltip to icon in ports view --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 05a410862c3fa..8b5660b6b7586 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -358,6 +358,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer Date: Fri, 19 Feb 2021 11:03:37 +0100 Subject: [PATCH 45/45] Add icon column --- .../contrib/remote/browser/tunnelView.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 8b5660b6b7586..25e862a2a30d9 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -148,6 +148,33 @@ export class TunnelViewModel implements ITunnelViewModel { } } +class IconColumn implements ITableColumn { + readonly label: string = ''; + readonly tooltip: string = ''; + readonly weight: number = 1; + readonly maximumWidth: number = 40; + readonly templateId: string = 'actionbar'; + project(row: ITunnelItem): ActionBarCell { + const icon = row.processDescription ? forwardedPortWithProcessIcon : forwardedPortWithoutProcessIcon; + const context: [string, any][] = + [ + ['view', TUNNEL_VIEW_ID], + ['tunnelType', row.tunnelType], + ['tunnelCloseable', row.closeable] + ]; + let tooltip: string; + if (row instanceof TunnelItem) { + tooltip = `${row.processDescription ? nls.localize('tunnel.iconColumn.running', "Port has running process.") : + nls.localize('tunnel.iconColumn.notRunning', "No running process.")} ${row.tooltipPostfix}`; + } else { + tooltip = ''; + } + return { + label: '', icon, tunnel: row, context, editId: TunnelEditId.None, tooltip + }; + } +} + class PortColumn implements ITableColumn { readonly label: string = nls.localize('tunnel.portColumn.label', "Port"); readonly tooltip: string = nls.localize('tunnel.portColumn.tooltip', "The label and remote port number of the forwarded port."); @@ -155,7 +182,6 @@ class PortColumn implements ITableColumn { readonly templateId: string = 'actionbar'; project(row: ITunnelItem): ActionBarCell { const label = row.name ? `${row.name} (${row.remotePort})` : `${row.remotePort}`; - const icon = row.processDescription ? forwardedPortWithProcessIcon : forwardedPortWithoutProcessIcon; const context: [string, any][] = [ ['view', TUNNEL_VIEW_ID], @@ -169,7 +195,7 @@ class PortColumn implements ITableColumn { tooltip = label; } return { - label, icon, tunnel: row, context, menuId: MenuId.TunnelPortInline, + label, tunnel: row, context, menuId: MenuId.TunnelPortInline, editId: row.tunnelType === TunnelType.Add ? TunnelEditId.New : TunnelEditId.Label, tooltip }; } @@ -602,7 +628,7 @@ export class TunnelPanel extends ViewPane { const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, this.menuService, this.contextViewService, this.themeService, this.remoteExplorerService); - const columns = [new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; + const columns = [new IconColumn(), new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; if (this.tunnelService.canMakePublic) { columns.push(new PrivacyColumn()); }