diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js index 8e7b65536c86d87141de69b35a650c52175da053..b24956ad4884efb8b41f009dcd211a103ead616a 100644 --- a/exec/load/dd_lightbar_menu.js +++ b/exec/load/dd_lightbar_menu.js @@ -355,11 +355,15 @@ if (typeof(require) === "function") { require("sbbsdefs.js", "K_UPPER"); require("mouse_getkey.js", "mouse_getkey"); + require("userdefs.js", "USER_UTF8"); + require("utf8_cp437.js", "utf8_cp437"); } else { load("sbbsdefs.js"); load("mouse_getkey.js"); + load("userdefs.js"); + load("utf8_cp437.js"); } @@ -543,6 +547,7 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) this.DrawPartial = DDLightbarMenu_DrawPartial; this.DrawPartialAbs = DDLightbarMenu_DrawPartialAbs; this.GetItemText = DDLightbarMenu_GetItemText; + this.ItemTextIsUTF8 = DDLightbarMenu_ItemTextIsUTF8; this.Erase = DDLightbarMenu_Erase; this.SetItemHotkey = DDLightbarMenu_SetItemHotkey; this.AddItemHotkey = DDLightbarMenu_AddItemHotkey; @@ -633,10 +638,12 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) // pRetval: The value to return when the item is chosen. Can be any type of value. // pHotkey: Optional - A key to select the item when pressed by the user // pSelectable: Optional - Whether or not the item is to be selectable. Defaults to true. -function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable) +// pIsUTF8: Optional boolenan - Whether or not the text is UTF-8. Defaults to false. +function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable, pIsUTF8) { var item = getDefaultMenuItem(); item.text = pText; + item.textIsUTF8 = (typeof(pIsUTF8) === "boolean" ? pIsUTF8 : false); item.retval = (pRetval == undefined ? this.NumItems() : pRetval); item.isSelectable = (typeof(pSelectable) === "boolean" ? pSelectable : true); // If pHotkey is defined, then use it as the hotkey. Otherwise, if @@ -1165,6 +1172,16 @@ function DDLightbarMenu_DrawBorder() function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreenX, pScreenY) { var itemText = this.GetItemText(pIdx, pItemLen, pHighlight, pSelected); + // If the text is UTF-8 and the user's terminal is UTF-8, then set the mode bit accordingly. + // If the text is UTF-8 and the user's terminal doesn't support UTF-8, convert the text to cp437. + var printModeBits = P_NONE; + if (this.ItemTextIsUTF8(pIdx)) + { + if (console.term_supports(USER_UTF8)) + printModeBits = P_UTF8; + else + itemText = utf8_cp437(itemText); + } // If this.nextDrawOnlyItemSubstr is an object with start & end properties, // then create a string that is shortened from itemText from those start & end // indexes, and add color to it. @@ -1174,10 +1191,10 @@ function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreen var len = this.nextDrawOnlyItemSubstr.end - this.nextDrawOnlyItemSubstr.start; var shortenedText = substrWithAttrCodes(itemText, this.nextDrawOnlyItemSubstr.start, len); console.gotoxy(pScreenX+this.nextDrawOnlyItemSubstr.start, pScreenY); - console.print(shortenedText + "\x01n"); + console.print(shortenedText + "\x01n", printModeBits); } else - console.print(itemText + "\x01n"); + console.print(itemText + "\x01n", printModeBits); } // Writes a menu item at its location on the menu. This should only be called @@ -1595,6 +1612,22 @@ function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected) return itemText; } +// Returns whether or not an item's text is UTF-8, as specified in the item. +// +// Parameters: +// pIdx: The index of the item +// +// Return value: Whether or not the item's text is UTF-8 +function DDLightbarMenu_ItemTextIsUTF8(pIdx) +{ + if (typeof(pIdx) !== "number") + return false; + if (pIdx < 0 || pIdx >= this.NumItems()) + return false; + + return this.GetItem(pIdx).textIsUTF8; +} + // Erases the menu - Draws black (normal color) where the menu was function DDLightbarMenu_Erase() { @@ -3576,6 +3609,7 @@ function getBackgroundAttrAtIdx(pAttrs, pIdx) function getDefaultMenuItem() { return { text: "", + textIsUTF8: false, retval: null, hotkeys: "", useAltColors: false, diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js index 4220d57160e884780e1e6c2a12bc34abb384313f..3ff6640591c0f4ba83a34228589e0a33f163b25a 100644 --- a/xtrn/DDMsgReader/DDMsgReader.js +++ b/xtrn/DDMsgReader/DDMsgReader.js @@ -145,6 +145,9 @@ * indexedModeMenuSnapToNextWithNewAftarMarkAllRead * 2024-01-23 Eric Oulashin Version 1.95a * Bug fix: Abort when sub-board code isn't available when editing personal email + * 2024-02-04 Eric Oulashin Version 1.95b + * Bug fix: Use the P_UTF8 mode bit when printing UTF-8 message header info (such as 'from' and 'to'). + * A dd_lightbar_menu.js update goes along with this. */ "use strict"; @@ -248,10 +251,9 @@ load('822header.js'); var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a'); var hexdump = load('hexdump_lib.js'); - // Reader version information -var READER_VERSION = "1.95a"; -var READER_DATE = "2024-01-23"; +var READER_VERSION = "1.95b"; +var READER_DATE = "2024-02-04"; // Keyboard key codes for displaying on the screen var UP_ARROW = ascii(24); @@ -4407,6 +4409,7 @@ function DigDistMsgReader_CreateLightbarMsgListMenu() // When setting the item text, call PrintMessageInfo with true as // the last parameter to return the string instead menuItemObj.text = strip_ctrl(this.msgReader.PrintMessageInfo(msgHdr, false, itemIdx+1, true)); + menuItemObj.textIsUTF8 = msgHdr.hasOwnProperty("is_utf8") && msgHdr.is_utf8; menuItemObj.retval = msgHdr.number; var msgIsToUser = userHandleAliasNameMatch(msgHdr.to); var msgIsFromUser = userHandleAliasNameMatch(msgHdr.from); @@ -4753,11 +4756,10 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet var fromName = pMsgHeader.from; var toName = pMsgHeader.to; var subject = pMsgHeader.subject; - if (pMsgHeader.hasOwnProperty("is_utf8") && pMsgHeader.is_utf8) + var userConsoleSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false); + var msgIsUTF8 = pMsgHeader.hasOwnProperty("is_utf8") && pMsgHeader.is_utf8; + if (msgIsUTF8) { - var userConsoleSupportsUTF8 = false; - if (typeof(USER_UTF8) != "undefined") - userConsoleSupportsUTF8 = console.term_supports(USER_UTF8); if (!userConsoleSupportsUTF8) { fromName = utf8_cp437(fromName); @@ -4862,7 +4864,8 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet var returnStrInstead = (typeof(pReturnStrInstead) == "boolean" ? pReturnStrInstead : false); if (!returnStrInstead) { - console.print(msgHdrStr); + var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE); + console.print(msgHdrStr, printMode); console.cleartoeol("\x01n"); // To clear away any extra text that may have been entered by the user } return msgHdrStr; @@ -9788,6 +9791,13 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr) if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return; + // Check whether the header is UTF-8 and whether the user's terminal supports UTF-8, and + // set the print mode accordingly (the header fields should already be converted from + // UTF-8 to cp437 if the header is UTF-8 and the user's terminal doesn't support UTF-8) + var terminalSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false); + var hdrIsUTF8 = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8; + var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE); + // Note: The message header has the following fields: // 'number': The message number // 'offset': The message offset @@ -9800,7 +9810,7 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr) //var dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", msgHeader.when_imported_time) // Use the date text in the message header, without the time // zone offset at the end. - var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, ""); + var dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, ""); // Check to see if there is a msghdr file in the sbbs/text/menu // directory. If there is, then use it to display the message @@ -9828,7 +9838,7 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr) // message read prompt, this script now has to parse & replace some of // the @-codes in the message header line, since Synchronet doesn't know // that the user is reading a message. - console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true)); + console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true), printMode); console.crlf(); } msgHdrFile.close(); @@ -9845,15 +9855,15 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr) console.print("\x01n\x01w" + charStr(HORIZONTAL_DOUBLE, 78)); console.crlf(); var horizSingleFive = charStr(HORIZONTAL_SINGLE, 5); - console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr["from"].substr(0, console.screen_columns-12)); + console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr.from.substr(0, console.screen_columns-12), printMode); console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cTo \x01w\x01h: \x01b" + pMsgHdr["to"].substr(0, console.screen_columns-12)); + console.print("\x01n\x01w" + horizSingleFive + "\x01cTo \x01w\x01h: \x01b" + pMsgHdr.to.substr(0, console.screen_columns-12), printMode); console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr["subject"].substr(0, console.screen_columns-12)); + console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr.subject.substr(0, console.screen_columns-12), printMode); console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12)); + console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12), printMode); console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12)); + console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12), printMode); console.crlf(); } } @@ -11492,7 +11502,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS return; if ((enhMsgHdrLines.length == 0) || (this.enhMsgHeaderWidth == 0)) return; - + // Create a formatted date & time string. Adjust the message's time to // the BBS local time zone if possible. var dateTimeStr = ""; @@ -11505,7 +11515,25 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS } else dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, ""); + + // Check whether the header is UTF-8 and whether the user's terminal supports UTF-8, and + // set the print mode accordingly (the header fields should already be converted from + // UTF-8 to cp437 if the header is UTF-8 and the user's terminal doesn't support UTF-8) + var terminalSupportsUTF8 = (typeof(USER_UTF8) != "undefined" ? console.term_supports(USER_UTF8) : false); + var hdrIsUTF8 = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8; + var printMode = (terminalSupportsUTF8 && hdrIsUTF8 ? P_UTF8 : P_NONE); + // If the message is in UTF-8 and the user's terminal supports UTF-8, then we'll be printing + // with the P_UTF8 mode bit. We'll need to convert the display header lines to UTF-8 so they'll + // display properly. + if (hdrIsUTF8 && terminalSupportsUTF8) + { + var hdrLines_UTF8 = []; // We need to make a copy so we don't modify the original + for (var i = 0; i < enhMsgHdrLines.length; ++i) + hdrLines_UTF8.push(utf8_encode(enhMsgHdrLines[i])); + enhMsgHdrLines = hdrLines_UTF8; + } + var enhHdrLines = enhMsgHdrLines.slice(0); // Do some things if using the internal header (not loaded externally) if (this.usingInternalEnhMsgHdr) @@ -11547,7 +11575,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS { console.gotoxy(screenX, screenY++); console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr, - pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false)); + pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false), printMode); } // Older - Used to center the header lines, but I'm not sure this is necessary, // and it might even make the header off by one, which could be bad. @@ -11582,7 +11610,7 @@ function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartS for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx) { console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr, - pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false)); + pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false), printMode); } // Note: Avatar display is only supported for ANSI } @@ -20145,7 +20173,7 @@ function parseLoadableModuleArgs(argv) { // TODO: Allow indexed reader mode? //argVals.indexedmode = true; - + var argVals = getDefaultArgParseObj(); var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits diff --git a/xtrn/DDMsgReader/dd_lightbar_menu.js b/xtrn/DDMsgReader/dd_lightbar_menu.js new file mode 100644 index 0000000000000000000000000000000000000000..91b641466bb3d5d8dffcf0f25a103e156c0fbf68 --- /dev/null +++ b/xtrn/DDMsgReader/dd_lightbar_menu.js @@ -0,0 +1,4017 @@ +/* Digital Distortion Lightbar Menu library + * Author: Eric Oulashin (AKA Nightfox) + * BBS: Digital Distortion + * Addresses: digitaldistortionbbs.com + * digdist.synchro.net + +This is a lightbar menu library. This allows creating a scrollable menu of +text items for the user to choose from. The user can naviate the list using +the up & down arrows, PageUp, PageDown, Home, and End keys. The enter key +selects an item. The ESC key will exit the menu and return null. +This menu library requires the use of an ANSI terminal. +By default, this menu library does not display a border around the menu. +If you want this library to draw a border around the menu, you can set the +borderEnabled property to true. Without a border, the menu gains 2 +characters of width and 2 lines of height. If using a border, a title (text) +can be displayed in the top border by setting the topBorderText property (it +defaults to an empty string, for no title). + +This script provides an object, DDLightbarMenu. Use the DDLightbarMenu +constructor to create the object. Some other notable methods: +Add() +SetItemHotkey() +AddItemHotkey() +SetPos() +SetSize() +GetVal() +AddAdditionalSelectItemKeys() +SetBorderChars() +SetColors() + +To change the colors used for displaying the items, you can change the values +in the colors object within the DDLightbarMenu object. These are the current +supported colors: +itemColor: The color to use for non-selected items (current default is white + on blue). This can be a string (with the color/attribute values) + or an array to specify colors for different sections of the item + text to display in the menu. See the note on item color arrays + below. +selectedItemColor: The color to use for selected items (current default is blue + on white). This can be a string (with the color/attribute values) + or an array to specify colors for different sections of the item + text to display in the menu. See the note on item color arrays + below. +itemTextCharHighlightColor: The color of a highlighted non-space character in an + item text (specified by having a & in the item text). + It's important not to specify a "\x01n" in here in case + the item text should have a background color. +unselectableItemColor: The color to use for items that are not selectable +borderColor: The color for the borders (if borders are enabled) +You can also call SetColors() and pass in a JS object with any or all of the +above properties to set the colors internally in the DDLightbarMenu object. + +Item color arrays: Currently, colors.itemColor and colors.seletedItemColor within +a DDLightbarMenu object can be either a string (containing color/attribute codes) +or an array with color/attribute codes for different sections of the item strings +to display in the menu. The array is to contain objects with the following +properties: +start: The index of the first character in the item string to apply the colors to +end: One past the last character index in the string to apply the colors to +attrs: The Synchronet attribute codes to apply to the section of the item string +For the last item, the 'end' property can be -1, 0, or greater than the length +of the item to apply the color/attribute codes to the rest of the string. + + +By default, the menu selection will wrap around to the beginning/end when using +the down/up arrows. That behavior can be disabled by setting the wrapNavigation +property to false. + +You can enable the display of a scrollbar by setting the scrollbarEnabled property +to true. By default, it is false. For instance (assuming the menu object is lbMenu): +lbMenu.scrollbarEnabled = true; +The scrollbar can help to visually show how far the user is through the menu. When +enabled, the scrollbar will appear on the right side of the menu. If borders are enabled, +the scrollbar will appear just inside the right border. Also, if the scrollbar is +enabled but all the items would fit in a single "page" in the menu, then the scrollbar +won't be displayed. +The scrollbar uses block characters to draw the scrollbar: ASCII character 176 for +the background and ASCII 177 for the block that moves on the scrollbar. If you want +to change those characters, you can change the scrollbarInfo.BGChar and +scrollbarInfo.blockChar properties in the menu object. +By default, the scrollbar colors are high (bright) black for the background and high +(bright) white for the moving block character. If desired, those can be changed +with the colors.scrollbarBGColor and colors.scrollbarScrollBlockColor properties in +the menu object. + +This menu object supports adding multiple hotkeys to each menu item. A hotkey +can be specified in the Add() method a couple of different ways - By specifying +a hotkey as the 3rd parameter and/or by putting a & in the menu item text just +before a key you want to use as the hotkey. For example, in the text "E&xit", +"x" would be used as the hotkey for the item. If you want to disable the use of +ampersands for hotkeys in menu items (for instance, if you want an item to +literally display an ampersand before a character), set the +ampersandHotkeysInItems property to false. For instance: +lbMenu.ampersandHotkeysInItems = false; +Note that ampersandHotkeysInItems must be set before adding menu items. + +You can call the SetItemHotkey() method to set a single hotkey to be used for +a menu item or AddItemHotkey() to add an additional hotkey for an item in +addition to any existing hotkeys it might already have. + +You can call AddAdditionalSelectItemKeys() to add additional keys that can be +used to select any item (in addition to Enter). That function takes a string +of characters, and the keys are case-sensitive. For example, to add the key E +to select an item: +lbMenu.AddAdditionalSelectItemKeys("E"); +To make a case-insensitive verison, both the uppercase and lowercase letter +would need to be added, as in the following example for E: +lbMenu.AddAdditionalSelectItemKeys("Ee"); + +Also, after showing the menu & getting a value from the user (using the GetVal() +function), the lastUserInput property will have the user's last keypress. + +This menu class also supports an optional "numbered mode", where each option is +displayed with a number to the left (starting at 1), and the user is allowed to +choose an option by typing the number of the item. Numbered mode is disabled +by default and can be enabled by setting the numberedMode property to true. +For example: +lbMenu.numberedMode = true; +When numbered mode is enabled and the user starts typing a number, the menu will +prompt the user for an item number. Note that the prompt will be located on the +line below the menu, so in addition to the menu's height, you'll also need an +extra line on the screen to account for the item prompt. In addition, when the +user presses the enter key after the item number, a carriage return/line feed +will be outputted, so in numbered mode, the menu's height should not go further +than 2 lines below the console height. Otherwise, the display of the menu will +not be correct if the user decides not to enter a number. +When numbered mode is enabled, you can specify the color used to display the +item numbers. For a non-selected item, set .colors.itemNumColor. For selected items, +set .colors.highlightedItemNumColor. This is separate from the item color setting +(.colors.itemColor). For example: +lbMenu.colors.itemNumColor = "\x01c"; // Use cyan for the item numbers for non-selected items. +// For the selected item, use high cyan with a blue background for the item number +lbMenu.colors.highlightedItemNumColor = "\x01" + "4\x01c\x01h"; + +This menu also supports multiple options selected (by default, that is not enabled). +To enable that, set the multiSelect property to true. When enabled, the GetVal() +method will return an array of the user's selections rather than a string (or null if +the user aborted). You can also set a limit on the number of items selected in +multi-select mode by setting the maxNumSelections property. The default value is -1, +which means no limit (0 also means no limit). +Example, with a limit: +lbMenu.multiSelect = true; +lbMenu.maxNumSelections = 5; + +Example usage: +require("dd_lightbar_menu.js", "DDLightbarMenu"); +// Create a menu at position 1, 3 with width 45 and height of 10 +var lbMenu = new DDLightbarMenu(1, 3, 45, 10); +// Add 12 items to the menu, each of which will return the text of the item +for (var i = 0; i < 12; ++i) + lbMenu.Add("Item " + +(i+1), "Item " + +(i+1)); +// Set up the hotkey "s" to select the 2nd item +lbMenu.SetItemHotkey(1, "s"); +// Show the menu and get the chosen item from the user +var val = lbMenu.GetVal(); +// Output the chosen menu item +console.print("\x01n\r\n"); +console.print("Value:" + val + ":, type: " + typeof(val) + "\r\n"); +console.pause(); + +// Changing the normal item color to green & selected item color to bright green: +lbMenu.colors.itemColor = "\x01n\x01g"; +lbMenu.colors.selectedItemColor = "\x01n\x01h\x01g"; + +// Disabling the navigation wrap behavior: +lbMenu.wrapNavigation = false; + +// If you want a particular character in an item's text highlighted with +// a different color, you can put a & character immediately before it, as +// long as it's not a space. For instance, to highlight the "x" in "Exit": +lbMenu.Add("E&xit", -1); + +// The last parameter to Add() is a boolean specifying whether the item text is +// in UTF-8 format. +// Adding an item where the text is known to be in UTF-8 format +lbMenu.Add(someonesName, 1, null, true, true); + +To enable borders, set the borderEnabled property to true. Example: +lbMenu.borderEnabled = true; + +The menu object has an object called borderChars, which stores the characters used +to draw the border. you can change the characters used to draw the border by +setting the following properties of the borderChars object: + upperLeft: The character to use for the upper-left corner + upperRight: The character to use for the upper-right corner + lowerLeft: The character to use for the lower-left corner + lowerRight: The character to use for the lower-right corner + top: The character to use for the top border + bottom: The character to use for the bottom border + left: The character to use for the left border + right: The character to use for the right border +For example: +lbMenu.borderChars.upperLeft = "\xDA"; // Single-line upper-left character +Alternately, you can call the SetBorderChars() function and pass in a JS object +with any or all of the above properties to set those values internally in the +DDLightbarMenu object. + +If you want hotkeys to be case-sensitive, you can set the hotkeyCaseSensitive +property to true (it is false by default). For example: +lbMenu.hotkeyCaseSensitive = true; + +To add additional key characters as quit keys (in addition to ESC), call +AddAdditionalQuitKeys() with a string of characters. For example: +lbMenu.AddAdditionalQuitKeys("qQ"); + +To clear the additional quit keys: ClearAdditionalQuitKeys() +lbMenu.ClearAdditionalQuitKeys(); + +Similarly for additional PageUp keys: AddAdditionalPageUpKeys and ClearAdditionalPageUpKeys +For additional PageDown: AddAdditionalPageDownKeys and ClearAdditionalPageDownKeys +For additional first page (like HOME): AddAdditionalFirstPageKeys and ClearAdditionalFirstPageKeys +For additional last page (like END): AddAdditionalLastPageKeys and ClearAdditionalLastPageKeys + + +To enable the border and set top and bottom border text: +lbMenu.borderEnabled = true; +lbMenu.topBorderText = "Options"; +lbMenu.bottomBorderText = "Enter = Select"; + + +For a more advanced usage, if you have another large list of items you want +to use in the menu instead of the menu's own list of items, you can replace +the NumItems and GetItem functions in the menu object and write your own +versions that access a different list of items. This can be useful, for instance, +if you're working with a Synchronet messagebase (which may include a large number +of messages), so you can avoid the time taken to add those items to a DDLightbarMenu. +NumItems() needs to return the number of items in the list. GetItem() takes an item +index as a parameter and needs to return an item object that is compatible with +DDLightbarMenu. You can get a default item object by calling MakeItemWithRetval() +or MakeItemWithTextAndRetval(), then change its text and retval properties as +needed, then return the item object. In the item object, the 'text' property +is the text to display in the menu, and the 'retval' proprety is the value to return +when the user chooses that item. +An example (assuming the lightbar menu object is called lbMenu): +lbMenu.NumItems = function() { + // Do your own thing to get the number of items in your list. + // ... + // Assuming myNumItems is the number of items in your list: + return myNumItems; +}; +lbMenu.GetItem = function(pItemIndex) { + // Get a default item object from the menu with an initial return value of -1 + var menuItemObj = this.MakeItemWithRetval(-1); + // Do your own thing to get the item text and return value for the menu. + // ... + // Assuming itemText is the text to display in the menu and itemRetval is + // the return value to return from the menu: + menuItemObj.text = itemText; + menuItemObj.retval = itemRetval; + + // And if the text in the item is UTF-8, you must specify so as follows: + menuItemObj.textIsUTF8 = true; + + return menuItemObj; // The DDLightbarMenu object will use this when displaying the menu +}; + +If you want to set the currently selected item before calling GetVal() to allow user input, +you should call the SetSelectedItemIdx() function and pass the index to that. +lbMenu.SetSelectedItemIdx(5); + +The property inputTimeoutMS sets the input timeout in milliseconds (defaults to 300000). +lbMenu.inputTimeoutMS = 300000; // 300,000 milliseconds (5 minutes) + +The property mouseEnabled can be used to enable mouse support. By default it is false. +When mouse support is enabled, there can be problems inputting the ESC key from the user. +lbMenu.mouseEnabled = true; + +For selecting an item, it may be desirable to validate whether a user should be allowed +to select the item. DDLightbarMenu has a member function it calls, ValidateSelectItem(), +to do just that. It takes the selected item's return value and returns a boolean to signify +whether the user can select it. By default, it just returns true (allowing the user to +select any item). When the user can't choose a value, your code should output why. +To change its behavior, you can overwrite it as follows (assuming lbMenu +is a DDLightbarMenu object): + +lbMenu.ValidateSelectItem = function(pItemRetval) { + // Should the user be able to select the item with the return val indicated + // by pItemRetval? + if (yourValidationCode(pItemRetval)) + return true; + else + { + console.print("* Can't choose " + pItemRetval + " because blah blah blah!\r\n\x01p"); + return false; + } +} + +OnItemSelect is a function that is called when an item is selected, or toggled +if multi-select is enabled. + +Parameters: + pItemRetval: The return value of the item selected + pSelected: Boolean - Whether the item was selected or de-selected. De-selection + is possible when multi-select is enabled. +lbMenu.OnItemSelect = function(pItemRetval, pSelected) +{ + // Do something with pItemRetval. pSelected tells whether the item was selected, + // or de-selected if multi-select is enabled. +} + +The property exitOnItemSelect specifies whether or not to exit the input loop when an item is +selected/submitted (i.e. with ENTER; not for toggling with multi-select). This is true by +default. It can be desirable to set this to false in some situations, such as when you want a +menu with a custom OnItemSelect() function specified and you want the menu to continue to +be displayed allowing the user to select an item. +lbMenu.exitOnItemSelect = false; + +OnItemNav is a function that is called when the user navigates to a new item (i.e., via +the up or down arrow, PageUp, PageDown, Home, End, etc.). Its parameters are the old +item index and the new item index. +lbMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) { } + +To have the menu object call OnItemNav() when it is first displayed to get the user's +choice, set the callOnItemNavOnStartup property to true: +lbMenu.callOnItemNavOnStartup = true; +By default, it is false. + +The 'key down' behavior can be called explicitly, if needed, by calling the DoKeyDown() function. +It takes 2 parameters: An object of selected item indexes (as passed to GetVal()) and, optionally, +the pre-calculated number of items. +lbMenu.DoKeyDown(pNumItems, pSelectedItemIndexes); + + +For screen refreshing, DDLightbarMenu includes the function DrawPartial(), which can be used to +redraw only a portion of the menu, specified by starting X & Y coordinates, width, and height. +The starting X & Y coordinates are relative to the upper-left corner of the menu (not absolute +screen coordinates) and start at (1, 1). The function signature looks like this: + DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +The parameters: + pStartX: The column of the character in the menu to start at + pStartY: The row of the character in the menu to start at + pWidth: The width of the content to draw + pHeight: The height of the content to draw + pSelectedItemIndexes: Optional - An object containing indexes of selected items + +Another function, DrawPartialAbs(), provies the same functionality but with absolute screen coordinates +(also starting at (1, 1) in the upper-left corner): + DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +The parameters: + pStartX: The column of the character in the menu to start at + pStartY: The row of the character in the menu to start at + pWidth: The width of the content to draw + pHeight: The height of the content to draw + pSelectedItemIndexes: Optional - An object containing indexes of selected items + + +Menu items can be marked not selectable by setting the isSelectable proprty of the item to false. +Alternately, the menu function ToggleItemSelectable can be used for this purpose too: +Parameters: +- The index of the item to be toggled +- Boolean: Whether or not the item should be selectable +Example - Making the first item not selectable: +lbMenu.ToggleItemSelectable(0, false); + +By default, DDLightbarMenu ignores the isSelectable attribute of items and considers all items +selectable (for efficiency). To enable usage of unselectable items, set the allowUnselectableItems +property to true: +lbMenu.allowUnselectableItems = true; +*/ + +"use strict"; + +if (typeof(require) === "function") +{ + require("sbbsdefs.js", "K_UPPER"); + require("mouse_getkey.js", "mouse_getkey"); + require("userdefs.js", "USER_UTF8"); + require("utf8_cp437.js", "utf8_cp437"); +} +else +{ + load("sbbsdefs.js"); + load("mouse_getkey.js"); + load("userdefs.js"); + load("utf8_cp437.js"); +} + + +// Keyboard keys +var KEY_ENTER = "\x0d"; +// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017 +// use CTRL-P and CTRL-N for PageUp and PageDown, respectively. key_defs.js +// defines them as KEY_PAGEUP and KEY_PAGEDN (key_defs.js is loaded by +// sbbsdefs.js). +//var KEY_ESC = ascii(27); + +// Box-drawing/border characters: Single-line +var UPPER_LEFT_SINGLE = "\xDA"; +var HORIZONTAL_SINGLE = "\xC4"; +var UPPER_RIGHT_SINGLE = "\xBF"; +var VERTICAL_SINGLE = "\xB3"; +var LOWER_LEFT_SINGLE = "\xC0"; +var LOWER_RIGHT_SINGLE = "\xD9"; +var T_SINGLE = "\xC2"; +var LEFT_T_SINGLE = "\xC3"; +var RIGHT_T_SINGLE = "\xB4"; +var BOTTOM_T_SINGLE = "\xC1"; +var CROSS_SINGLE = "\xC5"; +// Box-drawing/border characters: Double-line +var UPPER_LEFT_DOUBLE = "\xC9"; +var HORIZONTAL_DOUBLE = "\xCD"; +var UPPER_RIGHT_DOUBLE = "\xBB"; +var VERTICAL_DOUBLE = "\xBA"; +var LOWER_LEFT_DOUBLE = "\xC8"; +var LOWER_RIGHT_DOUBLE = "\xBC"; +var T_DOUBLE = "\xCB"; +var LEFT_T_DOUBLE = "\xCC"; +var RIGHT_T_DOUBLE = "\xB9"; +var BOTTOM_T_DOUBLE = "\xCA"; +var CROSS_DOUBLE = "\xCE"; +// Box-drawing/border characters: Vertical single-line with horizontal double-line +var UPPER_LEFT_VSINGLE_HDOUBLE = "\xD5"; +var UPPER_RIGHT_VSINGLE_HDOUBLE = "\xB8"; +var LOWER_LEFT_VSINGLE_HDOUBLE = "\xD4"; +var LOWER_RIGHT_VSINGLE_HDOUBLE = "\xBE"; +// Other characters +var CHECK_CHAR = "\xFB"; +var BLOCK1 = "\xB0"; // Dimmest block +var BLOCK2 = "\xB1"; +var BLOCK3 = "\xB2"; +var BLOCK4 = "\xDB"; // Brightest block + +// Border types for a menu +var BORDER_NONE = 0; +var BORDER_SINGLE = 1; +var BORDER_DOUBLE = 2; + +// DDLightbarMenu object contstructor +// +// Parameters: +// pX: Optional - The column (X) of the upper-left corner. Defaults to 1. +// pY: Optional - The row (Y) of the upper-left corner. Defaults to 1. +// pWidth: Optional - The width of the menu. Defaults to 45. +// pHeight: Optional - The height of the menu. Defaults to 10. +function DDLightbarMenu(pX, pY, pWidth, pHeight) +{ + // Data members + this.items = []; + this.pos = { + x: 1, + y: 1 + }; + this.size = { + width: 45, + height: 10 + }; + this.scrollbarEnabled = false; + this.borderEnabled = false; + this.drawnAlready = false; + this.colors = { + itemColor: "\x01n\x01w\x01" + "4", // Can be either a string or an array specifying colors within the item + selectedItemColor: "\x01n\x01b\x01" + "7", // Can be either a string or an array specifying colors within the item + altItemColor: "\x01n\x01w\x01" + "4", // Alternate item color. Can be either a string or an array specifying colors within the item + altSelectedItemColor: "\x01n\x01b\x01" + "7", // Alternate selected item color. Can be either a string or an array specifying colors within the item + unselectableItemColor: "\x01n\x01b\x01h", // Can be either a string or an array specifying colors within the item + itemTextCharHighlightColor: "\x01y\x01h", + borderColor: "\x01n\x01b", + scrollbarScrollBlockColor: "\x01h\x01w", + scrollbarBGColor: "\x01h\x01k", + itemNumColor: "\x01n", + highlightedItemNumColor: "\x01n" + }; + // Characters to use to draw the border + this.borderChars = { + upperLeft: UPPER_LEFT_DOUBLE, + upperRight: UPPER_RIGHT_DOUBLE, + lowerLeft: LOWER_LEFT_DOUBLE, + lowerRight: LOWER_RIGHT_DOUBLE, + top: HORIZONTAL_DOUBLE, + bottom: HORIZONTAL_DOUBLE, + left: VERTICAL_DOUBLE, + right: VERTICAL_DOUBLE + }; + // Scrollbar information (characters, etc.) + this.scrollbarInfo = { + blockChar: BLOCK2, + BGChar: BLOCK1, + numSolidScrollBlocks: 0, + numNonSolidScrollBlocks: 0, + solidBlockLastStartRow: 0 + }; + + this.selectedItemIdx = 0; + this.topItemIdx = 0; + this.wrapNavigation = true; + this.hotkeyCaseSensitive = false; + this.ampersandHotkeysInItems = true; + this.multiSelect = false; + this.maxNumSelections = -1; // -1 or 0 means no limit on the number of selections + this.multiSelectItemChar = CHECK_CHAR; // The character to display for a selected item in multi-select mode + this.numberedMode = false; + this.itemNumLen = 0; // For the length of the item numbers in numbered mode + this.additionalQuitKeys = ""; // A string of additional keys besides ESC to quit out of the menu + this.additionalPageUpKeys = ""; // A string of additional keys besides PageUp for page up + this.additionalPageDnKeys = ""; // A string of additional keys besides PageDown for page down + this.additionalFirstPageKeys = ""; // A string of additional keys besides HOME for going to the first page + this.additionalLastPageKeys = ""; // A string of additional keys besides END for going to the last page + this.additionalSelectItemKeys = ""; // A string of additional keys to select any item + this.topBorderText = ""; // Text to display in the top border + this.bottomBorderText = ""; // Text to display in the bottom border + this.lastUserInput = null; // The user's last keypress when the menu was shown/used + // nextDrawOnlyItemSubstr can be an object containing start & end properties, as + // indexes (end is one past last index) for drawing shortened versions of items on the + // next draw + this.nextDrawOnlyItemSubstr = null; + // nextDrawOnlyItems is an array specifying the indexes of the items to write + // on the next draw. If this is empty, then all items on the page will be drawn. + this.nextDrawOnlyItems = []; + + // This is a regex to do a case-insensitive test for Synchronet attribute + // codes in strings. + // For one that looks at the whole word having only Synchronet attribute + // codes, it would have ^ and $ around it, as in + // /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i + this.syncAttrRegex = /\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]/i; + + // Whether or not to exit the input loop when an item is selected/submitted + // (i.e. with ENTER; not for toggling with multi-select) + this.exitOnItemSelect = true; + + this.inputTimeoutMS = 300000; // Input timeout in ms + this.mouseEnabled = false; + + // Whether or not to call OnItemNav() when the menu is first displayed + // to get the user's choice + this.callOnItemNavOnStartup = false; + + // Whether or not to allow unselectable items (pay attention to the isSelectable attribute of items). + // Defaults to false, mainly for backwards compatibility. + this.allowUnselectableItems = false; + + // Whether or not to allow ANSI behavior. Mainly for testing (this should be true). + this.allowANSI = true; + + // Member functions + this.Add = DDLightbarMenu_Add; + this.Remove = DDLightbarMenu_Remove; + this.RemoveAllItems = DDLightbarMenu_RemoveAllItems; + this.NumItems = DDLightbarMenu_NumItems; + this.GetItem = DDLightbarMenu_GetItem; + this.ItemIsSelectable = DDLightbarMenu_ItemIsSelectable; + this.FindSelectableItemForward = DDLightbarMenu_FindSelectableItemForward; + this.FindSelectableItemBackward = DDLightbarMenu_FindSelectableItemBackward; + this.HasAnySelectableItems = DDLightbarMenu_HasAnySelectableItems; + this.ToggleItemSelectable = DDLightbarMenu_ToggleItemSelectable; + this.FirstSelectableItemIdx = DDLightbarMenu_FirstSelectableItemIdx; + this.LastSelectableItemIdx = DDLightbarMenu_LastSelectableItemIdx; + this.SetPos = DDLightbarMenu_SetPos; + this.SetSize = DDLightbarMenu_SetSize; + this.SetWidth = DDLightbarMenu_SetWidth; + this.SetHeight = DDLightbarMenu_SetHeight; + this.Draw = DDLightbarMenu_Draw; + this.DrawBorder = DDLightbarMenu_DrawBorder; + this.WriteItem = DDLightbarMenu_WriteItem; + this.WriteItemAtItsLocation = DDLightbarMenu_WriteItemAtItsLocation; + this.DrawPartial = DDLightbarMenu_DrawPartial; + this.DrawPartialAbs = DDLightbarMenu_DrawPartialAbs; + this.GetItemText = DDLightbarMenu_GetItemText; + this.ItemTextIsUTF8 = DDLightbarMenu_ItemTextIsUTF8; + this.Erase = DDLightbarMenu_Erase; + this.SetItemHotkey = DDLightbarMenu_SetItemHotkey; + this.AddItemHotkey = DDLightbarMenu_AddItemHotkey; + this.RemoveItemHotkey = DDLightbarMenu_RemoveItemHotkey; + this.RemoveItemHotkeys = DDLightbarMenu_RemoveItemHotkeys; + this.RemoveAllItemHotkeys = DDLightbarMenu_RemoveAllItemHotkeys; + this.GetMouseClickRegion = DDLightbarMenu_GetMouseClickRegion; + this.GetVal = DDLightbarMenu_GetVal; + this.DoKeyUp = DDLightbarMenu_DoKeyUp; + this.DoKeyDown = DDLightbarMenu_DoKeyDown; + this.DoPageUp = DDLightbarMenu_DoPageUp; + this.DoPageDown = DDLightbarMenu_DoPageDown; + this.NavMenuForNewSelectedItemTop = DDLightbarMenu_NavMenuForNewSelectedItemTop; + this.NavMenuForNewSelectedItemBottom = DDLightbarMenu_NavMenuForNewSelectedItemBottom; + this.SetBorderChars = DDLightbarMenu_SetBorderChars; + this.SetColors = DDLightbarMenu_SetColors; + this.GetNumItemsPerPage = DDLightbarMenu_GetNumItemsPerPage; + this.GetTopItemIdxOfLastPage = DDLightbarMenu_GetTopItemIdxOfLastPage; + this.CalcAndSetTopItemIdxToTopOfLastPage = DDLightbarMenu_CalcAndSetTopItemIdxToTopOfLastPage; + this.CalcPageForItemAndSetTopItemIdx = DDLightbarMenu_CalcPageForItemAndSetTopItemIdx; + this.AddAdditionalQuitKeys = DDLightbarMenu_AddAdditionalQuitKeys; + this.QuitKeysIncludes = DDLightbarMenu_QuitKeysIncludes; + this.ClearAdditionalQuitKeys = DDLightbarMenu_ClearAdditionalQuitKeys; + this.AddAdditionalPageUpKeys = DDLightbarMenu_AddAdditionalPageUpKeys; + this.PageUpKeysIncludes = DDLightbarMenu_PageUpKeysIncludes; + this.ClearAdditionalPageUpKeys = DDLightbarMenu_ClearAdditionalPageUpKeys; + this.AddAdditionalPageDownKeys = DDLightbarMenu_AddAdditionalPageDownKeys; + this.PageDownKeysIncludes = DDLightbarMenu_PageDownKeysIncludes; + this.ClearAdditionalPageDownKeys = DDLightbarMenu_ClearAdditionalPageDownKeys; + this.AddAdditionalFirstPageKeys = DDLightbarMenu_AddAdditionalFirstPageKeys; + this.FirstPageKeysIncludes = DDLightbarMenu_FirstPageKeysIncludes; + this.ClearAdditionalFirstPageKeys = DDLightbarMenu_ClearAdditionalFirstPageKeys; + this.AddAdditionalLastPageKeys = DDLightbarMenu_AddAdditionalLastPageKeys; + this.LastPageKeysIncludes = DDLightbarMenu_LastPageKeysIncludes; + this.ClearAdditionalLastPageKeys = DDLightbarMenu_ClearAdditionalLastPageKeys; + this.AddAdditionalSelectItemKeys = DDLightbarMenu_AddAdditionalSelectItemKeys; + this.SelectItemKeysIncludes = DDLightbarMenu_SelectItemKeysIncludes; + this.ClearAdditionalSelectItemKeys = DDLightbarMenu_ClearAdditionalSelectItemKeys; + this.DisplayInitialScrollbar = DDLightbarMenu_DisplayInitialScrollbar; + this.UpdateScrollbar = DDLightbarMenu_UpdateScrollbar; + this.CalcScrollbarBlocks = DDLightbarMenu_CalcScrollbarBlocks; + this.CalcScrollbarSolidBlockStartRow = DDLightbarMenu_CalcScrollbarSolidBlockStartRow; + this.UpdateScrollbarWithHighlightedItem = DDLightbarMenu_UpdateScrollbarWithHighlightedItem; + this.CanShowAllItemsInWindow = DDLightbarMenu_CanShowAllItemsInWindow; + this.MakeItemWithTextAndRetval = DDLightbarMenu_MakeItemWithTextAndRetval; + this.MakeItemWithRetval = DDLightbarMenu_MakeItemWithRetval; + this.ItemUsesAltColors = DDLightbarMenu_ItemUsesAltColors; + this.GetColorForItem = DDLightbarMenu_GetColorForItem; + this.GetSelectedColorForItem = DDLightbarMenu_GetSelectedColorForItem; + this.SetSelectedItemIdx = DDLightbarMenu_SetSelectedItemIdx; + this.GetBottomItemIdx = DDLightbarMenu_GetBottomItemIdx; + this.GetTopDisplayedItemPos = DDLightbarMenu_GetTopDisplayedItemPos; + this.GetBottomDisplayedItemPos = DDLightbarMenu_GetBottomDisplayedItemPos; + this.ScreenRowForItem = DDLightbarMenu_ScreenRowForItem; + this.ANSISupported = DDLightbarMenu_ANSISupported; + + // ValidateSelectItem is a function for validating that the user can select an item. + // It takes the selected item's return value and returns a boolean to signify whether + // the user can select it. + this.ValidateSelectItem = function(pItemRetval) { return true; } + + // OnItemSelect is a function that is called when an item is selected, or toggled + // if multi-select is enabled. + // + // Parameters: + // pItemRetval: The return value of the item selected + // pSelected: Boolean - Whether the item was selected or de-selected. De-selection + // is possible when multi-select is enabled. + this.OnItemSelect = function(pItemRetval, pSelected) { } + + // OnItemNav is a function that is called when the user navigates to + // new item (i.e., up/down arrow, pageUp, pageDown, home, end) + this.OnItemNav = function(pOldItemIdx, pNewItemIdx) { } + + // Set some things based on the parameters passed in + if ((typeof(pX) == "number") && (typeof(pY) == "number")) + this.SetPos(pX, pY); + if (typeof(pWidth) == "number") + this.SetWidth(pWidth); + if (typeof(pHeight) == "number") + this.SetHeight(pHeight); +} + +// Adds an item to the menu +// +// Parameters: +// pText: The text of the menu item +// pRetval: The value to return when the item is chosen. Can be any type of value. +// pHotkey: Optional - A key to select the item when pressed by the user +// pSelectable: Optional - Whether or not the item is to be selectable. Defaults to true. +// pIsUTF8: Optional boolenan - Whether or not the text is UTF-8. Defaults to false. +function DDLightbarMenu_Add(pText, pRetval, pHotkey, pSelectable, pIsUTF8) +{ + var item = getDefaultMenuItem(); + item.text = pText; + item.textIsUTF8 = (typeof(pIsUTF8) === "boolean" ? pIsUTF8 : false); + item.retval = (pRetval == undefined ? this.NumItems() : pRetval); + item.isSelectable = (typeof(pSelectable) === "boolean" ? pSelectable : true); + // If pHotkey is defined, then use it as the hotkey. Otherwise, if + // ampersandHotkeysInItems is true, look for the first & in the item text + // and if there's a non-space after it, then use that character as the + // hotkey. + if (typeof(pHotkey) == "string") + item.hotkeys += pHotkey; + + if (this.ampersandHotkeysInItems) + { + var ampersandIndex = pText.indexOf("&"); + if (ampersandIndex > -1) + { + // See if the next character is a space character. If not, then + // don't count it in the length. + if (pText.length > ampersandIndex+1) + { + var nextChar = pText.substr(ampersandIndex+1, 1); + if (nextChar != " ") + item.hotkeys += nextChar; + } + } + } + + this.items.push(item); +} + +// Removes an item +// +// Parameters: +// pIdx: The index of the item to remove +function DDLightbarMenu_Remove(pIdx) +{ + if ((typeof(pIdx) != "number") || (pIdx < 0) || (pIdx >= this.items.length)) + return; // pIdx is invalid + + this.items.splice(pIdx, 1); + if (this.items.length > 0) + { + if (this.selectedItemIdx >= this.items.length) + this.selectedItemIdx = this.items.length - 1; + } + else + { + this.selectedItemIdx = 0; + this.topItemIdx = 0; + } +} + +// Removes all items +function DDLightbarMenu_RemoveAllItems() +{ + this.items = []; + this.selectedItemIdx = 0; + this.topItemIdx = 0; +} + +// Returns the number of items in the menu +function DDLightbarMenu_NumItems() +{ + return this.items.length; +} + +// Returns an item from the list +// +// Parameters: +// pItemIndex: The index of the item to get +// +// Return value: The item (or null if pItemIndex is invalid) +function DDLightbarMenu_GetItem(pItemIndex) +{ + if ((pItemIndex < 0) || (pItemIndex >= this.NumItems())) + return null; + return this.items[pItemIndex]; +} + +// Returns whether an item is selectable +// +// Parameters: +// pItemIndex: The index of the item to check +// +// Return value: Boolean - Whether or not the item is selectable +function DDLightbarMenu_ItemIsSelectable(pItemIndex) +{ + if ((pItemIndex < 0) || (pItemIndex >= this.NumItems())) + return false; + + if (!this.allowUnselectableItems) + return true; + + var item = this.GetItem(pItemIndex); + if (item == null || typeof(item) !== "object") + return false; + if (item.hasOwnProperty("isSelectable")) + return item.isSelectable; + else + return false; +} + +// Finds a selectable menu item index going forward, starting at a given item index +// +// Parameters: +// pStartItemIdx: The index of the item to start at. This will be included in the search. +// pWrapAround: Boolean - Whether or not to wrap around. Defaults to false. +// +// Return value: The index of the next selectable item, or -1 if none is found. +function DDLightbarMenu_FindSelectableItemForward(pStartItemIdx, pWrapAround) +{ + var numItems = this.NumItems(); + if (typeof(pStartItemIdx) !== "number" || pStartItemIdx < 0 || pStartItemIdx >= numItems) + return -1; + + if (!this.allowUnselectableItems) + return pStartItemIdx; + + var wrapAround = (typeof(pWrapAround) === "boolean" ? pWrapAround : false); + + var selectableItemIdx = -1; + var wrappedAround = false; + var onePastLastItemIdx = numItems; + for (var i = pStartItemIdx; i < onePastLastItemIdx && selectableItemIdx == -1; ++i) + { + var item = this.GetItem(i); + if (item.isSelectable) + selectableItemIdx = i; + else + { + if (i == pStartItemIdx - 1 && wrappedAround) + break; + else if (i == numItems-1 && wrapAround) + { + i = -1; + onePastLastItemIdx = pStartItemIdx; + wrappedAround = true; + } + } + } + return selectableItemIdx; +} + +// Finds a selectable menu item index going backward, starting at a given item index +// +// Parameters: +// pStartItemIdx: The index of the item to start at. This will be included in the search. +// pWrapAround: Boolean - Whether or not to wrap around. Defaults to false. +// +// Return value: The index of the previous selectable item, or -1 if none is found. +function DDLightbarMenu_FindSelectableItemBackward(pStartItemIdx, pWrapAround) +{ + var numItems = this.NumItems(); + if (typeof(pStartItemIdx) !== "number" || pStartItemIdx < 0 || pStartItemIdx >= numItems) + return -1; + + if (!this.allowUnselectableItems) + return pStartItemIdx; + + var wrapAround = (typeof(pWrapAround) === "boolean" ? pWrapAround : false); + + var selectableItemIdx = -1; + var wrappedAround = false; + for (var i = pStartItemIdx; i >= 0 && selectableItemIdx == -1; --i) + { + var item = this.GetItem(i); + if (item.isSelectable) + selectableItemIdx = i; + else + { + if (i == pStartItemIdx - 1 && wrappedAround) + break; + else if (i == numItems-1 && wrapAround) + { + i = this.NumItems() + 1; + onePastLastItemIdx = pStartItemIdx; + wrappedAround = true; + } + } + } + return selectableItemIdx; +} + +// Returns whether there are any selectable items in the menu +function DDLightbarMenu_HasAnySelectableItems(pNumItems) +{ + if (!this.allowUnselectableItems) + return true; + + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + var anySelectable = false; + for (var i = 0; i < numItems && !anySelectable; ++i) + anySelectable = this.GetItem(i).isSelectable; + return anySelectable; +} + +// Toggles whether an item is selectable +// +// Parameters: +// pItemIdx: The index of the item to toggle +// pSelectable: Boolean - Whether or not the item should be selectable +function DDLightbarMenu_ToggleItemSelectable(pItemIdx, pSelectable) +{ + if (typeof(pItemIdx) !== "number" || pItemIdx < 0 || pItemIdx >= this.NumItems() || typeof(pSelectable) !== "boolean") + return; + this.GetItem(pItemIdx).isSelectable = false; +} + +// Returns the index of the first electable item, or -1 if there is none. +function DDLightbarMenu_FirstSelectableItemIdx(pNumItems) +{ + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + if (numItems == 0) + return -1; + + if (!this.allowUnselectableItems) + return 0; + + var selectableItemIdx = -1; + var anySelectable = false; + for (var i =0; i < numItems && selectableItemIdx == -1; ++i) + { + if (this.GetItem(i).isSelectable) + selectableItemIdx = i; + } + return selectableItemIdx; +} + +// Returns the index of the last selectable item, or -1 if there is none. +function DDLightbarMenu_LastSelectableItemIdx(pNumItems) +{ + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + if (numItems == 0) + return -1; + + if (!this.allowUnselectableItems) + return numItems - 1; + + var selectableItemIdx = -1; + var anySelectable = false; + for (var i = numItems-1; i >= 0 && selectableItemIdx == -1; --i) + { + if (this.GetItem(i).isSelectable) + selectableItemIdx = i; + } + return selectableItemIdx; +} + +// Sets the menu's upper-left corner position +// +// Parameters: +// pX: The column (X) of the upper-left corner. +// pY: The row (Y) of the upper-left corner. +function DDLightbarMenu_SetPos(pX, pY) +{ + if (typeof(pX) == "object") + { + if (pX.hasOwnProperty("x") && pX.hasOwnProperty("y")) + { + this.pos.x = pX.x; + this.pos.y = pX.y; + } + + } + else if ((typeof(pX) == "number") && (typeof(pY) == "number")) + { + this.pos.x = pX; + this.pos.y = pY; + } +} + +// Sets the menu's size. +// +// Parameters: +// pSize: An object containing 'width' and 'height' members (numeric) +function DDLightbarMenu_SetSize(pSize) +{ + if (typeof(pSize) == "object") + { + if (pSize.hasOwnProperty("width") && pSize.hasOwnProperty("height") && (typeof(pSize.width) == "number") && (typeof(pSize.height) == "number")) + { + if ((pSize.width > 0) && (pSize.width <= console.screen_columns)) + this.size.width = pSize.width; + if ((pSize.height > 0) && (pSize.height <= console.screen_rows)) + this.size.height = pSize.height; + } + } +} + +// Sets the menu's width +// +// Parameters: +// pWidth: The width of the menu +function DDLightbarMenu_SetWidth(pWidth) +{ + if (typeof(pWidth) == "number") + { + if ((pWidth > 0) && (pWidth <= console.screen_columns)) + this.size.width = pWidth; + } +} + +// Sets the height of the menu +// +// Parameters: +// pHeight: The height of the menu +function DDLightbarMenu_SetHeight(pHeight) +{ + if (typeof(pHeight) == "number") + { + if ((pHeight > 0) && (pHeight <= console.screen_rows)) + this.size.height = pHeight; + } +} + +// Draws the menu with all menu items. The selected item will be highlighted. +// +// Parameters: +// pSelectedItemIndexes: An object that can contain multiple indexes of selected +// items. Only for multi-select mode. These are used +// for drawing a marking character in the item text. +// pDrawBorders: Optional boolean - Whether or not to draw the borders, if borders +// are enabled. Defaults to true. +// pDrawScrollbar: Optional boolean - Whether or not to draw the scrollbar, if +// the scrollbar is enabled. Defaults to this.scrollbarEnabled, and the scrollbar +// will only be drawn if not all items can be shown in a single page. +// pNumItems: Optional - A cached value for the number of menu items. If not specified, this will +// call this.NumItems(); +function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar, pNumItems) +{ + var numMenuItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + if (this.ANSISupported()) + { + var drawBorders = (typeof(pDrawBorders) == "boolean" ? pDrawBorders : true); + var drawScrollbar = (typeof(pDrawScrollbar) == "boolean" ? pDrawScrollbar : true); + + var curPos = { x: this.pos.x, y: this.pos.y }; // For writing the menu items + var itemLen = this.size.width; + // If borders are enabled, then adjust the item length, starting x, and starting + // y accordingly, and draw the border. + if (this.borderEnabled) + { + itemLen -= 2; + ++curPos.x; + ++curPos.y; + if (drawBorders) + this.DrawBorder(); + } + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + --itemLen; // Leave room for the scrollbar in the item lengths + // If the scrollbar is enabled & needed and we are to update it, + // then calculate the scrollbar blocks and update it on the screen. + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow() && drawScrollbar) + { + this.CalcScrollbarBlocks(); + if (!this.drawnAlready) + this.DisplayInitialScrollbar(this.pos.y); + else + this.UpdateScrollbarWithHighlightedItem(true); + } + // For numbered mode, we'll need to know the length of the longest item number + // so that we can use that space to display the item numbers. + if (this.numberedMode) + { + this.itemNumLen = numMenuItems.toString().length; + itemLen -= this.itemNumLen; + --itemLen; // Have a space for separation between the numbers and items + } + + // Write the menu items, only up to the height of the menu + var numPossibleItems = (this.borderEnabled ? this.size.height - 2 : this.size.height); + var numItemsWritten = 0; + var writeTheItem = true; + for (var idx = this.topItemIdx; (idx < numMenuItems) && (numItemsWritten < numPossibleItems); ++idx) + { + writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(idx) > -1)); + if (writeTheItem) + { + console.gotoxy(curPos.x, curPos.y); + var showMultiSelectMark = (this.multiSelect && (typeof(pSelectedItemIndexes) == "object") && pSelectedItemIndexes.hasOwnProperty(idx)); + this.WriteItem(idx, itemLen, idx == this.selectedItemIdx, showMultiSelectMark, curPos.x, curPos.y); + } + ++curPos.y; + ++numItemsWritten; + } + // If there are fewer items than the height of the menu, then write blank lines to fill + // the rest of the height of the menu. + if (numItemsWritten < numPossibleItems) + { + var numberFormatStr = "%" + this.itemNumLen + "s "; + var itemFormatStr = "%-" + itemLen + "s"; + for (; numItemsWritten < numPossibleItems; ++numItemsWritten) + { + writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(numItemsWritten) > -1)); + if (writeTheItem) + { + console.gotoxy(curPos.x, curPos.y++); + console.attributes = "N"; + if (this.numberedMode) + printf(numberFormatStr, ""); + var itemText = addAttrsToString(format(itemFormatStr, ""), this.colors.itemColor); + console.print(itemText); + } + } + } + } + else + { + // ANSI mode disabled, or the user's terminal doesn't support ANSI + var numberedModeBackup = this.numberedMode; + this.numberedMode = true; + var itemLen = this.size.width; + // For numbered mode, we'll need to know the length of the longest item number + // so that we can use that space to display the item numbers. + this.itemNumLen = numMenuItems.toString().length; + itemLen -= this.itemNumLen; + --itemLen; // Have a space for separation between the numbers and items + console.attributes = "N"; + for (var i = 0; i < numMenuItems; ++i) + { + var showMultiSelectMark = (this.multiSelect && (typeof(pSelectedItemIndexes) == "object") && pSelectedItemIndexes.hasOwnProperty(idx)); + var itemText = this.GetItemText(i, itemLen, false, showMultiSelectMark); + // TODO: Once, it seemed the text must be shortened by 3 less than the console width or else + // it behaves like there's an extra CRLF, but 2 should be correct. + console.print(substrWithAttrCodes(itemText, 0, console.screen_columns-2) + "\x01n"); + console.crlf(); + } + this.numberedMode = numberedModeBackup; + } + + this.drawnAlready = true; + this.nextDrawOnlyItemSubstr = null; + this.nextDrawOnlyItems = []; +} + +// Draws the border around the menu items +function DDLightbarMenu_DrawBorder() +{ + if (!this.borderEnabled) + return; + + // Draw the border around the menu options + console.print("\x01n" + this.colors.borderColor); + // Upper border + console.gotoxy(this.pos.x, this.pos.y); + if (this.borderChars.hasOwnProperty("upperLeft") && (typeof(this.borderChars.upperLeft) == "string")) + console.print(this.borderChars.upperLeft); + else + console.print(" "); + var lineLen = this.size.width - 2; + if (this.borderChars.hasOwnProperty("top") && (typeof(this.borderChars.top) == "string")) + { + // Display the top border text (if any) in the top border. Ensure the text + // length is no longer than the maximum possible length (lineLen). + var borderText = shortenStrWithAttrCodes(this.topBorderText, lineLen); + console.print("\x01n" + borderText + "\x01n" + this.colors.borderColor); + var remainingLineLen = lineLen - console.strlen(borderText); + for (var i = 0; i < remainingLineLen; ++i) + console.print(this.borderChars.top); + } + else + { + for (var i = 0; i < lineLen; ++i) + console.print(" "); + } + if (this.borderChars.hasOwnProperty("upperRight") && (typeof(this.borderChars.upperRight) == "string")) + console.print(this.borderChars.upperRight); + else + console.print(" "); + // Lower border + console.gotoxy(this.pos.x, this.pos.y+this.size.height-1); + if (this.borderChars.hasOwnProperty("lowerLeft") && (typeof(this.borderChars.lowerLeft) == "string")) + console.print(this.borderChars.lowerLeft); + else + console.print(" "); + var lineLen = this.size.width - 2; + if (this.borderChars.hasOwnProperty("bottom") && (typeof(this.borderChars.bottom) == "string")) + { + // Display the bottom border text (if any) in the bottom border. Ensure the text + // length is no longer than the maximum possible length (lineLen). + var borderText = shortenStrWithAttrCodes(this.bottomBorderText, lineLen); + console.print("\x01n" + borderText + "\x01n" + this.colors.borderColor); + var remainingLineLen = lineLen - console.strlen(borderText); + for (var i = 0; i < remainingLineLen; ++i) + console.print(this.borderChars.bottom); + } + else + { + for (var i = 0; i < lineLen; ++i) + console.print(" "); + } + if (this.borderChars.hasOwnProperty("lowerRight") && (typeof(this.borderChars.lowerRight) == "string")) + console.print(this.borderChars.lowerRight); + else + console.print(" "); + // Side borders + var leftSideChar = " "; + var rightSideChar = " "; + if (this.borderChars.hasOwnProperty("left") && (typeof(this.borderChars.left) == "string")) + leftSideChar = this.borderChars.left; + if (this.borderChars.hasOwnProperty("right") && (typeof(this.borderChars.right) == "string")) + rightSideChar = this.borderChars.right; + lineLen = this.size.height - 2; + var lineNum = 1; + for (var lineNum = 1; lineNum <= lineLen; ++lineNum) + { + console.gotoxy(this.pos.x, this.pos.y+lineNum); + console.print(leftSideChar); + console.gotoxy(this.pos.x+this.size.width-1, this.pos.y+lineNum); + console.print(rightSideChar); + } +} + +// Writes a single menu item +// +// Parameters: +// pIdx: The index of the item to write +// pItemLen: Optional - Calculated length of the item (in case the scrollbar is showing). +// If this is not given, then this will be calculated. +// pHighlight: Optional - Whether or not to highlight the item. If this is not given, +// the item will be highlighted based on whether the current selected item +// matches the given index, pIdx. +// pSelected: Optional - Whether or not this item is selected (mainly intended for multi-select +// mode). Defaults to false. If true, then a mark character will be displayed +// at the end of the item's text. +// pScreenX: Optional - The horizontal screen coordinate of the start of the item +// pScreenY: Optional - The vertical screen coordinate of the start of the item +function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected, pScreenX, pScreenY) +{ + var itemText = this.GetItemText(pIdx, pItemLen, pHighlight, pSelected); + // If the text is UTF-8 and the user's terminal is UTF-8, then set the mode bit accordingly. + // If the text is UTF-8 and the user's terminal doesn't support UTF-8, convert the text to cp437. + var printModeBits = P_NONE; + if (this.ItemTextIsUTF8(pIdx)) + { + if (console.term_supports(USER_UTF8)) + printModeBits = P_UTF8; + else + itemText = utf8_cp437(itemText); + } + // If this.nextDrawOnlyItemSubstr is an object with start & end properties, + // then create a string that is shortened from itemText from those start & end + // indexes, and add color to it. + // Otherwise, just print the full item text. + if ((this.nextDrawOnlyItemSubstr != null) && (typeof(this.nextDrawOnlyItemSubstr) == "object") && this.nextDrawOnlyItemSubstr.hasOwnProperty("start") && this.nextDrawOnlyItemSubstr.hasOwnProperty("end") && (typeof(pScreenX) == "number") && (typeof(pScreenY) == "number")) + { + var len = this.nextDrawOnlyItemSubstr.end - this.nextDrawOnlyItemSubstr.start; + var shortenedText = substrWithAttrCodes(itemText, this.nextDrawOnlyItemSubstr.start, len); + console.gotoxy(pScreenX+this.nextDrawOnlyItemSubstr.start, pScreenY); + console.print(shortenedText + "\x01n", printModeBits); + } + else + console.print(itemText + "\x01n", printModeBits); +} + +// Writes a menu item at its location on the menu. This should only be called +// if the item is on the current page. +// +// Parameters: +// pIdx: The index of the item to write +// pHighlight: Whether or not the item should be highlighted +// pSelected: Whether or not the item is selected +function DDLightbarMenu_WriteItemAtItsLocation(pIdx, pHighlight, pSelected) +{ + if (this.borderEnabled) + console.gotoxy(this.pos.x+1, this.pos.y+pIdx-this.topItemIdx+1); + else + console.gotoxy(this.pos.x, this.pos.y+pIdx-this.topItemIdx); + this.WriteItem(pIdx, null, pHighlight, pSelected); +} + +// Draws part of the menu, starting at a certain location within the menu and +// with a given width & height (for screen refreshing). The start X and Y location +// are relative to the menu (not the screen), and they start at (1, 1) in the upper-left +// +// Parameters: +// pStartX: The column of the character in the menu to start at +// pStartY: The row of the character in the menu to start at +// pWidth: The width of the content to draw +// pHeight: The height of the content to draw +// pSelectedItemIndexes: Optional - An object containing indexes of selected items +function DDLightbarMenu_DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +{ + // Sanity check the parameters + if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number") + return; + if (pStartX < 1 || pStartX > this.size.width) + return; + if (pStartY < 1 || pStartY > this.size.height) + return; + + // Fix the width & height if needed + var width = pWidth; + if (width > (this.size.width - pStartX + 1)) + width = (this.size.width - pStartX + 1); + var height = pHeight; + if (height > (this.size.height - pStartY + 1)) + height = (this.size.height - pStartY + 1); + /* + // Temporary + if (user.is_sysop) + { + console.print("\x01n\r\n"); + printf("DrawPartial 1 - X, Y, width, height: %d, %d; %d, %d\r\n", pStartX, pStartY, width, height); + console.pause(); + } + // End Temporary + */ + + var selectedItemIndexes = { }; // For multi-select mode + if (typeof(pSelectedItemIndexes) == "object") + selectedItemIndexes = pSelectedItemIndexes; + + // If borders are enabled, draw any border characters in the region first + // The X & Y locations are 1-based + var lastLineNum = (pStartY + this.pos.y + height) - 1; // Last line # on the screen + if (lastLineNum > this.pos.y + this.size.height - 1) + lastLineNum = this.pos.y + this.size.height - 1; + if (this.borderEnabled) + { + var lastX = pStartX + width - 1; + for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum) + { + // Top line + if (lineNum == this.pos.y) + { + console.print("\x01n" + this.colors.borderColor); + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + console.print(this.borderChars.upperLeft); + else if (posX == this.pos.x + this.size.width - 1) + console.print(this.borderChars.upperRight); + else + console.print(this.borderChars.top); + } + } + // Bottom line + else if (lineNum == this.pos.y + this.size.height - 1) + { + console.print("\x01n" + this.colors.borderColor); + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + console.print(this.borderChars.lowerLeft); + else if (posX == this.pos.x + this.size.width - 1) + console.print(this.borderChars.lowerRight); + else + console.print(this.borderChars.bottom); + } + } + // Somewhere between the top & bottom line + else + { + var printedBorderColor = false; + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + { + if (!printedBorderColor) + { + console.print("\x01n" + this.colors.borderColor); + printedBorderColor = true; + } + console.print(this.borderChars.left); + } + else if (posX == this.pos.x + this.size.width - 1) + { + if (!printedBorderColor) + { + console.print("\x01n" + this.colors.borderColor); + printedBorderColor = true; + } + console.print(this.borderChars.right); + } + } + } + } + } + // Calculate the width and starting index of the menu items + // Note that pStartX is relative to the menu, not the screen + var itemLen = width; + var writeMenuItems = true; // Might not if the draw area only includes the scrollbar or border + var itemTxtStartIdx = pStartX - 1; + if (this.borderEnabled) + { + if (itemTxtStartIdx > 0) + --itemTxtStartIdx; // pStartX - 2 + if (pStartX == 1) + --itemLen; + // Starts on 2 & width is 5: 2, 3, 4, 5, 6 + var lastCol = this.pos.x + pStartX + width - 1; + if (this.pos.x + pStartX + width - 1 >= lastCol) // The last column drawn will contain the right border char + --itemLen; + if ((pStartX == 1 && width == 1) || pStartX == this.size.width) + writeMenuItems = false; + else if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow() && pStartX == this.size.width-1) + writeMenuItems = false; + } + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + { + var scrollbarCol = this.borderEnabled ? this.pos.x + this.size.width - 2 : this.pos.x + this.size.width - 1; + // If the rightmost column is at or past the scrollbar column, + // then subtract from the item length so that we don't overwrite + // the scrollbar. + var rightmostCol = this.pos.x + pStartX + width - 2; + if (rightmostCol >= scrollbarCol) + { + var lenDiff = scrollbarCol - rightmostCol + 1; // The amount to subtract from the length + itemLen -= lenDiff; + } + if (!this.borderEnabled && pStartX == this.size.width) + writeMenuItems = false; + // Just draw the whole srollbar to ensure it's updated + this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow, this.scrollbarInfo.numSolidScrollBlocks); + } + if (itemTxtStartIdx < 0) + itemTxtStartIdx = 0; + // Write the menu items + if (writeMenuItems) + { + /* + // Temporary + if (user.is_sysop) + { + console.print("\x01n\r\n"); + console.print("itemTxtStartIdx: " + itemTxtStartIdx + ", itemLen: " + itemLen + "\r\n"); + console.pause(); + } + // End Temporary + */ + var blankItemTextFormatStr = "\x01n%" + itemLen + "s"; + for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum) + { + var startX = pStartX; + // If borders are enabled, skip the top & bottom lines since borders were already drawn + if (this.borderEnabled) + { + if (lineNum == this.pos.y || lineNum == lastLineNum) + continue; + else + { + if (pStartX + this.pos.x - 1 == this.pos.x) + ++startX; + } + } + // Write the menu item text + var itemIdx = this.topItemIdx + (lineNum - this.pos.y); + if (this.borderEnabled) --itemIdx; + var highlightItem = itemIdx == this.selectedItemIdx; + var itemText = this.GetItemText(itemIdx, null, highlightItem, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + //var shortenedText = substrWithAttrCodes(itemText, itemTxtStartIdx, itemLen); + var shortenedText = substrWithAttrCodes(itemText, itemTxtStartIdx, itemLen, true); + // If shortenedText is empty (perhaps there's no menu item for this line), + // then make shortenedText consist of all spaces at the proper length + if (shortenedText.length == 0) + shortenedText = format(blankItemTextFormatStr, ""); + console.gotoxy(startX, lineNum); + console.print(shortenedText + "\x01n"); + } + } +} +// Draws part of the menu, starting at a certain location within the menu and +// with a given width & height (for screen refreshing). For this version, the start X +// and Y location are absolute on the screen. They start at (1, 1) in the upper-left. +// +// Parameters: +// pStartX: The column of the character in the menu to start at +// pStartY: The row of the character in the menu to start at +// pWidth: The width of the content to draw +// pHeight: The height of the content to draw +// pSelectedItemIndexes: Optional - An object containing indexes of selected items +function DDLightbarMenu_DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +{ + if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number") + return; + + // Calculate the start X & Y coordinates relative to the menu (1-based), and adjust height & + // width if necessary. Then draw partial. + var height = pHeight; + var width = pWidth; + var startX = pStartX - this.pos.x + 1; + var startY = pStartY - this.pos.y + 1; + /* + // Temporary + if (user.is_sysop) + { + console.print("\x01n\r\n"); + printf("Here 1 - X, Y, width, height: %d, %d; %d, %d\r\n", startX, startY, width, height); + console.pause(); + } + // End Temporary + */ + if (startX < 1) + { + var XDiff = 1 - startX; + startX += XDiff; + width -= XDiff; + } + if (startY < 1) + { + var YDiff = 1 - startY; + startY += YDiff; + height -= YDiff; + } + /* + // Temporary + if (user.is_sysop) + { + console.print("\x01n\r\n"); + printf("Here 2 - X, Y, width, height: %d, %d; %d, %d\r\n", startX, startY, width, height); + console.pause(); + } + // End Temporary + */ + this.DrawPartial(startX, startY, width, height, pSelectedItemIndexes); +} + + +// Gets the text of a menu item with colors applied +// +// Parameters: +// pIdx: The index of the item to get +// pItemLen: Optional - Calculated length of the item (in case the scrollbar is showing). +// If this is not given, then this will be calculated. +// pHighlight: Optional - Whether or not to highlight the item. If this is not given, +// the item will be highlighted based on whether the current selected item +// matches the given index, pIdx. +// pSelected: Optional - Whether or not this item is selected (mainly intended for multi-select +// mode). Defaults to false. If true, then a mark character will be displayed +// at the end of the item's text. +function DDLightbarMenu_GetItemText(pIdx, pItemLen, pHighlight, pSelected) +{ + var itemText = ""; + var numItems = this.NumItems(); + if ((pIdx >= 0) && (pIdx < numItems)) + { + var itemLen = 0; + if (typeof(pItemLen) === "number") + itemLen = pItemLen; + else + { + itemLen = this.size.width; + // If the scrollbar is enabled & we can't show all items in the window, + // then subtract 1 from itemLen to make room for the srollbar. + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + --itemLen; + // If borders are enabled, then subtract another 2 from itemLen to make + // room for the left & right borders + if (this.borderEnabled) + itemLen -= 2; + // For numbered mode, we'll need to know the length of the longest item number + // so that we can use that space to display the item numbers. + if (this.numberedMode) + { + this.itemNumLen = numItems.toString().length; + itemLen -= this.itemNumLen; + --itemLen; // Have a space for separation between the numbers and items + } + } + + // Decide which color(s) to use for the item text + var menuItem = this.GetItem(pIdx); + var normalItemColor; + var selectedItemColor; + if (menuItem.itemColor != null) + normalItemColor = menuItem.itemColor; + else + normalItemColor = (menuItem.useAltColors ? this.colors.altItemColor : this.colors.itemColor); + if (menuItem.itemSelectedColor != null) + selectedItemColor = menuItem.itemSelectedColor; + else + selectedItemColor = (menuItem.useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor); + var itemColor = ""; + if (this.allowUnselectableItems && !menuItem.isSelectable) + itemColor = this.colors.unselectableItemColor; + else if (typeof(pHighlight) === "boolean") + itemColor = (pHighlight ? selectedItemColor : normalItemColor); + else + itemColor = (pIdx == this.selectedItemIdx ? selectedItemColor : normalItemColor); + var selected = (typeof(pSelected) == "boolean" ? pSelected : false); + + // Get the item text + if ((typeof(itemColor) === "string" || Array.isArray(itemColor)) && itemColor.length > 0) + { + // Use strip_ctrl to ensure there are no attribute codes, since we will + // apply our own. This might be only a temporary item returned by a + // replaced GetItem(), so we just have to strip_ctrl() it here. + itemText = strip_ctrl(menuItem.text); + } + else // Allow other colors in the text to be specified if the configured item color is empty + itemText = menuItem.text; + // Truncate the item text to the displayable item width + if (itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems) > itemLen) + itemText = substrWithAttrCodes(itemText, 0, itemLen); //itemText = itemText.substr(0, itemLen); + // If the item text is empty, then fill it with spaces for the item length + // so that the line's colors/attributes will be applied for the whole line + // when written + if (strip_ctrl(itemText).length == 0) + itemText = format("%" + itemLen + "s", ""); + // Add the item color to the item text + itemText = addAttrsToString(itemText, itemColor); + // If ampersandHotkeysInItems is true, see if there's an ampersand in + // the item text. If so, we'll want to highlight the next character + // with a different color. + if (this.ampersandHotkeysInItems) + { + var ampersandIndex = itemText.indexOf("&"); + if (ampersandIndex > -1) + { + // See if the next character is a space character. If not, then remove + // the ampersand and highlight the next character in the text. + if (itemText.length > ampersandIndex+1) + { + var nextChar = itemText.substr(ampersandIndex+1, 1); + if (nextChar != " ") + { + itemText = itemText.substr(0, ampersandIndex) + this.colors.itemTextCharHighlightColor + + nextChar + "\x01n" + itemColor + itemText.substr(ampersandIndex+2); + } + } + } + } + // If the item is selected, then display a check mark at the end of the item text. + if (selected) + { + var itemTextLen = itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems); + if (itemTextLen < this.size.width) + { + var numSpaces = itemLen - itemTextLen - 2; + // Kludge? If numSpaces is positive, append a space and then the selected + // character, Otherwise, we'll need to replace the last character in + // itemText with the selected character. + if (numSpaces > 0) + itemText += format("%" + numSpaces + "s %s", "", this.multiSelectItemChar); + else + itemText = itemText.substr(0, itemText.length-1) + this.multiSelectItemChar; + } + else + { + // itemText should already be shortened to only the menu width, so + // take 2 characters off the end and add a space and mark character + itemText = itemText.substr(0, itemText.length-2) + " " + this.multiSelectItemChar; + } + } + // Ensure the item text fills the width of the menu (in case there's a + // background color, it should be used for the entire width of the item + // text). Then write the item. + var currentTextLen = itemTextDisplayableLen(itemText, this.ampersandHotkeysInItems); + if (currentTextLen < itemLen) + itemText += format("%" + +(itemLen-currentTextLen) + "s", ""); // Append spaces to the end of itemText + // If in numbered mode and the item is selectable, prepend the item number to the front of the item text. + if (this.numberedMode && menuItem.isSelectable) + { + if (this.itemNumLen == 0) + this.itemNumLen = numItems.toString().length; + var numColor = "\x01n" + this.colors.itemNumColor; + if (typeof(pHighlight) === "boolean") + numColor = (pHighlight ? this.colors.highlightedItemNumColor : this.colors.itemNumColor); + else + numColor = (pIdx == this.selectedItemIdx ? this.colors.highlightedItemNumColor : this.colors.itemNumColor); + itemText = format("\x01n" + numColor + "%" + this.itemNumLen + "d \x01n", pIdx+1) + itemText; + } + } + return itemText; +} + +// Returns whether or not an item's text is UTF-8, as specified in the item. +// +// Parameters: +// pIdx: The index of the item +// +// Return value: Whether or not the item's text is UTF-8 +function DDLightbarMenu_ItemTextIsUTF8(pIdx) +{ + if (typeof(pIdx) !== "number") + return false; + if (pIdx < 0 || pIdx >= this.NumItems()) + return false; + + return this.GetItem(pIdx).textIsUTF8; +} + +// Erases the menu - Draws black (normal color) where the menu was +function DDLightbarMenu_Erase() +{ + var formatStr = "%" + this.size.width + "s"; // For use with printf() + console.attributes = "N"; + var curPos = { x: this.pos.x, y: this.pos.y }; + for (var i = 0; i < this.size.height; ++i) + { + console.gotoxy(curPos.x, curPos.y++); + printf(formatStr, ""); + } + console.gotoxy(curPos); +} + +// Sets a hotkey for a menu item +// +// Parameters: +// pIdx: The index of the menu item +// pHotkey: The hotkey to set for the menu item +function DDLightbarMenu_SetItemHotkey(pIdx, pHotkey) +{ + if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length) && (typeof(pHotkey) == "string")) + this.items[pIdx].hotkeys = pHotkey; +} + +// Adds a hotkey for a menu item (in addition to the item's other hotkeys) +// +// Parameters: +// pIdx: The index of the menu item +// pHotkey: The hotkey to add for the menu item +function DDLightbarMenu_AddItemHotkey(pIdx, pHotkey) +{ + if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length) && (typeof(pHotkey) == "string") && (this.items[pIdx].hotkeys.indexOf(pHotkey) == -1)) + this.items[pIdx].hotkeys += pHotkey; +} + +// Removes a specific hotkey from an item. +// +// Parameters: +// pIdx: The index of the item +// pHotkey: The hotkey to remove from the item +function DDLightbarMenu_RemoveItemHotkey(pIdx, pHotkey) +{ + if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length)) + { + var hotkeyIdx = this.items[pIdx].hotkeys.indexOf(pHotkey); + while (hotkeyIdx > -1) + { + this.items[pIdx].hotkeys = this.items[pIdx].hotkeys.substr(0, hotkeyIdx) + this.items[pIdx].hotkeys.substr(hotkeyIdx+1); + hotkeyIdx = this.items[pIdx].hotkeys.indexOf(pHotkey); + } + } +} + +// Removes all hotkeys for an item +// +// Parameters: +// pIdx: The index of the item +function DDLightbarMenu_RemoveItemHotkeys(pIdx) +{ + if ((typeof(pIdx) == "number") && (pIdx >= 0) && (pIdx < this.items.length)) + this.items[pIdx].hotkeys = ""; +} + +// Removes the hotkeys from all items +function DDLightbarMenu_RemoveAllItemHotkeys() +{ + for (var i = 0; i < this.items.length; ++i) + this.items[i].hotkeys = ""; +} + +// Returns an object specifying the mouse valid click region for the menu, +// with properties left, right, top, and bottom. +function DDLightbarMenu_GetMouseClickRegion() +{ + var clickRegion = { + left: this.pos.x, + right: this.pos.x + this.size.width - 1, + top: this.pos.y, + bottom: this.pos.y + this.size.height - 1 + }; + if (this.borderEnabled) + { + ++clickRegion.left; + ++clickRegion.top; + --clickRegion.right; + --clickRegion.bottom; + } + return clickRegion; +} + +// Waits for user input, optionally drawing the menu first. +// +// Parameters: +// pDraw: Optional - Whether or not to draw the menu first. By default, the +// menu will be drawn first. +// pSelectedItemIndexes: Optional - An object containing indexes of selected items +function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) +{ + this.lastUserInput = null; + + var numItems = this.NumItems(); + if (numItems == 0) + return null; + + // If allowing unselectable items, then make sure there are selectable items before + // doing the input loop (and if not, return null). If there are selectable items, + // make sure the current selected item is selectable (if not, go to the next one). + if (this.allowUnselectableItems) + { + if (this.HasAnySelectableItems()) + { + if (!this.ItemIsSelectable(this.selectedItemIdx)) + { + var nextSelectableItemIdx = this.FindSelectableItemForward(this.selectedItemIdx+1, true); + // nextSelectableItemIdx should be valid since we know there are selectable items + if (nextSelectableItemIdx > -1 && nextSelectableItemIdx != this.selectedItemIdx) + { + this.selectedItemIdx = nextSelectableItemIdx; + this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems); + } + } + } + else // No selectable items + return null; + } + + if (typeof(this.lastMouseClickTime) == "undefined") + this.lastMouseClickTime = -1; + + var draw = (typeof(pDraw) == "boolean" ? pDraw : true); + if (draw) + { + this.Draw(pSelectedItemIndexes, null, null, numItems); + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow); + } + + if (this.callOnItemNavOnStartup && typeof(this.OnItemNav) === "function") + this.OnItemNav(0, this.selectedItemIdx); + + var selectedItemIndexes = { }; // For multi-select mode + if (typeof(pSelectedItemIndexes) == "object") + selectedItemIndexes = pSelectedItemIndexes; + if (this.ANSISupported()) + { + // User input loop + var userChoices = null; // For multi-select mode + var retVal = null; // For single-choice mode + // mouseInputOnly_continue specifies whether to continue to the + // next iteration if the mouse was clicked & there's no need to + // process user input further + var mouseInputOnly_continue = false; + var continueOn = true; + while (continueOn) + { + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + this.UpdateScrollbarWithHighlightedItem(); + + mouseInputOnly_continue = false; + + // TODO: With mouse_getkey(), it seems you need to press ESC twice + // to get the ESC key and exit the menu + var inputMode = K_NOECHO|K_NOSPIN|K_NOCRLF; + var mk = null; // Will be used for mouse support + var mouseNoAction = false; + if (this.mouseEnabled) + { + mk = mouse_getkey(inputMode, this.inputTimeoutMS > 1 ? this.inputTimeoutMS : undefined, this.mouseEnabled); + if (mk.mouse !== null) + { + // See if the user clicked anywhere in the region where items are + // listed or the scrollbar + var clickRegion = this.GetMouseClickRegion(); + // Button 0 is the left/main mouse button + if (mk.mouse.press && (mk.mouse.button == 0) && (mk.mouse.motion == 0) && + (mk.mouse.x >= clickRegion.left) && (mk.mouse.x <= clickRegion.right) && + (mk.mouse.y >= clickRegion.top) && (mk.mouse.y <= clickRegion.bottom)) + { + var isDoubleClick = ((this.lastMouseClickTime > -1) && (system.timer - this.lastMouseClickTime <= 0.4)); + + // If the scrollbar is enabled, then see if the mouse click was + // in the scrollbar region. If below the scrollbar bright blocks, + // then we'll want to do a PageDown. If above the scrollbar bright + // blocks, then we'll want to do a PageUp. + var scrollbarX = this.pos.x + this.size.width - 1; + if (this.borderEnabled) + --scrollbarX; + if ((mk.mouse.x == scrollbarX) && this.scrollbarEnabled) + { + var scrollbarSolidBlockEndRow = this.scrollbarInfo.solidBlockLastStartRow + this.scrollbarInfo.numSolidScrollBlocks - 1; + if (mk.mouse.y < this.scrollbarInfo.solidBlockLastStartRow) + this.lastUserInput = KEY_PAGEUP; + else if (mk.mouse.y > scrollbarSolidBlockEndRow) + this.lastUserInput = KEY_PAGEDN; + else + { + // Mouse click no-action + // TODO: Can we detect if they're holding the mouse down + // and scroll while the user holds the mouse & scrolls on + // the scrollbar? + this.lastUserInput = ""; + mouseNoAction = true; + mouseInputOnly_continue = true; + } + } + else + { + // The user didn't click on the scrollbar or the scrollbar + // isn't enabled. + // For a double-click, if multi-select is enabled, set the + // last user input to a space to select/de-select the item. + if (isDoubleClick) + { + if (this.multiSelect) + this.lastUserInput = " "; + else + { + // No mouse action + this.lastUserInput = ""; + mouseNoAction = true; + mouseInputOnly_continue = true; + } + } + else + { + // Make the clicked-on item the currently highlighted + // item. Only select the item if the index is valid. + var topItemY = (this.borderEnabled ? this.pos.y + 1 : this.pos.y); + var distFromTopY = mk.mouse.y - topItemY; + var itemIdx = this.topItemIdx + distFromTopY; + if ((itemIdx >= 0) && (itemIdx < this.NumItems())) + { + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.selectedItemIdx = itemIdx; + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + } + // Don't have the later code do anything + this.lastUserInput = ""; + mouseNoAction = true; + mouseInputOnly_continue = true; + } + } + + this.lastMouseClickTime = system.timer; + } + else + { + // The mouse click is outside the click region. Set the appropriate + // variables for mouse no-action. + // TODO: Perhaps this may also need to be done in some places above + // where no action needs to be taken + this.lastUserInput = ""; + mouseNoAction = true; + mouseInputOnly_continue = true; + } + } + else + { + // mouse is null, so a keybaord key must have been pressed + this.lastUserInput = mk.key; + } + } + else // this.mouseEnabled is false + this.lastUserInput = getKeyWithESCChars(inputMode, this.inputTimeoutMS); + + // If no further input processing needs to be done due to a mouse click + // action, then continue to the next loop iteration. + if (mouseInputOnly_continue) + continue; + + // Take the appropriate action based on the user's last input/keypress + if ((this.lastUserInput == KEY_ESC) || this.QuitKeysIncludes(this.lastUserInput) || console.aborted) + { + // Only exit if there was not a no-action mouse click + // TODO: Is this logic good and clean? + var goAheadAndExit = true; + if (mk !== null && mk.mouse !== null) + { + goAheadAndExit = !mouseNoAction; // Only really needed with an input timer? + } + if (goAheadAndExit) + { + continueOn = false; + // Ensure any returned choice objects are null/empty to signal + // that the user aborted + userChoices = null; // For multi-select mode + selectedItemIndexes = { }; // For multi-select mode + retVal = null; // For single-choice mode + } + } + else if ((this.lastUserInput == KEY_UP) || (this.lastUserInput == KEY_LEFT)) + this.DoKeyUp(selectedItemIndexes, numItems); + else if ((this.lastUserInput == KEY_DOWN) || (this.lastUserInput == KEY_RIGHT)) + this.DoKeyDown(selectedItemIndexes, numItems); + else if (this.lastUserInput == KEY_PAGEUP || this.PageUpKeysIncludes(this.lastUserInput)) + this.DoPageUp(selectedItemIndexes, numItems); + else if (this.lastUserInput == KEY_PAGEDN || this.PageDownKeysIncludes(this.lastUserInput)) + this.DoPageDown(selectedItemIndexes, numItems); + else if (this.lastUserInput == KEY_HOME || this.FirstPageKeysIncludes(this.lastUserInput)) + { + // Go to the first item in the list + var firstSelectableItemIdx = this.FindSelectableItemForward(0, false); + if (this.selectedItemIdx > firstSelectableItemIdx) + this.NavMenuForNewSelectedItemTop(firstSelectableItemIdx, this.GetNumItemsPerPage(), numItems, selectedItemIndexes); + } + else if (this.lastUserInput == KEY_END || this.LastPageKeysIncludes(this.lastUserInput)) + { + // Go to the last item in the list + var lastSelectableItem = this.FindSelectableItemBackward(numItems-1, false); + if (this.selectedItemIdx < lastSelectableItem) + this.NavMenuForNewSelectedItemBottom(lastSelectableItem, this.GetNumItemsPerPage(), numItems, selectedItemIndexes, true); + } + // Enter key or additional select-item key: Select the item & quit out of the input loop + else if ((this.lastUserInput == KEY_ENTER) || (this.SelectItemKeysIncludes(this.lastUserInput))) + { + // Let the user select the item if ValidateSelectItem() returns true + var allowSelectItem = true; + if (typeof(this.ValidateSelectItem) === "function") + allowSelectItem = this.ValidateSelectItem(this.GetItem(this.selectedItemIdx).retval); + if (allowSelectItem) + { + // If multi-select is enabled and if the user hasn't made any choices, + // then add the current item to the user choices. Otherwise, choose + // the current item. Then exit. + if (this.multiSelect) + { + if (Object.keys(selectedItemIndexes).length == 0) + selectedItemIndexes[+(this.selectedItemIdx)] = true; + } + else + retVal = this.GetItem(this.selectedItemIdx).retval; + + // Run the OnItemSelect event function + if (typeof(this.OnItemSelect) === "function") + this.OnItemSelect(retVal, true); + + // Exit the input loop if this.exitOnItemSelect is set to true + if (this.exitOnItemSelect) + continueOn = false; + } + } + else if (this.lastUserInput == " ") // Add the current item to multi-select + { + // Add the current item to multi-select if multi-select is enabled + if (this.multiSelect) + { + // Only let the user select the item if ValidateSelectItem() returns true + var allowSelectItem = true; + if (typeof(this.ValidateSelectItem) === "function") + allowSelectItem = this.ValidateSelectItem(this.GetItem(this.selectedItemIdx).retval); + if (allowSelectItem) + { + var added = false; // Will be true if added or false if deleted + if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))) + delete selectedItemIndexes[+(this.selectedItemIdx)]; + else + { + var addIt = true; + if (this.maxNumSelections > 0) + addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections); + if (addIt) + { + selectedItemIndexes[+(this.selectedItemIdx)] = true; + added = true; + } + } + + // Run the OnItemSelect event function + if (typeof(this.OnItemSelect) === "function") + { + //this.OnItemSelect = function(pItemRetval, pSelected) { } + this.OnItemSelect(this.GetItem(this.selectedItemIdx).retval, added); + } + + // Draw a character next to the item if it's selected, or nothing if it's not selected + var XPos = this.pos.x + this.size.width - 2; + var YPos = this.pos.y+(this.selectedItemIdx-this.topItemIdx); + if (this.borderEnabled) + { + --XPos; + ++YPos; + } + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + --XPos; + console.gotoxy(XPos, YPos); + if (added) + { + // If the item color is an array, then default to a color string here + var itemColor = this.GetColorForItem(this.selectedItemIdx, true); + if (Array.isArray(itemColor)) + { + var bkgColor = getBackgroundAttrAtIdx(itemColor, this.size.width-1); + itemColor = "\x01n\x01h\x01g" + bkgColor; + } + console.print(itemColor + " " + this.multiSelectItemChar + "\x01n"); + } + else + { + // Display the last 2 characters of the regular item text + var itemText = this.GetItemText(this.selectedItemIdx, null, true, false); + var textToPrint = substrWithAttrCodes(itemText, console.strlen(itemText)-2, 2); + console.print(textToPrint + "\x01n"); + } + } + } + } + // For numbered mode, if the user enters a number, allow the user to + // choose an item by typing its number. + else if (/[0-9]/.test(this.lastUserInput) && this.numberedMode) + { + var originalCurpos = console.getxy(); + + // Put the user's input back in the input buffer to + // be used for getting the rest of the message number. + console.ungetstr(this.lastUserInput); + // Move the cursor to the bottom of the screen and + // prompt the user for the message number. + var promptX = this.pos.x; + var promptY = this.pos.y+this.size.height; + console.gotoxy(promptX, promptY); + printf("\x01n%" + this.size.width + "s", ""); // Blank out what might be on the screen already + console.gotoxy(promptX, promptY); + console.print("\x01cItem #: \x01h"); + var userEnteredItemNum = console.getnum(numItems); + // Blank out the input prompt + console.gotoxy(promptX, promptY); + printf("\x01n%" + this.size.width + "s", ""); + // If the user entered a number, then get that item's return value + // and stop the input loop. + if (userEnteredItemNum > 0) + { + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = userEnteredItemNum-1; + if (this.multiSelect) + { + if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))) + delete selectedItemIndexes[+(this.selectedItemIdx)]; + else + { + var addIt = true; + if (this.maxNumSelections > 0) + addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections); + if (addIt) + selectedItemIndexes[+(this.selectedItemIdx)] = true; + } + } + else + { + retVal = this.GetItem(this.selectedItemIdx).retval; + continueOn = false; + } + // If the item typed by the user is different than the current selected item + // index, then refresh the selected item on the menu (if they're visible). + // If multi-select mode is enabled, also toggle the checkmark in the item text. + if (this.selectedItemIdx != oldSelectedItemIdx) + { + if (this.ScreenRowForItem(oldSelectedItemIdx) > -1) + { + var oldIsSelected = selectedItemIndexes.hasOwnProperty(oldSelectedItemIdx); + this.WriteItemAtItsLocation(oldSelectedItemIdx, false, oldIsSelected); + } + if (this.ScreenRowForItem(this.selectedItemIdx) > -1) + { + var newIsSelected = selectedItemIndexes.hasOwnProperty(this.selectedItemIdx); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, newIsSelected); + } + } + + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + else + console.gotoxy(originalCurpos); // Move the cursor back where it was + } + else + { + // See if the user pressed a hotkey set for one of the items. If so, + // then choose that item. + for (var i = 0; i < numItems; ++i) + { + var theItem = this.GetItem(i); + for (var h = 0; h < theItem.hotkeys.length; ++h) + { + var userPressedHotkey = false; + if (this.hotkeyCaseSensitive) + userPressedHotkey = (this.lastUserInput == theItem.hotkeys.charAt(h)); + else + userPressedHotkey = (this.lastUserInput.toUpperCase() == theItem.hotkeys.charAt(h).toUpperCase()); + if (userPressedHotkey) + { + if (this.multiSelect) + { + if (selectedItemIndexes.hasOwnProperty(i)) + delete selectedItemIndexes[i]; + else + { + var addIt = true; + if (this.maxNumSelections > 0) + addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections); + if (addIt) + selectedItemIndexes[i] = true; + } + // TODO: Screen refresh? + } + else + { + retVal = theItem.retval; + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = i; + continueOn = false; + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + break; + } + } + } + } + } + } + else + { + // The user's terminal doesn't support ANSI + var userAnswerIsValid = false; + do + { + console.print("\x01n\x01c\x01hY\x01n\x01cour \x01hC\x01n\x01choice\x01h\x01g: \x01c"); + console.attributes = "N"; + var userEnteredItemNum = console.getnum(numItems); + this.lastUserInput = userEnteredItemNum.toString(); + if (!console.aborted && userEnteredItemNum > 0) + { + if (this.ItemIsSelectable(userEnteredItemNum-1)) + { + var chosenItem = this.GetItem(userEnteredItemNum-1); + if (typeof(chosenItem) === "object" && chosenItem.hasOwnProperty("retval")) + retVal = chosenItem.retval; + userAnswerIsValid = true; + } + } + else + { + this.lastUserInput = "Q"; // To signify quitting + userAnswerIsValid = true; + } + } while (!userAnswerIsValid); + } + + // Set the screen color back to normal so that text written to the screen + // after this looks good. + console.attributes = "N"; + + // If in multi-select mode, populate userChoices with the choices + // that the user selected. + if (this.multiSelect && (Object.keys(selectedItemIndexes).length > 0)) + { + userChoices = []; + for (var prop in selectedItemIndexes) + userChoices.push(this.GetItem(prop).retval); + } + + return (this.multiSelect ? userChoices : retVal); +} +// Performs the key-up behavior for showing the menu items +// +// Parameters: +// pSelectedItemIndexes: An object containing indexes of selected items. This is +// normally a temporary object created/used in GetVal(). +// pNumItems: The pre-calculated number of menu items. If this not given, this +// will be retrieved by calling NumItems(). +function DDLightbarMenu_DoKeyUp(pSelectedItemIndexes, pNumItems) +{ + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + if (this.selectedItemIdx > this.FirstSelectableItemIdx(numItems)) + { + var prevSelectableItemIdx = this.FindSelectableItemBackward(this.selectedItemIdx-1, false); + if (prevSelectableItemIdx < this.selectedItemIdx && prevSelectableItemIdx > -1) + { + // Draw the current item in regular colors + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = prevSelectableItemIdx; + var numItemsDiff = oldSelectedItemIdx - prevSelectableItemIdx; + // Draw the new current item in selected colors + // If the selected item is above the top of the menu, then we'll need to + // scroll the items down. + if (this.selectedItemIdx < this.topItemIdx) + { + this.topItemIdx -= numItemsDiff; + this.Draw(selectedItemIndexes); + } + else + { + // The selected item is not above the top of the menu, so we can + // just draw the selected item highlighted. + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + } + else + { + // selectedItemIdx is 0. If wrap navigation is enabled, then go to the + // last item. + // If there are unselectable items above the current one, then scroll the item list up before + // wrapping down to the last selectable item + /* + var canWrapNav = false; + if (this.allowUnselectableItems && this.selectedItemIdx > 0) + { + if (this.topItemIdx > 0) + { + --this.topItemIdx; + this.Draw(selectedItemIndexes); + } + else + canWrapNav = true; + } + */ + var canWrapNav = true; + if (this.allowUnselectableItems) + { + canWrapNav = false; + if (this.selectedItemIdx > 0) + { + if (this.topItemIdx > 0) + { + --this.topItemIdx; + this.Draw(selectedItemIndexes); + } + else + canWrapNav = true; + } + } + if (canWrapNav && this.wrapNavigation) + { + // If there are more items than can fit on the menu, then ideally, the top + // item index would be the one at the top of the page where the rest of the items + // fill the menu. + var prevSelectableItemIdx = this.FindSelectableItemBackward(numItems-1, false); + if (prevSelectableItemIdx > this.selectedItemIdx && prevSelectableItemIdx > -1) + { + // Draw the current item in regular colors + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + // Set the new selected item index, and figure out what page it's on + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = prevSelectableItemIdx; + // Calculate the top index for the page of the new selected item. If the page + // is different, go to that page. + if (this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems)) + this.Draw(selectedItemIndexes); + else // The selected item is on the current page + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + } + } +} +// Performs the key-down behavior for showing the menu items +// +// Parameters: +// pSelectedItemIndexes: An object containing indexes of selected items. This is +// normally a temporary object created/used in GetVal(). +// pNumItems: The pre-calculated number of menu items. If this not given, this +// will be retrieved by calling NumItems(). +function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems) +{ + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + + if (this.selectedItemIdx < this.LastSelectableItemIdx(numItems)) + { + var nextSelectableItemIdx = this.FindSelectableItemForward(this.selectedItemIdx+1, false); + if (nextSelectableItemIdx > this.selectedItemIdx) + { + // Draw the current item in regular colors + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = nextSelectableItemIdx; + var numItemsDiff = nextSelectableItemIdx - oldSelectedItemIdx; + // Draw the new current item in selected colors + // If the selected item is below the bottom of the menu, then we'll need to + // scroll the items up. + var numItemsPerPage = this.GetNumItemsPerPage(); + if (this.selectedItemIdx > this.topItemIdx + numItemsPerPage-1) + { + this.topItemIdx += numItemsDiff; + this.Draw(selectedItemIndexes); + } + else + { + // The selected item is not below the bottom of the menu, so we can + // just draw the selected item highlighted. + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + } + else + { + // selectedItemIdx is the last item index. If wrap navigation is enabled, + // then go to the first item. + // If there are unselectable items below the current one, then scroll the item down up before + // wrapping up to the first selectable item + /* + var canWrapNav = false; + if (this.allowUnselectableItems && this.selectedItemIdx > 0) + { + var topIndexForLastPage = numItems - this.GetNumItemsPerPage(); + if (topIndexForLastPage < 0) + topIndexForLastPage = 0; + else if (topIndexForLastPage >= numItems) + topIndexForLastPage = numItems - 1; + if (this.topItemIdx < topIndexForLastPage) + { + ++this.topItemIdx; + this.Draw(selectedItemIndexes); + } + else + canWrapNav = true; + } + */ + var canWrapNav = true; + if (this.allowUnselectableItems) + { + canWrapNav = false; + if (this.selectedItemIdx > 0) + { + var topIndexForLastPage = numItems - this.GetNumItemsPerPage(); + if (topIndexForLastPage < 0) + topIndexForLastPage = 0; + else if (topIndexForLastPage >= numItems) + topIndexForLastPage = numItems - 1; + if (this.topItemIdx < topIndexForLastPage) + { + ++this.topItemIdx; + this.Draw(selectedItemIndexes); + } + else + canWrapNav = true; + } + } + if (canWrapNav && this.wrapNavigation) + { + // If there are more items than can fit on the menu, then ideally, the top + // item index would be the one at the top of the page where the rest of the items + // fill the menu. + //var nextSelectableItemIdx = this.FindSelectableItemForward(0, false); + var nextSelectableItemIdx = this.FirstSelectableItemIdx(numItems); + if (nextSelectableItemIdx < this.selectedItemIdx && nextSelectableItemIdx > -1) + { + // Draw the current item in regular colors + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + // Set the new selected item index, and figure out what page it's on + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = nextSelectableItemIdx; + // Calculate the top index for the page of the new selected item. If the page + // is different, go to that page. + if (this.CalcPageForItemAndSetTopItemIdx(this.GetNumItemsPerPage(), numItems)) + this.Draw(selectedItemIndexes); + else // The selected item is on the current page + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + + // Older, before non-selectable items: + /* + // Draw the current item in regular colors + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); + // Go to the first item and scroll to the top if necessary + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = 0; + var oldTopItemIdx = this.topItemIdx; + this.topItemIdx = 0; + if (this.topItemIdx != oldTopItemIdx) + this.Draw(selectedItemIndexes); + else + { + // Draw the new current item in selected colors + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + */ + } + } +} +// Performs the page-up behavior for showing the menu items +// +// Parameters: +// pSelectedItemIndexes: An object containing indexes of selected items. This is +// normally a temporary object created/used in GetVal(). +// pNumItems: The pre-calculated number of menu items. If this not given, this +// will be retrieved by calling NumItems(). +function DDLightbarMenu_DoPageUp(pSelectedItemIndexes, pNumItems) +{ + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + var numItemsPerPage = this.GetNumItemsPerPage(); + + var prevSelectableItemIdx = 0; + var currentPageNum = findPageNumOfItemNum(this.selectedItemIdx+1, numItemsPerPage, numItems, false); + if (currentPageNum > 1) + { + var startIdxToCheck = this.selectedItemIdx - numItemsPerPage; + if (startIdxToCheck < 0) + { + //startIdxToCheck = 0; + startIdxToCheck = (this.selectedItemIdx > 0 ? this.selectedItemIdx - 1 : 0); + } + prevSelectableItemIdx = this.FindSelectableItemBackward(startIdxToCheck, this.wrapNavigation); + //this.NavMenuForNewSelectedItemTop(prevSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes); + } + else + prevSelectableItemIdx = this.FindSelectableItemForward(0, this.wrapNavigation); + this.NavMenuForNewSelectedItemTop(prevSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes); + + // Older, before un-selectable items: + /* + // Only do this if we're not already at the top of the list + if (this.topItemIdx > 0) + { + var oldSelectedItemIdx = this.selectedItemIdx; + var numItemsPerPage = this.GetNumItemsPerPage(); + var newTopItemIdx = this.topItemIdx - numItemsPerPage; + if (newTopItemIdx < 0) + newTopItemIdx = 0; + if (newTopItemIdx != this.topItemIdx) + { + this.topItemIdx = newTopItemIdx; + this.selectedItemIdx -= numItemsPerPage; + if (this.selectedItemIdx < 0) + this.selectedItemIdx = 0; + this.Draw(selectedItemIndexes); + } + else + { + // The top index is the top index for the last page. + // If wrapping is enabled, then go back to the first page. + if (this.wrapNavigation) + { + var topIndexForLastPage = numItems - numItemsPerPage; + if (topIndexForLastPage < 0) + topIndexForLastPage = 0; + else if (topIndexForLastPage >= numItems) + topIndexForLastPage = numItems - 1; + + this.topItemIdx = topIndexForLastPage; + this.selectedItemIdx = topIndexForLastPage; + this.Draw(selectedItemIndexes); + } + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + else + { + // We're already showing the first page of items. + // If the currently selected item is not the first + // item, then make it so. + if (this.selectedItemIdx > 0) + { + var oldSelectedItemIdx = this.selectedItemIdx; + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.selectedItemIdx = 0; + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + } + */ +} +// Performs the page-down behavior for showing the menu items +// +// Parameters: +// pSelectedItemIndexes: An object containing indexes of selected items. This is +// normally a temporary object created/used in GetVal(). +// pNumItems: The pre-calculated number of menu items. If this not given, this +// will be retrieved by calling NumItems(). +function DDLightbarMenu_DoPageDown(pSelectedItemIndexes, pNumItems) +{ + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + + var numItemsPerPage = this.GetNumItemsPerPage(); + var startIdxToCheck = this.selectedItemIdx + numItemsPerPage; + if (startIdxToCheck >= numItems) + startIdxToCheck = numItems - 1; + var nextSelectableItemIdx = this.FindSelectableItemForward(startIdxToCheck, this.wrapNavigation); + this.NavMenuForNewSelectedItemBottom(nextSelectableItemIdx, numItemsPerPage, numItems, selectedItemIndexes, true); + + // Older, before un-selectable items: + /* + // Only do the pageDown if we're not showing the last item already + var lastItemIdx = numItems - 1; + if (lastItemIdx > this.topItemIdx+numItemsPerPage-1) + { + var oldSelectedItemIdx = this.selectedItemIdx; + // Figure out the top index for the last page. + var topIndexForLastPage = numItems - numItemsPerPage; + if (topIndexForLastPage < 0) + topIndexForLastPage = 0; + else if (topIndexForLastPage >= numItems) + topIndexForLastPage = numItems - 1; + if (topIndexForLastPage != this.topItemIdx) + { + // Update the selected & top item indexes + this.selectedItemIdx += numItemsPerPage; + this.topItemIdx += numItemsPerPage; + if (this.selectedItemIdx >= topIndexForLastPage) + this.selectedItemIdx = topIndexForLastPage; + if (this.topItemIdx > topIndexForLastPage) + this.topItemIdx = topIndexForLastPage; + this.Draw(selectedItemIndexes); + } + else + { + // The top index is the top index for the last page. + // If wrapping is enabled, then go back to the first page. + if (this.wrapNavigation) + { + this.topItemIdx = 0; + this.selectedItemIdx = 0; + } + this.Draw(selectedItemIndexes); + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + else + { + // We're already showing the last page of items. + // If the currently selected item is not the last + // item, then make it so. + if (this.selectedItemIdx < lastItemIdx) + { + var oldSelectedItemIdx = this.selectedItemIdx; + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.selectedItemIdx = lastItemIdx; + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } + } + */ +} + +function DDLightbarMenu_NavMenuForNewSelectedItemTop(pNewSelectedItemIdx, pNumItemsPerPage, pNumItems, pSelectedItemIndexes) +{ + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + if (pNewSelectedItemIdx > -1 && pNewSelectedItemIdx != this.selectedItemIdx) + { + var indexDiff = 0; + if (pNewSelectedItemIdx < this.selectedItemIdx) + indexDiff = this.selectedItemIdx - pNewSelectedItemIdx; + else if (pNewSelectedItemIdx > this.selectedItemIdx) + indexDiff = pNewSelectedItemIdx - this.selectedItemIdx; + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = pNewSelectedItemIdx; + var pageNum = findPageNumOfItemNum(this.selectedItemIdx + 1, pNumItemsPerPage, numItems, false); + if (pageNum > 0) + { + var newTopItemIdx = pNumItemsPerPage * (pageNum-1); + if (newTopItemIdx != this.topItemIdx) + { + this.topItemIdx = newTopItemIdx; + this.Draw(selectedItemIndexes); + } + else + { + // We're already showing the first page of items. + // Re-draw the old & new selected items with the proper highlighting + this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + } + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } +} + +function DDLightbarMenu_NavMenuForNewSelectedItemBottom(pNewSelectedItemIdx, pNumItemsPerPage, pNumItems, pSelectedItemIndexes, pLastItemAtBottom) +{ + var numItemsPerPage = (typeof(pNumItemsPerPage) === "number" ? pNumItemsPerPage : this.GetNumItemsPerPage()); + var selectedItemIndexes = (typeof(pSelectedItemIndexes) === "object" ? pSelectedItemIndexes : {}); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + var lastItemAtBottom = (typeof(pLastItemAtBottom) === "boolean" ? pLastItemAtBottom : false); + if (pNewSelectedItemIdx > -1 && pNewSelectedItemIdx != this.selectedItemIdx) + { + if (lastItemAtBottom) + { + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = pNewSelectedItemIdx; + var newTopItemIdx = pNewSelectedItemIdx - numItemsPerPage + 1; + if (newTopItemIdx < 0) + newTopItemIdx = 0; + if (newTopItemIdx != this.topItemIdx) + { + this.topItemIdx = newTopItemIdx; + this.Draw(selectedItemIndexes); + } + else + { + // We're already showing the page with the calculated top index + // Re-draw the old & new selected items with the proper highlighting + this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + } + } + else + { + var indexDiff = 0; + if (pNewSelectedItemIdx < this.selectedItemIdx) + indexDiff = this.selectedItemIdx - pNewSelectedItemIdx; + else if (pNewSelectedItemIdx > this.selectedItemIdx) + indexDiff = pNewSelectedItemIdx - this.selectedItemIdx; + var oldSelectedItemIdx = this.selectedItemIdx; + this.selectedItemIdx = pNewSelectedItemIdx; + var pageNum = findPageNumOfItemNum(this.selectedItemIdx + 1, numItemsPerPage, numItems, false); + if (pageNum > 0) + { + var newTopItemIdx = numItemsPerPage * (pageNum-1); + // Figure out the top index for the last page. + var topIndexForLastPage = numItems - numItemsPerPage; + if (topIndexForLastPage < 0) + topIndexForLastPage = 0; + else if (topIndexForLastPage >= numItems) + topIndexForLastPage = numItems - 1; + if (newTopItemIdx != topIndexForLastPage) + { + this.topItemIdx = newTopItemIdx; + this.Draw(selectedItemIndexes); + } + else + { + // We're already showing the last page of items. + // Re-draw the old & new selected items with the proper highlighting + this.WriteItemAtItsLocation(oldSelectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + } + } + } + if (typeof(this.OnItemNav) === "function") + this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx); + } +} + +// Sets the characters to use for drawing the border. Takes an object specifying +// the values to set, but does not overwrite the whole borderChars object in the +// menu object. +// +// Parameters: +// pBorderChars: An object with the following properties: +// upperLeft: The character to use for the upper-left corner +// upperRight: The character to use for the upper-right corner +// lowerLeft: The character to use for the lower-left corner +// lowerRight: The character to use for the lower-right corner +// top: The character to use for the top border +// bottom: The character to use for the bottom border +// left: The character to use for the left border +// right: The character to use for the right border +function DDLightbarMenu_SetBorderChars(pBorderChars) +{ + if (typeof(pBorderChars) !== "object") + return; + + var borderPropNames = [ "upperLeft", "upperRight", "lowerLeft", "lowerRight", + "top", "bottom", "left", "right" ]; + for (var i = 0; i < borderPropNames.length; ++i) + { + if (pBorderChars.hasOwnProperty(borderPropNames[i])) + this.borderChars[borderPropNames[i]] = pBorderChars[borderPropNames[i]]; + } +} + +// Sets the colors to use with the menu. Takes an object specifying the values +// to set, but does not overwrite the whole colors object in the menu object. +// +// Parameters: +// pColors: An object with the following properties: +// itemColor: The color to use for non-highlighted items +// selectedItemColor: The color to use for selected items +// itemTextCharHighlightColor: The color to use for a highlighted +// non-space character in an item text +// (specified by having a & in the item +// text). +// It's important not to specify a "\x01n" +// in here in case the item text should +// have a background color. +// borderColor: The color to use for the border +// scrollbarScrollBlockColor: The color to use for the scrollbar block +// scrollbarBGColor: The color to use for the scrollbar background +function DDLightbarMenu_SetColors(pColors) +{ + if (typeof(pColors) != "object") + return; + + var colorPropNames = ["itemColor", "selectedItemColor", "altItemColor", "altSelectedItemColor", + "itemTextCharHighlightColor", "borderColor", "scrollbarScrollBlockColor", + "scrollbarBGColor", "unselectableItemColor"]; + for (var i = 0; i < colorPropNames.length; ++i) + { + if (pColors.hasOwnProperty(colorPropNames[i])) + this.colors[colorPropNames[i]] = pColors[colorPropNames[i]]; + } +} + +// Returns the number of (possible) items per page +function DDLightbarMenu_GetNumItemsPerPage() +{ + var numItemsPerPage = this.size.height; + if (this.borderEnabled) + numItemsPerPage -= 2; + return numItemsPerPage; +} + +// Gets the top item index of the last page of items +function DDLightbarMenu_GetTopItemIdxOfLastPage() +{ + var numItemsPerPage = this.size.height; + if (this.borderEnabled) + numItemsPerPage -= 2; + var topItemIndex = this.NumItems() - numItemsPerPage; + if (topItemIndex < 0) + topItemIndex = 0; + return topItemIndex; +} + +// Calculates & sets the top item index to the top item of the last page of items +function DDLightbarMenu_CalcAndSetTopItemIdxToTopOfLastPage(pNumItems) +{ + var numItemsPerPage = this.size.height; + if (this.borderEnabled) + numItemsPerPage -= 2; + + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + this.topItemIdx = numItems - numItemsPerPage; + if (this.topItemIdx < 0) + this.topItemIdx = 0; +} + +// Calculates the page for an item (by its index) and sets the top index for the menu +// based on that page. +// +// Parameters: +// pNumItemsPerPage: Optional - The number of items per page, if already calculated +// pNumItems: Optional - The number of items in the menu, if already known +// +// +// Return value: Boolean - Whether or not the top index of the menu changed +function DDLightbarMenu_CalcPageForItemAndSetTopItemIdx(pNumItemsPerPage, pNumItems) +{ + var numItemsPerPage = (typeof(pNumItemsPerPage) === "number" ? pNumItemsPerPage : this.GetNumItemsPerPage()); + var numItems = (typeof(pNumItems) === "number" ? pNumItems : this.NumItems()); + + var topItemIdxChanged = false; + var pageNum = findPageNumOfItemNum(this.selectedItemIdx+1, numItemsPerPage, numItems, false); + if (pageNum > 0) + { + var topItemIdxOnNewPage = numItemsPerPage * (pageNum-1); + if (topItemIdxOnNewPage + numItemsPerPage >= numItems) + topItemIdxOnNewPage = numItems - numItemsPerPage; + if (topItemIdxOnNewPage < 0) + topItemIdxOnNewPage = 0; + if (topItemIdxOnNewPage != this.topItemIdx) + { + this.topItemIdx = topItemIdxOnNewPage; + topItemIdxChanged = true; + } + } + return topItemIdxChanged; +} + +// Adds additional key characters to cause quitting out of the menu +// in addition to ESC. The keys will be case-sensitive. +// +// Parameters: +// pAdditionalQuitKeys: A string of key characters +function DDLightbarMenu_AddAdditionalQuitKeys(pAdditionalQuitKeys) +{ + if (typeof(pAdditionalQuitKeys) === "string") + this.additionalQuitKeys += pAdditionalQuitKeys; +} + +// Returns whether or not the additional quit keys array contains a given +// key character. +// +// Parameters: +// pKey: The key to look for in the additional quit keys +// +// Return value: Boolean - Whether or not the additional quit keys includes +// pKey +function DDLightbarMenu_QuitKeysIncludes(pKey) +{ + return (this.additionalQuitKeys.indexOf(pKey) > -1); +} + +// Clears the string of additional key characters to quit out of the menu +function DDLightbarMenu_ClearAdditionalQuitKeys() +{ + this.additionalQuitKeys = ""; +} + +function DDLightbarMenu_AddAdditionalPageUpKeys(pAdditionalKeys) +{ + if (typeof(pAdditionalKeys) === "string") + this.additionalPageUpKeys += pAdditionalKeys; +} + +function DDLightbarMenu_PageUpKeysIncludes(pKey) +{ + return (this.additionalPageUpKeys.indexOf(pKey) > -1); +} + +function DDLightbarMenu_ClearAdditionalPageUpKeys() +{ + this.additionalPageUpKeys = ""; +} + +function DDLightbarMenu_AddAdditionalPageDownKeys(pAdditionalKeys) +{ + if (typeof(pAdditionalKeys) === "string") + this.additionalPageDnKeys += pAdditionalKeys; +} + +function DDLightbarMenu_PageDownKeysIncludes(pKey) +{ + return (this.additionalPageDnKeys.indexOf(pKey) > -1); +} + +function DDLightbarMenu_ClearAdditionalPageDownKeys() +{ + this.additionalPageDnKeys = ""; +} + +function DDLightbarMenu_AddAdditionalFirstPageKeys(pAdditionalKeys) +{ + if (typeof(pAdditionalKeys) === "string") + this.additionalFirstPageKeys += pAdditionalKeys; +} + +function DDLightbarMenu_FirstPageKeysIncludes(pKey) +{ + return (this.additionalFirstPageKeys.indexOf(pKey) > -1); +} + +function DDLightbarMenu_ClearAdditionalFirstPageKeys() +{ + this.additionalFirstPageKeys = ""; +} + +function DDLightbarMenu_AddAdditionalLastPageKeys(pAdditionalKeys) +{ + if (typeof(pAdditionalKeys) === "string") + this.additionalLastPageKeys += pAdditionalKeys; +} + +function DDLightbarMenu_LastPageKeysIncludes(pKey) +{ + return (this.additionalLastPageKeys.indexOf(pKey) > -1); +} + +function DDLightbarMenu_ClearAdditionalLastPageKeys() +{ + this.additionalLastPageKeys = ""; +} + +// Adds additional key characters to select any item. The keys will be case-sensitive. +// +// Parameters: +// pAdditionalAddItemKeys: A string containing key characters +function DDLightbarMenu_AddAdditionalSelectItemKeys(pAdditionalAddItemKeys) +{ + this.additionalSelectItemKeys += pAdditionalAddItemKeys; +} + +// Returns whether or not the additional select-item keys array contains a given +// key character. +// +// Parameters: +// pKey: The key to look for in the additional select-itemkeys +// +// Return value: Boolean - Whether or not the additional select-item keys includes +// pKey +function DDLightbarMenu_SelectItemKeysIncludes(pKey) +{ + return (this.additionalSelectItemKeys.indexOf(pKey) > -1); +} + +// Clears the string of additional key characters to select any item +function DDLightbarMenu_ClearAdditionalSelectItemKeys() +{ + this.additionalSelectItemKeys = ""; +} + +// Displays an initial scrollbar +// +// Parameters: +// pSolidBlockStartRow: The starting row for the solid/bright blocks +// pNumSolidBlocks: The number of solid/bright blocks to write. If this +// is omitted, this.scrollbarInfo.numSolidScrollBlocks +// will be used. +function DDLightbarMenu_DisplayInitialScrollbar(pSolidBlockStartRow, pNumSolidBlocks) +{ + var numSolidBlocks = (typeof(pNumSolidBlocks) == "number" ? pNumSolidBlocks : this.scrollbarInfo.numSolidScrollBlocks); + + var numSolidBlocksWritten = 0; + var wroteBrightBlockColor = false; + var wroteDimBlockColor = false; + var startY = this.pos.y; + var screenBottomRow = this.pos.y + this.size.height - 1; + var scrollbarCol = this.pos.x + this.size.width - 1; + if (this.borderEnabled) + { + ++startY; + --screenBottomRow; + --scrollbarCol; + } + this.scrollbarInfo.solidBlockLastStartRow = startY; + for (var screenY = startY; screenY <= screenBottomRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + if ((screenY >= pSolidBlockStartRow) && (numSolidBlocksWritten < numSolidBlocks)) + { + if (!wroteBrightBlockColor) + { + console.print("\x01n" + this.colors.scrollbarScrollBlockColor); + wroteBrightBlockColor = true; + wroteDimBlockColor = false; + } + console.print(this.scrollbarInfo.blockChar); + ++numSolidBlocksWritten; + } + else + { + if (!wroteDimBlockColor) + { + console.print("\x01n" + this.colors.scrollbarBGColor); + wroteDimBlockColor = true; + } + console.print(this.scrollbarInfo.BGChar); + } + } +} + +// For the DigDistMsgReader class: Updates the scrollbar for a message, for use +// in enhanced reader mode. This does only the necessary character updates to +// minimize the number of characters that need to be updated on the screen. +// +// Parameters: +// pNewStartRow: The new (current) start row for solid/bright blocks +// pOldStartRow: The old start row for solid/bright blocks +// pNumSolidBlocks: The number of solid/bright blocks. If this is omitted, +// this.scrollbarInfo.numSolidScrollBlocks will be used. +function DDLightbarMenu_UpdateScrollbar(pNewStartRow, pOldStartRow, pNumSolidBlocks) +{ + var numSolidBlocks = (typeof(pNumSolidBlocks) == "number" ? pNumSolidBlocks : this.scrollbarInfo.numSolidScrollBlocks); + + var startY = this.pos.y; + var screenBottomRow = this.pos.y + this.size.height - 1; + var scrollbarCol = this.pos.x + this.size.width - 1; + if (this.borderEnabled) + { + ++startY; + --screenBottomRow; + --scrollbarCol; + } + + // Calculate the difference in the start row. If the difference is positive, + // then the solid block section has moved down; if the diff is negative, the + // solid block section has moved up. + var solidBlockStartRowDiff = pNewStartRow - pOldStartRow; + var oldLastRow = pOldStartRow + numSolidBlocks - 1; + var newLastRow = pNewStartRow + numSolidBlocks - 1; + if (solidBlockStartRowDiff > 0) + { + // The solid block section has moved down + if (pNewStartRow > oldLastRow) + { + // No overlap + // Write dim blocks over the old solid block section + console.print("\x01n" + this.colors.scrollbarBGColor); + for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.BGChar); + } + // Write solid blocks in the new locations + console.print("\x01n" + this.colors.scrollbarScrollBlockColor); + for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.blockChar); + } + } + else + { + // There is some overlap + // Write dim blocks on top + console.print("\x01n" + this.colors.scrollbarBGColor); + for (var screenY = pOldStartRow; screenY < pNewStartRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.BGChar); + } + // Write bright blocks on the bottom + console.print("\x01n" + this.colors.scrollbarScrollBlockColor); + for (var screenY = oldLastRow+1; screenY <= newLastRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.blockChar); + } + } + } + else if (solidBlockStartRowDiff < 0) + { + // The solid block section has moved up + if (pOldStartRow > newLastRow) + { + // No overlap + // Write dim blocks over the old solid block section + console.print("\x01n" + this.colors.scrollbarBGColor); + for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.BGChar); + } + // Write solid blocks in the new locations + console.print("\x01n" + this.colors.scrollbarScrollBlockColor); + for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.blockChar); + } + } + else + { + // There is some overlap + // Write bright blocks on top + console.print("\x01n" + this.colors.scrollbarScrollBlockColor); + var endRow = pOldStartRow; + for (var screenY = pNewStartRow; screenY < endRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.blockChar); + } + // Write dim blocks on the bottom + console.print("\x01n" + this.colors.scrollbarBGColor); + endRow = pOldStartRow + numSolidBlocks; + for (var screenY = pNewStartRow+numSolidBlocks; screenY < endRow; ++screenY) + { + console.gotoxy(scrollbarCol, screenY); + console.print(this.scrollbarInfo.BGChar); + } + } + } +} + +// Calculates the starting row for the solid blocks on the scrollbar +// +// Return value: The starting row for the solid blocks on the scrollbar +function DDLightbarMenu_CalcScrollbarSolidBlockStartRow() +{ + var scrollbarStartY = this.pos.y; + var scrollbarHeight = this.size.height; + if (this.borderEnabled) + { + ++scrollbarStartY; + scrollbarHeight -= 2; + } + var scrollbarBottomY = scrollbarStartY + scrollbarHeight - 1; + var solidBlockStartRow = scrollbarStartY; + var numMenuItems = this.NumItems(); + if (numMenuItems > 0) + { + var scrollbarFraction = this.selectedItemIdx / numMenuItems; + var scrollbarStartRow = scrollbarStartY + Math.floor(scrollbarHeight * scrollbarFraction); + solidBlockStartRow = scrollbarStartRow - Math.floor(this.scrollbarInfo.numSolidScrollBlocks / 2); + // Don't let the solid blocks go above the starting screen row or below the ending + // screen row of the scrollbar + if (solidBlockStartRow < scrollbarStartY) + solidBlockStartRow = scrollbarStartY; + else if (solidBlockStartRow + this.scrollbarInfo.numSolidScrollBlocks > scrollbarBottomY) + solidBlockStartRow = scrollbarBottomY - this.scrollbarInfo.numSolidScrollBlocks + 1; + } + return solidBlockStartRow; +} + +// Updates the scrollbar position based on the currently-selected +// item index, this.selectedItemIdx. +// +// Parameters: +// pForceUpdate: Boolean - Whether or not to force the redraw regardless of block location. +// Defaults to false. +function DDLightbarMenu_UpdateScrollbarWithHighlightedItem(pForceUpdate) +{ + var forceUpdate = (typeof(pForceUpdate) === "boolean" ? pForceUpdate : false); + var solidBlockStartRow = this.CalcScrollbarSolidBlockStartRow(); + if (forceUpdate || (solidBlockStartRow != this.scrollbarInfo.solidBlockLastStartRow)) + this.UpdateScrollbar(solidBlockStartRow, this.scrollbarInfo.solidBlockLastStartRow, this.scrollbarInfo.numSolidScrollBlocks); + this.scrollbarInfo.solidBlockLastStartRow = solidBlockStartRow; +} + +function DDLightbarMenu_CanShowAllItemsInWindow() +{ + var pageHeight = (this.borderEnabled ? this.size.height - 2 : this.size.height); + return (this.NumItems() <= pageHeight); +} + +// Makes an item object that is compatible with DDLightbarMenu, with a given +// item text and return value. +// +// Parameters: +// pText: The text to show in the menu for the item +// pRetval: The return value of the item when the user selects it from the menu +// +// Return value: An object with the given text & return value compatible with DDLightbarMenu +function DDLightbarMenu_MakeItemWithTextAndRetval(pText, pRetval) +{ + var item = getDefaultMenuItem(); + item.text = pText; + item.retval = pRetval; + return item; +} + +// Makes an item object that is compatible with DDLightbarMenu, with a given +// return value. +// +// Parameters: +// pRetval: The return value of the item when the user selects it from the menu +// +// Return value: An object with the given return value compatible with DDLightbarMenu +function DDLightbarMenu_MakeItemWithRetval(pRetval) +{ + var item = getDefaultMenuItem(); + item.retval = pRetval; + return item; +} + +// Returns whether an item is set to use the alternate item colors +// +// Parameters: +// pItemIndex: The index of the item +// +// Return value: Boolean - Whether or not an item is configured to use alternate item colors +function DDLightbarMenu_ItemUsesAltColors(pItemIndex) +{ + if ((pItemIndex < 0) || (pItemIndex >= this.NumItems())) + return false; + + return this.GetItem(pItemIndex).useAltColors; +} + +// Returns either the normal or alternate color for an item +// +// Parameters: +// pItemIndex: The index of the item +// pSelected: Whether or not to use selected item colors. Defaults to false. +// +// Return value: Either colors.itemColor or colors.altItemColor +function DDLightbarMenu_GetColorForItem(pItemIndex, pSelected) +{ + if ((pItemIndex < 0) || (pItemIndex >= this.NumItems())) + return ""; + + var selected = (typeof(pSelected) == "boolean" ? pSelected : false); + if (selected) + return (this.GetItem(pItemIndex).useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor); + else + return (this.GetItem(pItemIndex).useAltColors ? this.colors.altItemColor : this.colors.itemColor); +} + +// Returns either the selected or alternate selected color for an item +// +// Parameters: +// pItemIndex: The index of the item +// +// Return value: Either colors.selectedItemColor or colors.altSelectedItemColor +function DDLightbarMenu_GetSelectedColorForItem(pItemIndex) +{ + if (typeof(pItemIndex) !== "number") + return; + if ((pItemIndex < 0) || (pItemIndex >= this.NumItems())) + return ""; + + return (this.GetItem(pItemIndex).useAltColors ? this.colors.altSelectedItemColor : this.colors.selectedItemColor); +} + +// Sets the selected item index for the menu, and sets anything else as appropriate +// (such as the index of the topmost menu item). +// +// Parameters: +// pSelectedItemIdx: The index of the selected item +function DDLightbarMenu_SetSelectedItemIdx(pSelectedItemIdx) +{ + if (typeof(pSelectedItemIdx) !== "number") + return; + if ((pSelectedItemIdx < 0) || (pSelectedItemIdx >= this.NumItems())) + return; + + this.selectedItemIdx = pSelectedItemIdx; + if (this.selectedItemIdx == 0) + this.topItemIdx = 0; + else if (this.selectedItemIdx >= this.topItemIdx+this.GetNumItemsPerPage()) + this.topItemIdx = this.selectedItemIdx - this.GetNumItemsPerPage() + 1; + else if (this.selectedItemIdx < this.topItemIdx) + this.topItemIdx = this.selectedItemIdx; +} + +// Gets the index of the bottommost item on the menu +function DDLightbarMenu_GetBottomItemIdx() +{ + var bottomItemIdx = this.topItemIdx + this.size.height - 1; + if (this.borderEnabled) + bottomItemIdx -= 2; + return bottomItemIdx; +} + +// Returns the absolute screen position (x, y) of the topmost displayed item on the menu +// +// Return value: An object with the following properties: +// x: The horizontal screen location of the top item (1-based) +// y: The vertical screen location of the top item (1-based) +function DDLightbarMenu_GetTopDisplayedItemPos() +{ + var itemPos = { + x: this.pos.x, + y: this.pos.y + }; + if (this.borderEnabled) + { + ++itemPos.x; + ++itemPos.y; + } + return itemPos; +} + +// Returns the absolute screen position (x, y) of the bottommost displayed item on the menu +// +// Return value: An object with the following properties: +// x: The horizontal screen location of the top item (1-based) +// y: The vertical screen location of the top item (1-based) +function DDLightbarMenu_GetBottomDisplayedItemPos() +{ + var itemPos = { + x: this.pos.x, + y: this.pos.y + this.size.height - 1 + }; + if (this.borderEnabled) + { + ++itemPos.x; + --itemPos.y; + } + return itemPos; +} + +// Returns the absolute screen row number for an item index, if it is visible +// on the menu. If the item is not visible on the menu, this will return -1. +// +// Parameters: +// pItemIdx: The index of the menu item to check +// +// Return value: The absolute row number on the screen where the item is, if it is +// visible on the menu, or -1 if the item is not visible on the menu. +function DDLightbarMenu_ScreenRowForItem(pItemIdx) +{ + if (typeof(pItemIdx) !== "number") + return -1; + if (pItemIdx < 0 || pItemIdx >= this.NumItems()) + return -1; + + var screenRow = -1; + if (pItemIdx >= this.topItemIdx && pItemIdx <= this.GetBottomItemIdx()) + { + if (this.borderEnabled) + screenRow = this.pos.y + pItemIdx - this.topItemIdx + 1; + else + screenRow = this.pos.y + pItemIdx - this.topItemIdx; + } + return screenRow; +} + +// Returns whether ANSI is supported by the user's terminal. Also checks this.allowANSI +function DDLightbarMenu_ANSISupported() +{ + return (console.term_supports(USER_ANSI) && this.allowANSI); +} + +// Calculates the number of solid scrollbar blocks & non-solid scrollbar blocks +// to use. Saves the information in this.scrollbarInfo.numSolidScrollBlocks and +// this.scrollbarInfo.numNonSolidScrollBlocks. +function DDLightbarMenu_CalcScrollbarBlocks() +{ + var menuDisplayHeight = this.size.height; + if (this.borderEnabled) + menuDisplayHeight -= 2; + var numMenuItems = this.NumItems(); + if (numMenuItems > 0) + { + var menuListFractionShown = menuDisplayHeight / numMenuItems; + if (menuListFractionShown > 1) + menuListFractionShown = 1.0; + this.scrollbarInfo.numSolidScrollBlocks = Math.floor(menuDisplayHeight * menuListFractionShown); + if (this.scrollbarInfo.numSolidScrollBlocks <= 0) + this.scrollbarInfo.numSolidScrollBlocks = 1; + else if (this.scrollbarInfo.numSolidScrollBlocks > menuDisplayHeight) + this.scrollbarInfo.numSolidScrollBlocks = menuDisplayHeight; + this.scrollbarInfo.numNonSolidScrollBlocks = menuDisplayHeight - this.scrollbarInfo.numSolidScrollBlocks; + } + else + { + this.scrollbarInfo.numSolidScrollBlocks = menuDisplayHeight; + this.scrollbarInfo.numNonSolidScrollBlocks = 0; + } +} + + + + +////////////////////////////////////////////////////////// +// Helper functions, not part of the DDLightbarMenu class + +// Returns the length of an item's text, not counting non-displayable +// characters (such as Synchronet color attributes and an ampersand +// immediately before a non-space) +// +// Parameters: +// pText: The text to test +// pAmpersandHotkeysInItems: Boolean - Whether or not ampersand hotkeys are enabled for the item text +function itemTextDisplayableLen(pText, pAmpersandHotkeysInItems) +{ + var textLen = console.strlen(pText); + // If pAmpersandHotkeysInItems is true, look for ampersands immediately + // before a non-space and if found, don't count those. + if (pAmpersandHotkeysInItems) + { + var startIdx = 0; + var ampersandIndex = pText.indexOf("&", startIdx); + while (ampersandIndex > -1) + { + // See if the next character is a space character. If not, then + // don't count it in the length. + if (pText.length > ampersandIndex+1) + { + var nextChar = pText.substr(ampersandIndex+1, 1); + if (nextChar != " ") + --textLen; + } + startIdx = ampersandIndex+1; + ampersandIndex = pText.indexOf("&", startIdx); + } + } + return textLen; +} + +// Shortens a string, accounting for control/attribute codes. Returns a new +// (shortened) copy of the string. +// +// Parameters: +// pStr: The string to shorten +// pNewLength: The new (shorter) length of the string +// pFromLeft: Optional boolean - Whether to start from the left (default) or +// from the right. Defaults to true. +// +// Return value: The shortened version of the string +function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft) +{ + if (typeof(pStr) != "string") + return ""; + if (typeof(pNewLength) != "number") + return pStr; + if (pNewLength >= console.strlen(pStr)) + return pStr; + + var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true); + var strCopy = ""; + var tmpStr = ""; + var strIdx = 0; + var lengthGood = true; + if (fromLeft) + { + while (lengthGood && (strIdx < pStr.length)) + { + tmpStr = strCopy + pStr.charAt(strIdx++); + if (console.strlen(tmpStr) <= pNewLength) + strCopy = tmpStr; + else + lengthGood = false; + } + } + else + { + strIdx = pStr.length - 1; + while (lengthGood && (strIdx >= 0)) + { + tmpStr = pStr.charAt(strIdx--) + strCopy; + if (console.strlen(tmpStr) <= pNewLength) + strCopy = tmpStr; + else + lengthGood = false; + } + } + return strCopy; +} + +// Returns whether or not all string attribute objects in an array have the +// expected properties, and that the property types are correct, for menu item +// string color definitions. +// +// Parameters: +// pAttrsArray: An array of objects which are expected to containg the +// following properties: start, end, attrs +// +// Return value: Boolean - Whether or not all elements in the array +// have all the expected properties +function attrsArrayElementsHaveAllCorrectProps(pAttrsArray) +{ + var allElementsHaveCorrectProps = true; + for (var i = 0; (i < pAttrsArray.length) && allElementsHaveCorrectProps; ++i) + { + allElementsHaveCorrectProps = ((typeof(pAttrsArray[i]) == "object") && + pAttrsArray[i].hasOwnProperty("start") && + pAttrsArray[i].hasOwnProperty("end") && + pAttrsArray[i].hasOwnProperty("attrs") && + (typeof(pAttrsArray[i].start) == "number") && + (typeof(pAttrsArray[i].end) == "number") && + (typeof(pAttrsArray[i].attrs) == "string")); + } + return allElementsHaveCorrectProps; +} + +// Adds color/attribute codes to a string. +// +// Parameters: +// pStr: The string to add attribute codes to +// pAttrs: This can be either a string containing attribute codes or an array +// of objects with start, end, and color properties, for applying attribute +// codes to different parts of the string. These are the properties of +// each object in the string (note: for the last one, end can be 0 or -1 +// to apply the attributes to the rest of the string): +// start: The start index in the string to apply the attributes to +// end: One past the last index in the part of the string to apply the attributes to +// attrs: The attributes to apply to that part of the string +// +// Return value: A copy of the string with attributes applied +function addAttrsToString(pStr, pAttrs) +{ + if (typeof(pStr) != "string") + return ""; + else if (pStr.length == 0) + return ""; + + var str; + if (Array.isArray(pAttrs)) + { + // To use the attributes array, the array must have some objects and + // each element of the array must have start, end, and attrs properties + if ((pAttrs.length > 0) && attrsArrayElementsHaveAllCorrectProps(pAttrs)) + { + // Colorize the string with the object in pAttrs. + // Don't do the last object in this loop, because for the last object, + // we'll want to check if its end index is valid. + str = ""; + var lastEnd = -1; + for (var i = 0; i < pAttrs.length; ++i) + { + // If the current object's start is more than 1 character after + // the last's end, then append the gap in the string with the + // normal attribute + if ((i > 0) && (pAttrs[i].start > pAttrs[i-1].end)) + str += "\x01n" + pStr.substring(pAttrs[i-1].end, pAttrs[i].start); + // If the properties for the current attrib object are all valid, append + // the current part of the string with the given attributes + if ((pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && (pAttrs[i].end > pAttrs[i].start) && (pAttrs[i].end <= pStr.length)) + str += "\x01n" + pAttrs[i].attrs + pStr.substring(pAttrs[i].start, pAttrs[i].end); + // For the last attribute object, allow the end index to be <= 0 or + // more than the length of the string to apply the attributes to the + // rest of the string. + //else if ((i == pAttrs.length-1) && (pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && (pAttrs[i].end <= 0)) + else if ((i == pAttrs.length-1) && (pAttrs[i].start >= lastEnd) && (pAttrs[i].start >= 0) && (pAttrs[i].start < pStr.length) && ((pAttrs[i].end <= 0) || (pAttrs[i].end > pStr.length))) + str += "\x01n" + pAttrs[i].attrs + pStr.substring(pAttrs[i].start); + lastEnd = pAttrs[i].end; + } + + // If str is shorter than the passed-in string, then append the rest of the string + // with the normal attribute. + var theStrLen = console.strlen(str); + if (theStrLen < pStr.length) + str += "\x01n" + pStr.substring(theStrLen); + } + else + str = pStr; + } + else if (typeof(pAttrs) == "string") + str = "\x01n" + pAttrs + pStr; + else + str = pStr; + return str; +} + +function getBackgroundAttrAtIdx(pAttrs, pIdx) +{ + if (typeof(pIdx) != "number") + return ""; + if (pIdx < 0) + return ""; + + // Synchronet background color codes: + // Black: 0 + // Red: 1 + // Green: 2 + // Yellow/brown: 3 + // Blue: 4 + // Magenta: 5 + // Cyan: 6 + // White/grey: 7 + var syncBkgAttrRegex = /\x01[01234567]/; + var bkgAttr = ""; + if (Array.isArray(pAttrs)) + { + if ((pAttrs.length > 0) && attrsArrayElementsHaveAllCorrectProps(pAttrs)) + { + // Go through the array, and if a start & end is found where pIdx + // falls between, check that objects attrs property for its last + // background attribute, if there is one + for (var i = 0; i < pAttrs.length; ++i) + { + if ((pIdx >= pAttrs[i].start) && ((pIdx < pAttrs[i].end) || (pAttrs[i].end == 0))) + { + // Check the attrs for the last background attribute, starting + // from the end + if (pAttrs[i].attrs.length >= 2) + { + for (var attrIdx = pAttrs[i].attrs.length - 2; attrIdx >= 0; attrIdx -= 2) + { + var currentTwo = pAttrs[i].attrs.substr(attrIdx, 2); + if (syncBkgAttrRegex.test(currentTwo)) + { + bkgAttr = currentTwo; + break; + } + } + } + break; + } + } + } + } + else if (typeof(pAttrs) == "string") + { + if ((pIdx >= 0) || (pIdx < pAttrs.length)) + { + // Starting from pIdx, go backwards through pAttrs, and if a Synchronet + // background attribute code is found, then use it. + for (var i = pIdx - 2; i >= 0; i -= 2) + { + var currentTwo = pAttrs.substr(i, 2); + if (syncBkgAttrRegex.test(currentTwo)) + { + bkgAttr = currentTwo; + break; + } + } + } + } + return bkgAttr; +} + +// Returns a default item object for a DDLightbarMenu +function getDefaultMenuItem() { + return { + text: "", + textIsUTF8: false, + retval: null, + hotkeys: "", + useAltColors: false, + itemColor: null, + itemSelectedColor: null, + isSelectable: true + }; +} + +// Returns a substring of a string, accounting for Synchronet attribute +// codes (not including the attribute codes in the start index or length) +// +// Parameters: +// pStr: The string to perform the substring on +// pLen: Optional: The length of the substring. If not specified, the rest of the string will be used. +// +// Return value: A substring of the string according to the parameters +function substrWithAttrCodes(pStr, pStartIdx, pLen) +{ + if (typeof(pStr) !== "string") + return ""; + if (typeof(pStartIdx) !== "number") + return ""; + var len = typeof(pLen) === "number" ? pLen : console.strlen(pStr)-pStartIdx; + if (len <= 0) + return ""; + if ((pStartIdx <= 0) && (pLen >= console.strlen(pStr))) + return pStr; + var startIdx = 0; + var screenLen = console.strlen(pStr); + if (typeof(pStartIdx) === "number" && pStartIdx >= 0 && pStartIdx < screenLen) + startIdx = pStartIdx; + + // Find the actual start & end indexes, considering (not counting) attribute codes, + // and return the substring including any applicable attributes from the string + var actualStartIdx = findIdxConsideringAttrs(pStr, startIdx); + var actualEndIdx = findIdxConsideringAttrs(pStr, startIdx+len+1); + // With the actual start & end indexes, make sure we'll get the string + // length desired; if not, adjust actualEndIdx; + var lenWithActualIndexes = actualEndIdx - actualStartIdx; + if (actualEndIdx-actualStartIdx < len) + actualEndIdx += len - lenWithActualIndexes; + return getAttrsBeforeStrIdx(pStr, actualStartIdx) + pStr.substring(actualStartIdx, actualEndIdx); +} +// Helper for substrWithAttrCodes(): Maps a 'visual' character index in a string to its +// actual index within the string, considering any attribute codes in the string. +// +// Parameters: +// pStr: A string +// pIndex: The index of a character as displayed on the screen, to be mapped to actual string index +// +// Return value: The character index mapped to the actual index within the string, considering attribute codes +function findIdxConsideringAttrs(pStr, pIndex) +{ + if (typeof(pStr) !== "string" || pStr.length == 0) + return 0; + if (typeof(pIndex) !== "number" || pIndex < 0) + return 0; + + var printableLen = console.strlen(pStr); + var index = (pIndex >= printableLen ? printableLen - 1 : pIndex); + + var actualIdx = 0; + var numTimesUpdated = 0; + for (var i = 0; i < pStr.length && numTimesUpdated <= index; ++i) + { + // If this character is the attribute control character, skip it along with + // the next character + if (pStr.charAt(i) == "\x01") + { + ++i; + continue; + } + actualIdx = i; + ++numTimesUpdated; + } + // Alternate implementation: + /* + var previousChar = ""; + var syncAttrLen = 0; + var numTimesUpdated = 0; + for (var i = 0; i < pStr.length && numTimesUpdated <= index; ++i) + { + // If this character is the attribute control character, skip it along with + // the next character + var currentChar = pStr.charAt(i); + if (currentChar == "\x01") + syncAttrLen = 1; + else if (previousChar == "\x01") + ++syncAttrLen; + else + syncAttrLen = 0; + if (syncAttrLen == 0) + { + actualIdx = i; + ++numTimesUpdated; + } + previousChar = currentChar; + } + */ + return actualIdx; +} +// Helper for substrWithAttrCodes(): Returns a string with any Synchronet color/attribute +// codes found in a string before a given index. +// +// Parameters: +// pStr: The string to search in +// pIdx: The index in the string to search before +// +// Return value: A string containing any Synchronet attribute codes found before +// the given index in the given string +function getAttrsBeforeStrIdx(pStr, pIdx) +{ + if (typeof(pStr) !== "string") + return ""; + if (typeof(pIdx) !== "number") + return ""; + if (pIdx < 0) + return ""; + + var idx = (pIdx < pStr.length ? pIdx : pStr.length-1); + var attrStartIdx = strIdxOfSyncAttrBefore(pStr, idx, true, false); + var attrEndIdx = strIdxOfSyncAttrBefore(pStr, idx, false, false); // Start of 2-character code + var attrsStr = ""; + if ((attrStartIdx > -1) && (attrEndIdx > -1)) + attrsStr = pStr.substring(attrStartIdx, attrEndIdx+2); + return attrsStr; +} + +// Returns the index of the first Synchronet attribute code before a given index +// in a string. +// +// Parameters: +// pStr: The string to search in +// pIdx: The index to search back from +// pSeriesOfAttrs: Optional boolean - Whether or not to look for a series of +// attributes. Defaults to false (look for just one attribute). +// pOnlyInWord: Optional boolean - Whether or not to look only in the current word +// (with words separated by whitespace). Defaults to false. +// +// Return value: The index of the first Synchronet attribute code before the given +// index in the string, or -1 if there is none or if the parameters +// are invalid +function strIdxOfSyncAttrBefore(pStr, pIdx, pSeriesOfAttrs, pOnlyInWord) +{ + if (typeof(pStr) != "string") + return -1; + if (typeof(pIdx) != "number") + return -1; + if ((pIdx < 0) || (pIdx >= pStr.length)) + return -1; + + var seriesOfAttrs = (typeof(pSeriesOfAttrs) == "boolean" ? pSeriesOfAttrs : false); + var onlyInWord = (typeof(pOnlyInWord) == "boolean" ? pOnlyInWord : false); + + var attrCodeIdx = pStr.lastIndexOf("\x01", pIdx-1); + if (attrCodeIdx > -1) + { + // If we are to only check the current word, then continue only if + // there isn't a space between the attribute code and the given index. + if (onlyInWord) + { + if (pStr.lastIndexOf(" ", pIdx-1) >= attrCodeIdx) + attrCodeIdx = -1; + } + } + if (attrCodeIdx > -1) + { + var syncAttrRegexWholeWord = /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i; + if (syncAttrRegexWholeWord.test(pStr.substr(attrCodeIdx, 2))) + { + if (seriesOfAttrs) + { + for (var i = attrCodeIdx - 2; i >= 0; i -= 2) + { + if (syncAttrRegexWholeWord.test(pStr.substr(i, 2))) + attrCodeIdx = i; + else + break; + } + } + } + else + attrCodeIdx = -1; + } + return attrCodeIdx; +} + +// Converts a 'printed' index in a string to its real index in the string +// +// Parameters: +// pStr: The string to search in +// pIdx: The printed index in the string +// +// Return value: The actual index in the string object, or -1 on error +function printedToRealIdxInStr(pStr, pIdx) +{ + if (typeof(pStr) != "string") + return -1; + if ((pIdx < 0) || (pIdx >= pStr.length)) + return -1; + + // Store the character at the given index if the string didn't have attribute codes. + // Also, to help ensure this returns the correct index, get a substring with several + // characters starting at the given index to match a word within the string + var strWithoutAttrCodes = strip_ctrl(pStr); + var substr_len = 5; + var substrWithoutAttrCodes = strWithoutAttrCodes.substr(pIdx, substr_len); + var printableCharAtIdx = strWithoutAttrCodes.charAt(pIdx); + // Iterate through pStr until we find that character and return that index. + var realIdx = 0; + for (var i = 0; i < pStr.length; ++i) + { + // tempStr is the string to compare with substrWithoutAttrCodes + var tempStr = strip_ctrl(pStr.substr(i)).substr(0, substr_len); + if ((pStr.charAt(i) == printableCharAtIdx) && (tempStr == substrWithoutAttrCodes)) + { + realIdx = i; + break; + } + } + return realIdx; +} + +// Inputs a keypress from the user and handles some ESC-based +// characters such as PageUp, PageDown, and ESC. If PageUp +// or PageDown are pressed, this function will return the +// string defined by KEY_PAGE_UP or EY_PAGE_DOWN, +// respectively. Also, F1-F5 will be returned as "\x01F1" +// through "\x01F5", respectively. +// Thanks goes to Psi-Jack for the original impementation +// of this function. +// +// Parameters: +// pGetKeyMode: Optional - The mode bits for console.getkey(). +// If not specified, K_NONE will be used. +// pInputTimeoutMS: The input timeout in milliseconds (defaults to 300000). +// If the user is a sysop, this will use a timeout of 0 for no timeout. +// +// Return value: The user's keypress +function getKeyWithESCChars(pGetKeyMode, pInputTimeoutMS) +{ + var getKeyMode = (typeof(pGetKeyMode) === "number" ? pGetKeyMode : K_NONE); + var inputTimeoutMS = (typeof(pInputTimeoutMS) === "number" ? pInputTimeoutMS : 300000); + if (inputTimeoutMS == 0) + inputTimeoutMS = 300000; + // Input a key from the user and take action based on the user's input. If + // the user is a sysop, don't use an input timeout. + var userInput = ""; + if (user.compare_ars("SYSOP")) + userInput = console.getkey(getKeyMode); + else + userInput = console.inkey(getKeyMode, inputTimeoutMS); + if (userInput == KEY_ESC) + { + switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) + { + case '[': + switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) + { + case 'V': + userInput = KEY_PAGE_UP; + break; + case 'U': + userInput = KEY_PAGE_DOWN; + break; + } + break; + case 'O': + switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) + { + case 'P': + userInput = KEY_F1; + break; + case 'Q': + userInput = KEY_F2; + break; + case 'R': + userInput = KEY_F3; + break; + case 'S': + userInput = KEY_F4; + break; + case 't': + userInput = KEY_F5; + break; + } + default: + break; + } + } + + return userInput; +} + +// Calculates & returns a page number. +// +// Parameters: +// pTopIndex: The index (0-based) of the topmost item on the page +// pNumPerPage: The number of items per page +// +// Return value: The page number +function calcPageNum(pTopIndex, pNumPerPage) +{ + return ((pTopIndex / pNumPerPage) + 1); +} + +// Finds the (1-based) page number of an item by number (1-based). If no page +// is found, then the return value will be 0. +// +// Parameters: +// pItemNum: The item number (1-based) +// pNumPerPage: The number of items per page +// pTotoalNum: The total number of items in the list +// pReverseOrder: Boolean - Whether or not the list is in reverse order. If not specified, +// this will default to false. +// +// Return value: The page number (1-based) of the item number. If no page is found, +// the return value will be 0. +function findPageNumOfItemNum(pItemNum, pNumPerPage, pTotalNum, pReverseOrder) +{ + if ((typeof(pItemNum) !== "number") || (typeof(pNumPerPage) !== "number") || (typeof(pTotalNum) !== "number")) + return 0; + if ((pItemNum < 1) || (pItemNum > pTotalNum)) + return 0; + + var reverseOrder = (typeof(pReverseOrder) == "boolean" ? pReverseOrder : false); + var itemPageNum = 0; + if (reverseOrder) + { + var pageNum = 1; + for (var topNum = pTotalNum; ((topNum > 0) && (itemPageNum == 0)); topNum -= pNumPerPage) + { + if ((pItemNum <= topNum) && (pItemNum >= topNum-pNumPerPage+1)) + itemPageNum = pageNum; + ++pageNum; + } + } + else // Forward order + itemPageNum = Math.ceil(pItemNum / pNumPerPage); + + return itemPageNum; +} + + + + +function logStackTrace(levels) { + var callstack = []; + var isCallstackPopulated = false; + try { + i.dont.exist += 0; //doesn't exist- that's the point + } catch (e) { + if (e.stack) { //Firefox / chrome + var lines = e.stack.split('\n'); + for (var i = 0, len = lines.length; i < len; i++) { + callstack.push(lines[i]); + } + //Remove call to logStackTrace() + callstack.shift(); + isCallstackPopulated = true; + } + else if (window.opera && e.message) { //Opera + var lines = e.message.split('\n'); + for (var i = 0, len = lines.length; i < len; i++) { + if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) { + var entry = lines[i]; + //Append next line also since it has the file info + if (lines[i + 1]) { + entry += " at " + lines[i + 1]; + i++; + } + callstack.push(entry); + } + } + //Remove call to logStackTrace() + callstack.shift(); + isCallstackPopulated = true; + } + } + if (!isCallstackPopulated) { //IE and Safari + var currentFunction = arguments.callee.caller; + while (currentFunction) { + var fn = currentFunction.toString(); + var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous"; + callstack.push(fname); + currentFunction = currentFunction.caller; + } + } + if (levels) { + console.print(callstack.slice(0, levels).join("\r\n")); + } + else { + console.print(callstack.join("\r\n")); + } +} diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt index 22cf366c2bc0ed9be4d05c3d468454fd353032cb..fa798ecf9111ac80f66d85e94770e8f7eaacd5be 100644 --- a/xtrn/DDMsgReader/readme.txt +++ b/xtrn/DDMsgReader/readme.txt @@ -1,6 +1,6 @@ Digital Distortion Message Reader - Version 1.95a - Release date: 2024-01-24 + Version 1.95b + Release date: 2024-02-04 by diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt index 398a8ee82e3768b69d7f92600d5f7b3271b94582..f79a140603301ba83a9ff3264d95d89bded1de84 100644 --- a/xtrn/DDMsgReader/revision_history.txt +++ b/xtrn/DDMsgReader/revision_history.txt @@ -5,8 +5,11 @@ Revision History (change log) ============================= Version Date Description ------- ---- ----------- -1.95a 2024-01-23 Bug fix: Aborting when the sub-board isn't available when - editing a personal email message +1.95b 2024-02-04 Bug fix: Use the P_UTF8 mode bit when printing UTF-8 + message header info (such as 'from' and 'to'). + A dd_lightbar_menu.js update goes along with this. +1.95a 2024-01-23 Bug fix: Was aborting when sub-board code isn't available + when editing personal email 1.95 2024-01-20 Removed user option to display indexed mode menu in newscan after all new messages are read. Command-line option -indexedMode can now be specified with