GuiElement#focusScrollsList | bool [optional] If false, changing the current focus does not affect scrolling. |
GuiElement#updateSelectionOnOpen | bool [optional] If false, will not re-select the currently selected item when the list is opened. |
GuiElement#useSelectionOnLeave | bool [optional] If true, will force selection on the last selected list item when the list loses focus. |
GuiElement#selectOnScroll | bool [optional] If true, will move the current selection when scrolling. |
GuiElement#maxNumItems | int [optional] Maximum number of list items, defaults to no limit. Adding any more elements than this value will have no effect. |
GuiElement#doubleClickInterval | int [optional] Maximum time in milliseconds between consecutive clicks to interpret them as one double click. |
GuiElement#selectOnClick | bool [optional] If true, mouse clicks will only select a list item and not trigger the onClick callback. |
GuiElement#ignoreMouse | bool [optional] If true, will ignore all mouse interactions (clicking, scrolling) |
GuiElement#itemsPerRow | int [optional] Number of items laid out horizontally per row, defaults to 1. |
GuiElement#itemsPerCol | int [optional] Number of items laid out vertically per column, defaults to 1. |
GuiElement#listItemStartXOffset | string [optional] Pixel X offset from this list's origin for list items, defaults to 0. Format: "[x]px" |
GuiElement#listItemStartYOffset | string [optional] Pixel Y offset from this list's origin for list items, defaults to 0. Format: "[y]px" |
GuiElement#listItemWidth | string [optional] Pixel width of list items in reference resolution. Format: "[width]px" |
GuiElement#listItemHeight | string [optional] Pixel height of list items in reference resolution. Format: "[height]px" |
GuiElement#listItemAutoSize | bool [optional, default=false] If true, will set listItemWidth and listItemHeight automatically to the dimensions of the first child ListElement instance. |
GuiElement#listItemPadding | string [optional] Pixel size of horizontal space between list items in reference resolution, defaults to 0. This space will be added after each item. Format: "[padding]px" |
GuiElement#listItemSpacing | string [optional] Pixel size of vertical space between list items in reference resolution, defaults to 0. This space will be added after each item. Format: "[spacing]px" |
GuiElement#rowBackgroundProfile | string [optional] Profile identifier for the default row background element. |
GuiElement#rowBackgroundProfileAlternate | string [optional] Profile identifier for an alternating row background element. If both this and rowBackgroundProfile are specified, table row backgrounds alternate between these profiles for visibility. |
GuiElement#onSelectionChanged | callback [optional] onSelectionChanged(newIndex) Called when list item selection changes. Receives new item index. |
GuiElement#onScroll | callback [optional] onScroll(firstVisibleItemIndex) Called when the list is being scrolled by user input. |
GuiElement#onDoubleClick | callback [optional] onDoubleClick(selectedIndex, selectedElement) Called when a list item is double-clicked. Receives the clicked item index and the item itself. |
GuiElement#onClick | callback [optional] onClick(selectedIndex, selectedElement) Called when a list item is clicked / activated. Receives the clicked item index and the item itself. |
443 | function ListElement:addElementAtPosition(element, position) |
444 | ListElement:superClass().addElement(self, element) |
445 | |
446 | if self.maxNumItems == nil or #self.listItems <= self.maxNumItems then |
447 | table.insert(self.listItems, position, element) |
448 | |
449 | element:fadeOut() |
450 | self:setDisabled(self.disabled) |
451 | |
452 | self:updateAlternatingBackground() |
453 | |
454 | if self.selectedIndex >= position then |
455 | self:setSelectedIndex(self.selectedIndex + 1) |
456 | if #self.listItems == 1 then |
457 | self:updateItemPositions() |
458 | end |
459 | else |
460 | self:updateItemPositions() |
461 | end |
462 | |
463 | self:raiseSliderUpdateEvent() |
464 | if not element.focusId then |
465 | FocusManager:loadElementFromCustomValues(element, nil, element.focusChangeData, element.focusActive, element.isAlwaysFocusedOnOpen) |
466 | end |
467 | end |
468 | |
469 | self:notifyIndexChange(self.selectedIndex, #self.listItems) |
470 | end |
183 | function ListElement:copyAttributes(src) |
184 | ListElement:superClass().copyAttributes(self, src) |
185 | |
186 | self.doesFocusScrollList = src.doesFocusScrollList |
187 | self.isHorizontalList = src.isHorizontalList |
188 | self.updateSelectionOnOpen = src.updateSelectionOnOpen |
189 | self.useSelectionOnLeave = src.useSelectionOnLeave |
190 | self.selectOnScroll = src.selectOnScroll |
191 | self.supportsMouseScrolling = src.supportsMouseScrolling |
192 | self.doubleClickInterval = src.doubleClickInterval |
193 | self.selectOnClick = src.selectOnClick |
194 | self.ignoreMouse = src.ignoreMouse |
195 | self.maxNumItems = src.maxNumItems |
196 | |
197 | self.visibleItems = src.visibleItems |
198 | self.itemsPerRow = src.itemsPerRow |
199 | self.itemsPerCol = src.itemsPerCol |
200 | |
201 | self.listItemStartXOffset = src.listItemStartXOffset |
202 | self.listItemStartYOffset = src.listItemStartYOffset |
203 | self.listItemWidth = src.listItemWidth |
204 | self.listItemHeight = src.listItemHeight |
205 | self.listItemPadding = src.listItemPadding |
206 | self.listItemSpacing = src.listItemSpacing |
207 | self.listItemAutoSize = src.listItemAutoSize |
208 | |
209 | self.rowBackgroundProfile = src.rowBackgroundProfile; |
210 | self.rowBackgroundProfileAlternate = src.rowBackgroundProfileAlternate; |
211 | |
212 | self.onSelectionChangedCallback = src.onSelectionChangedCallback |
213 | self.onScrollCallback = src.onScrollCallback |
214 | self.onDoubleClickCallback = src.onDoubleClickCallback |
215 | self.onClickCallback = src.onClickCallback |
216 | self.onItemAppearCallback = src.onItemAppearCallback |
217 | self.onItemDisappearCallback = src.onItemDisappearCallback |
218 | |
219 | GuiMixin.cloneMixin(IndexChangeSubjectMixin, src, self) |
220 | end |
110 | function ListElement:loadFromXML(xmlFile, key) |
111 | ListElement:superClass().loadFromXML(self, xmlFile, key) |
112 | |
113 | self.doesFocusScrollList = Utils.getNoNil(getXMLBool(xmlFile, key.."#focusScrollsList"), self.doesFocusScrollList) |
114 | self.isHorizontalList = Utils.getNoNil(getXMLBool(xmlFile, key.."#isHorizontalList"), self.isHorizontalList) |
115 | self.updateSelectionOnOpen = Utils.getNoNil(getXMLBool(xmlFile, key.."#updateSelectionOnOpen"), self.updateSelectionOnOpen) |
116 | self.useSelectionOnLeave = Utils.getNoNil(getXMLBool(xmlFile, key.."#useSelectionOnLeave"), self.useSelectionOnLeave) |
117 | self.selectOnScroll = Utils.getNoNil(getXMLBool(xmlFile, key.."#selectOnScroll"), self.selectOnScroll) |
118 | self.supportsMouseScrolling = Utils.getNoNil(getXMLBool(xmlFile, key.."#supportsMouseScrolling"), self.supportsMouseScrolling) |
119 | self.maxNumItems = Utils.getNoNil(getXMLInt(xmlFile, key.."#maxNumItems"), self.maxNumItems) |
120 | self.doubleClickInterval = Utils.getNoNil(getXMLInt(xmlFile, key.."#doubleClickInterval"), self.doubleClickInterval) |
121 | self.selectOnClick = Utils.getNoNil(getXMLBool(xmlFile, key .. "#selectOnClick"), self.selectOnClick) |
122 | self.ignoreMouse = Utils.getNoNil(getXMLBool(xmlFile, key .. "#ignoreMouse"), self.ignoreMouse) |
123 | |
124 | self.itemsPerRow = Utils.getNoNil(getXMLInt(xmlFile, key.."#itemsPerRow"), self.itemsPerRow) |
125 | self.itemsPerCol = Utils.getNoNil(getXMLInt(xmlFile, key.."#itemsPerCol"), self.itemsPerCol) |
126 | self.visibleItems = self.itemsPerRow * self.itemsPerCol |
127 | |
128 | self.listItemStartXOffset = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemStartXOffset"), {self.outputSize[1]}, {self.listItemStartXOffset})) |
129 | self.listItemStartYOffset = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemStartYOffset"), {self.outputSize[2]}, {self.listItemStartYOffset})) |
130 | self.listItemWidth = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemWidth"), {self.outputSize[1]}, {self.listItemWidth})) |
131 | self.listItemHeight = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemHeight"), {self.outputSize[2]}, {self.listItemHeight})) |
132 | self.listItemPadding = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemPadding"), {self.outputSize[1]}, {self.listItemPadding})) |
133 | self.listItemSpacing = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#listItemSpacing"), {self.outputSize[2]}, {self.listItemSpacing})) |
134 | self.listItemAutoSize = getXMLBool(xmlFile, key .. "#listItemAutoSize") or self.listItemAutoSize |
135 | |
136 | self.rowBackgroundProfile = Utils.getNoNil(getXMLString(xmlFile, key.."#rowBackgroundProfile"), self.rowBackgroundProfile); |
137 | self.rowBackgroundProfileAlternate = Utils.getNoNil(getXMLString(xmlFile, key.."#rowBackgroundProfileAlternate"), self.rowBackgroundProfileAlternate); |
138 | |
139 | self:addCallback(xmlFile, key.."#onSelectionChanged", "onSelectionChangedCallback") |
140 | self:addCallback(xmlFile, key.."#onScroll", "onScrollCallback") |
141 | self:addCallback(xmlFile, key.."#onDoubleClick", "onDoubleClickCallback") |
142 | self:addCallback(xmlFile, key.."#onClick", "onClickCallback") |
143 | self:addCallback(xmlFile, key.."#onItemAppear", "onItemAppearCallback") |
144 | self:addCallback(xmlFile, key.."#onItemDisappear", "onItemDisappearCallback") |
145 | end |
149 | function ListElement:loadProfile(profile, applyProfile) |
150 | ListElement:superClass().loadProfile(self, profile, applyProfile) |
151 | |
152 | self.doesFocusScrollList = profile:getBool("focusScrollsList", self.doesFocusScrollList) |
153 | self.isHorizontalList = profile:getBool("isHorizontalList", self.isHorizontalList) |
154 | self.updateSelectionOnOpen = profile:getBool("updateSelectionOnOpen", self.updateSelectionOnOpen) |
155 | self.useSelectionOnLeave = profile:getBool("useSelectionOnLeave", self.useSelectionOnLeave) |
156 | self.selectOnScroll = profile:getBool("selectOnScroll", self.selectOnScroll) |
157 | self.supportsMouseScrolling = profile:getBool("supportsMouseScrolling", self.supportsMouseScrolling) |
158 | self.maxNumItems = profile:getNumber("maxNumItems", self.maxNumItems) |
159 | self.itemsPerRow = profile:getNumber("itemsPerRow", self.itemsPerRow) |
160 | self.itemsPerCol = profile:getNumber("itemsPerCol", self.itemsPerCol) |
161 | self.doubleClickInterval = profile:getNumber("doubleClickInterval", self.doubleClickInterval) |
162 | self.selectOnClick = profile:getBool("selectOnClick", self.selectOnClick) |
163 | self.ignoreMouse = profile:getBool("ignoreMouse", self.ignoreMouse) |
164 | |
165 | self.rowBackgroundProfile = profile:getValue("rowBackgroundProfile", self.rowBackgroundProfile); |
166 | self.rowBackgroundProfileAlternate = profile:getValue("rowBackgroundProfileAlternate", self.rowBackgroundProfileAlternate); |
167 | |
168 | self.listItemStartXOffset = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemStartXOffset"), {self.outputSize[1]}, {self.listItemStartXOffset})) |
169 | self.listItemStartYOffset = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemStartYOffset"), {self.outputSize[2]}, {self.listItemStartYOffset})) |
170 | self.listItemWidth = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemWidth"), {self.outputSize[1]}, {self.listItemWidth})) |
171 | self.listItemHeight = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemHeight"), {self.outputSize[2]}, {self.listItemHeight})) |
172 | self.listItemPadding = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemPadding"), {self.outputSize[1]}, {self.listItemPadding})) |
173 | self.listItemSpacing = unpack(GuiUtils.getNormalizedValues(profile:getValue("listItemSpacing"), {self.outputSize[2]}, {self.listItemSpacing})) |
174 | self.listItemAutoSize = profile:getBool("listItemAutoSize", self.listItemAutoSize) |
175 | |
176 | if applyProfile then |
177 | self:applyListAspectScale() |
178 | end |
179 | end |
609 | function ListElement:mouseEvent(posX, posY, isDown, isUp, button, eventUsed) |
610 | if self:getIsActive() and not self.ignoreMouse then |
611 | if ListElement:superClass().mouseEvent(self, posX, posY, isDown, isUp, button, eventUsed) then |
612 | eventUsed = true |
613 | end |
614 | |
615 | self.mouseRow = 0 |
616 | self.mouseCol = 0 |
617 | if not eventUsed and GuiUtils.checkOverlayOverlap(posX, posY, self.absPosition[1], self.absPosition[2], self.size[1], self.size[2]) then |
618 | self.mouseRow, self.mouseCol = self:getRowColumnForScreenPosition(posX, posY) |
619 | |
620 | if isDown then |
621 | if button == Input.MOUSE_BUTTON_LEFT then |
622 | self:onMouseDown() |
623 | eventUsed = true |
624 | end |
625 | |
626 | if self.supportsMouseScrolling then |
627 | local deltaIndex = 0 |
628 | if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_UP) then |
629 | deltaIndex = -1 |
630 | elseif Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_DOWN) then |
631 | deltaIndex = 1 |
632 | end |
633 | |
634 | if deltaIndex ~= 0 then |
635 | eventUsed = true |
636 | |
637 | if self.selectOnScroll then |
638 | -- clamp the new index to an always valid range for scrolling, setSelectedIndex would also |
639 | -- allow an index value of 0 meaning "no selection" |
640 | local newIndex = MathUtil.clamp(self.selectedIndex + deltaIndex, 1, self:getItemCount()) |
641 | self:setSelectedIndex(newIndex, nil, deltaIndex) |
642 | else |
643 | self:scrollList(deltaIndex) |
644 | end |
645 | end |
646 | end |
647 | end |
648 | |
649 | if isUp and button == Input.MOUSE_BUTTON_LEFT and self.mouseDown then |
650 | self:onMouseUp() |
651 | eventUsed = true |
652 | end |
653 | end |
654 | end |
655 | |
656 | return eventUsed |
657 | end |
54 | function ListElement:new(target, custom_mt) |
55 | local self = GuiElement:new(target, custom_mt or ListElement_mt) |
56 | self:include(IndexChangeSubjectMixin) -- add index change subject mixin for index state observers |
57 | |
58 | self.doesFocusScrollList = true |
59 | self.isHorizontalList = false |
60 | self.useSelectionOnLeave = false |
61 | self.selectOnScroll = false |
62 | self.updateSelectionOnOpen = true |
63 | self.supportsMouseScrolling = true |
64 | self.ignoreMouse = false |
65 | |
66 | self.maxNumItems = nil |
67 | self.visibleItems = 5 |
68 | self.doubleClickInterval = 1000 |
69 | |
70 | self.listItems = {} |
71 | self.listItemStartXOffset = 0.00 |
72 | self.listItemStartYOffset = 0.00 |
73 | self.listItemWidth = 0 |
74 | self.listItemHeight = 0 |
75 | self.listItemPadding = 0 |
76 | self.listItemSpacing = 0 |
77 | self.listItemAutoSize = false |
78 | |
79 | self.firstVisibleItem = 1 |
80 | self.lastFirstVisibleItem = 1 |
81 | self.selectedIndex = 1 |
82 | self.mouseRow = 0 |
83 | self.mouseCol = 0 |
84 | self.lastClickTime = nil |
85 | self.selectOnClick = false |
86 | |
87 | self.rowBackgroundProfile = ""; -- default row background profile, overrides configured profile if specified |
88 | self.rowBackgroundProfileAlternate = ""; -- alternating row background profile |
89 | |
90 | self.itemsPerRow = 1 |
91 | self.itemsPerCol = 1 |
92 | |
93 | self.currentRow = 1 |
94 | self.currentCol = 1 |
95 | |
96 | self.sliderElement = nil -- sliders register themselves with lists in this field if they point at them via configuration |
97 | |
98 | return self |
99 | end |
474 | function ListElement:removeElement(element) |
475 | for i, v in ipairs(self.listItems) do |
476 | if v == element then |
477 | table.remove(self.listItems, i) |
478 | FocusManager:removeElement(element) |
479 | self:setDisabled(self.disabled) |
480 | |
481 | if self.selectedIndex >= #self.listItems then |
482 | self:setSelectedIndex(self.selectedIndex - 1) |
483 | end |
484 | |
485 | self:raiseSliderUpdateEvent() |
486 | break |
487 | end |
488 | end |
489 | |
490 | -- shift visible part of list if possible and needed |
491 | if (self.firstVisibleItem > 1 and (self.firstVisibleItem > (#self.listItems - self.visibleItems))) then |
492 | if self.selectedIndex > #self.listItems then |
493 | self:setSelectedIndex(#self.listItems) |
494 | else |
495 | self:scrollTo(#self.listItems) |
496 | end |
497 | end |
498 | |
499 | ListElement:superClass().removeElement(self, element) |
500 | end |
282 | function ListElement:scrollTo(index, updateSlider) |
283 | local itemFactor = self:getItemFactor() |
284 | |
285 | -- convert to valid firstVisibleItem (always has to be: n*itemFactor + 1 ) |
286 | index = math.ceil(index / itemFactor) * itemFactor - (itemFactor - 1) |
287 | -- clamp index to valid range |
288 | index = math.max(math.min(index, math.ceil(self:getItemCount() / itemFactor) * itemFactor - self.visibleItems + 1), 1) |
289 | |
290 | if index ~= self.firstVisibleItem then |
291 | self.firstVisibleItem = index |
292 | self:updateItemPositions() |
293 | |
294 | -- update scrolling |
295 | if updateSlider == nil or updateSlider then |
296 | if self.sliderElement ~= nil then |
297 | self.sliderElement:setValue(math.ceil(index / itemFactor), true) |
298 | end |
299 | end |
300 | |
301 | self:raiseCallback("onScrollCallback") |
302 | end |
303 | end |
350 | function ListElement:setSelectedIndex(index, force, direction) |
351 | local numItems = #self.listItems |
352 | local newIndex = MathUtil.clamp(index, 0, numItems) |
353 | |
354 | if newIndex ~= self.selectedIndex then |
355 | self.lastClickTime = nil |
356 | end |
357 | |
358 | if self.listItems[newIndex] ~= nil and self.listItems[newIndex].disabled then |
359 | return |
360 | end |
361 | |
362 | -- Try to scroll over disabled items |
363 | if self.listItems[newIndex] ~= nil and not self.listItems[newIndex].allowFocus then |
364 | newIndex = newIndex + (direction or 1) |
365 | |
366 | -- If we can't, stay where we are |
367 | if newIndex > #self.listItems or newIndex < 1 then |
368 | if newIndex == 0 then |
369 | -- make sure we are scrolled to the top |
370 | self:scrollTo(1) |
371 | end |
372 | |
373 | return |
374 | end |
375 | |
376 | -- Force no change when direction is explicitly absent (mouse clicks on an item) |
377 | if direction == 0 then |
378 | return |
379 | end |
380 | |
381 | return self:setSelectedIndex(newIndex, force, direction or 1) |
382 | end |
383 | |
384 | local hasChanged = self.selectedIndex ~= newIndex |
385 | self.selectedIndex = newIndex |
386 | |
387 | local newFirstVisibleItem = self:calculateFirstVisibleItem(newIndex) |
388 | |
389 | -- do we need to scroll? |
390 | if hasChanged or newFirstVisibleItem ~= self.firstVisibleItem then |
391 | self:scrollTo(newFirstVisibleItem) |
392 | end |
393 | |
394 | if hasChanged or force then |
395 | self:notifyIndexChange(newIndex, numItems) |
396 | self:raiseCallback("onSelectionChangedCallback", newIndex) |
397 | end |
398 | |
399 | -- update selection state |
400 | if self.firstVisibleItem > 0 then |
401 | for i = 1, self.visibleItems do |
402 | local index = self.firstVisibleItem + i - 1 |
403 | if index > numItems then |
404 | break |
405 | end |
406 | local listItem = self.listItems[index] |
407 | if listItem.setSelected ~= nil then |
408 | listItem:setSelected(newIndex == index) |
409 | end |
410 | end |
411 | end |
412 | end |
663 | function ListElement:updateItemPositionsInRange(startIndex, endIndex) |
664 | local topPos = self.size[2] - self.listItemStartYOffset - self.listItemHeight |
665 | local leftPos = self.listItemStartXOffset |
666 | |
667 | for i = startIndex, endIndex do |
668 | local elem = self.listItems[i] |
669 | local index = i - self.firstVisibleItem |
670 | local wasVisible = elem:getIsVisible() |
671 | |
672 | local xPos, yPos = self:getItemPosition(leftPos, topPos, index, elem) |
673 | elem:setPosition(xPos, yPos) |
674 | |
675 | if i >= self.firstVisibleItem and i < self.firstVisibleItem + self.visibleItems then |
676 | -- make items visible in the designated range |
677 | elem:fadeIn() |
678 | |
679 | if not wasVisible then |
680 | self:raiseCallback("onItemAppearCallback", elem) |
681 | end |
682 | else |
683 | -- make all others invisible |
684 | elem:fadeOut() |
685 | |
686 | if wasVisible then |
687 | self:raiseCallback("onItemDisappearCallback", elem) |
688 | end |
689 | end |
690 | |
691 | elem:reset() |
692 | if elem.setSelected ~= nil then |
693 | elem:setSelected(i == self.selectedIndex) |
694 | end |
695 | end |
696 | end |