|
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, |
|
item_hashes = false, |
|
docked_win_count = 0, |
|
force_keep_items_spawned = false, |
|
} |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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%-", "") |
|
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] |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
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 |
|
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 |
|
|
|
|
|
|
|
|
|
function XList:DeleteChildren() |
|
self.focused_item = false |
|
self.selection = {} |
|
XWindow.DeleteChildren(self) |
|
end |
|
|
|
function XList:ChildLeaving(child) |
|
|
|
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 |
|
|
|
|
|
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) |
|
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 |
|
|
|
|
|
|
|
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 |
|
|