diff --git a/core/full_menu_view.js b/core/full_menu_view.js new file mode 100644 index 00000000..212b4d15 --- /dev/null +++ b/core/full_menu_view.js @@ -0,0 +1,511 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; + +// deps +const util = require('util'); +const _ = require('lodash'); + +exports.FullMenuView = FullMenuView; + +function FullMenuView(options) { + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + + + MenuView.call(this, options); + + + // Initialize paging + this.pages = []; + this.currentPage = 0; + + this.initDefaultWidth(); + + // we want page up/page down by default + if (!_.isObject(options.specialKeyMap)) { + Object.assign(this.specialKeyMap, { + 'page up': ['page up'], + 'page down': ['page down'], + }); + } + + this.autoAdjustHeightIfEnabled = () => { + if (this.autoAdjustHeight) { + this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); + this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); + } + + this.positionCacheExpired = true; + }; + + this.autoAdjustHeightIfEnabled(); + + this.clearPage = () => { + let width = this.dimens.width; + if (this.oldDimens) { + if (this.oldDimens.width > width) { + width = this.oldDimens.width; + } + delete this.oldDimens; + } + + for (let i = 0; i < this.dimens.height; i++) { + const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); + } + } + + this.cachePositions = () => { + if (this.positionCacheExpired) { + // first, clear the page + this.clearPage(); + + + this.autoAdjustHeightIfEnabled(); + + 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; + } + + let col = this.position.col; + let row = this.position.row; + const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); + + let itemInRow = 0; + let itemInCol = 0; + + let pageStart = 0; + + for (let i = 0; i < this.items.length; ++i) { + itemInRow++; + this.items[i].row = row; + this.items[i].col = col; + this.items[i].itemInRow = itemInRow; + + row += this.itemSpacing + 1; + + // have to calculate the max length on the last entry + if (i == this.items.length - 1) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + const itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let 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 + // skip for column 0, we need at least one + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { + // save previous page + this.pages.push({ start: pageStart, end: i - itemInRow }); + + // fix the last column processed + for (let 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; + + // restart row for next column + row = this.position.row; + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + // TODO: handle complex items + let itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column in the current page + // skip for first column, we need at least one + if (itemInCol != 0 && (col + maxLength > 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 (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].col = col; + } + + } + + // increment the column + col += maxLength + spacer.length; + itemInCol++; + } + + + // Set the current page if the current item is focused. + if (this.focusedItemIndex === i) { + this.currentPage = this.pages.length; + } + } + } + + this.positionCacheExpired = false; + }; + + this.drawItem = (index) => { + const item = this.items[index]; + if (!item) { + return; + } + + const cached = this.getRenderCacheItem(index, item.focused); + if (cached) { + return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + } + + let text; + let sgr; + 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 === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); + sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + } + + let renderLength = strUtil.renderStringLength(text); + if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { + text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; + } + + let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); + }; +} + +util.inherits(FullMenuView, MenuView); + +FullMenuView.prototype.redraw = function() { + FullMenuView.super_.prototype.redraw.call(this); + + this.cachePositions(); + + if (this.items.length) { + 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); + } + } +}; + +FullMenuView.prototype.setHeight = function(height) { + this.oldDimens = Object.assign({}, this.dimens); + + FullMenuView.super_.prototype.setHeight.call(this, height); + + this.positionCacheExpired = true; + this.autoAdjustHeight = false; +}; + +FullMenuView.prototype.setWidth = function(width) { + this.oldDimens = Object.assign({}, this.dimens); + + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setTextOverflow = function(overflow) { + FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); + + this.positionCacheExpired = true; + +} + +FullMenuView.prototype.setPosition = function(pos) { + FullMenuView.super_.prototype.setPosition.call(this, pos); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setFocus = function(focused) { + FullMenuView.super_.prototype.setFocus.call(this, focused); + this.positionCacheExpired = true; + this.autoAdjustHeight = false; + + this.redraw(); +}; + +FullMenuView.prototype.setFocusItemIndex = function(index) { + FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex +}; + +FullMenuView.prototype.onKeyPress = function(ch, key) { + if (key) { + if (this.isKeyMapped('up', key.name)) { + this.focusPrevious(); + } else if (this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if (this.isKeyMapped('left', key.name)) { + this.focusPreviousColumn(); + } else if (this.isKeyMapped('right', key.name)) { + this.focusNextColumn(); + } else if (this.isKeyMapped('page up', key.name)) { + this.focusPreviousPageItem(); + } else if (this.isKeyMapped('page down', key.name)) { + this.focusNextPageItem(); + } else if (this.isKeyMapped('home', key.name)) { + this.focusFirst(); + } else if (this.isKeyMapped('end', key.name)) { + this.focusLast(); + } + } + + FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); +}; + +FullMenuView.prototype.getData = function() { + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; +}; + +FullMenuView.prototype.setItems = function(items) { + // if we have items already, save off their drawing area so we don't leave fragments at redraw + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.setItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.removeItem = function(index) { + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.removeItem.call(this, index); + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.focusNext = function() { + if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = 0; + this.currentPage = 0; + } + else { + this.focusedItemIndex++; + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPrevious = function() { + if (0 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = this.items.length - 1; + this.currentPage = this.pages.length - 1; + } + else { + this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusPreviousColumn = function() { + + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; + if (this.focusedItemIndex < 0) { + this.clearPage(); + const lastItemRow = this.items[this.items.length - 1].itemInRow; + if (lastItemRow > currentRow) { + this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; + } + else { + // can't go to same column, so go to last item + this.focusedItemIndex = this.items.length - 1; + } + // 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(); + + // TODO: This isn't specific to Previous, may want to replace in the future + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusNextColumn = function() { + + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; + if (this.focusedItemIndex > this.items.length - 1) { + this.focusedItemIndex = currentRow - 1; + this.currentPage = 0; + this.clearPage(); + } + else if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + + this.redraw(); + + // TODO: This isn't specific to Next, may want to replace in the future + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPreviousPageItem = function() { + + // handle first page + if (this.currentPage == 0) { + // Do nothing, page up shouldn't go down on last page + return; + } + + this.currentPage--; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); + + this.redraw(); + + return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); +}; + +FullMenuView.prototype.focusNextPageItem = function() { + + // handle last page + if (this.currentPage == this.pages.length - 1) { + // Do nothing, page up shouldn't go down on last page + return; + } + + this.currentPage++; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); + + this.redraw(); + + return FullMenuView.super_.prototype.focusNextPageItem.call(this); +}; + +FullMenuView.prototype.focusFirst = function() { + + this.currentPage = 0; + this.focusedItemIndex = 0; + this.clearPage(); + + this.redraw(); + return FullMenuView.super_.prototype.focusFirst.call(this); +}; + +FullMenuView.prototype.focusLast = function() { + + this.currentPage = this.pages.length - 1; + this.focusedItemIndex = this.pages[this.currentPage].end; + this.clearPage(); + + this.redraw(); + return FullMenuView.super_.prototype.focusLast.call(this); +}; + +FullMenuView.prototype.setFocusItems = function(items) { + FullMenuView.super_.prototype.setFocusItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setItemSpacing = function(itemSpacing) { + FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setJustify = function(justify) { + FullMenuView.super_.prototype.setJustify.call(this, justify); + this.positionCacheExpired = true; +}; + + +FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); + + this.positionCacheExpired = true; +}; diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 037121e5..d6c37865 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView; const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const FullMenuView = require('./full_menu_view.js').FullMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; @@ -27,7 +28,7 @@ function MCIViewFactory(client) { } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', + 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', // // XY is a special MCI code that allows finding positions @@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new HorizontalMenuView(options); break; + // Full Menu + case 'FM' : + setOption(0, 'itemSpacing'); + setOption(1, 'itemHorizSpacing'); + setOption(2, 'justify'); + setOption(3, 'textStyle'); + + setFocusOption(0, 'focusTextStyle'); + + view = new FullMenuView(options); + break; + case 'SM' : setOption(0, 'textStyle'); setOption(1, 'justify'); diff --git a/core/menu_view.js b/core/menu_view.js index d9016153..9c750aba 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -38,14 +38,14 @@ function MenuView(options) { this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 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); @@ -68,6 +68,15 @@ function MenuView(options) { util.inherits(MenuView, View); +MenuView.prototype.setTextOverflow = function(overflow) { + this.textOverflow = overflow; + this.invalidateRenderCache(); +} + +MenuView.prototype.hasTextOverflow = function() { + return this.textOverflow != undefined; +} + MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; @@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + itemHorizSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); + + 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 'textOverflow' : this.setTextOverflow(value); break; case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; + case 'justify' : this.setJustify(value); break; + case 'fillChar' : this.setFillChar(value); break; case 'focusItemIndex' : this.focusedItemIndex = 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; @@ -274,6 +296,17 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setFillChar = function(fillChar) { + this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); + this.invalidateRenderCache(); +} + +MenuView.prototype.setJustify = function(justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +} + MenuView.prototype.setHotKeys = function(hotKeys) { if(_.isObject(hotKeys)) { if(this.caseInsensitiveHotKeys) { diff --git a/core/view.js b/core/view.js index fdf78916..c31ccca1 100644 --- a/core/view.js +++ b/core/view.js @@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + case 'focus' : this.setFocusProperty(value); break; case 'text' : if('setText' in this) { @@ -252,10 +252,16 @@ View.prototype.redraw = function() { this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; -View.prototype.setFocus = function(focused) { - enigAssert(this.acceptsFocus, 'View does not accept focus'); - +View.prototype.setFocusProperty = function(focused) { + // Either this should accept focus, or the focus should be false + enigAssert(this.acceptsFocus || !focused, 'View does not accept focus'); this.hasFocus = focused; +}; + +View.prototype.setFocus = function(focused) { + // Call separate method to differentiate between a value set as a + // property vs focus programmatically called. + this.setFocusProperty(focused); this.restoreCursor(); }; diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 42cc7bff..cd635928 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -49,6 +49,8 @@ - [General]({{ site.baseurl }}{% link art/general.md %}) - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) + - Views + - [Full Menu]({{ site.baseurl }}{% link art/views/full_menu_view.md %}) - Servers - Login Servers diff --git a/docs/art/mci.md b/docs/art/mci.md index 174643a4..79135554 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -123,6 +123,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `BT` | Button | A button | ...it's a button | | `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists | | `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar | +| `FM` | Full Menu | A menu that can go both vertical and horizontal. See [Full Menu](views/full_menu_view.md) | | `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options | | `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | | `KE` | Key Entry | A *single* key input control | Think hotkeys | @@ -233,4 +234,4 @@ Suppose a format object contains the following elements: `userName` and `affils` ![Example](../assets/images/text-format-example1.png "Text Format") -:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". \ No newline at end of file +:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md new file mode 100644 index 00000000..03db2cbb --- /dev/null +++ b/docs/art/views/full_menu_view.md @@ -0,0 +1,231 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu View +A full menu view supports displaying a list of times on a screen in a very configurable manner. A full menu view supports either a single row or column of values, similar to Horizontal Menu (HM) and Vertical Menu (VM), or in multiple columns. + +## General Information + +Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1` + +:information_source: See [Art](../art.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [Art](../art.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../art.md)| +| `itemSpacing` | Used to separate items vertically in the menu | +| `itemHorizSpacing` | Used to separate items horizontally in the menu | +| `height` | Sets the height of views to display multiple items vertically (default 1) | +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [Art](../art.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `textOverflow` | If a single column cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [Art](../art.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A full menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. Note, because columns are automatically calculated, this can only occur when the text is too long to fit the `width` using a single column. + +:information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Examples + +### A simple vertical menu - similar to VM + +![Example](../../assets/images/full_menu_view_example1.gif "Vertical menu") + +
+Configuration fragment (expand to view) + +``` +FM1: { + submit: true + argName: navSelect + width: 1 + items: [ + { + text: login + data: login + } + { + text: apply + data: new user + } + { + text: about + data: about + } + { + text: log off + data: logoff + } + ] +} + +``` +
+ +### A simple horizontal menu - similar to HM + +![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu") + +
+Configuration fragment (expand to view) + +``` +FM2: { + focus: true + height: 1 + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` +
+ +### A multi-column navigation menu with hotkeys + + +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") + +
+Configuration fragment (expand to view) + +``` +FM1: { + focus: true + height: 6 + width: 60 + submit: true + argName: navSelect + hotKeys: { M: 0, E: 1, D: 2 ,F: 3,!: 4, A: 5, C: 6, Y: 7, S: 8, R: 9, O: 10, L:11, U:12, W: 13, B:14, G:15, T: 16, Q:17 } + hotKeySubmit: true + items: [ + { + text: M) message area + data: message + } + { + text: E) private email + data: email + } + { + text: D) doors + data: doors + } + { + text: F) file base + data: files + } + { + text: !) global newscan + data: newscan + } + { + text: A) achievements + data: achievements + } + { + text: C) configuration + data: config + } + { + text: Y) user stats + data: userstats + } + { + text: S) system stats + data: systemstats + } + { + text: R) rumorz + data: rumorz + } + { + text: O) onelinerz + data: onelinerz + } + { + text: L) last callers + data: callers + } + { + text: U) user list + data: userlist + } + { + text: W) whos online + data: who + } + { + text: B) bbs list + data: bbslist + } + { + text: G) node-to-node messages + data: nodemessages + } + { + text: T) multi relay chat + data: mrc + } + { + text: Q) quit + data: quit + } + ] +} +``` +
+ diff --git a/docs/assets/images/full_menu_view_example1.gif b/docs/assets/images/full_menu_view_example1.gif new file mode 100644 index 00000000..7366d813 Binary files /dev/null and b/docs/assets/images/full_menu_view_example1.gif differ diff --git a/docs/assets/images/full_menu_view_example2.gif b/docs/assets/images/full_menu_view_example2.gif new file mode 100644 index 00000000..e02c60a4 Binary files /dev/null and b/docs/assets/images/full_menu_view_example2.gif differ diff --git a/docs/assets/images/full_menu_view_example3.gif b/docs/assets/images/full_menu_view_example3.gif new file mode 100644 index 00000000..b57375fa Binary files /dev/null and b/docs/assets/images/full_menu_view_example3.gif differ