From cbad1567acb43d9f2462e75aef3f0aaaf582dceb Mon Sep 17 00:00:00 2001 From: Florrie Date: Wed, 19 Dec 2018 21:53:28 -0400 Subject: Make a neater neato nice context menu! Not a lot of new potential utility here for now, but it's at the least easier to use and cleaner than the old look! Also.. shuffle-groups soon(TM). Maybe! --- ui.js | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 186 insertions(+), 16 deletions(-) diff --git a/ui.js b/ui.js index c4c0bae..175bf73 100644 --- a/ui.js +++ b/ui.js @@ -109,8 +109,8 @@ class AppElement extends FocusElement { grouplikeListing.on('browse', item => grouplikeListing.loadGrouplike(item)) grouplikeListing.on('menu', (item, opts) => this.menu.show(opts)) - grouplikeListing.on('queue', (item, {where = 'end', shuffle = false, play = false} = {}) => { - if (isGroup(item) && shuffle) { + grouplikeListing.on('queue', (item, {where = 'end', order = 'normal', play = false} = {}) => { + if (isGroup(item) && order === 'shuffle') { item = {items: shuffleArray(flattenGrouplike(item).items)} } @@ -120,7 +120,7 @@ class AppElement extends FocusElement { } this.queueGrouplikeItem(item, afterItem, { - movePlayingTrack: !shuffle + movePlayingTrack: order === 'normal' }) if (play) { @@ -1058,11 +1058,128 @@ class BasicGrouplikeItemElement extends Button { } } +class InlineListPickerElement extends FocusElement { + // And you thought my class names couldn't get any worse... + // This is an element that looks something like the following: + // Fruit? [Apple] + // (Imagine that "[Apple]" just looks like "Apple" written in blue text.) + // If you press the element (like a button), it'll pick the next item in its + // list of options, like "Banana" or "Canteloupe" in this example. The arrow + // keys also work to move through the list. You typically don't want to put + // too many items in the list, since there's no visual way of telling what's + // next or previous. (That's the point, it's inline.) This element is mainly + // useful in forms or ContextMenus. + + constructor(labelText, options) { + super() + this.labelText = labelText + this.options = options + this.curIndex = 0 + } + + fixLayout() { + // We want to fill the parent's width, but also fit ourselves, so we need + // to determine the ideal width which would fit us but not leave extra + // space. + const longestOptionLength = this.options.reduce( + (soFar, { label }) => Math.max(soFar, ansi.measureColumns(label)), 0) + const idealWidth = ( + ansi.measureColumns(this.labelText) + longestOptionLength + 4) + + // Then we use whichever is greater - our ideal width or the width of the + // parent - as our own width. The parent should respect our needs by + // growing if necessary. :) (ContextMenu does this, which is where you'd + // typically embed this element.) + // I shall fill you, parent, even beyond your own bounds!!! + this.w = Math.max(this.parent.contentW, idealWidth) + + // Height is always just 1. + this.h = 1 + } + + drawTo(writable) { + if (this.isSelected) { + writable.write(ansi.invert()) + } + + const curOption = this.options[this.curIndex].label.toString() + let drawX = 0 + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + + writable.write(this.labelText + ' ') + drawX += ansi.measureColumns(this.labelText) + 1 + + writable.write(ansi.setAttributes([ansi.A_BRIGHT, ansi.C_BLUE])) + writable.write(' ' + curOption + ' ') + drawX += ansi.measureColumns(curOption) + 2 + + writable.write(ansi.setForeground(ansi.C_RESET)) + writable.write(' '.repeat(Math.max(0, this.w - drawX))) + + writable.write(ansi.resetAttributes()) + } + + keyPressed(keyBuf) { + if (telc.isSelect(keyBuf) || telc.isRight(keyBuf)) { + this.nextOption() + } else if (telc.isLeft(keyBuf)) { + this.previousOption() + } else { + return true + } + } + + clicked(button) { + if (button === 'left') { + if (this.isSelected) { + this.nextOption() + } else { + this.root.select(this) + } + } else if (button === 'scroll-up') { + this.previousOption() + } else if (button === 'scroll-down') { + this.nextOption() + } else { + return true + } + return false + } + + nextOption() { + this.curIndex++ + if (this.curIndex === this.options.length) { + this.curIndex = 0 + } + } + + previousOption() { + this.curIndex-- + if (this.curIndex < 0) { + this.curIndex = this.options.length - 1 + } + } + + get curValue() { + return this.options[this.curIndex].value + } +} + class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { constructor(item, recordStore) { super(item.name) this.item = item this.recordStore = recordStore + + this.whereControl = new InlineListPickerElement('Where?', [ + {value: 'next', label: 'After current song'}, + {value: 'end', label: 'At end of queue'} + ]) + + this.orderControl = new InlineListPickerElement('Order?', [ + {value: 'shuffle', label: 'Shuffle'}, + {value: 'normal', label: 'In order'} + ]) } drawTo(writable) { @@ -1124,20 +1241,40 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { ')'}, // The actual controls! + {divider: true}, editMode && {label: this.isMarked ? 'Unmark' : 'Mark', action: () => this.emit('mark')}, anyMarked && {label: 'Paste (above)', action: () => this.emit('paste', {where: 'above'})}, anyMarked && {label: 'Paste (below)', action: () => this.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) - {label: 'Play', action: () => this.emit('queue', {where: 'next', play: true})}, - {label: 'Play next', action: () => this.emit('queue', {where: 'next'})}, - {label: 'Play at end', action: () => this.emit('queue', {where: 'end'})}, - this.isGroup && {label: 'Play next, shuffled', action: () => this.emit('queue', {where: 'next', shuffle: true})}, - this.isGroup && {label: 'Play at end, shuffled', action: () => this.emit('queue', {where: 'end', shuffle: true})}, + editMode && {divider: true}, + + {element: this.whereControl}, + {element: this.orderControl}, + {label: 'Play!', action: () => this.playWithControls()}, + {label: 'Queue!', action: () => this.queueWithControls()}, + {divider: true}, + {label: 'Remove from queue', action: () => this.emit('unqueue')} ] }) } + playWithControls() { + this.emitControls(true) + } + + queueWithControls() { + this.emitControls(false) + } + + emitControls(play) { + this.emit('queue', { + where: this.whereControl.curValue, + order: this.orderControl.curValue, + play: play + }) + } + writeStatus(writable) { if (this.isGroup) { // The ANSI attributes here will apply to the rest of the line, too. @@ -1737,15 +1874,22 @@ class ContextMenu extends FocusElement { this.y = y this.visible = true - for (const { label, action } of items.filter(Boolean)) { - const button = new Button(label) - if (action) { - button.on('pressed', () => { - this.close() - action() - }) + for (const item of items.filter(Boolean)) { + if (item.element) { + this.form.addInput(item.element) + } else if (item.divider) { + const element = new HorizontalRule() + this.form.addInput(element) + } else { + const button = new Button(item.label) + if (item.action) { + button.on('pressed', () => { + this.close() + item.action() + }) + } + this.form.addInput(button) } - this.form.addInput(button) } this.fixLayout() @@ -1819,4 +1963,30 @@ class ContextMenu extends FocusElement { } } +class HorizontalRule extends FocusElement { + // It's just a horizontal rule. Y'know.. + // -------------------------------------------------------------------------- + // You get the idea. :) + + get selectable() { + // Just return false. A HorizontalRule is technically a FocusElement, + // but that's just so that it can be used in place of other inputs + // (e.g. in a ContextMenu). + return false + } + + fixLayout() { + this.w = this.parent.contentW + this.h = 1 + } + + drawTo(writable) { + // For the character we draw with, we use an ordinary dash instead of + // an actual box-drawing horizontal line. That's so that the rule is + // distinguishable from the edge of a Pane. + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write('-'.repeat(this.w)) + } +} + module.exports.AppElement = AppElement -- cgit 1.3.0-6-gf8a5