From a8b64d31462aeb65c9286140f37f5c71a9965917 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 16 Sep 2020 13:29:40 -0300 Subject: multi-page menu support --- ui.js | 216 +++++++++++++++++++++++++++++++++++++++++------------------------- 1 file 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) } -- cgit 1.3.0-6-gf8a5