mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
Implement dnd for array settings (#128884)
* Implement drag and drop for array settings Fixes #127424 Also removes redundant options for uniqueItem enum array settings
This commit is contained in:
parent
d39f94357d
commit
ecc6785b1b
|
@ -84,6 +84,13 @@
|
|||
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row {
|
||||
display: flex;
|
||||
}
|
||||
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.draggable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.drag-hover * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row,
|
||||
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row-header {
|
||||
|
|
|
@ -218,13 +218,16 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData
|
|||
}
|
||||
|
||||
function createArraySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {
|
||||
return keys => {
|
||||
return (keys, idx) => {
|
||||
const enumOptions: IObjectEnumOption[] = [];
|
||||
|
||||
if (element.setting.enum) {
|
||||
element.setting.enum.forEach((staticKey, i) => {
|
||||
const description = element.setting.enumDescriptions?.[i];
|
||||
enumOptions.push({ value: staticKey, description });
|
||||
element.setting.enum.forEach((key, i) => {
|
||||
// include the currently selected value, even if uniqueItems is true
|
||||
if (!element.setting.uniqueItems || (idx !== undefined && key === keys[idx]) || !keys.includes(key)) {
|
||||
const description = element.setting.enumDescriptions?.[i];
|
||||
enumOptions.push({ value: key, description });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1064,7 +1067,14 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
|
|||
newValue = [...template.context.value];
|
||||
}
|
||||
|
||||
if (e.targetIndex !== undefined) {
|
||||
if (e.sourceIndex !== undefined) {
|
||||
// A drag and drop occurred
|
||||
const sourceIndex = e.sourceIndex;
|
||||
const targetIndex = e.targetIndex!;
|
||||
const splicedElem = newValue.splice(sourceIndex, 1)[0];
|
||||
newValue.splice(targetIndex, 0, splicedElem);
|
||||
} else if (e.targetIndex !== undefined) {
|
||||
const itemValueData = e.item?.value.data.toString() ?? '';
|
||||
// Delete value
|
||||
if (!e.item?.value.data && e.originalItem.value.data && e.targetIndex > -1) {
|
||||
newValue.splice(e.targetIndex, 1);
|
||||
|
@ -1072,19 +1082,20 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
|
|||
// Update value
|
||||
else if (e.item?.value.data && e.originalItem.value.data) {
|
||||
if (e.targetIndex > -1) {
|
||||
newValue[e.targetIndex] = e.item.value.data.toString();
|
||||
newValue[e.targetIndex] = itemValueData;
|
||||
}
|
||||
// For some reason, we are updating and cannot find original value
|
||||
// Just append the value in this case
|
||||
else {
|
||||
newValue.push(e.item.value.data.toString());
|
||||
newValue.push(itemValueData);
|
||||
}
|
||||
}
|
||||
// Add value
|
||||
else if (e.item?.value.data && !e.originalItem.value.data && e.targetIndex >= newValue.length) {
|
||||
newValue.push(e.item.value.data.toString());
|
||||
newValue.push(itemValueData);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
template.context.defaultValue &&
|
||||
isArray(template.context.defaultValue) &&
|
||||
|
@ -1093,7 +1104,6 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
|
|||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { isDefined, isUndefinedOrNull } from 'vs/base/common/types';
|
|||
import 'vs/css!./media/settingsWidgets';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { editorWidgetBorder, focusBorder, foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusBackground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorWidgetBorder, focusBorder, foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listDropBackground, listFocusBackground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons';
|
||||
|
@ -135,6 +135,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
|
|||
collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row:hover { color: ${listHoverForegroundColor}; }`);
|
||||
}
|
||||
|
||||
const listDropBackgroundColor = theme.getColor(listDropBackground);
|
||||
if (listDropBackgroundColor) {
|
||||
collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.drag-hover { background-color: ${listDropBackgroundColor}; }`);
|
||||
}
|
||||
|
||||
const listSelectBackgroundColor = theme.getColor(listActiveSelectionBackground);
|
||||
if (listSelectBackgroundColor) {
|
||||
collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected:focus { background-color: ${listSelectBackgroundColor}; }`);
|
||||
|
@ -244,6 +249,7 @@ export interface ISettingListChangeEvent<TDataItem extends object> {
|
|||
originalItem: TDataItem;
|
||||
item?: TDataItem;
|
||||
targetIndex?: number;
|
||||
sourceIndex?: number;
|
||||
}
|
||||
|
||||
export abstract class AbstractListSettingWidget<TDataItem extends object> extends Disposable {
|
||||
|
@ -264,6 +270,10 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
|
|||
return this.model.items;
|
||||
}
|
||||
|
||||
get inReadMode(): boolean {
|
||||
return this.model.items.every(item => !item.editing);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
@IThemeService protected readonly themeService: IThemeService,
|
||||
|
@ -303,7 +313,7 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
|
|||
protected abstract getEmptyItem(): TDataItem;
|
||||
protected abstract getContainerClasses(): string[];
|
||||
protected abstract getActionsForItem(item: TDataItem, idx: number): IAction[];
|
||||
protected abstract renderItem(item: TDataItem): HTMLElement;
|
||||
protected abstract renderItem(item: TDataItem, idx: number): HTMLElement;
|
||||
protected abstract renderEdit(item: TDataItem, idx: number): HTMLElement;
|
||||
protected abstract isItemNew(item: TDataItem): boolean;
|
||||
protected abstract getLocalizedRowTitle(item: TDataItem): string;
|
||||
|
@ -362,7 +372,6 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
|
|||
return selectBox;
|
||||
}
|
||||
|
||||
|
||||
protected editSetting(idx: number): void {
|
||||
this.model.setEditKey(idx);
|
||||
this.renderList();
|
||||
|
@ -396,7 +405,7 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
|
|||
}
|
||||
|
||||
private renderDataItem(item: IListViewItem<TDataItem>, idx: number, listFocused: boolean): HTMLElement {
|
||||
const rowElement = this.renderItem(item);
|
||||
const rowElement = this.renderItem(item, idx);
|
||||
|
||||
rowElement.setAttribute('data-index', idx + '');
|
||||
rowElement.setAttribute('tabindex', item.selected ? '0' : '-1');
|
||||
|
@ -517,6 +526,12 @@ export interface IListDataItem {
|
|||
sibling?: string
|
||||
}
|
||||
|
||||
interface ListSettingWidgetDragDetails {
|
||||
element: HTMLElement;
|
||||
item: IListDataItem;
|
||||
itemIndex: number;
|
||||
}
|
||||
|
||||
export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem> {
|
||||
private keyValueSuggester: IObjectKeySuggester | undefined;
|
||||
private showAddButton: boolean = true;
|
||||
|
@ -563,7 +578,15 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
|
|||
] as IAction[];
|
||||
}
|
||||
|
||||
protected renderItem(item: IListDataItem): HTMLElement {
|
||||
private dragDetails: ListSettingWidgetDragDetails | undefined;
|
||||
|
||||
private getDragImage(item: IListDataItem): HTMLElement {
|
||||
const dragImage = $('.monaco-drag-image');
|
||||
dragImage.textContent = item.value.data;
|
||||
return dragImage;
|
||||
}
|
||||
|
||||
protected renderItem(item: IListDataItem, idx: number): HTMLElement {
|
||||
const rowElement = $('.setting-list-row');
|
||||
const valueElement = DOM.append(rowElement, $('.setting-list-value'));
|
||||
const siblingElement = DOM.append(rowElement, $('.setting-list-sibling'));
|
||||
|
@ -571,17 +594,91 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
|
|||
valueElement.textContent = item.value.data.toString();
|
||||
siblingElement.textContent = item.sibling ? `when: ${item.sibling}` : null;
|
||||
|
||||
this.addDragAndDrop(rowElement, item, idx);
|
||||
return rowElement;
|
||||
}
|
||||
|
||||
protected addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) {
|
||||
if (this.inReadMode) {
|
||||
rowElement.draggable = true;
|
||||
rowElement.classList.add('draggable');
|
||||
} else {
|
||||
rowElement.draggable = false;
|
||||
rowElement.classList.remove('draggable');
|
||||
}
|
||||
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_START, (ev) => {
|
||||
this.dragDetails = {
|
||||
element: rowElement,
|
||||
item,
|
||||
itemIndex: idx
|
||||
};
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
const dragImage = this.getDragImage(item);
|
||||
document.body.appendChild(dragImage);
|
||||
ev.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
}
|
||||
}));
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => {
|
||||
if (!this.dragDetails) {
|
||||
return false;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
let counter = 0;
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_ENTER, (ev) => {
|
||||
counter++;
|
||||
rowElement.classList.add('drag-hover');
|
||||
}));
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_LEAVE, (ev) => {
|
||||
counter--;
|
||||
if (!counter) {
|
||||
rowElement.classList.remove('drag-hover');
|
||||
}
|
||||
}));
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DROP, (ev) => {
|
||||
// cancel the op if we dragged to a completely different setting
|
||||
if (!this.dragDetails) {
|
||||
return false;
|
||||
}
|
||||
ev.preventDefault();
|
||||
counter = 0;
|
||||
if (this.dragDetails.element !== rowElement) {
|
||||
this._onDidChangeList.fire({
|
||||
originalItem: this.dragDetails.item,
|
||||
sourceIndex: this.dragDetails.itemIndex,
|
||||
item,
|
||||
targetIndex: idx
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_END, (ev) => {
|
||||
counter = 0;
|
||||
rowElement.classList.remove('drag-hover');
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.clearData();
|
||||
}
|
||||
if (this.dragDetails) {
|
||||
this.dragDetails = undefined;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected renderEdit(item: IListDataItem, idx: number): HTMLElement {
|
||||
const rowElement = $('.setting-list-edit-row');
|
||||
let valueInput: InputBox | SelectBox;
|
||||
let currentDisplayValue: string;
|
||||
let currentEnumOptions: IObjectEnumOption[] | undefined;
|
||||
|
||||
if (this.isItemNew(item) && this.keyValueSuggester) {
|
||||
const enumData = this.keyValueSuggester(this.model.items.map(({ value: { data } }) => data));
|
||||
if (this.keyValueSuggester) {
|
||||
const enumData = this.keyValueSuggester(this.model.items.map(({ value: { data } }) => data), idx);
|
||||
item = {
|
||||
...item,
|
||||
value: {
|
||||
|
@ -756,6 +853,10 @@ export class ExcludeSettingWidget extends ListSettingWidget {
|
|||
return ['setting-list-exclude-widget'];
|
||||
}
|
||||
|
||||
protected override addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected override getLocalizedRowTitle({ value, sibling }: IListDataItem): string {
|
||||
return isUndefinedOrNull(sibling)
|
||||
? localize('excludePatternHintLabel', "Exclude files matching `{0}`", value.data)
|
||||
|
@ -810,7 +911,7 @@ export interface IObjectValueSuggester {
|
|||
}
|
||||
|
||||
export interface IObjectKeySuggester {
|
||||
(existingKeys: string[]): IObjectEnumData | undefined;
|
||||
(existingKeys: string[], idx?: number): IObjectEnumData | undefined;
|
||||
}
|
||||
|
||||
interface IObjectSetValueOptions {
|
||||
|
@ -912,7 +1013,7 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget<IObje
|
|||
return header;
|
||||
}
|
||||
|
||||
protected renderItem(item: IObjectDataItem): HTMLElement {
|
||||
protected renderItem(item: IObjectDataItem, idx: number): HTMLElement {
|
||||
const rowElement = $('.setting-list-row');
|
||||
rowElement.classList.add('setting-list-object-row');
|
||||
|
||||
|
@ -1229,7 +1330,7 @@ export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget<IObje
|
|||
return rowElement;
|
||||
}
|
||||
|
||||
protected renderItem(item: IObjectDataItem): HTMLElement {
|
||||
protected renderItem(item: IObjectDataItem, idx: number): HTMLElement {
|
||||
// return empty object, since we always render in edit mode
|
||||
const rowElement = $('.setting-list-row');
|
||||
return rowElement;
|
||||
|
|
Loading…
Reference in a new issue