myspace / CommonLua /X /XList.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
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