mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 02:08:47 +00:00
icon selection widget (#193518)
* icon selection widget * fix scrolling
This commit is contained in:
committed by
GitHub
parent
a18ef5588e
commit
17726b2b0c
30
src/vs/base/browser/ui/icons/iconSelectBox.css
Normal file
30
src/vs/base/browser/ui/icons/iconSelectBox.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
.icon-select-box>.icon-select-box-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-select-box .icon-select-icons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-select-box .icon-select-icons-container > .icon-container {
|
||||||
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-select-box .icon-select-icons-container > .icon-container.focused {
|
||||||
|
outline: 1px dashed var(--vscode-toolbar-hoverOutline);
|
||||||
|
outline-offset: -1px;
|
||||||
|
background-color: var(--vscode-toolbar-hoverBackground);
|
||||||
|
}
|
||||||
217
src/vs/base/browser/ui/icons/iconSelectBox.ts
Normal file
217
src/vs/base/browser/ui/icons/iconSelectBox.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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!./iconSelectBox';
|
||||||
|
import * as dom from 'vs/base/browser/dom';
|
||||||
|
import { IInputBoxStyles, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||||
|
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||||
|
import { Emitter } from 'vs/base/common/event';
|
||||||
|
import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
import { ThemeIcon } from 'vs/base/common/themables';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { IMatch } from 'vs/base/common/filters';
|
||||||
|
|
||||||
|
export interface IIconSelectBoxOptions {
|
||||||
|
readonly icons: ThemeIcon[];
|
||||||
|
readonly inputBoxStyles: IInputBoxStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IconSelectBox extends Disposable {
|
||||||
|
|
||||||
|
readonly domNode: HTMLElement;
|
||||||
|
|
||||||
|
private _onDidSelect = this._register(new Emitter<ThemeIcon>());
|
||||||
|
readonly onDidSelect = this._onDidSelect.event;
|
||||||
|
|
||||||
|
private renderedIcons: [ThemeIcon, HTMLElement][] = [];
|
||||||
|
|
||||||
|
private focusedItemIndex: number = 0;
|
||||||
|
private numberOfElementsPerRow: number = 1;
|
||||||
|
|
||||||
|
private inputBox: InputBox | undefined;
|
||||||
|
private scrollableElement: DomScrollableElement | undefined;
|
||||||
|
private readonly iconContainerWidth = 36;
|
||||||
|
private readonly iconContainerHeight = 32;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly options: IIconSelectBoxOptions,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.domNode = dom.$('.icon-select-box');
|
||||||
|
this._register(this.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
private create(): IDisposable {
|
||||||
|
const disposables = new DisposableStore();
|
||||||
|
|
||||||
|
const iconSelectBoxContainer = dom.append(this.domNode, dom.$('.icon-select-box-container'));
|
||||||
|
iconSelectBoxContainer.style.margin = '10px 15px';
|
||||||
|
|
||||||
|
const iconSelectInputContainer = dom.append(iconSelectBoxContainer, dom.$('.icon-select-input-container'));
|
||||||
|
iconSelectInputContainer.style.paddingBottom = '10px';
|
||||||
|
this.inputBox = disposables.add(new InputBox(iconSelectInputContainer, undefined, {
|
||||||
|
placeholder: localize('iconSelect.placeholder', "Search icons"),
|
||||||
|
inputBoxStyles: this.options.inputBoxStyles,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const iconsContainer = dom.$('.icon-select-icons-container');
|
||||||
|
iconsContainer.style.paddingRight = '10px';
|
||||||
|
this.scrollableElement = disposables.add(new DomScrollableElement(iconsContainer, { useShadows: false }));
|
||||||
|
dom.append(iconSelectBoxContainer, this.scrollableElement.getDomNode());
|
||||||
|
|
||||||
|
const iconsDisposables = disposables.add(new MutableDisposable());
|
||||||
|
iconsDisposables.value = this.renderIcons(this.options.icons, iconsContainer);
|
||||||
|
this.scrollableElement.scanDomNode();
|
||||||
|
|
||||||
|
disposables.add(this.inputBox.onDidChange(value => {
|
||||||
|
const icons = this.options.icons.filter(icon => {
|
||||||
|
return this.matchesContiguous(value, icon.id);
|
||||||
|
});
|
||||||
|
iconsDisposables.value = this.renderIcons(icons, iconsContainer);
|
||||||
|
this.scrollableElement?.scanDomNode();
|
||||||
|
}));
|
||||||
|
|
||||||
|
return disposables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderIcons(icons: ThemeIcon[], container: HTMLElement): IDisposable {
|
||||||
|
const disposables = new DisposableStore();
|
||||||
|
dom.clearNode(container);
|
||||||
|
const focusedIcon = this.renderedIcons[this.focusedItemIndex]?.[0];
|
||||||
|
let focusedIconIndex = 0;
|
||||||
|
const renderedIcons: [ThemeIcon, HTMLElement][] = [];
|
||||||
|
for (let index = 0; index < icons.length; index++) {
|
||||||
|
const icon = icons[index];
|
||||||
|
const iconContainer = dom.append(container, dom.$('.icon-container'));
|
||||||
|
iconContainer.style.width = `${this.iconContainerWidth}px`;
|
||||||
|
iconContainer.style.height = `${this.iconContainerHeight}px`;
|
||||||
|
iconContainer.tabIndex = -1;
|
||||||
|
iconContainer.role = 'button';
|
||||||
|
iconContainer.title = icon.id;
|
||||||
|
dom.append(iconContainer, dom.$(ThemeIcon.asCSSSelector(icon)));
|
||||||
|
renderedIcons.push([icon, iconContainer]);
|
||||||
|
|
||||||
|
disposables.add(dom.addDisposableListener(iconContainer, dom.EventType.CLICK, (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setSelection(index);
|
||||||
|
}));
|
||||||
|
|
||||||
|
disposables.add(dom.addDisposableListener(iconContainer, dom.EventType.MOUSE_OVER, (e: MouseEvent) => {
|
||||||
|
this.focusIcon(index);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (icon === focusedIcon) {
|
||||||
|
focusedIconIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderedIcons.splice(0, this.renderedIcons.length, ...renderedIcons);
|
||||||
|
this.focusIcon(focusedIconIndex);
|
||||||
|
|
||||||
|
return disposables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private focusIcon(index: number): void {
|
||||||
|
const existing = this.renderedIcons[this.focusedItemIndex];
|
||||||
|
if (existing) {
|
||||||
|
existing[1].classList.remove('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusedItemIndex = index;
|
||||||
|
const icon = this.renderedIcons[index]?.[1];
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.add('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reveal(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private reveal(index: number): void {
|
||||||
|
if (!this.scrollableElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index < 0 || index >= this.renderedIcons.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const icon = this.renderedIcons[index][1];
|
||||||
|
if (!icon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { height } = this.scrollableElement.getScrollDimensions();
|
||||||
|
const { scrollTop } = this.scrollableElement.getScrollPosition();
|
||||||
|
if (icon.offsetTop + this.iconContainerHeight > scrollTop + height) {
|
||||||
|
this.scrollableElement.setScrollPosition({ scrollTop: icon.offsetTop + this.iconContainerHeight - height });
|
||||||
|
} else if (icon.offsetTop < scrollTop) {
|
||||||
|
this.scrollableElement.setScrollPosition({ scrollTop: icon.offsetTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | null {
|
||||||
|
const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase());
|
||||||
|
if (matchIndex !== -1) {
|
||||||
|
return [{ start: matchIndex, end: matchIndex + word.length }];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(dimension: dom.Dimension): void {
|
||||||
|
this.domNode.style.width = `${dimension.width}px`;
|
||||||
|
this.domNode.style.height = `${dimension.height}px`;
|
||||||
|
if (this.scrollableElement) {
|
||||||
|
this.scrollableElement.getDomNode().style.height = `${dimension.height - 46}px`;
|
||||||
|
this.scrollableElement.scanDomNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconsContainerWidth = dimension.width - 40;
|
||||||
|
this.numberOfElementsPerRow = Math.floor(iconsContainerWidth / this.iconContainerWidth);
|
||||||
|
if (this.numberOfElementsPerRow === 0) {
|
||||||
|
throw new Error('Insufficient width');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraSpace = iconsContainerWidth % this.iconContainerWidth;
|
||||||
|
const margin = Math.floor(extraSpace / this.numberOfElementsPerRow);
|
||||||
|
for (const [, icon] of this.renderedIcons) {
|
||||||
|
icon.style.marginRight = `${margin}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFocus(): number[] {
|
||||||
|
return [this.focusedItemIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelection(index: number): void {
|
||||||
|
if (index < 0 || index >= this.renderedIcons.length) {
|
||||||
|
throw new Error(`Invalid index ${index}`);
|
||||||
|
}
|
||||||
|
this.focusIcon(index);
|
||||||
|
this._onDidSelect.fire(this.renderedIcons[index][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus(): void {
|
||||||
|
this.inputBox?.focus();
|
||||||
|
this.focusIcon(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusNext(): void {
|
||||||
|
this.focusIcon((this.focusedItemIndex + 1) % this.renderedIcons.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPrevious(): void {
|
||||||
|
this.focusIcon((this.focusedItemIndex - 1 + this.renderedIcons.length) % this.renderedIcons.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusNextRow(): void {
|
||||||
|
this.focusIcon((this.focusedItemIndex + this.numberOfElementsPerRow) % this.renderedIcons.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPreviousRow(): void {
|
||||||
|
this.focusIcon((this.focusedItemIndex - this.numberOfElementsPerRow + this.renderedIcons.length) % this.renderedIcons.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFocusedIcon(): ThemeIcon {
|
||||||
|
return this.renderedIcons[this.focusedItemIndex][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -324,7 +324,7 @@ export const Codicon = {
|
|||||||
note: register('note', 0xeb26),
|
note: register('note', 0xeb26),
|
||||||
octoface: register('octoface', 0xeb27),
|
octoface: register('octoface', 0xeb27),
|
||||||
openPreview: register('open-preview', 0xeb28),
|
openPreview: register('open-preview', 0xeb28),
|
||||||
package_: register('package', 0xeb29),
|
package: register('package', 0xeb29),
|
||||||
paintcan: register('paintcan', 0xeb2a),
|
paintcan: register('paintcan', 0xeb2a),
|
||||||
pin: register('pin', 0xeb2b),
|
pin: register('pin', 0xeb2b),
|
||||||
play: register('play', 0xeb2c),
|
play: register('play', 0xeb2c),
|
||||||
|
|||||||
98
src/vs/workbench/browser/iconSelectBox.ts
Normal file
98
src/vs/workbench/browser/iconSelectBox.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { IIconSelectBoxOptions, IconSelectBox } from 'vs/base/browser/ui/icons/iconSelectBox';
|
||||||
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||||
|
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||||
|
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||||
|
|
||||||
|
export const WorkbenchIconSelectBoxFocusContextKey = new RawContextKey<boolean>('iconSelectBoxFocus', true);
|
||||||
|
|
||||||
|
export class WorkbenchIconSelectBox extends IconSelectBox {
|
||||||
|
|
||||||
|
private static focusedWidget: WorkbenchIconSelectBox | undefined;
|
||||||
|
static getFocusedWidget(): WorkbenchIconSelectBox | undefined {
|
||||||
|
return WorkbenchIconSelectBox.focusedWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options: IIconSelectBoxOptions,
|
||||||
|
@IContextKeyService contextKeyService: IContextKeyService
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
WorkbenchIconSelectBoxFocusContextKey.bindTo(this._register(contextKeyService.createScoped(this.domNode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
override focus(): void {
|
||||||
|
super.focus();
|
||||||
|
WorkbenchIconSelectBox.focusedWidget = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||||
|
id: 'iconSelectBox.focusUp',
|
||||||
|
weight: KeybindingWeight.WorkbenchContrib,
|
||||||
|
when: WorkbenchIconSelectBoxFocusContextKey,
|
||||||
|
primary: KeyCode.UpArrow,
|
||||||
|
handler: (accessor, arg2) => {
|
||||||
|
const selectBox = WorkbenchIconSelectBox.getFocusedWidget();
|
||||||
|
if (selectBox) {
|
||||||
|
selectBox.focusPreviousRow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||||
|
id: 'iconSelectBox.focusDown',
|
||||||
|
weight: KeybindingWeight.WorkbenchContrib,
|
||||||
|
when: WorkbenchIconSelectBoxFocusContextKey,
|
||||||
|
primary: KeyCode.DownArrow,
|
||||||
|
handler: (accessor, arg2) => {
|
||||||
|
const selectBox = WorkbenchIconSelectBox.getFocusedWidget();
|
||||||
|
if (selectBox) {
|
||||||
|
selectBox.focusNextRow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||||
|
id: 'iconSelectBox.focusNext',
|
||||||
|
weight: KeybindingWeight.WorkbenchContrib,
|
||||||
|
when: WorkbenchIconSelectBoxFocusContextKey,
|
||||||
|
primary: KeyCode.RightArrow,
|
||||||
|
handler: (accessor, arg2) => {
|
||||||
|
const selectBox = WorkbenchIconSelectBox.getFocusedWidget();
|
||||||
|
if (selectBox) {
|
||||||
|
selectBox.focusNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||||
|
id: 'iconSelectBox.focusPrevious',
|
||||||
|
weight: KeybindingWeight.WorkbenchContrib,
|
||||||
|
when: WorkbenchIconSelectBoxFocusContextKey,
|
||||||
|
primary: KeyCode.LeftArrow,
|
||||||
|
handler: (accessor, arg2) => {
|
||||||
|
const selectBox = WorkbenchIconSelectBox.getFocusedWidget();
|
||||||
|
if (selectBox) {
|
||||||
|
selectBox.focusPrevious();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||||
|
id: 'iconSelectBox.selectFocused',
|
||||||
|
weight: KeybindingWeight.WorkbenchContrib,
|
||||||
|
when: WorkbenchIconSelectBoxFocusContextKey,
|
||||||
|
primary: KeyCode.Enter,
|
||||||
|
handler: (accessor, arg2) => {
|
||||||
|
const selectBox = WorkbenchIconSelectBox.getFocusedWidget();
|
||||||
|
if (selectBox) {
|
||||||
|
selectBox.setSelection(selectBox.getFocus()[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -47,6 +47,7 @@ import 'vs/workbench/browser/parts/paneCompositePart';
|
|||||||
import 'vs/workbench/browser/parts/banner/bannerPart';
|
import 'vs/workbench/browser/parts/banner/bannerPart';
|
||||||
import 'vs/workbench/browser/parts/statusbar/statusbarPart';
|
import 'vs/workbench/browser/parts/statusbar/statusbarPart';
|
||||||
import 'vs/workbench/browser/parts/views/viewsService';
|
import 'vs/workbench/browser/parts/views/viewsService';
|
||||||
|
import 'vs/workbench/browser/iconSelectBox';
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user