diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 4b66fc02bdd..0676a9e3ab2 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -6,9 +6,10 @@ import 'vs/css!./list'; import { IDisposable } from 'vs/base/common/lifecycle'; import { range } from 'vs/base/common/arrays'; -import { IDelegate, IRenderer } from './list'; +import { IDelegate, IRenderer, IFocusChangeEvent, ISelectionChangeEvent } from './list'; import { List } from './listWidget'; import { PagedModel } from 'vs/base/common/paging'; +import Event, { mapEvent } from 'vs/base/common/event'; export interface IPagedRenderer extends IRenderer { renderPlaceholder(index: number, templateData: TTemplateData): void; @@ -57,8 +58,9 @@ class PagedRenderer implements IRenderer extends List { +export class PagedList { + private list: List; private _model: PagedModel; constructor( @@ -67,7 +69,15 @@ export class PagedList extends List { renderers: IPagedRenderer[] ) { const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); - super(container, delegate, pagedRenderers); + this.list = new List(container, delegate, pagedRenderers); + } + + get onFocusChange(): Event> { + return mapEvent(this.list.onFocusChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + } + + get onSelectionChange(): Event> { + return mapEvent(this.list.onSelectionChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); } get model(): PagedModel { @@ -76,6 +86,14 @@ export class PagedList extends List { set model(model: PagedModel) { this._model = model; - this.splice(0, this.length, ...range(model.length)); + this.list.splice(0, this.list.length, ...range(model.length)); + } + + get scrollTop(): number { + return this.list.scrollTop; + } + + layout(height?: number): void { + this.list.layout(height); } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index d9555eec843..b2fc96e9f4f 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -130,6 +130,8 @@ class Controller implements IDisposable { } private onClick(e: IListMouseEvent) { + e.preventDefault(); + e.stopPropagation(); this.list.setSelection(e.index); } @@ -193,6 +195,10 @@ export class List implements IDisposable { return this.view.getScrollHeight(); } + get scrollTop(): number { + return this.view.getScrollTop(); + } + layout(height?: number): void { this.view.layout(height); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsPart.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsPart.ts index 765c7c22a3d..b1c59b09645 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsPart.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsPart.ts @@ -5,11 +5,13 @@ 'use strict'; +import 'vs/css!./media/extensions2'; import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { ThrottledDelayer, always } from 'vs/base/common/async'; -import { Dimension, Builder } from 'vs/base/browser/builder'; -import { append, emmet as $, addClass, removeClass } from 'vs/base/browser/dom'; +import { IDisposable, toDisposable, empty, dispose } from 'vs/base/common/lifecycle'; +import { Builder } from 'vs/base/browser/builder'; +import { append, emmet as $, addClass, removeClass, getDomNodePosition, addDisposableListener } from 'vs/base/browser/dom'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { Position } from 'vs/platform/editor/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -17,9 +19,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer, PagedList } from 'vs/base/browser/ui/list/listPaging'; import { IExtension, IGalleryService } from '../common/extensions'; +import { getExtensionId } from '../common/extensionsUtil'; import { PagedModel, mapPager } from 'vs/base/common/paging'; interface ITemplateData { + id: string; root: HTMLElement; extension: HTMLElement; icon: HTMLImageElement; @@ -59,9 +63,14 @@ class Delegate implements IDelegate { class Renderer implements IPagedRenderer { + private _templates: ITemplateData[]; + get templates(): ITemplateData[] { return this._templates; } + constructor( @IInstantiationService private instantiationService: IInstantiationService - ) {} + ) { + this._templates = []; + } get templateId() { return 'extension'; } @@ -76,15 +85,18 @@ class Renderer implements IPagedRenderer { const version = append(subtitle, $('span.version')); const author = append(subtitle, $('span.author')); const description = append(body, $('.description')); - - return { - root, extension, icon, + const result = { + id: null, root, extension, icon, name, version, author, description }; + + this._templates.push(result); + return result; } renderPlaceholder(index: number, data: ITemplateData): void { addClass(data.extension, 'loading'); + data.id = null; data.icon.style.display = 'none'; data.name.textContent = ''; data.version.textContent = ''; @@ -97,6 +109,7 @@ class Renderer implements IPagedRenderer { const publisher = extension.galleryInformation ? extension.galleryInformation.publisherDisplayName : extension.publisher; const version = extension.galleryInformation.versions[0]; + data.id = getExtensionId(extension); removeClass(data.extension, 'loading'); data.icon.style.display = 'block'; data.icon.src = version.iconUrl; @@ -107,7 +120,11 @@ class Renderer implements IPagedRenderer { } disposeTemplate(data: ITemplateData): void { - // TODO + const index = this._templates.indexOf(data); + + if (index > -1) { + this._templates.splice(index, 1); + } } } @@ -124,8 +141,15 @@ export class ExtensionsPart extends BaseEditor { private list: PagedList; private searchDelayer: ThrottledDelayer; + private root: HTMLElement; private searchBox: HTMLInputElement; private extensionsBox: HTMLElement; + private overlay: HTMLElement; + + private _highlight: ITemplateData; + private highlightDisposable: IDisposable; + + private toDispose: IDisposable[]; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -134,37 +158,76 @@ export class ExtensionsPart extends BaseEditor { ) { super(ExtensionsPart.ID, telemetryService); this.searchDelayer = new ThrottledDelayer(500); + this._highlight = null; + this.highlightDisposable = empty; + this.toDispose = []; } createEditor(parent: Builder): void { const container = parent.getHTMLElement(); - const root = append(container, $('.extension-manager')); - const search = append(root, $('.search')); + this.root = append(container, $('.extension-manager')); + + this.toDispose.push(addDisposableListener(this.root, 'click', e => { + if (e.target === this.root && this.highlight) { + this.highlight = null; + } + })); + + const search = append(this.root, $('.search')); this.searchBox = append(search, $('input.search-box')); this.searchBox.placeholder = localize('searchExtensions', "Search Extensions"); - this.extensionsBox = append(root, $('.extensions')); + this.extensionsBox = append(this.root, $('.extensions')); const delegate = new Delegate(); const renderer = this.instantiationService.createInstance(Renderer); this.list = new PagedList(this.extensionsBox, delegate, [renderer]); + this.overlay = append(this.extensionsBox, $('.overlay')); + this.searchBox.oninput = () => this.triggerSearch(this.searchBox.value); + + this.list.onSelectionChange(({ elements }) => { + if (this.highlightDisposable) { + removeClass(this.overlay, 'animated'); + this.highlightDisposable.dispose(); + this.highlightDisposable = null; + } + + const [selected] = elements; + + if (!selected) { + return; + } + + const id = getExtensionId(selected.extension); + const [data] = renderer.templates.filter(t => t.id === id); + + if (!data) { + return; + } + + this.highlight = data; + }); } setVisible(visible: boolean, position?: Position): TPromise { return super.setVisible(visible, position).then(() => { if (visible) { + this.highlight = null; this.searchBox.value = ''; this.triggerSearch('', 0); } }); } - layout(dimension: Dimension): void { - const height = dimension.height - 72; - + layout({ height }): void { + height -= 72; this.extensionsBox.style.height = `${ height }px`; this.list.layout(height); + + if (this.highlight) { + this.overlay.style.height = this.extensionsBox.style.height; + } } focus(): void { @@ -186,7 +249,45 @@ export class ExtensionsPart extends BaseEditor { .then(model => this.list.model = model); } + private get highlight(): ITemplateData { + return this._highlight; + } + + private set highlight(data: ITemplateData) { + this._highlight = data; + + if (!data) { + removeClass(this.root, 'highlighted'); + this.highlightDisposable.dispose(); + return; + } + + const position = getDomNodePosition(data.root); + const rootPosition = getDomNodePosition(this.extensionsBox); + + removeClass(this.overlay, 'animated'); + + this.overlay.style.top = `${ position.top - rootPosition.top - this.list.scrollTop }px`; + this.overlay.style.height = `${ position.height }px`; + let _ = this.overlay.offsetHeight; _ = _; // trigger reflow + addClass(this.overlay, 'animated'); + this.overlay.style.top = '0'; + this.overlay.style.height = this.extensionsBox.style.height; + + const container = data.root.parentElement; + this.overlay.appendChild(data.root); + addClass(this.root, 'highlighted'); + + this.highlightDisposable = toDisposable(() => { + container.appendChild(data.root); + removeClass(this.overlay, 'animated'); + this.overlay.style.height = '0'; + }); + } + dispose(): void { + this._highlight = null; + this.toDispose = dispose(this.toDispose); super.dispose(); } } diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 9e4b0cac4e0..90968970ee4 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -141,107 +141,6 @@ transform: none; } -/* Extension Manager */ - -.extension-manager { - background: #f1f1f1; - width: 100%; - height: 100%; -} - -.extension-manager > .search, -.extension-manager > .extensions { - width: 100%; - max-width: 600px; - margin: 0 auto; -} - -.extension-manager > .search { - box-sizing: border-box; - padding: 10px; - height: 72px; -} - -.extension-manager > .search > .search-box { - margin: 10px 0; - width: calc(100% - 28px); /* TODO: HACK */ - padding: 10px 8px; - font-size: 16px; - border: 1px solid gainsboro; -} - -.extension-manager .extensions.loading { - background-image: url('loading.svg'); - background-position: top center; - background-repeat: no-repeat; -} - -.extension-manager .monaco-list-row.selected { - background-color: inherit; - color: inherit; -} - -.extension-manager .monaco-list-row:hover { - background-color: inherit; -} - -.extension-manager .extension-container { - width: 100%; - height: 100%; -} - -.extension-manager .extension-container > .extension { - margin: 10px 20px 10px 10px; - width: calc(100% - 30px); - height: calc(100% - 20px); - background-color: white; - box-shadow: 2px 2px 5px rgba(0,0,0,.1); -} - -.extension-manager .extension-container > .extension:hover { - box-shadow: 2px 2px 5px rgba(0,0,0,.25); -} - -.extension-manager .extension { - display: flex; -} - -.extension-manager .extension.loading { - background-image: url('loading.svg'); - background-position: center center; - background-repeat: no-repeat; -} - -.extension-manager .extension > .icon { - box-sizing: border-box; - width: 70px; - height: 70px; - padding: 8px; -} - -.extension-manager .extension > .body { - flex: 1; - padding: 8px; -} - -.extension-manager .extension > .body > .title > .name { - font-weight: bold; -} - -.extension-manager .extension > .body > .subtitle { - font-size: 90%; -} - -.extension-manager .extension > .body > .subtitle > .author { - font-style: italic; -} - -.extension-manager .extension > .body > div { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - /* Status bar */ .monaco-shell .extensions-statusbar { display: flex !important; diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions2.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions2.css new file mode 100644 index 00000000000..5fc2bfb2008 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions2.css @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Extension Manager */ + +.extension-manager { + position: relative; + background: #f1f1f1; + width: 100%; + height: 100%; +} + +.extension-manager.highlighted .monaco-list { + display: none; +} + +.extension-manager > .search, +.extension-manager > .extensions { + width: 100%; + max-width: 600px; + margin: 0 auto; +} + +.extension-manager > .search { + box-sizing: border-box; + padding: 10px; + height: 72px; +} + +.extension-manager > .search > .search-box { + margin: 10px 0; + width: calc(100% - 28px); /* TODO: HACK */ + padding: 10px 8px; + font-size: 16px; + border: 1px solid gainsboro; +} + +.extension-manager > .extensions { + position: relative; + overflow: hidden; +} + +.extension-manager .extensions.loading { + background-image: url('loading.svg'); + background-position: top center; + background-repeat: no-repeat; +} + +.extension-manager > .extensions > .overlay { + box-sizing: border-box; + position: absolute; + width: 100%; + left: 0; + padding: 0 20px 0 10px; +} + +.extension-manager > .extensions > .overlay.animated { + transition-property: top, height, padding; + transition-duration: 0.2s; + transition-timing-function: ease-out; +} + +.extension-manager .monaco-list-row.selected { + background-color: inherit; + color: inherit; +} + +.extension-manager .monaco-list-row:hover { + background-color: inherit; +} + +.extension-manager .extension-container { + width: 100%; + height: 100%; +} + +.extension-manager .extension-container > .extension { + margin: 10px 20px 10px 10px; + width: calc(100% - 30px); + height: calc(100% - 20px); +} + +.extension-manager .overlay .extension-container > .extension { + margin: 0; + width: 100%; + height: 100%; +} + +.extension-manager > .extensions > .overlay > .highlighted-extension, +.extension-manager .extension-container > .extension { + background-color: white; +} + +.extension-manager .monaco-list .extension-container > .extension { + box-shadow: 2px 2px 5px rgba(0,0,0,.1); +} + +.extension-manager .monaco-list .extension-container > .extension:hover { + box-shadow: 2px 2px 5px rgba(0,0,0,.25); +} + +.extension-manager .extension { + display: flex; +} + +.extension-manager .extension.loading { + background-image: url('loading.svg'); + background-position: center center; + background-repeat: no-repeat; +} + +.extension-manager .extension > .icon { + box-sizing: border-box; + width: 70px; + height: 70px; + padding: 8px; +} + +.extension-manager .extension > .body { + flex: 1; + padding: 8px; +} + +.extension-manager .extension > .body > .title > .name { + font-weight: bold; +} + +.extension-manager .extension > .body > .subtitle { + font-size: 90%; +} + +.extension-manager .extension > .body > .subtitle > .author { + font-style: italic; +} + +.extension-manager .extension > .body > div { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} \ No newline at end of file