From 4fe55e1e1b9c4b777238b24091f90e45d8230dd9 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 16 Jan 2022 12:12:41 -0600 Subject: [PATCH] Added support for multiple pages --- core/full_menu_view.js | 224 +++++++++++++++++------- core/menu_view.js | 378 +++++++++++++++++++++-------------------- 2 files changed, 351 insertions(+), 251 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 87346b9d..3350d567 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -21,8 +21,15 @@ function FullMenuView(options) { MenuView.call(this, options); + + // Initialize paging + this.pages = []; + this.currentPage = 0; + this.initDefaultWidth(); + + const self = this; // we want page up/page down by default @@ -39,48 +46,111 @@ function FullMenuView(options) { this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); } - // Calculate number of items visible after adjusting height - this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); - // handle case where one can fit at the end - if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { - this.itemsPerRow++; - } - - // Final check to make sure we don't try to display more than we have - if (this.itemsPerRow > this.items.length) { - this.itemsPerRow = this.items.length; - } - + this.positionCacheExpired = true; }; this.autoAdjustHeightIfEnabled(); + + this.getSpacer = function() { return new Array(self.itemHorizSpacing + 1).join(this.fillChar); } + + this.clearPage = function() { + for (var i = 0; i < this.itemsPerRow; i++) { + let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`; + self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); + } + } + this.cachePositions = function() { if (this.positionCacheExpired) { this.autoAdjustHeightIfEnabled(); - var col = self.position.col; - var row = self.position.row; - var spacer = self.getSpacer(); + this.pages = []; // reset + + // Calculate number of items visible per column + this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); + // handle case where one can fit at the end + if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { + this.itemsPerRow++; + } + + // Final check to make sure we don't try to display more than we have + if (this.itemsPerRow > this.items.length) { + this.itemsPerRow = this.items.length; + } + + var col = this.position.col; + var row = this.position.row; + var spacer = this.getSpacer(); var itemInRow = 0; - for (var i = 0; i < self.items.length; ++i) { + this.viewWindow = { + start: this.focusedItemIndex, + end: this.items.length - 1, // this may be adjusted later + }; + + var pageStart = 0; + + for (var i = 0; i < this.items.length; ++i) { itemInRow++; - self.items[i].row = row; - self.items[i].col = col; + this.items[i].row = row; + this.items[i].col = col; row += this.itemSpacing + 1; - // handle going to next column - if (itemInRow == this.itemsPerRow) { + // have to calculate the max length on the last entry + if (i == this.items.length - 1) { + var maxLength = 0; + for (var j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + var itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (var j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + this.items[i - j].fixedLength = maxLength; + } + + + // Check if we have room for this column + if (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width) { + // save previous page + this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + + // fix the last column processed + for (var j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != col) { + break; + } + this.items[i - j].col = this.position.col; + pageStart = i - j; + } + + } + + // Since this is the last page, save the current page as well + this.pages.push({ start: pageStart, end: i }); + + } + // also handle going to next column + else if (itemInRow == this.itemsPerRow) { itemInRow = 0; - row = self.position.row; + // restart row for next column + row = this.position.row; var maxLength = 0; for (var j = 0; j < this.itemsPerRow; j++) { // TODO: handle complex items @@ -92,34 +162,37 @@ function FullMenuView(options) { // set length on each item in the column for (var j = 0; j < this.itemsPerRow; j++) { - self.items[i - j].fixedLength = maxLength; + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column in the current page + if (col + maxLength > this.position.col + this.dimens.width) { + // save previous page + this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + + // restart page start for next page + pageStart = i - this.itemsPerRow + 1; + + // reset + col = this.position.col; + itemInRow = 0; + + // fix the last column processed + for (var j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].col = col; + } + + } // increment the column col += maxLength + spacer.length + 1; } - // also have to calculate the max length on the last column - else if (i == self.items.length - 1) { - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { - if (self.items[i - j].col != self.items[i].col) { - break; - } - var itemLength = this.items[i - j].text.length; - if (itemLength > maxLength) { - maxLength = itemLength; - } - } - - // set length on each item in the column - for (var j = 0; j < this.itemsPerRow; j++) { - if (self.items[i - j].col != self.items[i].col) { - break; - } - self.items[i - j].fixedLength = maxLength; - } + // Set the current page if the current item is focused. + if (this.focusedItemIndex === i) { + this.currentPage = this.pages.length; } } } @@ -128,32 +201,32 @@ function FullMenuView(options) { }; this.drawItem = function(index) { - const item = self.items[index]; + const item = this.items[index]; if (!item) { return; } const cached = this.getRenderCacheItem(index, item.focused); if (cached) { - return self.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); } let text; let sgr; - if (item.focused && self.hasFocusItems()) { - const focusItem = self.focusItems[index]; + if (item.focused && this.hasFocusItems()) { + const focusItem = this.focusItems[index]; text = focusItem ? focusItem.text : item.text; sgr = ''; } else if (this.complexItems) { text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); - sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } else { - text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); + sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } - text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; - self.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -166,11 +239,6 @@ FullMenuView.prototype.redraw = function() { this.cachePositions(); - // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such - if (this.positionCacheExpired) { - this.autoAdjustHeightIfEnabled(); - this.positionCacheExpired = false; - } // erase old items // :TODO: optimize this: only needed if a item is removed or new max width < old. @@ -189,7 +257,7 @@ FullMenuView.prototype.redraw = function() { } if (this.items.length) { - for (let i = 0; i < this.items.length; ++i) { + for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) { this.items[i].focused = this.focusedItemIndex === i; this.drawItem(i); } @@ -203,6 +271,12 @@ FullMenuView.prototype.setHeight = function(height) { this.autoAdjustHeight = false; }; +FullMenuView.prototype.setWidth = function(width) { + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; +}; + FullMenuView.prototype.setPosition = function(pos) { FullMenuView.super_.prototype.setPosition.call(this, pos); @@ -273,11 +347,16 @@ FullMenuView.prototype.removeItem = function(index) { FullMenuView.prototype.focusNext = function() { if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); this.focusedItemIndex = 0; - - } else { + this.currentPage = 0; + } + else { this.focusedItemIndex++; - + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } } this.redraw(); @@ -288,10 +367,14 @@ FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusPrevious = function() { if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; - - - } else { + this.currentPage = this.pages.length - 1; + } + else { + this.clearPage(); this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.currentPage--; + } } this.redraw(); @@ -303,8 +386,17 @@ FullMenuView.prototype.focusPreviousColumn = function() { this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { + this.clearPage(); // add the negative index to the end of the list this.focusedItemIndex = this.items.length + this.focusedItemIndex; + // set to last page + this.currentPage = this.pages.length - 1; + } + else { + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } } this.redraw(); @@ -319,6 +411,12 @@ FullMenuView.prototype.focusNextColumn = function() { if (this.focusedItemIndex > this.items.length - 1) { // add the overflow to the beginning of the list this.focusedItemIndex = this.focusedItemIndex - this.items.length; + this.currentPage = 0; + this.clearPage(); + } + else if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; } this.redraw(); diff --git a/core/menu_view.js b/core/menu_view.js index e9c39900..26b3467b 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,298 +2,300 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if(options.items) { - this.setItems(options.items); - } else { - this.items = []; + if (options.items) { + this.setItems(options.items); + } else { + this.items = []; + } + + this.renderCache = {}; + + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; + + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.justify = options.justify || 'none'; + + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; + + this.getHotKeyItemIndex = function(ch) { + if (ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if (_.isNumber(keyIndex)) { + return keyIndex; + } } + return -1; + }; - this.renderCache = {}; - - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - - this.setHotKeys(options.hotKeys); - - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; - - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; - - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; - - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; - - this.getHotKeyItemIndex = function(ch) { - if(ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if(_.isNumber(keyIndex)) { - return keyIndex; - } - } - return -1; - }; - - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - }; + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - if(Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if (Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if(stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if (stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - } - - this.invalidateRenderCache(); + if (this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; } + + this.invalidateRenderCache(); + } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { - delete this.renderCache[index]; + delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { - this.renderCache = {}; + this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { - if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; + if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } + + const key = true === sort ? 'text' : sort; + if ('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort((a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if (!a1) { + return -1; } - - const key = true === sort ? 'text' : sort; - if('text' !== sort && !this.complexItems) { - return; // need a valid sort key + if (!b1) { + return 1; } + return a1.localeCompare(b1, { sensitivity: false, numeric: true }); + }); - this.items.sort( (a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if(!a1) { - return -1; - } - if(!b1) { - return 1; - } - return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); - }); - - this.sorted = true; + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if(this.focusItems) { - this.focusItems.splice(index, 1); - } + if (this.focusItems) { + this.focusItems.splice(index, 1); + } - if(this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if (this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.removeRenderCacheItem(index); + this.removeRenderCacheItem(index); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if(this.complexItems) { - return this.items; - } + if (this.complexItems) { + return this.items; + } - return this.items.map( item => { - return item.text; - }); + return this.items.map(item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if(this.complexItems) { - return this.items[index]; - } + if (this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if(itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if (itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if(true === this.hotKeySubmit) { - this.emit('action', 'accept'); - } + if (true === this.hotKeySubmit) { + this.emit('action', 'accept'); } + } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if(items) { - this.focusItems = []; - items.forEach( itemText => { - this.focusItems.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if (items) { + this.focusItems = []; + items.forEach(itemText => { + this.focusItems.push( + { + text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemSpacing = parseInt(itemHorizSpacing); - assert(_.isNumber(itemHorizSpacing)); + itemSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; + switch (propName) { + case 'itemSpacing': this.setItemSpacing(value); break; + case 'itemHorizSpacing': this.setItemHorizSpacing(value); break; + case 'items': this.setItems(value); break; + case 'focusItems': this.setFocusItems(value); break; + case 'hotKeys': this.setHotKeys(value); break; + case 'hotKeySubmit': this.hotKeySubmit = value; break; + case 'justify': this.justify = value; break; + case 'focusItemIndex': this.focusedItemIndex = value; break; - case 'itemFormat' : - case 'focusItemFormat' : - this[propName] = value; - break; + case 'itemFormat': + case 'focusItemFormat': + this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); + break; - case 'sort' : this.setSort(value); break; - } + case 'sort': this.setSort(value); break; + } - MenuView.super_.prototype.setPropertyValue.call(this, propName, value); + MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; MenuView.prototype.setHotKeys = function(hotKeys) { - if(_.isObject(hotKeys)) { - if(this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for(var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; - } + if (_.isObject(hotKeys)) { + if (this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for (var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; } + } };