« get me outta code hell

Drag to select multiple items - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-07-18 23:46:59 -0300
committerFlorrie <towerofnix@gmail.com>2019-07-18 23:47:34 -0300
commitcdff9f0c47ca8cb24d7f61de51856c3d05f39adc (patch)
treea94010e1d3ba60423ab453018b55a5eb7e06857f
parent0424c49035d35d9bc8ec9c64e834ff9fefd2d1ad (diff)
Drag to select multiple items
m---------tui-lib0
-rw-r--r--ui.js153
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