diff options
author | Florrie <towerofnix@gmail.com> | 2019-07-18 23:46:59 -0300 |
---|---|---|
committer | Florrie <towerofnix@gmail.com> | 2019-07-18 23:47:34 -0300 |
commit | cdff9f0c47ca8cb24d7f61de51856c3d05f39adc (patch) | |
tree | a94010e1d3ba60423ab453018b55a5eb7e06857f | |
parent | 0424c49035d35d9bc8ec9c64e834ff9fefd2d1ad (diff) |
Drag to select multiple items
m--------- | tui-lib | 0 | ||||
-rw-r--r-- | ui.js | 153 |
2 files changed, 138 insertions, 15 deletions
diff --git a/tui-lib b/tui-lib -Subproject 9210cbf5986f4e7b796d39fe36d81aeab1992ae +Subproject 27c7e362d1f6719af0d2c47b815b23d648d699a diff --git a/ui.js b/ui.js index a19c82d..4bd6d2b 100644 --- a/ui.js +++ b/ui.js @@ -157,7 +157,7 @@ class AppElement extends FocusElement { // TODO: Move edit mode stuff to the backend! this.undoManager = new UndoManager() - this.markGrouplike = {name: 'Marked', items: []} + this.markGrouplike = {name: 'Selected Items', items: []} this.editMode = false // We add this is a child later (so that it's on top of every element). @@ -540,6 +540,10 @@ class AppElement extends FocusElement { this.queueListingElement.selectAndShow(item) } + deselectAll() { + this.markGrouplike.items.splice(0) + } + showMenuForItemElement(el, listing) { const emitControls = play => () => { this.handleQueueOptions(item, { @@ -549,7 +553,16 @@ class AppElement extends FocusElement { }) } - const { item, isGroup, isMarked } = el + let item + if (this.markGrouplike.items.length) { + item = this.markGrouplike + } else { + item = el.item + } + + // TODO: Implement this! :P + const isMarked = false + const { editMode } = this const { canControlQueue, canProcessMetadata } = this.config const anyMarked = editMode && this.markGrouplike.items.length > 0 @@ -568,7 +581,7 @@ class AppElement extends FocusElement { // A label that just shows some brief information about the item. {label: `(${item.name ? `"${item.name}"` : 'Unnamed'}` + - (isGroup ? ( + (isGroup(item) ? ( ' -' + ` ${item.items.length} item${item.items.length === 1 ? '' : 's'}` + `, ${countTotalItems(item)} total`) @@ -584,21 +597,26 @@ class AppElement extends FocusElement { // to move the "mark"/"paste" (etc) code into separate functions, // instead of just defining their behavior inside the listing event // handlers. + /* editMode && {label: isMarked ? 'Unmark' : 'Mark', action: () => el.emit('mark')}, anyMarked && {label: 'Paste (above)', action: () => el.emit('paste', {where: 'above'})}, anyMarked && {label: 'Paste (below)', action: () => el.emit('paste', {where: 'below'})}, // anyMarked && !this.isReal && {label: 'Paste', action: () => this.emit('paste')}, // No "above" or "elow" in the label because the "fake" item/row will be replaced (it'll disappear, since there'll be an item in the group) {divider: true}, + */ canControlQueue && {element: this.whereControl}, - canControlQueue && isGroup && {element: this.orderControl}, + canControlQueue && isGroup(item) && {element: this.orderControl}, canControlQueue && {label: 'Play!', action: emitControls(true)}, canControlQueue && {label: 'Queue!', action: emitControls(false)}, {divider: true}, canProcessMetadata && {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)}, canProcessMetadata && {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)}, - canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)} + canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)}, + {divider: true}, + + item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} ] } @@ -1104,7 +1122,7 @@ class GrouplikeListingElement extends Form { } getNewForm() { - return new GrouplikeListingForm() + return new GrouplikeListingForm(this.app) } fixLayout() { @@ -1160,6 +1178,8 @@ class GrouplikeListingElement extends Form { this.form.selectAndShow(this.grouplike.items[this.grouplike.items.length - 1]) } else if (keyBuf[0] === 12 && this.grouplike.isTheQueue) { // ctrl-L this.form.selectAndShow(this.app.backend.playingTrack) + } else if (keyBuf[0] === 1) { // ctrl-A + this.toggleSelectAll() } else { return super.keyPressed(keyBuf) } @@ -1177,6 +1197,16 @@ class GrouplikeListingElement extends Form { this.form.scrollItems = 0 } + toggleSelectAll() { + const { items } = this.grouplike + if (items.every(item => this.app.markGrouplike.items.includes(item))) { + this.app.markGrouplike.items = [] + } else { + this.app.markGrouplike.items = items.slice(0) // Don't share the array! :) + } + } + + buildItems(resetIndex = false) { if (!this.grouplike) { throw new Error('Attempted to call buildItems before a grouplike was loaded') @@ -1347,9 +1377,11 @@ class GrouplikeListingElement extends Form { } class GrouplikeListingForm extends ListScrollForm { - constructor() { + constructor(app) { super('vertical') + this.app = app + this.dragInputs = [] this.captureTab = false } @@ -1378,6 +1410,86 @@ class GrouplikeListingForm extends ListScrollForm { } return false } + + clicked(button, allData) { + const { line, ctrl } = allData + if (button === 'left') { + this.dragStartLine = line - this.absTop + this.scrollItems + this.dragStartIndex = this.inputs.findIndex(inp => inp.absTop === line - 1) + if (this.dragStartIndex >= 0) { + const input = this.inputs[this.dragStartIndex] + if (!(input instanceof InteractiveGrouplikeItemElement)) { + this.dragStartIndex = -1 + return + } + const { item } = input + if (this.app.markGrouplike.items.includes(item)) { + this.selectMode = 'deselect' + } else { + if (!ctrl) { + this.app.markGrouplike.items = [] + } + this.selectMode = 'select' + } + if (ctrl) { + this.dragInputs = [item] + this.dragEnteredRange(item) + } else { + this.dragInputs = [] + } + this.oldMarkedItems = this.app.markGrouplike.items.slice() + } + } else if (button === 'drag-left' && this.dragStartIndex >= 0) { + const offset = (line - this.absTop + this.scrollItems) - this.dragStartLine + const rangeA = this.dragStartIndex + const rangeB = this.dragStartIndex + offset + const inputs = ((rangeA < rangeB) + ? this.inputs.slice(rangeA, rangeB + 1) + : this.inputs.slice(rangeB, rangeA + 1)) + let enteredRange = inputs.filter(inp => !this.dragInputs.includes(inp)) + let leftRange = this.dragInputs.filter(inp => !inputs.includes(inp)) + for (const { item } of enteredRange) { + this.dragEnteredRange(item) + } + for (const { item } of leftRange) { + this.dragLeftRange(item) + } + if (this.inputs[rangeB]) { + this.root.select(this.inputs[rangeB]) + } + this.dragInputs = inputs + } else if (button === 'release') { + this.dragStartIndex = -1 + } else { + return super.clicked(button, allData) + } + } + + dragEnteredRange(item) { + const { items } = this.app.markGrouplike + if (this.selectMode === 'select') { + if (!items.includes(item)) { + items.push(item) + } + } else if (this.selectMode === 'deselect') { + if (items.includes(item)) { + items.splice(items.indexOf(item), 1) + } + } + } + + dragLeftRange(item) { + const { items } = this.app.markGrouplike + if (this.selectMode === 'select') { + if (items.includes(item) && !this.oldMarkedItems.includes(item)) { + items.splice(items.indexOf(item), 1) + } + } else if (this.selectMode === 'deselect') { + if (!items.includes(item) && this.oldMarkedItems.includes(item)) { + items.push(item) + } + } + } } class BasicGrouplikeItemElement extends Button { @@ -1514,7 +1626,6 @@ class BasicGrouplikeItemElement extends Button { clicked(button) { super.clicked(button) - return false } } @@ -1876,18 +1987,22 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } } - clicked(button) { + clicked(button, {ctrl}) { if (button === 'left') { if (this.isSelected) { + if (ctrl) { + return + } if (this.isGroup) { this.emit('browse') + return false } else { this.emit('queue', {where: 'next', play: true}) + return false } } else { this.parent.selectInput(this) } - return false } else if (button === 'right') { this.parent.selectInput(this) this.emit('menu', this) @@ -1900,7 +2015,15 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { // The ANSI attributes here will apply to the rest of the line, too. // (We don't reset the active attributes until after drawing the rest of // the line.) - writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) + if (this.isMarked) { + writable.write(ansi.setAttributes([ansi.C_BLUE + 10])) + } else { + writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) + } + } else { + if (this.isMarked) { + writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) + } } this.drawX += 3 @@ -1911,7 +2034,7 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { const record = this.app.backend.getRecordFor(this.item) if (this.isMarked) { - writable.write('M') + writable.write('>') } else { writable.write(' ') } @@ -1930,7 +2053,7 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } get isMarked() { - return this.app.editMode && this.app.markGrouplike.items.includes(this.item) + return this.app.markGrouplike.items.includes(this.item) } get isGroup() { @@ -2615,7 +2738,7 @@ class ContextMenu extends FocusElement { // to forget, that one time when I was figuring out menus in the queue. // This makes them work.) this.form.children = this.form.children.filter( - child => !this.form.inputs.includes(child)); + child => !this.form.inputs.includes(child)) this.form.inputs = [] } @@ -2634,7 +2757,7 @@ class ContextMenu extends FocusElement { width = Math.max(width, input.w) } - let height = Math.min(10, this.form.inputs.length) + let height = Math.min(14, this.form.inputs.length) width += 2 // Space for the pane border height += 2 // Space for the pane border |