From 744806f30d87e9cb823945ddd2fb6a75c20ad8f5 Mon Sep 17 00:00:00 2001 From: nightfox <> Date: Wed, 1 Apr 2020 04:06:54 +0000 Subject: [PATCH] Scrollbar functionality finally exists now, even though there was already a property to toggle a scrollbar from the beginning.. A scrollbar can be useful to visually show how far the user is in the list of items, particularly if the list is long. 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; 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. --- exec/load/dd_lightbar_menu.js | 319 ++++++++++++++++++++++++++++++++-- 1 file changed, 308 insertions(+), 11 deletions(-) diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js index b7d3c6aec3..bd5f817807 100644 --- a/exec/load/dd_lightbar_menu.js +++ b/exec/load/dd_lightbar_menu.js @@ -4,7 +4,6 @@ * Author: Eric Oulashin (AKA Nightfox) * BBS: Digital Distortion * Addresses: digitaldistortionbbs.com - * digdist.bbsindex.com * digdist.synchro.net This is a lightbar menu library. This allows creating a scrollable menu of @@ -46,6 +45,23 @@ 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 @@ -157,7 +173,11 @@ lbMenu.topBorderText = "Options"; lbMenu.bottomBorderText = "Enter = Select"; */ -load("sbbsdefs.js"); +if (typeof(require) === "function") + require("sbbsdefs.js", "K_UPPER"); +else + load("sbbsdefs.js"); + // Keyboard keys var KEY_ESC = ascii(27); @@ -207,6 +227,10 @@ 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; @@ -232,13 +256,16 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) width: 45, height: 10 }; - this.showScrollbar = false; + this.scrollbarEnabled = false; this.borderEnabled = false; + this.drawnAlready = false; this.colors = { itemColor: "\1n\1w\1" + "4", selectedItemColor: "\1n\1b\1" + "7", itemTextCharHighlightColor: "\1y\1h", - borderColor: "\1n\1b" + borderColor: "\1n\1b", + scrollbarScrollBlockColor: "\1h\1w", + scrollbarBGColor: "\1h\1k" }; // Characters to use to draw the border this.borderChars = { @@ -251,6 +278,15 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) 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; @@ -295,6 +331,12 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) 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; // Set some things based on the parameters passed in if ((typeof(pX) == "number") && (typeof(pY) == "number")) @@ -449,15 +491,19 @@ function DDLightbarMenu_SetHeight(pHeight) // 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: Boolean - Whether or not to draw the borders, if borders are enabled. -// Defaults to true. -function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders) +// 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. +function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar) { var drawBorders = (typeof(pDrawBorders) == "boolean" ? pDrawBorders : true); + var drawScrollbar = (typeof(pDrawScrollbar) == "boolean" ? pDrawScrollbar : true); - var itemLen = (this.showScrollbar ? this.size.width - 1 : this.size.width); var curPos = { x: this.pos.x, y: this.pos.y }; // For writing the menu items - // If there is a border, then adjust the item length, starting x, and starting + 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) { @@ -467,6 +513,15 @@ function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders) if (drawBorders) this.DrawBorder(); } + if (this.scrollbarEnabled && drawScrollbar && !this.CanShowAllItemsInWindow()) + { + --itemLen; // Leave room for the scrollbar in the item lengths + this.CalcScrollbarBlocks(); + if (!this.drawnAlready) + this.DisplayInitialScrollbar(this.pos.y); + else + this.UpdateScrollbarWithHighlightedItem(); + } // 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) @@ -499,6 +554,8 @@ function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders) printf(this.colors.itemColor + "%-" + itemLen + "s", ""); } } + + this.drawnAlready = true; } // Draws the border around the menu items @@ -600,7 +657,13 @@ function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected) itemLen = pItemLen; else { - itemLen = (this.showScrollbar ? this.size.width - 1 : this.size.width); + 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 @@ -612,6 +675,7 @@ function DDLightbarMenu_WriteItem(pIdx, pItemLen, pHighlight, pSelected) --itemLen; // Have a space for separation between the numbers and items } } + var itemColor = ""; if (typeof(pHighlight) == "boolean") itemColor = (pHighlight ? this.colors.selectedItemColor : this.colors.itemColor); @@ -774,6 +838,9 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) var continueOn = true; while (continueOn) { + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + this.UpdateScrollbarWithHighlightedItem(); + this.lastUserInput = getKeyWithESCChars(K_NOECHO|K_NOSPIN|K_NOCRLF); if ((this.lastUserInput == KEY_UP) || (this.lastUserInput == KEY_LEFT)) { @@ -1056,7 +1123,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) --XPos; ++YPos; } - if (this.showScrollbar) + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) --XPos; console.gotoxy(XPos, YPos); if (added) @@ -1306,6 +1373,236 @@ 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("\1n" + this.colors.scrollbarScrollBlockColor); + wroteBrightBlockColor = true; + wroteDimBlockColor = false; + } + console.print(this.scrollbarInfo.blockChar); + ++numSolidBlocksWritten; + } + else + { + if (!wroteDimBlockColor) + { + console.print("\1n" + this.colors.scrollbarBGColor); + wroteDimBlockColor = true; + } + 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; + if (this.items.length > 0) + { + var scrollbarFraction = this.selectedItemIdx / this.items.length; + 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. +function DDLightbarMenu_UpdateScrollbarWithHighlightedItem() +{ + var solidBlockStartRow = this.CalcScrollbarSolidBlockStartRow(); + if (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.items.length <= pageHeight); +} + +// 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("\1n" + 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("\1n" + 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("\1n" + 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("\1n" + 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("\1n" + 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("\1n" + 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("\1n" + 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("\1n" + 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 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 menuListFractionShown = menuDisplayHeight / this.items.length; + 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; +} + +////////////////////////////////////////////////////////// +// Helper functions, not part of the DDLightbarMenu class + // 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 -- GitLab