diff options
Diffstat (limited to 'ui.js')
-rw-r--r-- | ui.js | 220 |
1 files changed, 165 insertions, 55 deletions
diff --git a/ui.js b/ui.js index 6cabb32..cb38990 100644 --- a/ui.js +++ b/ui.js @@ -49,6 +49,7 @@ import { parentSymbol, reverseOrderOfGroups, searchForItem, + selectTracks, shuffleOrderOfGroups, } from './playlist-utils.js' @@ -210,6 +211,8 @@ export default class AppElement extends FocusElement { this.timestampDictionary = new WeakMap() + this.itemMenuPage = 'cursor' + // We add this is a child later (so that it's on top of every element). this.menuLayer = new DisplayElement() this.menuLayer.clickThrough = true @@ -344,6 +347,15 @@ export default class AppElement extends FocusElement { {value: 'normal', label: 'In order'} ], this.showContextMenu) + this.selectGrouplikeItemsControl = new InlineListPickerElement('Which?', [ + {value: 'all', label: 'all tracks'}, + {value: '1', label: 'one track, randomly'}, + {value: '4', label: 'four tracks, randomly'}, + {value: '25%', label: '25% of the tracks, randomly'}, + {value: '50%', label: '50% of the tracks, randomly'}, + {value: '75%', label: '75% of the tracks, randomly'}, + ], this.showContextMenu) + this.menubar.buildItems([ {text: 'mtui', menuItems: [ {label: 'mtui (perpetual development)'}, @@ -980,32 +992,95 @@ export default class AppElement extends FocusElement { this.emitMarkChanged() } - markItem(item) { - if (isGroup(item)) { - for (const child of item.items) { - this.markItem(child) + selectTracksForMarking(item, forUnmarking = false, useGroupSelectionControl = false) { + let mode = 'all' + let modeValue = 0 + + if (useGroupSelectionControl) { + const value = this.selectGrouplikeItemsControl.curValue + if (value === 'all') { + // Do nothing, this is the default + } else if (value.endsWith('%')) { + mode = 'random' + modeValue = value + } else if (parseInt(value).toString() === value) { + mode = 'random' + modeValue = value } + } + + const range = { + items: + (flattenGrouplike(item) + .items + .filter(isTrack) + .filter(track => + this.markGrouplike.items.includes(track) === forUnmarking)) + } + + return selectTracks(range, mode, modeValue).items + } + + markItem(item, useGroupSelectionControl = false) { + const { items: mark } = this.markGrouplike + + let add + if (isGroup(item)) { + add = this.selectTracksForMarking(item, false, useGroupSelectionControl) + } else if (mark.includes(item)) { + add = [] } else { - const { items } = this.markGrouplike - if (!items.includes(item)) { - items.push(item) - this.emitMarkChanged() - } + add = [item] } + + if (!add.length) { + return + } + + // If this is the first addition (starting from empty), switch the + // remembered context menu page so that the next context menu will show + // the marked items automatically. + if (!mark.length) { + this.itemMenuPage = 'mark' + } + + for (const track of add) { + mark.push(track) + } + + this.emitMarkChanged() } - unmarkItem(item) { + unmarkItem(item, useGroupSelectionControl) { + const { items: mark } = this.markGrouplike + + let remove if (isGroup(item)) { - for (const child of item.items) { - this.unmarkItem(child) - } + remove = this.selectTracksForMarking(item, true, useGroupSelectionControl) + } else if (mark.includes(item)) { + remove = [item] } else { - const { items } = this.markGrouplike - if (items.includes(item)) { - items.splice(items.indexOf(item), 1) - this.emitMarkChanged() - } + remove = [] } + + if (!remove.length) { + return + } + + for (const track of remove) { + mark.splice(mark.indexOf(track), 1) + } + + // If this is the last removal (going to empty), switch the remembered + // context menu page so that the next context menu will show the usual + // controls for the item under the cursor. This isn't exactly necessary + // since various fallbacks will handle this value pointing to a page that + // doesn't exist anymore, but it's nice for consistency. + if (!mark.length) { + this.itemMenuPage = 'cursor' + } + + this.emitMarkChanged() } getMarkStatus(item) { @@ -1466,6 +1541,9 @@ export default class AppElement extends FocusElement { } } + // TODO: Implement this! :P + // const isMarked = false + // const hasNotesFile = !!getCorrespondingFileForItem(item, '.txt') const timestampsItem = this.hasTimestampsFile(item) && (this.timestampsExpanded(item, listing) ? {label: 'Collapse saved timestamps', action: () => this.collapseTimestamps(item, listing)} @@ -1519,50 +1597,66 @@ export default class AppElement extends FocusElement { ...((this.config.showPartyControls && !rootGroup.isPartySources) ? [ - {label: 'Share with party', action: () => this.shareWithParty(item)}, - {divider: true} - ] + {label: 'Share with party', action: () => this.shareWithParty(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}, - 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)}, - isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)}, - {divider: true}, - - timestampsItem, - ...(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)} - ]) - ]) + 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}, + 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)}, + isTrack(item) && isQueued && {label: 'Reveal in queue', action: () => this.revealInQueue(item)}, + {divider: true}, + + timestampsItem, + ...(item === this.markGrouplike + ? [{label: 'Deselect all', action: () => this.unmarkAll()}] + : [ + isGroup(item) && {element: this.selectGrouplikeItemsControl}, + 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) + const pageInfo = [ + this.markGrouplike.items.length && { + id: 'mark', + page: generatePageForItem(this.markGrouplike), + }, + + el.item && { + id: 'cursor', + page: generatePageForItem(el.item), + } ].filter(Boolean) - // TODO: Implement this! :P - // const isMarked = false + const pages = pageInfo.map(({ page }) => page) - this.showContextMenu({ + let pageNum = pageInfo.findIndex(({ id }) => id === this.itemMenuPage) + if (pageNum === -1) pageNum = pageInfo.findIndex(({ id }) => id === 'cursor') + if (pageNum === -1) pageNum = 0 + + const menu = this.showContextMenu({ x: el.absLeft, y: el.absTop + 1, - pages + pages, + pageNum, + }) + + menu.on('page changed', pageNum => { + this.itemMenuPage = pageInfo[pageNum].id }) } @@ -4906,6 +5000,7 @@ class ContextMenu extends FocusElement { } this.close(false) this.show({x, y, pages, pageNum}) + this.emit('page changed', pageNum) } } @@ -4917,6 +5012,7 @@ class ContextMenu extends FocusElement { } this.close(false) this.show({x, y, pages, pageNum}) + this.emit('page changed', pageNum) } } @@ -4928,7 +5024,7 @@ class ContextMenu extends FocusElement { if (pages.length === 0) { return } - itemsArg = pages[pageNum] + itemsArg = pages[Math.min(pages.length - 1, pageNum)] } let items = (typeof itemsArg === 'function') ? itemsArg() : itemsArg @@ -5133,16 +5229,30 @@ class ContextMenu extends FocusElement { width += 2 // Space for the pane border height += 2 // Space for the pane border - if (this.form.scrollBarShown) width++ + + // Hilarity time: at THIS point, we can't identify if the form has its + // scroll bar visible, because we haven't constrained the height of the + // form. We're going to apply all the dimensions leading to the form twice, + // adapting the second time to be one wider if the form shows its scroll + // bar the first time. + this.w = width this.h = height - this.fitToParent() this.pane.fillParent() this.form.fillParent() this.form.fixLayout() + // We only need to update our own (and the descendants') width this time, + // because height won't have changed from the earlier measurement. + if (this.form.scrollBarShown) width++ + this.w = width + + this.pane.fillParent() + this.form.fillParent() + this.form.fixLayout() + // After everything else, do a second pass to apply the decided width // to every element, so that they expand to all be the same width. // In order to change the width of a button (which is what these elements |