- 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
|
||||
item.selected = false
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
local List = require("elements/List")
|
||||
local ScrollBar = require("elements/ScrollBar")
|
||||
local tHex = require("libraries/colorHex")
|
||||
|
||||
---@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")
|
||||
end
|
||||
return true
|
||||
elseif isOpen and relY > 1 and self.get("selectable") 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
|
||||
elseif isOpen and relY > 1 then
|
||||
-- Forward to List handler for scrollbar handling
|
||||
return List.mouse_click(self, button, x, y - 1)
|
||||
end
|
||||
return false
|
||||
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
|
||||
--- @protected
|
||||
function DropDown:render()
|
||||
@@ -157,53 +194,13 @@ function DropDown:render()
|
||||
text = text:sub(1, self.get("width") - 2)
|
||||
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"),
|
||||
string.rep(tHex[self.getResolved("foreground")], 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
|
||||
|
||||
--- Called when the DropDown gains focus
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local Collection = require("elements/Collection")
|
||||
local tHex = require("libraries/colorHex")
|
||||
---@configDescription A scrollable list of selectable items
|
||||
|
||||
--- 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
|
||||
|
||||
---@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
|
||||
List.defineEvent(List, "mouse_click")
|
||||
List.defineEvent(List, "mouse_up")
|
||||
List.defineEvent(List, "mouse_drag")
|
||||
List.defineEvent(List, "mouse_scroll")
|
||||
List.defineEvent(List, "key")
|
||||
|
||||
local entrySchema = {
|
||||
text = { type = "string", default = "Entry" },
|
||||
@@ -47,6 +76,21 @@ function List:init(props, basalt)
|
||||
Collection.init(self, props, basalt)
|
||||
self._entrySchema = entrySchema
|
||||
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
|
||||
end
|
||||
|
||||
@@ -57,34 +101,98 @@ end
|
||||
--- @return boolean Whether the event was handled
|
||||
--- @protected
|
||||
function List:mouse_click(button, x, y)
|
||||
if Collection.mouse_click(self, button, x, y) and self.get("selectable") then
|
||||
local _, index = self:getRelativePosition(x, y)
|
||||
local adjustedIndex = index + self.get("offset")
|
||||
if Collection.mouse_click(self, button, x, y) then
|
||||
local relX, relY = self:getRelativePosition(x, y)
|
||||
local width = self.get("width")
|
||||
local items = self.get("items")
|
||||
local height = self.get("height")
|
||||
local showScrollBar = self.get("showScrollBar")
|
||||
|
||||
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
|
||||
if showScrollBar and #items > height and relX == width then
|
||||
local maxOffset = #items - height
|
||||
local handleSize = math.max(1, math.floor((height / #items) * height))
|
||||
|
||||
local currentPercent = maxOffset > 0 and (self.get("offset") / maxOffset * 100) or 0
|
||||
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
|
||||
|
||||
item.selected = not item.selected
|
||||
item.selected = not item.selected
|
||||
|
||||
if item.callback then
|
||||
item.callback(self)
|
||||
if item.callback then
|
||||
item.callback(self)
|
||||
end
|
||||
self:fireEvent("select", adjustedIndex, item)
|
||||
self:updateRender()
|
||||
end
|
||||
self:fireEvent("select", adjustedIndex, item)
|
||||
self:updateRender()
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
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
|
||||
--- @param direction number The direction of the scroll (1 for down, -1 for up)
|
||||
--- @param x number The x-coordinate of the scroll
|
||||
@@ -130,9 +238,78 @@ function List:scrollToTop()
|
||||
return self
|
||||
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
|
||||
--- @protected
|
||||
function List:render()
|
||||
function List:render(vOffset)
|
||||
Collection.render(self)
|
||||
|
||||
local items = self.get("items")
|
||||
@@ -141,6 +318,25 @@ function List:render()
|
||||
local width = self.get("width")
|
||||
local listBg = self.getResolved("background")
|
||||
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
|
||||
local itemIndex = i + offset
|
||||
@@ -148,15 +344,15 @@ function List:render()
|
||||
|
||||
if item then
|
||||
if item.separator then
|
||||
local separatorChar = (item.text or "-"):sub(1,1)
|
||||
local separatorText = string.rep(separatorChar, width)
|
||||
local separatorChar = ((item.text or "-") ~= "" and item.text or "-"):sub(1,1)
|
||||
local separatorText = string.rep(separatorChar, contentWidth)
|
||||
local fg = item.fg or listFg
|
||||
local bg = item.bg or listBg
|
||||
|
||||
self:textBg(1, i, string.rep(" ", width), bg)
|
||||
self:textFg(1, i, separatorText:sub(1, width), fg)
|
||||
self:textBg(1, i + vOffset, string.rep(" ", contentWidth), bg)
|
||||
self:textFg(1, i + vOffset, separatorText, fg)
|
||||
else
|
||||
local text = item.text
|
||||
local text = item.text or ""
|
||||
local isSelected = item.selected
|
||||
local bg = isSelected and
|
||||
(item.selectedBg or self.getResolved("selectedBackground")) or
|
||||
@@ -165,11 +361,42 @@ function List:render()
|
||||
local fg = isSelected and
|
||||
(item.selectedFg or self.getResolved("selectedForeground")) or
|
||||
(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
|
||||
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
|
||||
|
||||
return List
|
||||
return List
|
||||
@@ -20,15 +20,40 @@ local XMLNode = {
|
||||
}
|
||||
|
||||
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 .. "\"")
|
||||
end)
|
||||
local _, _ = string.gsub(s, "(%w+)={(.-)}", function(attribute, expression)
|
||||
local _, _ = string.gsub(s, "([%w:]+)={(.-)}", function(attribute, expression)
|
||||
node:addAttribute(attribute, expression)
|
||||
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)
|
||||
local stack = {}
|
||||
local top = XMLNode.new()
|
||||
@@ -120,7 +145,15 @@ local function convertValue(value, scope)
|
||||
for k,v in pairs(scope) do
|
||||
env[k] = v
|
||||
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
|
||||
|
||||
if value == "true" then
|
||||
@@ -168,6 +201,25 @@ local function createTableFromNode(node, scope)
|
||||
return list
|
||||
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 = {}
|
||||
|
||||
function BaseElement.setup(element)
|
||||
@@ -183,32 +235,56 @@ end
|
||||
function BaseElement:fromXML(node, scope)
|
||||
if(node.attributes)then
|
||||
for k, v in pairs(node.attributes) do
|
||||
if(self._properties[k])then
|
||||
self.set(k, convertValue(v, scope))
|
||||
elseif self[k] then
|
||||
if(k:sub(1,2)=="on")then
|
||||
local val = v:gsub("\"", "")
|
||||
if(scope[val])then
|
||||
if(type(scope[val]) ~= "function")then
|
||||
errorManager.error("XMLParser: variable '" .. val .. "' is not a function for element '" .. self:getType() .. "' "..k)
|
||||
if not parseStateAttribute(self, k, v, scope) then
|
||||
if(self._properties[k])then
|
||||
self.set(k, convertValue(v, scope))
|
||||
elseif self[k] then
|
||||
if(k:sub(1,2)=="on")then
|
||||
local val = v:gsub("\"", "")
|
||||
if(scope[val])then
|
||||
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
|
||||
self[k](self, scope[val])
|
||||
else
|
||||
errorManager.error("XMLParser: variable '" .. val .. "' not found in scope")
|
||||
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
|
||||
end
|
||||
else
|
||||
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
|
||||
local customXML = self.get("customXML")
|
||||
customXML.attributes[k] = convertValue(v, scope)
|
||||
end
|
||||
else
|
||||
local customXML = self.get("customXML")
|
||||
customXML.attributes[k] = convertValue(v, scope)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if(node.children)then
|
||||
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
|
||||
self.set(child.tag, createTableFromNode(child, scope))
|
||||
else
|
||||
@@ -280,9 +356,15 @@ function Container:fromXML(nodes, scope)
|
||||
if(nodes.children)then
|
||||
for _, node in ipairs(nodes.children) do
|
||||
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)
|
||||
element:fromXML(node, scope)
|
||||
else
|
||||
log.warn("XMLParser: Unknown tag '" .. node.tag .. "' - no handler or element found")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user