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),
|
||||
octoface: register('octoface', 0xeb27),
|
||||
openPreview: register('open-preview', 0xeb28),
|
||||
package_: register('package', 0xeb29),
|
||||
package: register('package', 0xeb29),
|
||||
paintcan: register('paintcan', 0xeb2a),
|
||||
pin: register('pin', 0xeb2b),
|
||||
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/statusbar/statusbarPart';
|
||||
import 'vs/workbench/browser/parts/views/viewsService';
|
||||
import 'vs/workbench/browser/iconSelectBox';
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user