650 lines
21 KiB
JavaScript
650 lines
21 KiB
JavaScript
|
/*
|
||
|
* SELECTIONMENU.JS (MODULE)
|
||
|
*
|
||
|
* Version: 1.3.1
|
||
|
* Author: VideoPlayerCode
|
||
|
* URL: https://github.com/VideoPlayerCode/mpv-tools
|
||
|
* License: Apache License, Version 2.0
|
||
|
*/
|
||
|
|
||
|
/* jshint -W097 */
|
||
|
/* global mp, module, require, setInterval, clearInterval, setTimeout, clearTimeout */
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
/* This is a modified version of the SelectionMenu module from VideoPlayerCode
|
||
|
that has been changed to better work with the mpvDLNA plugin.
|
||
|
|
||
|
Specifically it has additional support for:
|
||
|
Coloring empty folders red
|
||
|
Displaying an indicator on currently playing entries
|
||
|
Displaying a text input system
|
||
|
*/
|
||
|
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var Ass = require('AssFormat'),
|
||
|
Utils = require('MicroUtils');
|
||
|
|
||
|
var SelectionMenu = function(settings)
|
||
|
{
|
||
|
settings = settings || {};
|
||
|
|
||
|
this.uniqueId = 'M'+String(mp.get_time_ms()).replace(/\./g, '').substring(3)+
|
||
|
Math.floor((100+(Math.random()*899)));
|
||
|
this.metadata = null;
|
||
|
this.title = 'No title';
|
||
|
this.options = [];
|
||
|
this.selectionIdx = 0;
|
||
|
this.cbMenuShow = typeof settings.cbMenuShow === 'function' ? settings.cbMenuShow : null;
|
||
|
this.cbMenuHide = typeof settings.cbMenuHide === 'function' ? settings.cbMenuHide : null;
|
||
|
this.cbMenuLeft = typeof settings.cbMenuLeft === 'function' ? settings.cbMenuLeft : null;
|
||
|
this.cbMenuRight = typeof settings.cbMenuRight === 'function' ? settings.cbMenuRight : null;
|
||
|
this.cbMenuOpen = typeof settings.cbMenuOpen === 'function' ? settings.cbMenuOpen : null;
|
||
|
this.cbMenuUndo = typeof settings.cbMenuUndo === 'function' ? settings.cbMenuUndo : null;
|
||
|
this.maxLines = typeof settings.maxLines === 'number' &&
|
||
|
settings.maxLines >= 3 ? Math.floor(settings.maxLines) : 10;
|
||
|
this.menuFontAlpha = Ass.convertPercentToHex( // Throws if invalid input.
|
||
|
(typeof settings.menuFontAlpha === 'number' &&
|
||
|
settings.menuFontAlpha >= 0 && settings.menuFontAlpha <= 1 ?
|
||
|
settings.menuFontAlpha : 1),
|
||
|
true // Invert input range so "1.0" is visible and "0.0" is invisible.
|
||
|
);
|
||
|
this.menuFontSize = typeof settings.menuFontSize === 'number' &&
|
||
|
settings.menuFontSize >= 1 ? Math.floor(settings.menuFontSize) : 40;
|
||
|
this.originalFontSize = null;
|
||
|
this.hasRegisteredKeys = false; // Also means that menu is active/open.
|
||
|
this.useTextColors = true;
|
||
|
this.currentMenuText = '';
|
||
|
this.typingText = '';
|
||
|
this.active = false; // Menu is visible
|
||
|
this.menuActive = false; // the menu portion of the UI is active
|
||
|
this.typingActive = false; // the typing portion of the UI is active
|
||
|
this.isShowingMessage = false;
|
||
|
this.currentMessageText = '';
|
||
|
this.menuInterval = null;
|
||
|
this.stopMessageTimeout = null;
|
||
|
this.autoCloseDelay = typeof settings.autoCloseDelay === 'number' &&
|
||
|
settings.autoCloseDelay >= 0 ? settings.autoCloseDelay : 5; // 0 = Off.
|
||
|
this.autoCloseActiveAt = 0;
|
||
|
this.keyBindings = { // Default keybindings.
|
||
|
'Menu-Up':{repeatable:true, keys:['up']},
|
||
|
'Menu-Down':{repeatable:true, keys:['down']},
|
||
|
'Menu-Up-Fast':{repeatable:true, keys:['shift+up']},
|
||
|
'Menu-Down-Fast':{repeatable:true, keys:['shift+down']},
|
||
|
'Menu-Left':{repeatable:true, keys:['left']},
|
||
|
'Menu-Right':{repeatable:false, keys:['right']},
|
||
|
'Menu-Open':{repeatable:false, keys:['enter']},
|
||
|
'Menu-Undo':{repeatable:false, keys:['bs']},
|
||
|
'Menu-Help':{repeatable:false, keys:['h']},
|
||
|
'Menu-Close':{repeatable:false, keys:['esc']}
|
||
|
};
|
||
|
|
||
|
// Apply custom rebinding overrides if provided.
|
||
|
// Format: `{'Menu-Open':['a','shift+b']}`
|
||
|
// Note that all "shift variants" MUST be specified as "shift+<key>".
|
||
|
var i, action, key, allKeys, erasedDefaults,
|
||
|
rebinds = settings.keyRebindings;
|
||
|
if (rebinds) {
|
||
|
for (action in rebinds) {
|
||
|
if (!rebinds.hasOwnProperty(action))
|
||
|
continue;
|
||
|
if (!this.keyBindings.hasOwnProperty(action))
|
||
|
throw 'Invalid menu action "'+action+'" in rebindings';
|
||
|
erasedDefaults = false;
|
||
|
allKeys = rebinds[action];
|
||
|
for (i = 0; i < allKeys.length; ++i) {
|
||
|
key = allKeys[i];
|
||
|
if (typeof key !== 'string')
|
||
|
throw 'Invalid non-string key ('+JSON.stringify(key)+') in custom rebindings';
|
||
|
key = key.toLowerCase(); // Unify case of all keys for de-dupe.
|
||
|
key = Utils.trim(key); // Trim whitespace.
|
||
|
if (!key.length)
|
||
|
continue;
|
||
|
if (!erasedDefaults) { // Erase default keys for this action.
|
||
|
erasedDefaults = true;
|
||
|
this.keyBindings[action].keys = [];
|
||
|
}
|
||
|
this.keyBindings[action].keys.push(key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Verify that no duplicate bindings exist for the same key.
|
||
|
var boundKeys = {};
|
||
|
for (action in this.keyBindings) {
|
||
|
if (!this.keyBindings.hasOwnProperty(action))
|
||
|
continue;
|
||
|
allKeys = this.keyBindings[action].keys;
|
||
|
for (i = 0; i < allKeys.length; ++i) {
|
||
|
key = allKeys[i];
|
||
|
if (boundKeys.hasOwnProperty(key))
|
||
|
throw 'Invalid duplicate menu bindings for key "'+key+'" (detected in action "'+action+'")';
|
||
|
boundKeys[key] = true;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setMetadata = function(metadata)
|
||
|
{
|
||
|
this.metadata = metadata;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.getMetadata = function()
|
||
|
{
|
||
|
return this.metadata;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setTitle = function(newTitle)
|
||
|
{
|
||
|
if (typeof newTitle !== 'string')
|
||
|
throw 'setTitle: No title value provided';
|
||
|
this.title = newTitle;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setOptions = function(newOptions, initialSelectionIdx)
|
||
|
{
|
||
|
if (typeof newOptions === 'undefined')
|
||
|
throw 'setOptions: No options value provided';
|
||
|
this.options = newOptions;
|
||
|
this.selectionIdx = typeof initialSelectionIdx === 'number' &&
|
||
|
initialSelectionIdx >= 0 && initialSelectionIdx < newOptions.length ?
|
||
|
initialSelectionIdx : 0;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuShow = function(newCbMenuShow)
|
||
|
{
|
||
|
this.cbMenuShow = typeof newCbMenuShow === 'function' ? newCbMenuShow : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuHide = function(newCbMenuHide)
|
||
|
{
|
||
|
this.cbMenuHide = typeof newCbMenuHide === 'function' ? newCbMenuHide : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuLeft = function(newCbMenuLeft)
|
||
|
{
|
||
|
this.cbMenuLeft = typeof newCbMenuLeft === 'function' ? newCbMenuLeft : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuRight = function(newCbMenuRight)
|
||
|
{
|
||
|
this.cbMenuRight = typeof newCbMenuRight === 'function' ? newCbMenuRight : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuOpen = function(newCbMenuOpen)
|
||
|
{
|
||
|
this.cbMenuOpen = typeof newCbMenuOpen === 'function' ? newCbMenuOpen : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setCallbackMenuUndo = function(newCbMenuUndo)
|
||
|
{
|
||
|
this.cbMenuUndo = typeof newCbMenuUndo === 'function' ? newCbMenuUndo : null;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.setUseTextColors = function(value)
|
||
|
{
|
||
|
var hasChanged = this.useTextColors !== value;
|
||
|
this.useTextColors = !!value;
|
||
|
// Update text cache, and redraw menu if visible (otherwise don't show it).
|
||
|
if (hasChanged)
|
||
|
this.renderMenu(null, 1); // 1 = Only redraw if menu is onscreen.
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.isMenuActive = function()
|
||
|
{
|
||
|
return this.active;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.getSelectedItem = function()
|
||
|
{
|
||
|
if (this.selectionIdx < 0 || this.selectionIdx >= this.options.length)
|
||
|
return '';
|
||
|
else
|
||
|
return this.options[this.selectionIdx];
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._processBindings = function(fnCb)
|
||
|
{
|
||
|
if (typeof fnCb !== 'function')
|
||
|
throw 'Missing callback for _processBindings';
|
||
|
|
||
|
var i, key, allKeys, action, identifier,
|
||
|
bindings = this.keyBindings;
|
||
|
for (action in bindings) {
|
||
|
if (!bindings.hasOwnProperty(action))
|
||
|
continue;
|
||
|
|
||
|
allKeys = bindings[action].keys;
|
||
|
for (i = 0; i < allKeys.length; ++i) {
|
||
|
key = allKeys[i];
|
||
|
identifier = this.uniqueId+'_'+action+'_'+key;
|
||
|
fnCb(
|
||
|
identifier, // Unique identifier for this binding.
|
||
|
action, // What action the key is assigned to trigger.
|
||
|
key, // What key.
|
||
|
bindings[action] // Details about this binding.
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._registerMenuKeys = function()
|
||
|
{
|
||
|
if (this.hasRegisteredKeys)
|
||
|
return;
|
||
|
|
||
|
// Necessary in order to preserve "this" in the called function, since mpv's
|
||
|
// callbacks don't receive "this" if the object's func is keybound directly.
|
||
|
var createFn = function(obj, fn) {
|
||
|
return function() {
|
||
|
obj._menuAction(fn);
|
||
|
};
|
||
|
};
|
||
|
|
||
|
var self = this;
|
||
|
this._processBindings(function(identifier, action, key, details) {
|
||
|
mp.add_forced_key_binding(
|
||
|
key, // What key.
|
||
|
identifier, // Unique identifier for the binding.
|
||
|
createFn(self, action), // Generate anonymous func to execute.
|
||
|
{repeatable:details.repeatable} // Extra options.
|
||
|
);
|
||
|
});
|
||
|
|
||
|
this.hasRegisteredKeys = true;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._unregisterMenuKeys = function()
|
||
|
{
|
||
|
if (!this.hasRegisteredKeys)
|
||
|
return;
|
||
|
|
||
|
var self = this;
|
||
|
this._processBindings(function(identifier, action, key, details) {
|
||
|
mp.remove_key_binding(
|
||
|
identifier // Remove binding by its unique identifier.
|
||
|
);
|
||
|
});
|
||
|
|
||
|
|
||
|
this.hasRegisteredKeys = false;
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._menuAction = function(action)
|
||
|
{
|
||
|
if (this.isShowingMessage && action !== 'Menu-Close')
|
||
|
return; // Block everything except "close" while showing a message.
|
||
|
|
||
|
switch (action) {
|
||
|
case 'Menu-Up':
|
||
|
case 'Menu-Down':
|
||
|
case 'Menu-Up-Fast':
|
||
|
case 'Menu-Down-Fast':
|
||
|
var maxIdx = this.options.length - 1;
|
||
|
|
||
|
if (action === 'Menu-Up' || action === 'Menu-Up-Fast')
|
||
|
this.selectionIdx -= (action === 'Menu-Up-Fast' ? 10 : 1);
|
||
|
else
|
||
|
this.selectionIdx += (action === 'Menu-Down-Fast' ? 10 : 1);
|
||
|
|
||
|
// Handle wraparound in single-move mode, or clamp in fast-move mode.
|
||
|
if (this.selectionIdx < 0)
|
||
|
this.selectionIdx = (action === 'Menu-Up-Fast' ? 0 : maxIdx);
|
||
|
else if (this.selectionIdx > maxIdx)
|
||
|
this.selectionIdx = (action === 'Menu-Down-Fast' ? maxIdx : 0);
|
||
|
|
||
|
this.renderMenu();
|
||
|
break;
|
||
|
case 'Menu-Left':
|
||
|
case 'Menu-Right':
|
||
|
case 'Menu-Open':
|
||
|
case 'Menu-Undo':
|
||
|
var cbName = 'cb'+action.replace(/-/g, '');
|
||
|
if (typeof this[cbName] === 'function') {
|
||
|
// We don't know what the callback will do, and it may be slow, so
|
||
|
// we'll disable the menu's auto-close timeout while it runs.
|
||
|
this._disableAutoCloseTimeout(); // Soft-disable.
|
||
|
this[cbName](action);
|
||
|
}
|
||
|
break;
|
||
|
case 'Menu-Help':
|
||
|
// List all keybindings to help the user remember them.
|
||
|
var entry, entryTitle, allKeys,
|
||
|
c = this.useTextColors,
|
||
|
helpLines = 0,
|
||
|
helpString = Ass.startSeq(c)+Ass.alpha(this.menuFontAlpha, c),
|
||
|
bindings = this.keyBindings;
|
||
|
for (entry in bindings) {
|
||
|
if (!bindings.hasOwnProperty(entry))
|
||
|
continue;
|
||
|
allKeys = bindings[entry].keys;
|
||
|
if (!entry.match(/^Menu-/) || !allKeys || !allKeys.length)
|
||
|
continue;
|
||
|
entryTitle = entry.substring(5);
|
||
|
if (!entryTitle.length)
|
||
|
continue;
|
||
|
Utils.quickSort(allKeys, {caseInsensitive: true});
|
||
|
++helpLines;
|
||
|
helpString += Ass.yellow(c)+Ass.esc(entryTitle, c)+': '+
|
||
|
Ass.white(c)+Ass.esc('{'+allKeys.join('}, {')+'}', c)+'\n';
|
||
|
}
|
||
|
helpString += Ass.stopSeq(c);
|
||
|
if (!helpLines)
|
||
|
helpString = 'No help available.';
|
||
|
this.showMessage(helpString, 5000);
|
||
|
break;
|
||
|
case 'Menu-Close':
|
||
|
this.hideMenu();
|
||
|
break;
|
||
|
default:
|
||
|
mp.msg.error('Unknown menu action "'+action+'"');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._updateAutoCloseTimeout(); // Soft-update.
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._disableAutoCloseTimeout = function(forceLock)
|
||
|
{
|
||
|
this.autoCloseActiveAt = forceLock ? -2 : -1; // -2 = hard, -1 = soft.
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._updateAutoCloseTimeout = function(forceUnlock)
|
||
|
{
|
||
|
if (!forceUnlock && this.autoCloseActiveAt === -2)
|
||
|
return; // Do nothing while autoclose is locked in "disabled" mode.
|
||
|
|
||
|
this.autoCloseActiveAt = mp.get_time();
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._handleAutoClose = function()
|
||
|
{
|
||
|
if (this.autoCloseDelay <= 0 || this.autoCloseActiveAt <= -1) // -2 = hard, -1 = soft.
|
||
|
return; // Do nothing while autoclose is disabled (0) or locked (< 0).
|
||
|
|
||
|
var now = mp.get_time();
|
||
|
if (this.autoCloseActiveAt <= (now - this.autoCloseDelay))
|
||
|
this.hideMenu();
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype._renderActiveText = function()
|
||
|
{
|
||
|
// If nothing is supposed to be showing
|
||
|
if (!this.active)
|
||
|
return;
|
||
|
|
||
|
// Determine which text to render (critical messages take precedence).
|
||
|
var msg = this.isShowingMessage ? this.currentMessageText : (this.menuActive ? this.currentMenuText : '');
|
||
|
|
||
|
if (typeof msg !== 'string')
|
||
|
msg = '';
|
||
|
|
||
|
// Append the typing display to the correct location in the message
|
||
|
if (this.typingActive) {
|
||
|
var newlines = (this.menuActive) ? 3 : 2;
|
||
|
newlines += this.maxLines - msg.split("\n").length;
|
||
|
|
||
|
msg += Ass.startSeq(true) + Ass.alpha("FF", true);
|
||
|
// Make sure we get aligned properly
|
||
|
for (var i = 0; i < newlines; i++) {
|
||
|
msg += "-\n";
|
||
|
}
|
||
|
|
||
|
// This spacing is annoyingly fiddly
|
||
|
if (!this.menuActive)
|
||
|
msg+="\n";
|
||
|
|
||
|
msg += Ass.alpha("00", true);
|
||
|
msg += "--------------\n";
|
||
|
|
||
|
msg += Ass.stopSeq(true);
|
||
|
msg += this.typingText;
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
// Tell mpv's OSD to show the text. It will automatically be replaced and
|
||
|
// refreshed every second while the menu remains open, to ensure that
|
||
|
// nothing else is able to overwrite our menu text.
|
||
|
// NOTE: The long display duration is important, because the JS engine lacks
|
||
|
// real threading, so any slow mpv API calls or slow JS functions will delay
|
||
|
// our redraw timer! Without a long display duration, the menu would vanish.
|
||
|
// NOTE: If a timer misses multiple intended ticks, it will only tick ONCE
|
||
|
// when catching up. So there can thankfully never be any large "backlog"!
|
||
|
mp.osd_message(msg, 1000);
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.renderMenu = function(selectionPrefix, renderMode)
|
||
|
{
|
||
|
var c = this.useTextColors,
|
||
|
finalString;
|
||
|
|
||
|
// Title.
|
||
|
finalString = Ass.startSeq(c)+Ass.alpha(this.menuFontAlpha, c)+
|
||
|
Ass.gray(c)+Ass.scale(75, c)+Ass.esc(this.title, c)+':'+
|
||
|
Ass.scale(100, c)+Ass.white(c)+'\n\n';
|
||
|
|
||
|
// Options.
|
||
|
if (this.options.length > 0) {
|
||
|
// Calculate start/end offsets around focal point.
|
||
|
var startIdx = this.selectionIdx - Math.floor(this.maxLines / 2);
|
||
|
if (startIdx < 0)
|
||
|
startIdx = 0;
|
||
|
|
||
|
var endIdx = startIdx + this.maxLines - 1,
|
||
|
maxIdx = this.options.length - 1;
|
||
|
if (endIdx > maxIdx)
|
||
|
endIdx = maxIdx;
|
||
|
|
||
|
// Increase number of leading lines if we've reached end of list.
|
||
|
var lineCount = (endIdx - startIdx) + 1, // "+1" to count start line too.
|
||
|
lineDiff = this.maxLines - lineCount;
|
||
|
startIdx -= lineDiff;
|
||
|
if (startIdx < 0)
|
||
|
startIdx = 0;
|
||
|
|
||
|
// Format and add all output lines.
|
||
|
var opt;
|
||
|
for (var i = startIdx; i <= endIdx; ++i) {
|
||
|
opt = this.options[i];
|
||
|
if (i === this.selectionIdx)
|
||
|
// NOTE: Prefix stays on screen until cursor-move or re-render.z
|
||
|
finalString += Ass.yellow(c)+'> '+(typeof selectionPrefix === 'string' ?
|
||
|
Ass.esc(selectionPrefix, c)+' ' : '');
|
||
|
|
||
|
// If the menu option has no children to move to
|
||
|
// then it should be colored red and ignored
|
||
|
if (opt.children != null && opt.children.length == 0)
|
||
|
finalString += Ass.color("FF0000", c);
|
||
|
|
||
|
finalString += (
|
||
|
i === startIdx && startIdx > 0 ? '...' :
|
||
|
(
|
||
|
i === endIdx && endIdx < maxIdx ? '...' : Ass.esc(
|
||
|
typeof opt === 'object' ? opt.name : opt,
|
||
|
c
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
// Display the now playing indicator
|
||
|
if (opt.isPlaying)
|
||
|
finalString += " <==";
|
||
|
|
||
|
if (i === this.selectionIdx || (opt.children != null && opt.children.length == 0))
|
||
|
finalString += Ass.white(c);
|
||
|
if (i !== endIdx)
|
||
|
finalString += '\n';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// End the Advanced SubStation command sequence.
|
||
|
finalString += Ass.stopSeq(c);
|
||
|
|
||
|
// Update cached menu text. But only open/redraw the menu if it's already
|
||
|
// active OR if we're NOT being prevented from going out of "hidden" state.
|
||
|
this.currentMenuText = finalString;
|
||
|
|
||
|
// Handle render mode:
|
||
|
// 1 = Only redraw if menu is onscreen (doesn't trigger open/redrawing if
|
||
|
// the menu is closed or busy showing a text message); 2 = Don't show/redraw
|
||
|
// at all (good for just updating the text cache silently); any other value
|
||
|
// (incl. undefined, aka default) = show/redraw the menu.
|
||
|
if ((renderMode === 1 && (!this.menuActive || this.isShowingMessage)) || renderMode === 2)
|
||
|
return;
|
||
|
this.showMenu();
|
||
|
};
|
||
|
|
||
|
|
||
|
SelectionMenu.prototype.show = function() {
|
||
|
var justOpened = false;
|
||
|
if (!this.isMenuActive()) {
|
||
|
justOpened = true;
|
||
|
this.originalFontSize = mp.get_property_number('osd-font-size');
|
||
|
mp.set_property('osd-font-size', this.menuFontSize);
|
||
|
|
||
|
|
||
|
// Redraw the currently active text every second and do periodic tasks.
|
||
|
// NOTE: This prevents other OSD scripts from removing our menu text.
|
||
|
var self = this;
|
||
|
if (this.menuInterval !== null)
|
||
|
clearInterval(this.menuInterval);
|
||
|
this.menuInterval = setInterval(function() {
|
||
|
self._renderActiveText();
|
||
|
self._handleAutoClose();
|
||
|
}, 1000);
|
||
|
|
||
|
// Get rid of any lingering "stop message" timeout and message.
|
||
|
this.stopMessage(true);
|
||
|
}
|
||
|
|
||
|
// Display the currently active text instantly.
|
||
|
this._renderActiveText();
|
||
|
|
||
|
if (justOpened) {
|
||
|
// Run "menu show" callback if registered.
|
||
|
if (typeof this.cbMenuShow === 'function') {
|
||
|
this._disableAutoCloseTimeout(); // Soft-disable while CB runs.
|
||
|
this.cbMenuShow('Menu-Show');
|
||
|
}
|
||
|
|
||
|
// Force an update/unlock of the activity timeout when menu opens.
|
||
|
this._updateAutoCloseTimeout(true); // Hard-update.
|
||
|
}
|
||
|
|
||
|
this.active = true;
|
||
|
}
|
||
|
|
||
|
SelectionMenu.prototype.hide = function() {
|
||
|
mp.osd_message('');
|
||
|
if (this.originalFontSize !== null)
|
||
|
mp.set_property('osd-font-size', this.originalFontSize);
|
||
|
if (this.menuInterval !== null) {
|
||
|
clearInterval(this.menuInterval);
|
||
|
this.menuInterval = null;
|
||
|
}
|
||
|
|
||
|
// Get rid of any lingering "stop message" timeout and message.
|
||
|
this.stopMessage(true);
|
||
|
|
||
|
// Run "menu hide" callback if registered.
|
||
|
if (typeof this.cbMenuHide === 'function')
|
||
|
this.cbMenuHide('Menu-Hide');
|
||
|
|
||
|
this.active = false;
|
||
|
}
|
||
|
|
||
|
SelectionMenu.prototype.showMenu = function()
|
||
|
{
|
||
|
if (!this.menuActive) {
|
||
|
this.menuActive = true;
|
||
|
this._registerMenuKeys();
|
||
|
}
|
||
|
|
||
|
if (!this.active)
|
||
|
this.show();
|
||
|
|
||
|
this._renderActiveText();
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.hideMenu = function()
|
||
|
{
|
||
|
if (this.menuActive) {
|
||
|
this.menuActive = false;
|
||
|
this._unregisterMenuKeys();
|
||
|
|
||
|
if (this.active && !this.typingActive)
|
||
|
this.hide();
|
||
|
else
|
||
|
this._renderActiveText();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.showTyping = function()
|
||
|
{
|
||
|
this.typingActive = true;
|
||
|
|
||
|
if (!this.active)
|
||
|
this.show();
|
||
|
|
||
|
this._renderActiveText();
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.hideTyping = function()
|
||
|
{
|
||
|
this.typingActive = false;
|
||
|
|
||
|
if (this.active && !this.menuActive)
|
||
|
this.hide();
|
||
|
else
|
||
|
this._renderActiveText();
|
||
|
|
||
|
if (this.menuActive) {
|
||
|
this._registerMenuKeys();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.showMessage = function(msg, durationMs, clearSelectionPrefix)
|
||
|
{
|
||
|
if (!this.isMenuActive())
|
||
|
return;
|
||
|
|
||
|
if (typeof msg !== 'string')
|
||
|
msg = 'showMessage: Invalid message value.';
|
||
|
if (typeof durationMs !== 'number')
|
||
|
durationMs = 800;
|
||
|
|
||
|
if (clearSelectionPrefix)
|
||
|
this.renderMenu(null, 2); // 2 = Only update text cache (no redraw).
|
||
|
|
||
|
this.isShowingMessage = true;
|
||
|
this.currentMessageText = msg;
|
||
|
this._renderActiveText();
|
||
|
this._disableAutoCloseTimeout(true); // Hard-disable (ignore msg idle time).
|
||
|
|
||
|
var self = this;
|
||
|
if (this.stopMessageTimeout !== null)
|
||
|
clearTimeout(this.stopMessageTimeout);
|
||
|
this.stopMessageTimeout = setTimeout(function() {
|
||
|
self.stopMessage();
|
||
|
}, durationMs);
|
||
|
};
|
||
|
|
||
|
SelectionMenu.prototype.stopMessage = function(preventRender)
|
||
|
{
|
||
|
if (this.stopMessageTimeout !== null) {
|
||
|
clearTimeout(this.stopMessageTimeout);
|
||
|
this.stopMessageTimeout = null;
|
||
|
}
|
||
|
this.isShowingMessage = false;
|
||
|
this.currentMessageText = '';
|
||
|
if (!preventRender)
|
||
|
this._renderActiveText();
|
||
|
this._updateAutoCloseTimeout(true); // Hard-update (last user activity).
|
||
|
};
|
||
|
|
||
|
module.exports = SelectionMenu;
|