- Added DropDown Scrollbar
- Added List Scrollbar - Added Statemanagementsystem for XML
This commit is contained in:
@@ -166,6 +166,65 @@ function Collection:clearItemSelection()
|
|||||||
for i, item in ipairs(items) do
|
for i, item in ipairs(items) do
|
||||||
item.selected = false
|
item.selected = false
|
||||||
end
|
end
|
||||||
|
self:updateRender()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets the index of the first selected item
|
||||||
|
--- @shortDescription Gets the index of the first selected item
|
||||||
|
--- @return number? index The index of the first selected item, or nil if none selected
|
||||||
|
--- @usage local index = Collection:getSelectedIndex()
|
||||||
|
function Collection:getSelectedIndex()
|
||||||
|
local items = self.get("items")
|
||||||
|
for i, item in ipairs(items) do
|
||||||
|
if type(item) == "table" and item.selected then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Selects the next item in the collection
|
||||||
|
--- @shortDescription Selects the next item
|
||||||
|
--- @return Collection self The Collection instance
|
||||||
|
function Collection:selectNext()
|
||||||
|
local items = self.get("items")
|
||||||
|
local currentIndex = self:getSelectedIndex()
|
||||||
|
|
||||||
|
if not currentIndex then
|
||||||
|
if #items > 0 then
|
||||||
|
self:selectItem(1)
|
||||||
|
end
|
||||||
|
elseif currentIndex < #items then
|
||||||
|
if not self.get("multiSelection") then
|
||||||
|
self:clearItemSelection()
|
||||||
|
end
|
||||||
|
self:selectItem(currentIndex + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:updateRender()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Selects the previous item in the collection
|
||||||
|
--- @shortDescription Selects the previous item
|
||||||
|
--- @return Collection self The Collection instance
|
||||||
|
function Collection:selectPrevious()
|
||||||
|
local items = self.get("items")
|
||||||
|
local currentIndex = self:getSelectedIndex()
|
||||||
|
|
||||||
|
if not currentIndex then
|
||||||
|
if #items > 0 then
|
||||||
|
self:selectItem(#items)
|
||||||
|
end
|
||||||
|
elseif currentIndex > 1 then
|
||||||
|
if not self.get("multiSelection") then
|
||||||
|
self:clearItemSelection()
|
||||||
|
end
|
||||||
|
self:selectItem(currentIndex - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:updateRender()
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
local VisualElement = require("elements/VisualElement")
|
local VisualElement = require("elements/VisualElement")
|
||||||
local List = require("elements/List")
|
local List = require("elements/List")
|
||||||
local ScrollBar = require("elements/ScrollBar")
|
|
||||||
local tHex = require("libraries/colorHex")
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
---@configDescription A DropDown menu that shows a list of selectable items
|
---@configDescription A DropDown menu that shows a list of selectable items
|
||||||
@@ -107,42 +106,80 @@ function DropDown:mouse_click(button, x, y)
|
|||||||
self:setState("opened")
|
self:setState("opened")
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
elseif isOpen and relY > 1 and self.get("selectable") then
|
elseif isOpen and relY > 1 then
|
||||||
local itemIndex = (relY - 1) + self.get("offset")
|
-- Forward to List handler for scrollbar handling
|
||||||
local items = self.get("items")
|
return List.mouse_click(self, button, x, y - 1)
|
||||||
|
|
||||||
if itemIndex <= #items then
|
|
||||||
local item = items[itemIndex]
|
|
||||||
if type(item) == "string" then
|
|
||||||
item = {text = item}
|
|
||||||
items[itemIndex] = item
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self.get("multiSelection") then
|
|
||||||
for _, otherItem in ipairs(items) do
|
|
||||||
if type(otherItem) == "table" then
|
|
||||||
otherItem.selected = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
item.selected = not item.selected
|
|
||||||
|
|
||||||
if item.callback then
|
|
||||||
item.callback(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
self:fireEvent("select", itemIndex, item)
|
|
||||||
self:unsetState("opened")
|
|
||||||
self:unsetState("clicked")
|
|
||||||
self.set("height", 1)
|
|
||||||
self:updateRender()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Handles mouse drag events for scrollbar
|
||||||
|
--- @param button number The mouse button being dragged
|
||||||
|
--- @param x number The x-coordinate of the drag
|
||||||
|
--- @param y number The y-coordinate of the drag
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function DropDown:mouse_drag(button, x, y)
|
||||||
|
if self:hasState("opened") then
|
||||||
|
return List.mouse_drag(self, button, x, y - 1)
|
||||||
|
end
|
||||||
|
return VisualElement.mouse_drag and VisualElement.mouse_drag(self, button, x, y) or false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Handles mouse up events to stop scrollbar dragging
|
||||||
|
--- @param button number The mouse button that was released
|
||||||
|
--- @param x number The x-coordinate of the release
|
||||||
|
--- @param y number The y-coordinate of the release
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function DropDown:mouse_up(button, x, y)
|
||||||
|
if self:hasState("opened") then
|
||||||
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
|
|
||||||
|
-- Only handle item selection in mouse_up (relY > 1 = list area)
|
||||||
|
if relY > 1 and self.get("selectable") and not self._scrollBarDragging then
|
||||||
|
local itemIndex = (relY - 1) + self.get("offset")
|
||||||
|
local items = self.get("items")
|
||||||
|
|
||||||
|
if itemIndex <= #items then
|
||||||
|
local item = items[itemIndex]
|
||||||
|
if type(item) == "string" then
|
||||||
|
item = {text = item}
|
||||||
|
items[itemIndex] = item
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.get("multiSelection") then
|
||||||
|
for _, otherItem in ipairs(items) do
|
||||||
|
if type(otherItem) == "table" then
|
||||||
|
otherItem.selected = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
item.selected = not item.selected
|
||||||
|
|
||||||
|
if item.callback then
|
||||||
|
item.callback(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:fireEvent("select", itemIndex, item)
|
||||||
|
self:unsetState("opened")
|
||||||
|
self:unsetState("clicked")
|
||||||
|
self.set("height", 1)
|
||||||
|
self:updateRender()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Always forward to List for cleanup and unset clicked state
|
||||||
|
List.mouse_up(self, button, x, y - 1)
|
||||||
|
self:unsetState("clicked")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return VisualElement.mouse_up and VisualElement.mouse_up(self, button, x, y) or false
|
||||||
|
end
|
||||||
|
|
||||||
--- @shortDescription Renders the DropDown
|
--- @shortDescription Renders the DropDown
|
||||||
--- @protected
|
--- @protected
|
||||||
function DropDown:render()
|
function DropDown:render()
|
||||||
@@ -157,53 +194,13 @@ function DropDown:render()
|
|||||||
text = text:sub(1, self.get("width") - 2)
|
text = text:sub(1, self.get("width") - 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if isOpen then
|
||||||
|
List.render(self, 1)
|
||||||
|
end
|
||||||
|
|
||||||
self:blit(1, 1, text .. string.rep(" ", self.get("width") - #text - 1) .. (isOpen and "\31" or "\17"),
|
self:blit(1, 1, text .. string.rep(" ", self.get("width") - #text - 1) .. (isOpen and "\31" or "\17"),
|
||||||
string.rep(tHex[self.getResolved("foreground")], self.get("width")),
|
string.rep(tHex[self.getResolved("foreground")], self.get("width")),
|
||||||
string.rep(tHex[self.getResolved("background")], self.get("width")))
|
string.rep(tHex[self.getResolved("background")], self.get("width")))
|
||||||
|
|
||||||
if isOpen then
|
|
||||||
local items = self.get("items")
|
|
||||||
local height = self.get("height") - 1
|
|
||||||
local offset = self.get("offset")
|
|
||||||
local width = self.get("width")
|
|
||||||
|
|
||||||
for i = 1, height do
|
|
||||||
local itemIndex = i + offset
|
|
||||||
local item = items[itemIndex]
|
|
||||||
|
|
||||||
if item then
|
|
||||||
if type(item) == "string" then
|
|
||||||
item = {text = item}
|
|
||||||
items[itemIndex] = item
|
|
||||||
end
|
|
||||||
|
|
||||||
if item.separator then
|
|
||||||
local separatorChar = (item.text or "-"):sub(1,1)
|
|
||||||
local separatorText = string.rep(separatorChar, width)
|
|
||||||
local fg = item.fg or self.getResolved("foreground")
|
|
||||||
local bg = item.bg or self.getResolved("background")
|
|
||||||
|
|
||||||
self:textBg(1, i + 1, string.rep(" ", width), bg)
|
|
||||||
self:textFg(1, i + 1, separatorText, fg)
|
|
||||||
else
|
|
||||||
local text = item.text
|
|
||||||
local isSelected = item.selected
|
|
||||||
text = text:sub(1, width)
|
|
||||||
|
|
||||||
local bg = isSelected and
|
|
||||||
(item.selectedBg or self.getResolved("selectedBackground")) or
|
|
||||||
(item.bg or self.getResolved("background"))
|
|
||||||
|
|
||||||
local fg = isSelected and
|
|
||||||
(item.selectedFg or self.getResolved("selectedForeground")) or
|
|
||||||
(item.fg or self.getResolved("foreground"))
|
|
||||||
|
|
||||||
self:textBg(1, i + 1, string.rep(" ", width), bg)
|
|
||||||
self:textFg(1, i + 1, text, fg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Called when the DropDown gains focus
|
--- Called when the DropDown gains focus
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local Collection = require("elements/Collection")
|
local Collection = require("elements/Collection")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
---@configDescription A scrollable list of selectable items
|
---@configDescription A scrollable list of selectable items
|
||||||
|
|
||||||
--- This is the list class. It provides a scrollable list of selectable items with support for
|
--- This is the list class. It provides a scrollable list of selectable items with support for
|
||||||
@@ -8,12 +9,40 @@ local List = setmetatable({}, Collection)
|
|||||||
List.__index = List
|
List.__index = List
|
||||||
|
|
||||||
---@property offset number 0 Current scroll offset for viewing long lists
|
---@property offset number 0 Current scroll offset for viewing long lists
|
||||||
List.defineProperty(List, "offset", {default = 0, type = "number", canTriggerRender = true})
|
List.defineProperty(List, "offset", {
|
||||||
|
default = 0,
|
||||||
|
type = "number",
|
||||||
|
canTriggerRender = true,
|
||||||
|
setter = function(self, value)
|
||||||
|
local maxOffset = math.max(0, #self.get("items") - self.get("height"))
|
||||||
|
return math.min(maxOffset, math.max(0, value))
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
---@property emptyText string "No items" Text to display when the list is empty
|
||||||
|
List.defineProperty(List, "emptyText", {default = "No items", type = "string", canTriggerRender = true})
|
||||||
|
|
||||||
|
---@property showScrollBar boolean true Whether to show the scrollbar when items exceed height
|
||||||
|
List.defineProperty(List, "showScrollBar", {default = true, type = "boolean", canTriggerRender = true})
|
||||||
|
|
||||||
|
---@property scrollBarSymbol string " " Symbol used for the scrollbar handle
|
||||||
|
List.defineProperty(List, "scrollBarSymbol", {default = " ", type = "string", canTriggerRender = true})
|
||||||
|
|
||||||
|
---@property scrollBarBackground string "\127" Symbol used for the scrollbar background
|
||||||
|
List.defineProperty(List, "scrollBarBackground", {default = "\127", type = "string", canTriggerRender = true})
|
||||||
|
|
||||||
|
---@property scrollBarColor color lightGray Color of the scrollbar handle
|
||||||
|
List.defineProperty(List, "scrollBarColor", {default = colors.lightGray, type = "color", canTriggerRender = true})
|
||||||
|
|
||||||
|
---@property scrollBarBackgroundColor color gray Background color of the scrollbar
|
||||||
|
List.defineProperty(List, "scrollBarBackgroundColor", {default = colors.gray, type = "color", canTriggerRender = true})
|
||||||
|
|
||||||
---@event onSelect {index number, item table} Fired when an item is selected
|
---@event onSelect {index number, item table} Fired when an item is selected
|
||||||
List.defineEvent(List, "mouse_click")
|
List.defineEvent(List, "mouse_click")
|
||||||
List.defineEvent(List, "mouse_up")
|
List.defineEvent(List, "mouse_up")
|
||||||
|
List.defineEvent(List, "mouse_drag")
|
||||||
List.defineEvent(List, "mouse_scroll")
|
List.defineEvent(List, "mouse_scroll")
|
||||||
|
List.defineEvent(List, "key")
|
||||||
|
|
||||||
local entrySchema = {
|
local entrySchema = {
|
||||||
text = { type = "string", default = "Entry" },
|
text = { type = "string", default = "Entry" },
|
||||||
@@ -47,6 +76,21 @@ function List:init(props, basalt)
|
|||||||
Collection.init(self, props, basalt)
|
Collection.init(self, props, basalt)
|
||||||
self._entrySchema = entrySchema
|
self._entrySchema = entrySchema
|
||||||
self.set("type", "List")
|
self.set("type", "List")
|
||||||
|
|
||||||
|
self:observe("items", function()
|
||||||
|
local maxOffset = math.max(0, #self.get("items") - self.get("height"))
|
||||||
|
if self.get("offset") > maxOffset then
|
||||||
|
self.set("offset", maxOffset)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:observe("height", function()
|
||||||
|
local maxOffset = math.max(0, #self.get("items") - self.get("height"))
|
||||||
|
if self.get("offset") > maxOffset then
|
||||||
|
self.set("offset", maxOffset)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,34 +101,98 @@ end
|
|||||||
--- @return boolean Whether the event was handled
|
--- @return boolean Whether the event was handled
|
||||||
--- @protected
|
--- @protected
|
||||||
function List:mouse_click(button, x, y)
|
function List:mouse_click(button, x, y)
|
||||||
if Collection.mouse_click(self, button, x, y) and self.get("selectable") then
|
if Collection.mouse_click(self, button, x, y) then
|
||||||
local _, index = self:getRelativePosition(x, y)
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
local adjustedIndex = index + self.get("offset")
|
local width = self.get("width")
|
||||||
local items = self.get("items")
|
local items = self.get("items")
|
||||||
|
local height = self.get("height")
|
||||||
|
local showScrollBar = self.get("showScrollBar")
|
||||||
|
|
||||||
if adjustedIndex <= #items then
|
if showScrollBar and #items > height and relX == width then
|
||||||
local item = items[adjustedIndex]
|
local maxOffset = #items - height
|
||||||
if not self.get("multiSelection") then
|
local handleSize = math.max(1, math.floor((height / #items) * height))
|
||||||
for _, otherItem in ipairs(items) do
|
|
||||||
if type(otherItem) == "table" then
|
local currentPercent = maxOffset > 0 and (self.get("offset") / maxOffset * 100) or 0
|
||||||
otherItem.selected = false
|
local handlePos = math.floor((currentPercent / 100) * (height - handleSize)) + 1
|
||||||
|
|
||||||
|
if relY >= handlePos and relY < handlePos + handleSize then
|
||||||
|
self._scrollBarDragging = true
|
||||||
|
self._scrollBarDragOffset = relY - handlePos
|
||||||
|
else
|
||||||
|
local newPercent = ((relY - 1) / (height - handleSize)) * 100
|
||||||
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||||
|
self.set("offset", math.max(0, math.min(maxOffset, newOffset)))
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.get("selectable") then
|
||||||
|
local adjustedIndex = relY + self.get("offset")
|
||||||
|
|
||||||
|
if adjustedIndex <= #items then
|
||||||
|
local item = items[adjustedIndex]
|
||||||
|
if not self.get("multiSelection") then
|
||||||
|
for _, otherItem in ipairs(items) do
|
||||||
|
if type(otherItem) == "table" then
|
||||||
|
otherItem.selected = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
item.selected = not item.selected
|
item.selected = not item.selected
|
||||||
|
|
||||||
if item.callback then
|
if item.callback then
|
||||||
item.callback(self)
|
item.callback(self)
|
||||||
|
end
|
||||||
|
self:fireEvent("select", adjustedIndex, item)
|
||||||
|
self:updateRender()
|
||||||
end
|
end
|
||||||
self:fireEvent("select", adjustedIndex, item)
|
|
||||||
self:updateRender()
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Handles mouse drag events for scrollbar
|
||||||
|
--- @param button number The mouse button being dragged
|
||||||
|
--- @param x number The x-coordinate of the drag
|
||||||
|
--- @param y number The y-coordinate of the drag
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function List:mouse_drag(button, x, y)
|
||||||
|
if self._scrollBarDragging then
|
||||||
|
local _, relY = self:getRelativePosition(x, y)
|
||||||
|
local items = self.get("items")
|
||||||
|
local height = self.get("height")
|
||||||
|
local handleSize = math.max(1, math.floor((height / #items) * height))
|
||||||
|
local maxOffset = #items - height
|
||||||
|
relY = math.max(1, math.min(height, relY))
|
||||||
|
|
||||||
|
local newPos = relY - (self._scrollBarDragOffset or 0)
|
||||||
|
local newPercent = ((newPos - 1) / (height - handleSize)) * 100
|
||||||
|
local newOffset = math.floor((newPercent / 100) * maxOffset + 0.5)
|
||||||
|
|
||||||
|
self.set("offset", math.max(0, math.min(maxOffset, newOffset)))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return Collection.mouse_drag and Collection.mouse_drag(self, button, x, y) or false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Handles mouse up events to stop scrollbar dragging
|
||||||
|
--- @param button number The mouse button that was released
|
||||||
|
--- @param x number The x-coordinate of the release
|
||||||
|
--- @param y number The y-coordinate of the release
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function List:mouse_up(button, x, y)
|
||||||
|
if self._scrollBarDragging then
|
||||||
|
self._scrollBarDragging = false
|
||||||
|
self._scrollBarDragOffset = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return Collection.mouse_up and Collection.mouse_up(self, button, x, y) or false
|
||||||
|
end
|
||||||
|
|
||||||
--- @shortDescription Handles mouse scroll events
|
--- @shortDescription Handles mouse scroll events
|
||||||
--- @param direction number The direction of the scroll (1 for down, -1 for up)
|
--- @param direction number The direction of the scroll (1 for down, -1 for up)
|
||||||
--- @param x number The x-coordinate of the scroll
|
--- @param x number The x-coordinate of the scroll
|
||||||
@@ -130,9 +238,78 @@ function List:scrollToTop()
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Scrolls to make a specific item visible
|
||||||
|
--- @shortDescription Scrolls to a specific item
|
||||||
|
--- @param index number The index of the item to scroll to
|
||||||
|
--- @return List self The List instance
|
||||||
|
--- @usage list:scrollToItem(5)
|
||||||
|
function List:scrollToItem(index)
|
||||||
|
local height = self.get("height")
|
||||||
|
local offset = self.get("offset")
|
||||||
|
|
||||||
|
if index < offset + 1 then
|
||||||
|
self.set("offset", math.max(0, index - 1))
|
||||||
|
elseif index > offset + height then
|
||||||
|
self.set("offset", index - height)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles key events for keyboard navigation
|
||||||
|
--- @shortDescription Handles key events
|
||||||
|
--- @param keyCode number The key code
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function List:key(keyCode)
|
||||||
|
if Collection.key(self, keyCode) and self.get("selectable") then
|
||||||
|
local items = self.get("items")
|
||||||
|
local currentIndex = self:getSelectedIndex()
|
||||||
|
|
||||||
|
if keyCode == keys.up then
|
||||||
|
self:selectPrevious()
|
||||||
|
if currentIndex and currentIndex > 1 then
|
||||||
|
self:scrollToItem(currentIndex - 1)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
elseif keyCode == keys.down then
|
||||||
|
self:selectNext()
|
||||||
|
if currentIndex and currentIndex < #items then
|
||||||
|
self:scrollToItem(currentIndex + 1)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
elseif keyCode == keys.home then
|
||||||
|
self:clearItemSelection()
|
||||||
|
self:selectItem(1)
|
||||||
|
self:scrollToTop()
|
||||||
|
return true
|
||||||
|
elseif keyCode == keys["end"] then
|
||||||
|
self:clearItemSelection()
|
||||||
|
self:selectItem(#items)
|
||||||
|
self:scrollToBottom()
|
||||||
|
return true
|
||||||
|
elseif keyCode == keys.pageUp then
|
||||||
|
local height = self.get("height")
|
||||||
|
local newIndex = math.max(1, (currentIndex or 1) - height)
|
||||||
|
self:clearItemSelection()
|
||||||
|
self:selectItem(newIndex)
|
||||||
|
self:scrollToItem(newIndex)
|
||||||
|
return true
|
||||||
|
elseif keyCode == keys.pageDown then
|
||||||
|
local height = self.get("height")
|
||||||
|
local newIndex = math.min(#items, (currentIndex or 1) + height)
|
||||||
|
self:clearItemSelection()
|
||||||
|
self:selectItem(newIndex)
|
||||||
|
self:scrollToItem(newIndex)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
--- @shortDescription Renders the list
|
--- @shortDescription Renders the list
|
||||||
--- @protected
|
--- @protected
|
||||||
function List:render()
|
function List:render(vOffset)
|
||||||
Collection.render(self)
|
Collection.render(self)
|
||||||
|
|
||||||
local items = self.get("items")
|
local items = self.get("items")
|
||||||
@@ -141,6 +318,25 @@ function List:render()
|
|||||||
local width = self.get("width")
|
local width = self.get("width")
|
||||||
local listBg = self.getResolved("background")
|
local listBg = self.getResolved("background")
|
||||||
local listFg = self.getResolved("foreground")
|
local listFg = self.getResolved("foreground")
|
||||||
|
local showScrollBar = self.get("showScrollBar")
|
||||||
|
|
||||||
|
local needsScrollBar = showScrollBar and #items > height
|
||||||
|
local contentWidth = needsScrollBar and width - 1 or width
|
||||||
|
|
||||||
|
if #items == 0 then
|
||||||
|
local emptyText = self.get("emptyText")
|
||||||
|
local y = math.floor(height / 2) + vOffset
|
||||||
|
local x = math.max(1, math.floor((width - #emptyText) / 2) + 1)
|
||||||
|
|
||||||
|
for i = 1, height do
|
||||||
|
self:textBg(1, i, string.rep(" ", width), listBg)
|
||||||
|
end
|
||||||
|
|
||||||
|
if y >= 1 and y <= height then
|
||||||
|
self:textFg(x, y + vOffset, emptyText, colors.gray)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
for i = 1, height do
|
for i = 1, height do
|
||||||
local itemIndex = i + offset
|
local itemIndex = i + offset
|
||||||
@@ -148,15 +344,15 @@ function List:render()
|
|||||||
|
|
||||||
if item then
|
if item then
|
||||||
if item.separator then
|
if item.separator then
|
||||||
local separatorChar = (item.text or "-"):sub(1,1)
|
local separatorChar = ((item.text or "-") ~= "" and item.text or "-"):sub(1,1)
|
||||||
local separatorText = string.rep(separatorChar, width)
|
local separatorText = string.rep(separatorChar, contentWidth)
|
||||||
local fg = item.fg or listFg
|
local fg = item.fg or listFg
|
||||||
local bg = item.bg or listBg
|
local bg = item.bg or listBg
|
||||||
|
|
||||||
self:textBg(1, i, string.rep(" ", width), bg)
|
self:textBg(1, i + vOffset, string.rep(" ", contentWidth), bg)
|
||||||
self:textFg(1, i, separatorText:sub(1, width), fg)
|
self:textFg(1, i + vOffset, separatorText, fg)
|
||||||
else
|
else
|
||||||
local text = item.text
|
local text = item.text or ""
|
||||||
local isSelected = item.selected
|
local isSelected = item.selected
|
||||||
local bg = isSelected and
|
local bg = isSelected and
|
||||||
(item.selectedBg or self.getResolved("selectedBackground")) or
|
(item.selectedBg or self.getResolved("selectedBackground")) or
|
||||||
@@ -165,11 +361,42 @@ function List:render()
|
|||||||
local fg = isSelected and
|
local fg = isSelected and
|
||||||
(item.selectedFg or self.getResolved("selectedForeground")) or
|
(item.selectedFg or self.getResolved("selectedForeground")) or
|
||||||
(item.fg or listFg)
|
(item.fg or listFg)
|
||||||
self:textBg(1, i, string.rep(" ", width), bg)
|
|
||||||
self:textFg(1, i, text:sub(1, width), fg)
|
local displayText = text
|
||||||
|
if #displayText > contentWidth then
|
||||||
|
displayText = displayText:sub(1, contentWidth - 3) .. "..."
|
||||||
|
else
|
||||||
|
displayText = displayText .. string.rep(" ", contentWidth - #displayText)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:textBg(1, i + vOffset, string.rep(" ", contentWidth), bg)
|
||||||
|
self:textFg(1, i + vOffset, displayText, fg)
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
self:textBg(1, i + vOffset, string.rep(" ", contentWidth), listBg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if needsScrollBar then
|
||||||
|
local handleSize = math.max(1, math.floor((height / #items) * height))
|
||||||
|
local maxOffset = #items - height
|
||||||
|
|
||||||
|
local currentPercent = maxOffset > 0 and (offset / maxOffset * 100) or 0
|
||||||
|
local handlePos = math.floor((currentPercent / 100) * (height - handleSize)) + 1
|
||||||
|
|
||||||
|
local scrollBarSymbol = self.getResolved("scrollBarSymbol")
|
||||||
|
local scrollBarBg = self.getResolved("scrollBarBackground")
|
||||||
|
local scrollBarColor = self.getResolved("scrollBarColor")
|
||||||
|
local scrollBarBgColor = self.getResolved("scrollBarBackgroundColor")
|
||||||
|
|
||||||
|
for i = 1, height do
|
||||||
|
self:blit(width, i + vOffset, scrollBarBg, tHex[listFg], tHex[scrollBarBgColor])
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = handlePos, math.min(height, handlePos + handleSize - 1) do
|
||||||
|
self:blit(width, i + vOffset, scrollBarSymbol, tHex[scrollBarColor], tHex[scrollBarBgColor])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return List
|
return List
|
||||||
@@ -20,15 +20,40 @@ local XMLNode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local parseAttributes = function(node, s)
|
local parseAttributes = function(node, s)
|
||||||
local _, _ = string.gsub(s, "(%w+)=([\"'])(.-)%2", function(attribute, _, value)
|
local _, _ = string.gsub(s, "([%w:]+)=([\"'])(.-)%2", function(attribute, _, value)
|
||||||
node:addAttribute(attribute, "\"" .. value .. "\"")
|
node:addAttribute(attribute, "\"" .. value .. "\"")
|
||||||
end)
|
end)
|
||||||
local _, _ = string.gsub(s, "(%w+)={(.-)}", function(attribute, expression)
|
local _, _ = string.gsub(s, "([%w:]+)={(.-)}", function(attribute, expression)
|
||||||
node:addAttribute(attribute, expression)
|
node:addAttribute(attribute, expression)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local XMLParser = {
|
local XMLParser = {}
|
||||||
|
XMLParser = {
|
||||||
|
_customTagHandlers = {},
|
||||||
|
|
||||||
|
--- Registers a custom tag handler
|
||||||
|
--- @param tagName string The name of the custom tag
|
||||||
|
--- @param handler function The handler function(node, parent, scope)
|
||||||
|
registerTagHandler = function(tagName, handler)
|
||||||
|
XMLParser._customTagHandlers[tagName] = handler
|
||||||
|
log.info("XMLParser: Registered custom tag handler for '" .. tagName .. "'")
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Unregisters a custom tag handler
|
||||||
|
--- @param tagName string The name of the custom tag
|
||||||
|
unregisterTagHandler = function(tagName)
|
||||||
|
XMLParser._customTagHandlers[tagName] = nil
|
||||||
|
log.info("XMLParser: Unregistered custom tag handler for '" .. tagName .. "'")
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Gets a custom tag handler
|
||||||
|
--- @param tagName string The name of the custom tag
|
||||||
|
--- @return function|nil handler The handler function or nil
|
||||||
|
getTagHandler = function(tagName)
|
||||||
|
return XMLParser._customTagHandlers[tagName]
|
||||||
|
end,
|
||||||
|
|
||||||
parseText = function(xmlText)
|
parseText = function(xmlText)
|
||||||
local stack = {}
|
local stack = {}
|
||||||
local top = XMLNode.new()
|
local top = XMLNode.new()
|
||||||
@@ -120,7 +145,15 @@ local function convertValue(value, scope)
|
|||||||
for k,v in pairs(scope) do
|
for k,v in pairs(scope) do
|
||||||
env[k] = v
|
env[k] = v
|
||||||
end
|
end
|
||||||
return load("return " .. cdata, nil, "bt", env)()
|
local fn, err = load("return " .. cdata, nil, "bt", env)
|
||||||
|
if not fn then
|
||||||
|
errorManager.error("XMLParser: CDATA syntax error: " .. tostring(err))
|
||||||
|
end
|
||||||
|
local success, result = pcall(fn)
|
||||||
|
if not success then
|
||||||
|
errorManager.error("XMLParser: CDATA execution error: " .. tostring(result))
|
||||||
|
end
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
if value == "true" then
|
if value == "true" then
|
||||||
@@ -168,6 +201,25 @@ local function createTableFromNode(node, scope)
|
|||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function parseStateAttribute(self, attribute, value, scope)
|
||||||
|
local propName, stateName = attribute:match("^(.+)State:(.+)$")
|
||||||
|
if propName and stateName then
|
||||||
|
stateName = stateName:gsub("^\"", ""):gsub("\"$", "")
|
||||||
|
|
||||||
|
local capitalizedName = propName:sub(1,1):upper() .. propName:sub(2)
|
||||||
|
local methodName = "set"..capitalizedName.."State"
|
||||||
|
|
||||||
|
if self[methodName] then
|
||||||
|
self[methodName](self, stateName, convertValue(value, scope))
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
log.warn("XMLParser: State method '" .. methodName .. "' not found for element '" .. self:getType() .. "'")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local BaseElement = {}
|
local BaseElement = {}
|
||||||
|
|
||||||
function BaseElement.setup(element)
|
function BaseElement.setup(element)
|
||||||
@@ -183,32 +235,56 @@ end
|
|||||||
function BaseElement:fromXML(node, scope)
|
function BaseElement:fromXML(node, scope)
|
||||||
if(node.attributes)then
|
if(node.attributes)then
|
||||||
for k, v in pairs(node.attributes) do
|
for k, v in pairs(node.attributes) do
|
||||||
if(self._properties[k])then
|
if not parseStateAttribute(self, k, v, scope) then
|
||||||
self.set(k, convertValue(v, scope))
|
if(self._properties[k])then
|
||||||
elseif self[k] then
|
self.set(k, convertValue(v, scope))
|
||||||
if(k:sub(1,2)=="on")then
|
elseif self[k] then
|
||||||
local val = v:gsub("\"", "")
|
if(k:sub(1,2)=="on")then
|
||||||
if(scope[val])then
|
local val = v:gsub("\"", "")
|
||||||
if(type(scope[val]) ~= "function")then
|
if(scope[val])then
|
||||||
errorManager.error("XMLParser: variable '" .. val .. "' is not a function for element '" .. self:getType() .. "' "..k)
|
if(type(scope[val]) ~= "function")then
|
||||||
|
errorManager.error("XMLParser: variable '" .. val .. "' is not a function for element '" .. self:getType() .. "' "..k)
|
||||||
|
end
|
||||||
|
self[k](self, scope[val])
|
||||||
|
else
|
||||||
|
errorManager.error("XMLParser: variable '" .. val .. "' not found in scope")
|
||||||
end
|
end
|
||||||
self[k](self, scope[val])
|
|
||||||
else
|
else
|
||||||
errorManager.error("XMLParser: variable '" .. val .. "' not found in scope")
|
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
|
local customXML = self.get("customXML")
|
||||||
|
customXML.attributes[k] = convertValue(v, scope)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
local customXML = self.get("customXML")
|
|
||||||
customXML.attributes[k] = convertValue(v, scope)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if(node.children)then
|
if(node.children)then
|
||||||
for _, child in pairs(node.children) do
|
for _, child in pairs(node.children) do
|
||||||
if(self._properties[child.tag])then
|
if child.tag == "state" then
|
||||||
|
local stateName = child.attributes and child.attributes.name
|
||||||
|
if not stateName then
|
||||||
|
errorManager.error("XMLParser: <state> tag requires 'name' attribute")
|
||||||
|
end
|
||||||
|
|
||||||
|
stateName = stateName:gsub("^\"", ""):gsub("\"$", "")
|
||||||
|
|
||||||
|
if child.children then
|
||||||
|
for _, stateChild in ipairs(child.children) do
|
||||||
|
local propName = stateChild.tag
|
||||||
|
local value = convertValue(stateChild.value, scope)
|
||||||
|
local capitalizedName = propName:sub(1,1):upper() .. propName:sub(2)
|
||||||
|
local methodName = "set"..capitalizedName.."State"
|
||||||
|
|
||||||
|
if self[methodName] then
|
||||||
|
self[methodName](self, stateName, value)
|
||||||
|
else
|
||||||
|
log.warn("XMLParser: State method '" .. methodName .. "' not found for element '" .. self:getType() .. "'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif(self._properties[child.tag])then
|
||||||
if(self._properties[child.tag].type == "table")then
|
if(self._properties[child.tag].type == "table")then
|
||||||
self.set(child.tag, createTableFromNode(child, scope))
|
self.set(child.tag, createTableFromNode(child, scope))
|
||||||
else
|
else
|
||||||
@@ -280,9 +356,15 @@ function Container:fromXML(nodes, scope)
|
|||||||
if(nodes.children)then
|
if(nodes.children)then
|
||||||
for _, node in ipairs(nodes.children) do
|
for _, node in ipairs(nodes.children) do
|
||||||
local capitalizedName = node.tag:sub(1,1):upper() .. node.tag:sub(2)
|
local capitalizedName = node.tag:sub(1,1):upper() .. node.tag:sub(2)
|
||||||
if self["add"..capitalizedName] then
|
|
||||||
|
local customHandler = XMLParser.getTagHandler(node.tag)
|
||||||
|
if customHandler then
|
||||||
|
local result = customHandler(node, self, scope)
|
||||||
|
elseif self["add"..capitalizedName] then
|
||||||
local element = self["add"..capitalizedName](self)
|
local element = self["add"..capitalizedName](self)
|
||||||
element:fromXML(node, scope)
|
element:fromXML(node, scope)
|
||||||
|
else
|
||||||
|
log.warn("XMLParser: Unknown tag '" .. node.tag .. "' - no handler or element found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user