From dc503da85ac936e438b759bc06628764bbf5d36f Mon Sep 17 00:00:00 2001 From: Damian Monogue <3660+demonnic@users.noreply.github.com> Date: Thu, 27 Apr 2023 22:48:34 -0400 Subject: [PATCH] Initial spinbox check in (#60) * Initial spinbox check in * Busted createComponents down into smaller functions. Added some ldoc * Add spinbox to readme * Bump version --- README.md | 3 + mfile | 2 +- src/resources/mdkversion.txt | 2 +- src/resources/spinbox.lua | 442 +++++++++++++++++++++++++++++++++++ 4 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 src/resources/spinbox.lua diff --git a/README.md b/README.md index e685225..426129d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ You should maybe also include demontools.lua, as it notes below several other of * sortbox.lua * SortBox, an alternative to H/VBox which can be either, and also provides options for sorting its contents. Overview at +* spinbox.lua + * SpinBox, a Geyser element for adjusting numbers with your mouse. Overview at + * sug.lua * Self Updating Gauges, will watch a set of variables and update itself on a timer based on what values those variables hold. Documentation at diff --git a/mfile b/mfile index 0358132..10f356f 100644 --- a/mfile +++ b/mfile @@ -1,6 +1,6 @@ { "package": "MDK", - "version": "2.8.4", + "version": "2.9.0", "author": "Demonnic", "title": "Collection of useful objects/classes", "icon": "computer.png", diff --git a/src/resources/mdkversion.txt b/src/resources/mdkversion.txt index 2701a22..c8e38b6 100644 --- a/src/resources/mdkversion.txt +++ b/src/resources/mdkversion.txt @@ -1 +1 @@ -2.8.4 +2.9.0 diff --git a/src/resources/spinbox.lua b/src/resources/spinbox.lua new file mode 100644 index 0000000..ca2c2da --- /dev/null +++ b/src/resources/spinbox.lua @@ -0,0 +1,442 @@ +--- A Geyser object to create a spinbox for adjusting a number +-- @classmod spinbox +-- @author Damian Monogue +-- @copyright 2023 +-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua +local spinbox = { + parent = Geyser.Container, + name = 'SpinboxClass', + min = 0, + max = 10, + delta = 1, + value = 0, + activeButtonColor = "gray", + inactiveButtonColor = "DimGray", + integer = true, + upArrowLocation = "https://demonnic.github.io/image-assets/uparrow.png", + downArrowLocation = "https://demonnic.github.io/image-assets/downarrow.png", +} +spinbox.__index = spinbox +setmetatable(spinbox, spinbox.parent) + +local gss = Geyser.StyleSheet +local directory = getMudletHomeDir() .. "/spinbox/" +local saveFile = directory .. "fileLocations.lua" +if not io.exists(directory) then + lfs.mkdir(directory) +end + +--- Creates a new spinbox. +-- @tparam table cons a table containing the options for this spinbox. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
minThe minimum value for this spinbox0
maxThe maximum value for this spinbox10
activeButtonColorThe color the up/down buttons should be when they are active/able to be usedgray
inactiveButtonColorThe color the up/down buttons should be when they are inactive/unable to be useddimgray
integerBoolean value. When true, values must always be integers (no decimal place)true
deltaThe amount to change the spinbox's value when the up or down button is pressed.1
upArrowLocationThe location of the up arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/uparrow.png
downArrowLocationThe location of the down arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/downarrow.png
+-- @param container The Geyser container for this spinbox +function spinbox:new(cons, container) + cons = cons or {} + local consType = type(cons) + if consType ~= "table" then + printError(f"spinbox:new(cons, container): cons as table of options expected, got {consType}!", true, true) + end + cons.name = cons.name or Geyser.nameGen("spinbox") + local me = self.parent:new(cons, container) + setmetatable(me, self) + me:createComponents() + return me +end + +--- Creates the components that make up the spinbox UI. +-- @local +-- Obtains the up and down arrow images specified in the spinbox options. +-- Generates styles for the spinbox. +-- Calculates the height of the up/down buttons and any remainder space. +-- Creates: +-- `self.upButton` - A button with an up arrow image for incrementing the value +-- `self.downButton` - A button with a down arrow image for decrementing the value +-- `self.displayLabel` - A label to display the current spinbox value +-- `self.input` - A command line input to allow directly entering a value +-- Hides the input by default. +-- Applies the generated styles. +function spinbox:createComponents() + self:obtainImages() + self:generateStyles() + self:calculateButtonDimensions() + + self.upButton = self:createButton("up") + self.downButton = self:createButton("down") + + self.displayLabel = self:createDisplayLabel() + + self.input = self:createInput() + self.input:hide() + + self:applyStyles() +end + +--- Calculates the button height. We use square buttons in this house. +-- @local +-- Calculates the height of the up/down buttons by dividing the spinbox height in half. +-- Stores the remainder (if any) in self.remainder. +-- Stores the calculated button height in self.buttonHeight. +function spinbox:calculateButtonDimensions() + self.buttonHeight = math.floor(self.get_height() / 2) + self.remainder = self.get_height() % 2 +end + +--- Creates a button (up or down arrow) for the spinbox. +-- @param type Either "up" or "down" to specify which direction the arrow should point +-- @return The created Geyser.Label button +-- @local +-- Creates a Geyser.Label button with an up or down arrow image. +-- Positions the button at the top or bottom of the spinbox respectively. +-- Sets a click callback on the button to call increment() or decrement() depending on the type. +-- Returns the created button. +function spinbox:createButton(type) + local button = Geyser.Label:new({ + name = self.name .. "spinbox_"..type.."Arrow", + height = self.buttonHeight, + width = self.buttonHeight, + x = "100%-" .. self.buttonHeight, + y = type == "up" and 0 or self.buttonHeight + self.remainder, + }, self) + + button:setClickCallback(function() + if type == "up" then + self:increment() + else + self:decrement() + end + end) + return button +end + +--- Creates the display label for the spinbox value. +-- @return The created Geyser.Label display label +-- @local +-- Creates a Geyser.Label to display the current spinbox value. +-- Centers the text in the label. +-- Sets a double click callback on the label to show the input, put the current +-- value in it, select the text, and hide the label. +-- Returns the created display label. +function spinbox:createDisplayLabel() + local displayLabel = Geyser.Label:new({ + name = self.name .. "spinbox_displayLabel", + x = 0, + y = 0, + width = "100%-" .. self.buttonHeight, + height = "100%", + message = self.value + }, self) + displayLabel:setAlignment("center") + displayLabel:setDoubleClickCallback(function() + self.input:show() + self.input:print(self.value) + self.input:selectText() + displayLabel:hide() + end) + return displayLabel +end + +--- Creates the input for directly entering a spinbox value. +-- @return The created Geyser.CommandLine input +-- @local +-- Creates a Geyser.CommandLine input. +-- Sets an action on the input to: +-- - Attempt to convert the input text to a number +-- - If successful, call setValue() with the number to set the spinbox value +-- - Hide the input +-- - Show the display label +-- - Put the new spinbox value in the input +-- Returns the created input. +function spinbox:createInput() + local input = Geyser.CommandLine:new({ + x = 0, + y = 0, + width = "100%-".. self.buttonHeight, + height = "100%", + }, self) + input:setAction(function(txt) + txt = tonumber(txt) + if txt then + self:setValue(txt) + input:hide() + end + self.displayLabel:show() + input:print(self.value) + end) + return input +end + +--- Used to increment the value by the delta amount +-- @local +-- Increments the spinbox value by the delta amount. +-- Checks if the new value would exceed the max, and if so sets it to the max. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:increment() + local val = self.value + self.delta + if val >= self.max then + val = self.max + end + self.value = val + self.displayLabel:echo(val) + self:applyStyles() +end + +--- Used to decrement the value by the delta amount +-- @local +-- Decrements the spinbox value by the delta amount. +-- Checks if the new value would be below the min, and if so sets it to the min. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:decrement() + local val = self.value - self.delta + if val <= self.min then + val = self.min + end + self.value = val + self.displayLabel:echo(val) + self:applyStyles() +end + +--- Used to directly set the value of the spinbox. +-- @param value The new value to set +-- Rounds the value to an integer if the spinbox is integer only. +-- Checks if the new value is within the min/max range and clamps it if not. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:setValue(value) + if self.integer then + value = math.floor(value) + end + if value >= self.max then + value = self.max + elseif value <= self.min then + value = self.min + end + self.value = value + self.displayLabel:echo(value) + self:applyStyles() +end + +--- Obtains the up and down arrow images for the spinbox. +-- @local +-- Gets the previously saved file locations. +-- Checks if the up arrow image exists at the upArrowLocation. +-- If not, it will download the image from a URL or copy a local file. It saves +-- the new location. +-- Does the same for the down arrow image and downArrowLocation. +-- Saves any new locations to the save file. +-- Sets self.upArrowFile and self.downArrowFile to the locations of the images. +function spinbox:obtainImages() + local locations = self:getFileLocs() + local upURL = self.upArrowLocation + local downURL = self.downArrowLocation + local upFile = locations[upURL] + local downFile = locations[downURL] + local locationsChanged = false + if not (upFile and io.exists(upFile)) then + if not upFile then + upFile = directory .. self.name .. "/uparrow.png" + locations[upURL] = upFile + locationsChanged = true + end + if upURL:match("^http") then + self:downloadFile(upURL, upFile) + elseif io.exists(upURL) then + upFile = upURL + locations[upURL] = upFile + locationsChanged = true + end + end + if not (downFile and io.exists(downFile)) then + if not downFile then + downFile = directory .. self.name .. "/downarrow.png" + locations[downURL] = downFile + locationsChanged = true + end + if downURL:match("^http") then + self:downloadFile(downURL, downFile) + elseif io.exists(downURL) then + downFile = downURL + locations[downURL] = downFile + locationsChanged = true + end + end + self.upArrowFile = upFile + self.downArrowFile = downFile + if locationsChanged then + table.save(saveFile, locations) + end +end + +--- Handles the actual download of a file from a url +-- @param url The url to download the file from +-- @param fileName The location to save the downloaded file +-- @local +-- Creates any missing directories in the file path. +-- Registers named event handlers to handle the download completing or erroring. +-- The completion handler stops the error handler. +-- The error handler prints an error message and stops the completion handler. +-- Downloads the file from the url to the fileName location. +function spinbox:downloadFile(url, fileName) + local parts = fileName:split("/") + parts[#parts] = nil + local dirName = table.concat(parts, "/") .. "/" + if not io.exists(dirName) then + lfs.mkdir(dirName) + end + local uname = "spinbox" + local handlerName = self.name .. url + local handler = function(event, ...) + local args = {...} + local file = #args == 1 and args[1] or args[2] + if file ~= fileName then + return true + end + if event == "sysDownloadDone" then + debugc(f"INFO:Spinbox successfully downloaded {file}") + stopNamedEventHandler(uname, handlerName .. "error") + return false + end + cecho(f"\nERROR:Spinbox had an issue downloading an image file to {file}: {args[1]}\n") + stopNamedEventHandler(uname, handlerName .. "done") + end + registerNamedEventHandler(uname, handlerName .. "done", "sysDownloadDone", handler, true) + registerNamedEventHandler(uname, handlerName .. "error", "sysDownloadError", handler, true) + downloadFile(fileName, url) +end + +--- Responsible for reading the file locations from disk and returning them +-- @local +function spinbox:getFileLocs() + local locations = {} + if io.exists(saveFile) then + table.load(saveFile, locations) + end + return locations +end + +--- (Re)generates the stylesheets for the spinbox +-- Should not need to call but if you change something and it doesn't take effect +-- you can try calling this followed by applyStyles +function spinbox:generateStyles() + self.baseStyle = gss:new([[ + border-radius: 2px; + border-color: black; + ]]) + self.activeStyle = gss:new(f[[ + background-color: {self.activeButtonColor}; + ]], self.baseStyle) + self.inactiveStyle = gss:new(f[[ + background-color: {self.inactiveButtonColor}; + ]], self.baseStyle) + self.upStyle = gss:new(f[[ + border-image: url("{self.upArrowFile}"); + ]]) + self.downStyle = gss:new(f[[ + border-image: url("{self.downArrowFile}"); + ]]) + self.displayStyle = gss:new(f[[ + background-color: {Geyser.Color.hex(self.color)}; + text-align: center; + ]], self.baseStyle) +end + +--- Applies updated stylesheets to the components of the spinbox +-- Should not need to call this directly +function spinbox:applyStyles() + if self.value >= self.max then + self.upStyle:setParent(self.inactiveStyle) + else + self.upStyle:setParent(self.activeStyle) + end + if self.value <= self.min then + self.downStyle:setParent(self.inactiveStyle) + else + self.downStyle:setParent(self.activeStyle) + end + self.upButton:setStyleSheet(self.upStyle:getCSS()) + self.downButton:setStyleSheet(self.downStyle:getCSS()) + self.displayLabel:setStyleSheet(self.displayStyle:getCSS()) +end + +--- sets the color for active buttons on the spinbox +-- @param color any valid color formatting string, such a "red" or "#880000" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +function spinbox:setActiveButtonColor(color) + local colorType = type(color) + local hex + if colorType == "table" then + hex = Geyser.Color.hex(unpack(color)) + else + hex = Geyser.Color.hex(color) + end + self.activeButtonColor = hex + self.activeStyle:set("background-color", hex) + self:applyStyles() +end + +--- sets the color for inactive buttons on the spinbox +-- @param color any valid color formatting string, such a "" or "red" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +function spinbox:setInactiveButtonColor(color) + local colorType = type(color) + local hex + if colorType == "table" then + hex = Geyser.Color.hex(unpack(color)) + else + hex = Geyser.Color.hex(color) + end + self.inactiveButtonColor = hex + self.inactiveStyle:set("background-color", hex) + self:applyStyles() +end + +return spinbox \ No newline at end of file