« get me outta code hell

multi-page menu support - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2020-09-16 13:29:40 -0300
committer(quasar) nebula <towerofnix@gmail.com>2020-09-16 13:29:40 -0300
commita8b64d31462aeb65c9286140f37f5c71a9965917 (patch)
treee58a35be7608ae98777a5253a79545e1f17eb154
parent82b243df1a54eaf4be83faa2ac6fbe2a33ffe4bf (diff)
multi-page menu support
-rw-r--r--ui.js216
1 files changed, 134 insertions, 82 deletions
diff --git a/ui.js b/ui.js
index 4b45bc9..d948208 100644
--- a/ui.js
+++ b/ui.js
@@ -1004,99 +1004,99 @@ class AppElement extends FocusElement {
   }
 
   showMenuForItemElement(el, listing) {
-    const emitControls = play => () => {
-      this.handleQueueOptions(item, {
-        where: this.whereControl.curValue,
-        order: this.orderControl.curValue,
-        play: play
-      })
-    }
-
-    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
-    const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
 
-    let items;
-    if (listing.grouplike.isTheQueue && isTrack(item)) {
-      items = [
-        item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)},
-        {divider: true},
-        canControlQueue && {label: 'Play later', action: () => this.playLater(item)},
-        canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)},
-        {divider: true},
-        canControlQueue && {label: 'Clear past this track', action: () => this.clearQueuePast(item)},
-        canControlQueue && {label: 'Clear up to this track', action: () => this.clearQueueUpTo(item)},
-        {divider: true},
-        {label: 'Autoscroll', action: () => listing.toggleAutoscroll()},
-        {divider: true},
-        canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)}
-      ]
-    } else {
-      const numTracks = countTotalTracks(item)
-      const { string: durationString } = this.backend.getDuration(item)
-      items = [
-        // A label that just shows some brief information about the item.
-        {label:
-          `(${item.name ? `"${item.name}"` : 'Unnamed'} - ` +
-          (isGroup(item) ? ` ${numTracks} track${numTracks === 1 ? '' : 's'}, ` : '') +
-          durationString +
-          ')',
-          keyboardIdentifier: item.name
-        },
+    const generatePageForItem = item => {
+      const emitControls = play => () => {
+        this.handleQueueOptions(item, {
+          where: this.whereControl.curValue,
+          order: this.orderControl.curValue,
+          play: play
+        })
+      }
 
-        // The actual controls!
-        {divider: true},
+      const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt')
+      if (listing.grouplike.isTheQueue && isTrack(item)) {
+        return [
+          item[parentSymbol] && this.tabberPane.visible && {label: 'Reveal', action: () => this.reveal(item)},
+          {divider: true},
+          canControlQueue && {label: 'Play later', action: () => this.playLater(item)},
+          canControlQueue && {label: 'Play sooner', action: () => this.playSooner(item)},
+          {divider: true},
+          canControlQueue && {label: 'Clear past this track', action: () => this.clearQueuePast(item)},
+          canControlQueue && {label: 'Clear up to this track', action: () => this.clearQueueUpTo(item)},
+          {divider: true},
+          {label: 'Autoscroll', action: () => listing.toggleAutoscroll()},
+          {divider: true},
+          canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)}
+        ]
+      } else {
+        const numTracks = countTotalTracks(item)
+        const { string: durationString } = this.backend.getDuration(item)
+        return [
+          // A label that just shows some brief information about the item.
+          {label:
+            `(${item.name ? `"${item.name}"` : 'Unnamed'} - ` +
+            (isGroup(item) ? ` ${numTracks} track${numTracks === 1 ? '' : 's'}, ` : '') +
+            durationString +
+            ')',
+            keyboardIdentifier: item.name,
+            isPageSwitcher: true
+          },
 
-        // TODO: Don't emit these on the element (and hence receive them from
-        // the listing) - instead, handle their behavior directly. We'll want
-        // 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},
-        */
+          // The actual controls!
+          {divider: true},
 
-        canControlQueue && isPlayable(item) && {element: this.whereControl},
-        canControlQueue && isGroup(item) && {element: this.orderControl},
-        canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)},
-        canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)},
-        {divider: true},
+          // TODO: Don't emit these on the element (and hence receive them from
+          // the listing) - instead, handle their behavior directly. We'll want
+          // 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},
+          */
 
-        canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))},
-        canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))},
-        canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))},
-        isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)},
-        isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)},
-        /*
-        !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)},
-        hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)},
-        */
-        canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
-        {divider: true},
+          canControlQueue && isPlayable(item) && {element: this.whereControl},
+          canControlQueue && isGroup(item) && {element: this.orderControl},
+          canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)},
+          canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)},
+          {divider: true},
 
-        item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()}
-      ]
+          canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => setTimeout(() => this.processMetadata(item, false))},
+          canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => setTimeout(() => this.processMetadata(item, true))},
+          canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => setTimeout(() => this.processMetadata(item, true))},
+          isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)},
+          isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)},
+          /*
+          !hasNotesFile && isPlayable(item) && {label: 'Create notes file', action: () => this.editNotesFile(item, true)},
+          hasNotesFile && isPlayable(item) && {label: 'Edit notes file', action: () => this.editNotesFile(item, true)},
+          */
+          canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)},
+          {divider: true},
+
+          item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()}
+        ]
+      }
     }
 
+    const pages = [
+      this.markGrouplike.items.length && generatePageForItem(this.markGrouplike),
+      el.item && generatePageForItem(el.item)
+    ].filter(Boolean)
+
+    // TODO: Implement this! :P
+    const isMarked = false
+
     this.showContextMenu({
       x: el.absLeft,
       y: el.absTop + 1,
-      items
+      pages
     })
   }
 
@@ -3842,7 +3842,7 @@ class ContextMenu extends FocusElement {
     this.submenu = null
   }
 
-  show({x = 0, y = 0, items: itemsArg, focusKey = null}) {
+  show({x = 0, y = 0, pages = null, items: itemsArg = null, focusKey = null, pageNum = 0}) {
     this.reload = () => {
       const els = [this.root.selectedElement, ...this.root.selectedElement.directAncestors]
       const focusKey = Object.keys(keyElementMap).find(key => els.includes(keyElementMap[key]))
@@ -3850,6 +3850,39 @@ class ContextMenu extends FocusElement {
       this.show({x, y, items: itemsArg, focusKey})
     }
 
+    this.nextPage = () => {
+      if (pages.length > 1) {
+        pageNum++
+        if (pageNum === pages.length) {
+          pageNum = 0
+        }
+        this.close(false)
+        this.show({x, y, pages, pageNum})
+      }
+    }
+
+    this.previousPage = () => {
+      if (pages.length > 1) {
+        pageNum--
+        if (pageNum === -1) {
+          pageNum = pages.length - 1
+        }
+        this.close(false)
+        this.show({x, y, pages, pageNum})
+      }
+    }
+
+    if (!pages && !itemsArg || pages && itemsArg) {
+      return
+    }
+
+    if (pages) {
+      if (pages.length === 0) {
+        return
+      }
+      itemsArg = pages[pageNum]
+    }
+
     let items = (typeof itemsArg === 'function') ? itemsArg() : itemsArg
 
     items = items.filter(Boolean)
@@ -3899,8 +3932,12 @@ class ContextMenu extends FocusElement {
         wantDivider = true
       } else {
         addDividerIfWanted()
-        const button = new Button(item.label)
-        button.keyboardIdentifier = item.keyboardIdentifier || item.label
+        let label = item.label
+        if (item.isPageSwitcher && pages.length > 1) {
+          label = `\x1b[2m(${pageNum + 1}/${pages.length}) « \x1b[22m${label}\x1b[2m »\x1b[22m`
+        }
+        const button = new Button(label)
+        button.keyboardIdentifier = item.keyboardIdentifier || label
         if (item.action) {
           button.on('pressed', async () => {
             this.restoreSelection()
@@ -3911,6 +3948,12 @@ class ContextMenu extends FocusElement {
             }
           })
         }
+        if (item.isPageSwitcher) {
+          button.on('pressed', async () => {
+            this.nextPage()
+          })
+        }
+        button.item = item
         focusEl = button
         this.form.addInput(button)
         if (item.isDefault) {
@@ -3961,6 +4004,15 @@ class ContextMenu extends FocusElement {
       this.form.scrollToBeginning()
     } else if (input.isScrollToEnd(keyBuf)) {
       this.form.lastInput()
+    } else if (input.isLeft(keyBuf) || input.isRight(keyBuf)) {
+      if (this.form.inputs[this.form.curIndex].item.isPageSwitcher) {
+        if (input.isLeft(keyBuf)) {
+          this.previousPage()
+        } else {
+          this.nextPage()
+        }
+        return false
+      }
     } else {
       return super.keyPressed(keyBuf)
     }