Download CommonLua/X/XList.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/X/XList.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/X/XList.lua
-
curl -L -o XList.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/X/XList.lua
18.8 kB
| DefineClass.XList = { | |
| __parents = { "XScrollArea" }, | |
| properties = { | |
| { category = "General", id = "MultipleSelection", editor = "bool", default = false, }, | |
| { category = "General", id = "LeftThumbScroll", editor = "bool", default = true, }, | |
| { category = "General", id = "GamepadInitialSelection", Name = "Gamepad Initial Selection", editor = "bool", default = true, }, | |
| { category = "General", id = "CycleSelection", Name = "Cycle selection", editor = "bool", default = false, }, | |
| { category = "General", id = "ForceInitialSelection", editor = "bool", default = false, }, | |
| { category = "General", id = "SetFocusOnOpen", editor = "bool", default = false, }, | |
| { category = "General", id = "WorkUnfocused", editor = "bool", default = false, }, | |
| { category = "Actions", id = "ActionContext", editor = "text", default = "", }, | |
| { category = "Actions", id = "ItemActionContext", editor = "text", default = "", }, | |
| { category = "Visual", id = "MaxRowsVisible", editor = "number", default = 0, invalidate = "measure" }, | |
| { category = "Interaction", id = "OnSelection", editor = "func", params = "self, focused_item, selection", }, | |
| { category = "Interaction", id = "OnDoubleClick", editor = "func", params = "self, item_idx", }, | |
| }, | |
| Clip = "parent & self", | |
| LayoutMethod = "VList", | |
| Padding = box(2, 2, 2, 2), | |
| BorderWidth = 1, | |
| BorderColor = RGB(32, 32, 32), | |
| Background = RGB(255, 255, 255), | |
| FocusedBackground = RGB(255, 255, 255), | |
| focused_item = false, | |
| selection = false, -- table | |
| item_hashes = false, -- table, used when the LayoutMethod is Grid | |
| docked_win_count = 0, -- these must be the last children, and are not considered list items (usually scrollbars) | |
| force_keep_items_spawned = false, | |
| } | |
| ----- helpers | |
| local function IsItemSelectable(child) | |
| return (not child:HasMember("IsSelectable") or child:IsSelectable()) and child:GetVisible() | |
| end | |
| local function SetItemSelected(child, selected) | |
| if not child or not child:HasMember("SetSelected") then | |
| return | |
| end | |
| child:SetSelected(selected) | |
| end | |
| local function SetItemFocused(child, focused) | |
| if not child or not child:HasMember("SetFocused") then | |
| return | |
| end | |
| child:SetFocused(focused) | |
| end | |
| ----- general methods | |
| function XList:Init(parent, context) | |
| self.selection = {} | |
| end | |
| function XList:Open(...) | |
| self:GenerateItemHashTable() | |
| XScrollArea.Open(self, ...) | |
| self:CreateThread("SetInitialSelection", self.SetInitialSelection, self) | |
| end | |
| function XList:Clear() | |
| self.focused_item = false | |
| self.selection = {} | |
| for i = #self, 1, -1 do | |
| local win = self[i] | |
| if not win.Dock or win.Dock == "ignore" then | |
| win:delete() | |
| end | |
| end | |
| XScrollArea.Clear(self, "keep_children") | |
| end | |
| -- Counts docked child windows (usually scrollbars), and makes sure they are at the end of the children list | |
| function XList:SortChildren() | |
| local docked = 0 | |
| for _, win in ipairs(self) do | |
| if win.Dock and win.Dock ~= "ignore" then | |
| docked = docked + 1 | |
| win.ZOrder = max_int | |
| end | |
| end | |
| self.docked_win_count = docked | |
| return XWindow.SortChildren(self) | |
| end | |
| function XList:GetItemCount() | |
| return #self - self.docked_win_count | |
| end | |
| function XList:GenerateItemHashTable() | |
| if self.LayoutMethod == "Grid" and self:GetItemCount() > 0 then | |
| self.item_hashes = {} | |
| for i, v in ipairs(self) do | |
| local x, y = v.GridX, v.GridY | |
| for j = x, x + v.GridWidth - 1 do | |
| for k = y, y + v.GridHeight - 1 do | |
| self.item_hashes[j .. k] = i | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function XList:CreateTextItem(text, props, context) | |
| props = props or {} | |
| local item = XListItem:new({ selectable = props.selectable }, self) | |
| props.selectable = nil | |
| local text_control = XText:new(props, item, context) | |
| text_control:SetText(text) | |
| return item | |
| end | |
| function XList:GetItemAt(pt, allow_outside_items) | |
| local target = false | |
| local method = allow_outside_items and "PointInWindow" or "MouseInWindow" | |
| for idx, win in ipairs(self) do | |
| if (not target or win.DrawOnTop) and win[method](win, pt) then | |
| target = idx | |
| if self.LayoutMethod ~= "HOverlappingList" and self.LayoutMethod ~= "VOverlappingList" then | |
| return target | |
| end | |
| end | |
| end | |
| return target | |
| end | |
| function XList:Measure(max_width, max_height) | |
| local width, height = XScrollArea.Measure(self, max_width, max_height) | |
| local elements = self:GetItemCount() | |
| if self.MaxRowsVisible > 0 and elements > 0 then | |
| assert(self.LayoutMethod == "VList" or self.LayoutMethod == "HWrap") | |
| height = Min(height, self[1].measure_height * self.MaxRowsVisible) | |
| end | |
| return width, height | |
| end | |
| ----- mouse/keyboard/controller | |
| function XList:OnMouseButtonDown(pt, button) | |
| local target = self:GetItemAt(pt) | |
| if button == "L" then | |
| if not self.WorkUnfocused then | |
| self:SetFocus(true) | |
| end | |
| if not target or not IsItemSelectable(self[target]) then | |
| return "break" | |
| end | |
| self:OnItemClicked(target, button) | |
| local shift = terminal.IsKeyPressed(const.vkShift) | |
| local ctrl = terminal.IsKeyPressed(const.vkControl) | |
| if not self.MultipleSelection or not (shift or ctrl) then | |
| self:SetSelection(target) | |
| elseif ctrl then | |
| self:ToggleSelected(target) | |
| elseif shift then | |
| self:SelectRange(self.focused_item or target, target) | |
| end | |
| if self.MultipleSelection then | |
| self.desktop:SetMouseCapture(self) | |
| end | |
| return "break" | |
| elseif button == "R" then | |
| if not self.WorkUnfocused then | |
| self:SetFocus(true) | |
| end | |
| local action_context = self.ItemActionContext | |
| if not target or not IsItemSelectable(self[target]) then | |
| action_context = self.ActionContext | |
| end | |
| local host = GetActionsHost(self, true) | |
| if host and host:OpenContextMenu(action_context, pt) then | |
| if target and IsItemSelectable(self[target]) and (not self.MultipleSelection or (not self:HasMember("selected")) or (#self.selected < 2)) then | |
| self:SetSelection(target) | |
| end | |
| end | |
| self:OnItemClicked(target, button) | |
| return "break" | |
| end | |
| end | |
| function XList:OnMouseButtonDoubleClick(pt, button) | |
| local shift = terminal.IsKeyPressed(const.vkShift) | |
| local ctrl = terminal.IsKeyPressed(const.vkControl) | |
| if button == "L" and not shift and not ctrl and self.focused_item then | |
| self:OnDoubleClick(self.focused_item) | |
| return "break" | |
| end | |
| end | |
| function XList:OnMousePos(pt) | |
| if self.desktop:GetMouseCapture() == self and self.focused_item then | |
| local target = self:GetItemAt(pt) | |
| if target and IsItemSelectable(self[target]) then | |
| self:SelectRange(self.focused_item, target) | |
| end | |
| return "break" | |
| end | |
| end | |
| function XList:OnMouseButtonUp(pt, button) | |
| if button == "L" then | |
| self.desktop:SetMouseCapture() | |
| return "break" | |
| end | |
| end | |
| function XList:OnShortcut(shortcut, source, ...) | |
| local target, arrow_key = nil, nil | |
| shortcut = string.gsub(shortcut, "Shift%-", "") -- ignore shift | |
| if (self.LayoutMethod == "HList" or self.LayoutMethod == "HOverlappingList" or self.LayoutMethod == "HWrap") and (shortcut == "Left" or shortcut == "Ctrl-Left") or | |
| (self.LayoutMethod == "VList" or self.LayoutMethod == "VOverlappingList" or self.LayoutMethod == "VWrap") and (shortcut == "Up" or shortcut == "Ctrl-Up") | |
| then | |
| target, arrow_key = self:NextSelectableItem(self.focused_item, -1, -1), true | |
| elseif (self.LayoutMethod == "HList" or self.LayoutMethod == "HOverlappingList" or self.LayoutMethod == "HWrap") and (shortcut == "Right" or shortcut == "Ctrl-Right") or | |
| (self.LayoutMethod == "VList" or self.LayoutMethod == "VOverlappingList" or self.LayoutMethod == "VWrap") and (shortcut == "Down" or shortcut == "Ctrl-Down") | |
| then | |
| target, arrow_key = self:NextSelectableItem(self.focused_item, 1, 1), true | |
| elseif self.LayoutMethod == "Grid" and (shortcut == "Left" or shortcut == "Right" or shortcut == "Up" or shortcut == "Down" or | |
| shortcut == "Ctrl-Left" or shortcut == "Ctrl-Right" or shortcut == "Ctrl-Up" or shortcut == "Ctrl-Down") then | |
| target, arrow_key = self:NextGridItem(self.focused_item, shortcut), true | |
| elseif shortcut == "Home" or shortcut == "Ctrl-Home" then | |
| target = self:NextSelectableItem(1, 0, 1) | |
| elseif shortcut == "End" or shortcut == "Ctrl-End" then | |
| target = self:NextSelectableItem(self:GetItemCount(), 0, -1) | |
| elseif shortcut == "Pageup" then | |
| if self.focused_item then | |
| local offset = (self.LayoutMethod == "VList" or self.LayoutMethod == "VOverlappingList" or self.LayoutMethod == "VWrap") and point(0, self.content_box:sizey()) or point(self.content_box:sizex(), 0) | |
| local child = self[self.focused_item] | |
| target = self:GetItemAt(child.content_box:Center() - offset, "allow_outside_items") | |
| end | |
| target = target or self:NextSelectableItem(1, 0, 1) | |
| elseif shortcut == "Pagedown" then | |
| if self.focused_item then | |
| local offset = (self.LayoutMethod == "VList" or self.LayoutMethod == "VOverlappingList" or self.LayoutMethod == "VWrap") and point(0, self.content_box:sizey()) or point(self.content_box:sizex(), 0) | |
| local child = self[self.focused_item] | |
| target = self:GetItemAt(child.content_box:Center() + offset, "allow_outside_items") | |
| end | |
| target = target or self:NextSelectableItem(self:GetItemCount(), 0, -1) | |
| elseif self.MultipleSelection and (shortcut == "Space" or shortcut == "Ctrl-Space") then | |
| if self.focused_item then | |
| self:ToggleSelected(self.focused_item) | |
| end | |
| return "break" | |
| elseif self.MultipleSelection and shortcut == "Ctrl-A" then | |
| self:SelectAll() | |
| return "break" | |
| end | |
| if target ~= nil then | |
| if target then | |
| if arrow_key and terminal.IsKeyPressed(const.vkControl) and self.MultipleSelection then | |
| self:SetFocusedItem(target) | |
| elseif terminal.IsKeyPressed(const.vkShift) and self.MultipleSelection then | |
| self:SelectRange(self.focused_item, target) | |
| else | |
| self:SetSelection(target) | |
| end | |
| end | |
| return "break" | |
| end | |
| if shortcut == "DPadUp" or (shortcut == "LeftThumbUp" and self.LeftThumbScroll) then | |
| return self:OnShortcut("Up", "keyboard", ...) | |
| elseif shortcut == "DPadDown" or (shortcut == "LeftThumbDown" and self.LeftThumbScroll) then | |
| return self:OnShortcut("Down", "keyboard", ...) | |
| elseif shortcut == "DPadLeft" or ((shortcut == "LeftThumbLeft" or shortcut == "LeftThumbDownLeft" or shortcut == "LeftThumbUpLeft") and self.LeftThumbScroll) then | |
| return self:OnShortcut("Left", "keyboard", ...) | |
| elseif shortcut == "DPadRight" or ((shortcut == "LeftThumbRight" or shortcut == "LeftThumbDownRight" or shortcut == "LeftThumbUpRight") and self.LeftThumbScroll) then | |
| return self:OnShortcut("Right", "keyboard", ...) | |
| elseif shortcut == "ButtonA" then | |
| return self:OnShortcut("Space", "keyboard", ...) | |
| end | |
| end | |
| function XList:NextSelectableItem(item, offset, step) | |
| local item_count = self:GetItemCount() | |
| if not item then | |
| return item_count > 0 and self:GetFirstValidItemIdx() or false | |
| end | |
| local i = item + offset | |
| while i > 0 and i <= item_count and not IsItemSelectable(self[i]) do | |
| i = i + step | |
| end | |
| if self.CycleSelection then | |
| if i <= 0 then | |
| i = item_count | |
| elseif i > item_count then | |
| i = 1 | |
| end | |
| end | |
| while i > 0 and i <= item_count and not IsItemSelectable(self[i]) do | |
| i = i + step | |
| end | |
| return i > 0 and i <= item_count and i or false | |
| end | |
| function XList:NextGridItem(item, dir) | |
| local item_count = self:GetItemCount() | |
| if not item then | |
| return item_count > 0 and 1 or false | |
| end | |
| local current = self[item] | |
| local x, y = current.GridX, current.GridY | |
| if dir == "Left" then | |
| x = x - 1 | |
| elseif dir == "Right" then | |
| x = x + (current.GridWidth - 1) + 1 | |
| elseif dir == "Up" then | |
| y = y - 1 | |
| elseif dir == "Down" then | |
| y = y + (current.GridHeight - 1) + 1 | |
| end | |
| if x > 0 and y > 0 then | |
| local i = self.item_hashes[x .. y] | |
| -- find the first selectable item on the desired row | |
| while not i and x > 1 do | |
| x = x - 1 | |
| i = self.item_hashes[x .. y] | |
| end | |
| while i and i > 0 and i <= item_count and not IsItemSelectable(self[i]) do | |
| i = self:NextGridItem(i, dir) | |
| end | |
| return i and i > 0 and i <= item_count and i or false | |
| end | |
| end | |
| ----- focus (focused item is also used as single selection) | |
| function XList:GetFocusedItem() | |
| return self.focused_item | |
| end | |
| function XList:GetScrollTarget() | |
| return self | |
| end | |
| function XList:SetFocusedItem(new_focused) | |
| if new_focused ~= self.focused_item then | |
| local old_focused = self.focused_item | |
| if old_focused then | |
| SetItemFocused(self[old_focused], false) | |
| end | |
| if new_focused then | |
| -- For potential virtual items, spawn a "sufficient quantity" of items between the last and the new | |
| -- focused item, so we can update the layout correctly - this is required for ScrollIntoView below | |
| if self.window_state == "open" and not self.content_box:IsEmpty() then | |
| local first, last = old_focused or 1, new_focused | |
| local step = first > new_focused and -1 or 1 | |
| local from = Clamp(first + step, last - 100*step, last + 100*step) -- assume page size with no more than 100 items | |
| for idx = from, last, step do | |
| local item = self[idx] | |
| if item:HasMember("SetSpawned") then | |
| item:SetSpawned(true) | |
| end | |
| end | |
| local box = self.desktop.box | |
| self.desktop:UpdateMeasure(box:sizex(), box:sizey()) | |
| self.force_keep_items_spawned = true -- prevent UpdateLayout below from despawning children outside of the list | |
| self:UpdateLayout() | |
| self.force_keep_items_spawned = false | |
| end | |
| local child = self[new_focused] | |
| self:GetScrollTarget():ScrollIntoView(child) | |
| local focus = self.desktop:GetKeyboardFocus() | |
| SetItemFocused(child, self.WorkUnfocused or (focus and focus:IsWithin(self))) | |
| end | |
| self.focused_item = new_focused | |
| end | |
| end | |
| function XList:OnSetFocus() | |
| if self.focused_item then | |
| SetItemFocused(self[self.focused_item], true) | |
| end | |
| end | |
| function XList:OnKillFocus() | |
| if self.focused_item then | |
| SetItemFocused(self[self.focused_item], false) | |
| end | |
| end | |
| ----- selection | |
| function XList:DeleteChildren() | |
| self.focused_item = false | |
| self.selection = {} | |
| XWindow.DeleteChildren(self) | |
| end | |
| function XList:ChildLeaving(child) | |
| -- keep selection valid by removing the entry and remapping the indexes | |
| local idx = XWindow.ChildLeaving(self, child) | |
| local selection = self.selection | |
| if #selection > 0 then | |
| table.remove_entry(selection, idx) | |
| for i, sel_idx in ipairs(selection) do | |
| if sel_idx > idx then | |
| self.selection[i] = sel_idx - 1 | |
| end | |
| end | |
| end | |
| end | |
| function XList:ToggleSelected(item) | |
| local selection = self.selection | |
| local idx = table.find(selection, item) | |
| if idx then | |
| table.remove(selection, idx) | |
| SetItemSelected(self[item], false) | |
| else | |
| table.insert(selection, item) | |
| SetItemSelected(self[item], true) | |
| end | |
| self:SetFocusedItem(item) | |
| self:OnSelection(item, selection) | |
| end | |
| function XList:ScrollSelectionIntoView() | |
| for _, item in ipairs(self.selection) do | |
| if self[item] then | |
| self:ScrollIntoView(self[item]) | |
| end | |
| end | |
| end | |
| function XList:SetBox(...) | |
| local old_box = self.content_box | |
| XScrollArea.SetBox(self, ...) | |
| if old_box ~= self.content_box then | |
| self:ScrollSelectionIntoView() | |
| end | |
| end | |
| function XList:SelectRange(from, to) | |
| local selection = self.selection | |
| if from < to then | |
| for i = from, to do | |
| local child = self[i] | |
| if IsItemSelectable(child) and not table.find(selection, i) then | |
| table.insert(selection, i) | |
| SetItemSelected(child, true) | |
| end | |
| end | |
| else | |
| for i = to, from do | |
| local child = self[i] | |
| if IsItemSelectable(child) and not table.find(selection, i) then | |
| table.insert(selection, i) | |
| SetItemSelected(child, true) | |
| end | |
| end | |
| end | |
| self:SetFocusedItem(to) | |
| self:OnSelection(to, selection) | |
| end | |
| function XList:GetSelection() | |
| return self.selection | |
| end | |
| function XList:SetSelection(selection, notify) | |
| for _, item in ipairs(self.selection) do | |
| SetItemSelected(self[item], false) | |
| end | |
| -- validate selection | |
| local item_count = self:GetItemCount() | |
| if type(selection) == "number" then | |
| if selection < 1 or selection > item_count or not IsItemSelectable(self[selection]) then | |
| selection = false | |
| end | |
| elseif type(selection) == "table" then | |
| selection = table.ifilter(selection, function(idx, value) | |
| return value >= 1 and value <= item_count and IsItemSelectable(self[value]) | |
| end) | |
| end | |
| if not selection then | |
| self.selection = {} | |
| self:SetFocusedItem(false) | |
| elseif type(selection) == "number" then | |
| self.selection = { selection } | |
| self:SetFocusedItem(selection) | |
| SetItemSelected(self[selection], true) | |
| else | |
| assert(type(selection) == "table") | |
| self.selection = selection | |
| self:SetFocusedItem(selection[1] or false) | |
| for _, item in ipairs(selection) do | |
| SetItemSelected(self[item], true) | |
| end | |
| end | |
| if notify ~= false then | |
| self:OnSelection(self.focused_item, self.selection) | |
| end | |
| end | |
| function XList:SetInitialSelection(selection, force_ui_style) | |
| if selection then | |
| local item = selection and self[selection] | |
| if item and item:GetEnabled() and IsItemSelectable(item) then | |
| self:SetSelection(selection) | |
| return | |
| end | |
| end | |
| if self.ForceInitialSelection or (self.GamepadInitialSelection and (GetUIStyleGamepad() or force_ui_style)) then | |
| if not self:SelectFirstValidItem() then | |
| self:SetSelection(1) -- if all are disabled | |
| end | |
| elseif self.SetFocusOnOpen then | |
| self:SetFocus(true) | |
| end | |
| end | |
| function XList:GetFirstValidItemIdx() | |
| for idx, item in ipairs(self) do | |
| if item:GetEnabled() and IsItemSelectable(item) then | |
| return idx | |
| end | |
| end | |
| end | |
| function XList:SelectFirstValidItem() | |
| local item_idx = self:GetFirstValidItemIdx() | |
| if item_idx then | |
| self:SetSelection(item_idx) | |
| return true | |
| end | |
| end | |
| function XList:SelectLastValidItem() | |
| for i = #self, 1, -1 do | |
| local item = self[i] | |
| if item:GetEnabled() and IsItemSelectable(item) then | |
| self:SetSelection(i) | |
| return true | |
| end | |
| end | |
| end | |
| function XList:SelectAll() | |
| local item_count = self:GetItemCount() | |
| if item_count > 0 then | |
| self:SelectRange(item_count, 1) | |
| end | |
| end | |
| function XList:OnSelection(focused_item, selection) | |
| end | |
| function XList:OnDoubleClick(item_idx) | |
| end | |
| function XList:OnItemClicked(target, button) | |
| end | |
| ----- XListItem | |
| DefineClass.XListItem = { | |
| __parents = { "XContextControl" }, | |
| properties = { | |
| { category = "Visual", id = "SelectionBackground", editor = "color", default = RGB(204, 232, 255), }, | |
| }, | |
| FocusedBorderColor = RGB(32, 32, 32), | |
| BorderColor = RGBA(0, 0, 0, 0), | |
| BorderWidth = 1, | |
| HandleMouse = false, | |
| selectable = true, | |
| selected = false, | |
| focused = false, | |
| } | |
| function XListItem:IsSelectable() | |
| return self.selectable and self.Dock ~= "ignore" | |
| end | |
| function XListItem:SetSelected(selected) | |
| if self.selected ~= selected then | |
| self.selected = selected | |
| self:Invalidate() | |
| end | |
| end | |
| function XListItem:SetFocused(focused) | |
| if self.focused ~= focused then | |
| self.focused = focused | |
| self:Invalidate() | |
| end | |
| end | |
| function XListItem:CalcBackground() | |
| if self.selected then | |
| return self.SelectionBackground | |
| end | |
| return XContextControl.CalcBackground(self) | |
| end | |
| function XListItem:CalcBorderColor() | |
| if self.enabled and self.focused then | |
| return self.FocusedBorderColor | |
| end | |
| return XContextControl.CalcBorderColor(self) | |
| end | |