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:
Raymond Zhao 2021-07-21 16:29:45 -07:00 committed by GitHub
parent d39f94357d
commit ecc6785b1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 19 deletions

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;