diff options
Diffstat (limited to 'ui.js')
-rw-r--r-- | ui.js | 457 |
1 files changed, 319 insertions, 138 deletions
diff --git a/ui.js b/ui.js index 3bac8c6..68cda91 100644 --- a/ui.js +++ b/ui.js @@ -65,6 +65,7 @@ const TuiTextEditor = require('tui-text-editor') const { promisify } = require('util') const { spawn } = require('child_process') +const { orderBy } = require('natural-orderby') const fs = require('fs') const open = require('open') const path = require('path') @@ -135,6 +136,7 @@ const keyBindings = [ ['isTogglePause', '5'], ['isBackspace', '.'], ['isMenu', '+'], + ['isMenu', '0'], ['isSkipBack', '1'], ['isSkipAhead', '3'], // Disabled because this is the jump key! Oops. @@ -210,6 +212,7 @@ class AppElement extends FocusElement { // TODO: Move edit mode stuff to the backend! this.undoManager = new UndoManager() this.markGrouplike = {name: 'Selected Items', items: []} + this.cachedMarkStatuses = new Map() this.editMode = false // We add this is a child later (so that it's on top of every element). @@ -324,11 +327,12 @@ class AppElement extends FocusElement { this.addChild(this.menuLayer) this.whereControl = new InlineListPickerElement('Where?', [ - {value: 'next-selected', label: 'After selected song'}, - {value: 'next', label: 'After current song'}, + {value: 'after-selected', label: 'After selected track'}, + {value: 'next', label: 'After current track'}, {value: 'end', label: 'At end of queue'}, {value: 'distribute-evenly', label: 'Distributed across queue evenly'}, - {value: 'distribute-randomly', label: 'Distributed across queue randomly'} + {value: 'distribute-randomly', label: 'Distributed across queue randomly'}, + {value: 'before-selected', label: 'Before selected track'} ], this.showContextMenu) this.orderControl = new InlineListPickerElement('Order?', [ @@ -336,6 +340,7 @@ class AppElement extends FocusElement { {value: 'shuffle-groups', label: 'Shuffle order of groups'}, {value: 'reverse', label: 'Reverse all'}, {value: 'reverse-groups', label: 'Reverse order of groups'}, + {value: 'alphabetic', label: 'Alphabetically'}, {value: 'normal', label: 'In order'} ], this.showContextMenu) @@ -358,6 +363,7 @@ class AppElement extends FocusElement { {divider: true}, playingTrack && {element: this.playingControl}, {element: this.loopingControl}, + {element: this.loopQueueControl}, {element: this.pauseNextControl}, {element: this.autoDJControl}, {element: this.volumeSlider}, @@ -413,6 +419,12 @@ class AppElement extends FocusElement { getEnabled: () => this.config.canControlPlayback }) + this.loopQueueControl = new ToggleControl('Loop queue?', { + setValue: val => this.SQP.setLoopQueueAtEnd(val), + getValue: () => this.SQP.loopQueueAtEnd, + getEnabled: () => this.config.canControlPlayback + }) + this.volumeSlider = new SliderElement('Volume', { setValue: val => this.SQP.setVolume(val), getValue: () => this.SQP.player.volume, @@ -761,7 +773,7 @@ class AppElement extends FocusElement { // Sets up event listeners that are common to ordinary grouplike listings // (made by newGrouplikeListing) as well as the queue grouplike listing. - grouplikeListing.pathElement.on('select', item => this.reveal(item)) + grouplikeListing.pathElement.on('select', (item, child) => this.reveal(item, child)) grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing)) /* grouplikeListing.on('select', item => this.editNotesFile(item, false)) @@ -782,7 +794,7 @@ class AppElement extends FocusElement { return menu } - reveal(item) { + reveal(item, child) { if (!this.tabberPane.visible) { return } @@ -793,6 +805,9 @@ class AppElement extends FocusElement { const parent = item[parentSymbol] if (isGroup(item)) { tabberListing.loadGrouplike(item) + if (child) { + tabberListing.selectAndShow(child) + } } else if (parent) { if (tabberListing.grouplike !== parent) { tabberListing.loadGrouplike(parent) @@ -866,8 +881,73 @@ class AppElement extends FocusElement { this.queueListingElement.selectAndShow(item) } - deselectAll() { - this.markGrouplike.items.splice(0) + replaceMark(items) { + this.markGrouplike.items = items.slice(0) // Don't share the array! :) + this.emitMarkChanged() + } + + unmarkAll() { + this.markGrouplike.items = [] + this.emitMarkChanged() + } + + markItem(item) { + if (isGroup(item)) { + for (const child of item.items) { + this.markItem(child) + } + } else { + const { items } = this.markGrouplike + if (!items.includes(item)) { + items.push(item) + this.emitMarkChanged() + } + } + } + + unmarkItem(item) { + if (isGroup(item)) { + for (const child of item.items) { + this.unmarkItem(child) + } + } else { + const { items } = this.markGrouplike + if (items.includes(item)) { + items.splice(items.indexOf(item), 1) + this.emitMarkChanged() + } + } + } + + getMarkStatus(item) { + if (!this.cachedMarkStatuses.get(item)) { + const { items } = this.markGrouplike + let status + if (isGroup(item)) { + const tracks = flattenGrouplike(item).items + if (tracks.every(track => items.includes(track))) { + status = 'marked' + } else if (tracks.some(track => items.includes(track))) { + status = 'partial' + } else { + status = 'unmarked' + } + } else { + if (items.includes(item)) { + status = 'marked' + } else { + status = 'unmarked' + } + } + this.cachedMarkStatuses.set(item, status) + } + return this.cachedMarkStatuses.get(item) + } + + emitMarkChanged() { + this.emit('mark changed') + this.cachedMarkStatuses = new Map() + this.scheduleDrawWithoutPropertyChange() } pauseAll() { @@ -1024,99 +1104,104 @@ 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 all', action: () => this.unmarkAll()}] + : [ + this.getMarkStatus(item) !== 'unmarked' && {label: 'Remove from selection', action: () => this.unmarkItem(item)}, + this.getMarkStatus(item) !== 'marked' && {label: 'Add to selection', action: () => this.markItem(item)} + ]) + ] + } } + 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 }) } @@ -1524,26 +1609,45 @@ class AppElement extends FocusElement { } else if (order === 'reverse-groups') { item = reverseOrderOfGroups(item) item.name = `${oldName} (group order reversed)` + } else if (order === 'alphabetic') { + item = { + name: `${oldName} (alphabetic)`, + items: orderBy( + flattenGrouplike(item).items, + t => getNameWithoutTrackNumber(t).replace(/[^a-zA-Z0-9]/g, '') + ) + } } } else { // Make it into a grouplike that just contains itself. item = {name: oldName, items: [item]} } - if (where === 'next' || where === 'next-selected' || where === 'end') { + if (where === 'next' || where === 'after-selected' || where === 'before-selected' || where === 'end') { + const selected = this.queueListingElement.currentItem let afterItem = null if (where === 'next') { afterItem = playingTrack - } else if (where === 'next-selected') { - afterItem = this.queueListingElement.currentItem + } else if (where === 'after-selected') { + afterItem = selected + } else if (where === 'before-selected') { + const { items } = this.SQP.queueGrouplike + const index = items.indexOf(selected) + if (index === 0) { + afterItem = 'FRONT' + } else if (index > 0) { + afterItem = items[index - 1] + } } this.SQP.queue(item, afterItem, { - movePlayingTrack: order === 'normal' + movePlayingTrack: order === 'normal' || order === 'alphabetic' }) if (isTrack(passedItem)) { this.queueListingElement.selectAndShow(passedItem) + } else { + this.queueListingElement.selectAndShow(selected) } } else if (where.startsWith('distribute-')) { this.SQP.distributeQueue(item, { @@ -1823,7 +1927,7 @@ class GrouplikeListingElement extends Form { */ } } else if (keyBuf[0] === 1) { // ctrl-A - this.toggleSelectAll() + this.toggleMarkAll() } else { return super.keyPressed(keyBuf) } @@ -1864,14 +1968,34 @@ class GrouplikeListingElement extends Form { this.form.scrollItems = 0 } - toggleSelectAll() { + toggleMarkAll() { const { items } = this.grouplike - if (items.every(item => this.app.markGrouplike.items.includes(item))) { - this.app.markGrouplike.items = [] + const actions = [] + const tracks = flattenGrouplike(this.grouplike).items + if (items.every(item => this.app.getMarkStatus(item) !== 'unmarked')) { + if (this.app.markGrouplike.items.length > tracks.length) { + actions.push({label: 'Remove from selection', action: () => this.app.unmarkItem(this.grouplike)}) + } + actions.push({label: 'Clear selection', action: () => this.app.unmarkAll()}) } else { - this.app.markGrouplike.items = items.slice(0) // Don't share the array! :) + actions.push({label: 'Add to selection', action: () => this.app.markItem(this.grouplike)}) + if (this.app.markGrouplike.items.some(item => !tracks.includes(item))) { + actions.push({label: 'Replace selection', action: () => { + this.app.unmarkAll() + this.app.markItem(this.grouplike) + }}) + } + } + if (actions.length === 1) { + actions[0].action() + } else { + const el = this.form.inputs[this.form.curIndex] + this.app.showContextMenu({ + x: el.absLeft, + y: el.absTop + 1, + items: actions + }) } - this.scheduleDrawWithoutPropertyChange() } /* @@ -2080,15 +2204,21 @@ class GrouplikeListingElement extends Form { } hideJumpElement(isCancel) { - if (isCancel) { - this.form.curIndex = this.oldFocusedIndex - this.form.scrollSelectedElementIntoView() - } - this.jumpElement.visible = false - if (this.jumpElement.isSelected) { - this.root.select(this) + if (this.jumpElement.visible) { + if (isCancel) { + this.form.curIndex = this.oldFocusedIndex + this.form.scrollSelectedElementIntoView() + } + this.jumpElement.visible = false + if (this.jumpElement.isSelected) { + this.root.select(this) + } + this.fixLayout() } - this.fixLayout() + } + + unselected() { + this.hideJumpElement(true) } get tabberLabel() { @@ -2168,13 +2298,13 @@ class GrouplikeListingForm extends ListScrollForm { return } const { item } = input - if (this.app.markGrouplike.items.includes(item)) { - this.selectMode = 'deselect' - } else { + if (this.app.getMarkStatus(item) === 'unmarked') { if (!ctrl) { - this.app.markGrouplike.items = [] + this.app.unmarkAll() } this.selectMode = 'select' + } else { + this.selectMode = 'deselect' } if (ctrl) { this.dragInputs = [item] @@ -2211,27 +2341,22 @@ class GrouplikeListingForm extends ListScrollForm { } dragEnteredRange(item) { - const { items } = this.app.markGrouplike if (this.selectMode === 'select') { - if (!items.includes(item)) { - items.push(item) - } + this.app.markItem(item) } else if (this.selectMode === 'deselect') { - if (items.includes(item)) { - items.splice(items.indexOf(item), 1) - } + this.app.unmarkItem(item) } } 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) + if (!this.oldMarkedItems.includes(item)) { + this.app.unmarkItem(item) } } else if (this.selectMode === 'deselect') { - if (!items.includes(item) && this.oldMarkedItems.includes(item)) { - items.push(item) + if (this.oldMarkedItems.includes(item)) { + this.app.markItem(item) } } } @@ -2270,11 +2395,13 @@ class GrouplikeListingForm extends ListScrollForm { return } this.keyboardDragDirection = direction - this.oldMarkedItems = this.app.markGrouplike.items.slice() - if (this.app.markGrouplike.items.includes(item)) { - this.selectMode = 'deselect' - } else { + this.oldMarkedItems = (this.inputs + .filter(input => input.item && this.app.getMarkStatus(input.item) !== 'unmarked') + .map(input => input.item)) + if (this.app.getMarkStatus(item) === 'unmarked') { this.selectMode = 'select' + } else { + this.selectMode = 'deselect' } this.dragEnteredRange(item) } @@ -2987,21 +3114,23 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } writeStatus(writable) { + const markStatus = this.app.getMarkStatus(this.item) + if (this.isGroup) { // 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.) - if (this.isMarked) { + if (markStatus === 'marked' || markStatus === 'partial') { writable.write(ansi.setAttributes([ansi.C_BLUE + 10])) } else { writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) } } else if (this.isTrack) { - if (this.isMarked) { + if (markStatus === 'marked') { writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) } } else if (!this.isPlayable) { - if (this.isMarked) { + if (markStatus === 'marked') { writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) } else { writable.write(ansi.setAttributes([ansi.A_DIM])) @@ -3015,8 +3144,10 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { const record = this.app.backend.getRecordFor(this.item) - if (this.isMarked) { - writable.write('>') + if (markStatus === 'marked') { + writable.write('+') + } else if (markStatus === 'partial') { + writable.write('*') } else { writable.write(' ') } @@ -3036,10 +3167,6 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { writable.write(' ') } - get isMarked() { - return this.app.markGrouplike.items.includes(this.item) - } - get isGroup() { return isGroup(this.item) } @@ -3113,10 +3240,12 @@ class PathElement extends ListScrollForm { const itemPath = getItemPath(item) const parentPath = itemPath.slice(0, -1) - for (const pathItem of parentPath) { - const isFirst = pathItem === parentPath[0] + for (let i = 0; i < parentPath.length; i++) { + const pathItem = parentPath[i] + const nextItem = itemPath[i + 1] + const isFirst = (i === 0) const element = new PathItemElement(pathItem, isFirst) - element.on('select', () => this.emit('select', pathItem)) + element.on('select', () => this.emit('select', pathItem, nextItem)) element.fixLayout() this.addInput(element) } @@ -3861,7 +3990,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])) @@ -3869,6 +3998,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) @@ -3918,8 +4080,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() @@ -3930,6 +4096,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) { @@ -3980,6 +4152,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) } |