Download CommonLua/X/XCombo.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/X/XCombo.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/X/XCombo.lua
-
curl -L -o XCombo.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/X/XCombo.lua
15.8 kB
| if FirstLoad then | |
| OpenXCombo = false | |
| end | |
| DefineClass.XComboButton = { | |
| __parents = { "XTextButton" }, | |
| Dock = "right", | |
| Padding = box(1, 3, 1, 1), | |
| Margins = box(2, 0, 0, 0), | |
| Icon = "CommonAssets/UI/arrowdown-40.tga", | |
| IconScale = point(500, 500), | |
| Background = RGB(38, 146, 227), | |
| RolloverBackground = RGB(24, 123, 197), | |
| PressedBackground = RGB(13, 113, 187), | |
| DisabledBackground = RGB(128, 128, 128), | |
| } | |
| -- The actual combo list items inherit XComboListItem and are defined as XTemplates | |
| DefineClass.XComboListItem = { | |
| __parents = { "XTextButton" }, | |
| Image = "CommonAssets/UI/round-frame-20.tga", | |
| FrameBox = box(9, 9, 9, 9), | |
| ImageScale = point(500, 500), | |
| Background = RGBA(0, 0, 0, 0), | |
| RolloverTemplate = "GedPropRollover", | |
| UseClipBox = false, | |
| } | |
| DefineClass.XCombo = { | |
| __parents = { "XFontControl", "XContextControl" }, | |
| properties = { | |
| { category = "General", id = "Translate", editor = "bool", default = false, }, | |
| { category = "General", id = "VirtualItems", editor = "bool", default = false, }, | |
| { category = "General", id = "Value", editor = "text", default = "", no_edit = true }, | |
| { category = "General", id = "DefaultValue", editor = "text", default = "" }, | |
| { category = "General", id = "Items", editor = "expression", default = false, params = "self" }, | |
| { category = "General", id = "RefreshItemsOnOpen", editor = "bool", default = false }, | |
| { category = "General", id = "MaxItems", editor = "number", default = 25, }, | |
| { category = "General", id = "ArbitraryValue", name = "Allow arbitrary value", editor = "bool", default = true, }, | |
| { category = "General", id = "AutoSelectAll", editor = "bool", default = true, }, | |
| { category = "General", id = "SetValueOnLoseFocus", editor = "bool", default = true, }, | |
| { category = "General", id = "ButtonTemplate", editor = "choice", default = "XComboButton", items = function() return XTemplateCombo("XTextButton") end, }, | |
| { category = "General", id = "ListItemTemplate", editor = "choice", default = "XComboListItemLight", items = function() return XTemplateCombo("XComboListItem") end, }, | |
| { category = "General", id = "Hint", editor = "text", default = "", }, | |
| { category = "Most Recently Used Items", id = "MRUStorageId", name = "Storage Id", editor = "text", default = "", }, | |
| { category = "Most Recently Used Items", id = "MRUCount", name = "Entries count", editor = "number", default = 5, }, | |
| { category = "Interaction", id = "OnValueChanged", editor = "func", params = "self, value", default = empty_func }, | |
| { category = "Interaction", id = "OnItemRightClicked", editor = "func", params = "self, value", default = empty_func }, | |
| }, | |
| Padding = box(2, 1, 1, 1), | |
| BorderWidth = 1, | |
| BorderColor = RGB(128, 128, 128), | |
| DisabledBorderColor = RGBA(128, 128, 128, 128), | |
| Background = RGB(240, 240, 240), | |
| FocusedBackground = RGB(255, 255, 255), | |
| PopupBackground = RGB(255, 255, 255), | |
| value = false, | |
| popup = false, | |
| last_text = false, | |
| mru_list = false, | |
| mru_value_changed = false, | |
| suppress_autosuggest = false, | |
| -- operation to be performed after items are fetched | |
| pending_input = false, | |
| pending_input_type = false, | |
| } | |
| local function ItemId(item) | |
| if type(item) == "table" then | |
| return item.id or (item.value ~= nil and item.value) | |
| end | |
| return item | |
| end | |
| local function ItemText(item) | |
| if type(item) == "table" then | |
| return item.name or item.text or item.id | |
| end | |
| return tostring(item) | |
| end | |
| local function RawText(text, translate) | |
| if translate then | |
| assert(not text or IsT(text)) | |
| return text and TDevModeGetEnglishText(text) | |
| end | |
| return text | |
| end | |
| local function StringText(text, translate) | |
| if translate then | |
| assert(not text or IsT(text)) | |
| return text and _InternalTranslate(text) | |
| end | |
| return text | |
| end | |
| function XCombo:Init(parent, context) | |
| local edit = XEdit:new({ | |
| Id = "idEdit", | |
| VAlign = "center", | |
| Padding = box(0, 0, 0, 0), | |
| Background = RGBA(0, 0, 0, 0), | |
| BorderColor = RGBA(0, 0, 0, 0), | |
| BorderWidth = 0, | |
| AllowEscape = false, | |
| Hint = self.Hint, | |
| OnMouseButtonDown = function(edit, pt, button) | |
| if button == "L" and not self.popup then | |
| self:OpenCombo() | |
| if self.AutoSelectAll then return "break" end | |
| end | |
| return XEdit.OnMouseButtonDown(edit, pt, button) | |
| end, | |
| OnShortcut = function(edit, shortcut, source, ...) | |
| if shortcut == "Enter" then | |
| self:TextChanged(self:GetText()) | |
| if self:IsPopupOpen() then | |
| self:CloseCombo() | |
| return "break" | |
| end | |
| end | |
| if shortcut == "ButtonA" then | |
| self:OpenCombo("select") | |
| return "break" | |
| end | |
| return XEdit.OnShortcut(edit, shortcut, source, ...) | |
| end, | |
| OnKillFocus = function(edit, new_focus) | |
| local popup = self.popup | |
| if not self.SetValueOnLoseFocus or popup and new_focus and new_focus:IsWithin(popup) then | |
| return XEdit.OnKillFocus(edit) | |
| end | |
| local text = self:GetText() | |
| if text ~= self.last_text then | |
| self.last_text = text | |
| self:TextChanged(text) | |
| end | |
| return XEdit.OnKillFocus(edit) | |
| end, | |
| OnTextChanged = function(edit) | |
| XEdit.OnTextChanged(edit) | |
| if self.suppress_autosuggest then | |
| self.suppress_autosuggest = false | |
| return | |
| end | |
| self:OpenCombo("suggest") | |
| end, | |
| }, self, context) | |
| edit:SetFontProps(self) | |
| edit:SetTranslate(self.Translate) | |
| self:SetButtonTemplate(self.ButtonTemplate) | |
| end | |
| function XCombo:IsPopupOpen() | |
| return self.popup and self.popup.window_state ~= "destroying" | |
| end | |
| function XCombo:Open(...) | |
| if rawget(self, "value") == nil then | |
| self:SetValue(self.DefaultValue, true) | |
| end | |
| XContextWindow.Open(self, ...) | |
| end | |
| function XCombo:SetButtonTemplate(template_id) | |
| if self:HasMember("idButton") then | |
| if self.idButton.window_state == "open" then | |
| self.idButton:Close() | |
| else | |
| self.idButton:delete() | |
| end | |
| end | |
| self.ButtonTemplate = template_id | |
| local button = XTemplateSpawn(self.ButtonTemplate, self, self.context) | |
| button:SetId("idButton") | |
| button.OnPress = function(button) | |
| if self:IsPopupOpen() then | |
| self:CloseCombo() | |
| else | |
| self:OpenCombo("select") | |
| end | |
| end | |
| end | |
| LinkFontPropertiesToChild(XCombo, "idEdit") | |
| LinkPropertyToChild(XCombo, "Translate", "idEdit") | |
| function XCombo:OnDelete() | |
| self:CloseCombo() | |
| end | |
| function XCombo:ResolveItems() | |
| local items = self.Items | |
| while type(items) == "function" do | |
| items = items(self) | |
| end | |
| return type(items) == "table" and items or empty_table | |
| end | |
| -- if value ~= text, SetValue needs to fetch the items. Use this function if you already know the text | |
| -- This function is not validating the value!!! | |
| function XCombo:SetValueWithText(value, text, dont_notify) | |
| self:SetText(text or "") | |
| local old_value = self:GetValue() | |
| if old_value ~= value then | |
| self.value = value | |
| if not dont_notify then | |
| self:OnValueChanged(self.value) | |
| end | |
| end | |
| end | |
| function XCombo:SetValue(value, do_not_validate) | |
| if not do_not_validate and not self.Items then | |
| self.pending_input = value | |
| self.pending_input_type = "value" | |
| self:FetchItemsAndValidate() | |
| return | |
| end | |
| for _, item in ipairs(self:ResolveItems()) do | |
| if ItemId(item) == value then | |
| self:SetText(ItemText(item)) | |
| if self.value ~= ItemId(item) then | |
| self.value = ItemId(item) | |
| self:OnValueChanged(self.value) | |
| end | |
| return | |
| end | |
| end | |
| local old_value = self:GetValue() | |
| if self.ArbitraryValue or do_not_validate then | |
| self.value = value | |
| self:SetText(value == Undefined() and value or tostring(value)) | |
| else | |
| self.value = nil | |
| end | |
| self:UpdateMRUList() | |
| if old_value ~= self:GetValue() then | |
| self:OnValueChanged(self:GetValue()) | |
| return true | |
| end | |
| end | |
| function XCombo:SetText(text) | |
| self.suppress_autosuggest = true | |
| if text == Undefined() then | |
| self.idEdit:SetHint("Undefined") | |
| text = "" | |
| else | |
| self.idEdit:SetHint("") | |
| end | |
| self.idEdit:SetText(text) | |
| self.last_text = text | |
| self.idEdit.cursor_pos = #text | |
| end | |
| function XCombo:TextChanged(text) | |
| local translate = self:GetTranslate() | |
| local raw_text = RawText(text, translate) | |
| if not self.Items then | |
| self.pending_input = raw_text | |
| self.pending_input_type = "text" | |
| self:FetchItemsAndValidate() | |
| return | |
| end | |
| for _, item in ipairs(self:ResolveItems()) do | |
| if RawText(ItemText(item), translate) == raw_text then | |
| if self.value ~= ItemId(item) then | |
| self.value = ItemId(item) | |
| self:OnValueChanged(self.value) | |
| end | |
| return | |
| end | |
| end | |
| if self.ArbitraryValue then | |
| if self.value ~= text then | |
| self.value = text | |
| self:OnValueChanged(self:GetValue()) | |
| end | |
| else | |
| self:SetValue(self:GetValue()) -- revert to last valid value | |
| end | |
| end | |
| function XCombo:GetValue() | |
| local value = rawget(self, "value") | |
| if value == nil then | |
| return self.DefaultValue | |
| else | |
| return value | |
| end | |
| end | |
| function XCombo:GetText() | |
| return self.idEdit:GetText() | |
| end | |
| function XCombo:OnShortcut(shortcut, source, ...) | |
| local popup = self.popup | |
| if shortcut == "Down" then | |
| if not popup then | |
| self:OpenCombo("select") | |
| else | |
| popup:SetFocus() | |
| popup:OnShortcut(shortcut, source, ...) | |
| end | |
| return "break" | |
| elseif shortcut == "Escape" or shortcut == "ButtonB" then | |
| if self:IsPopupOpen() then | |
| self:CloseCombo() | |
| return "break" | |
| else | |
| self:SetValue(self:GetValue(), "do_not_validate") -- revert text field to the current/old value | |
| end | |
| elseif popup and popup.window_state ~= "destroying" then | |
| local res = popup:OnShortcut(shortcut, source, ...) | |
| return res | |
| end | |
| end | |
| function XCombo:SetFocusOrder(focus_order) | |
| self.idEdit:SetFocusOrder(focus_order) | |
| end | |
| function XCombo:GetFocusOrder(focus_order) | |
| self.idEdit:GetFocusOrder(focus_order) | |
| end | |
| function XCombo:SetFocus(set, children) | |
| return self.idEdit:SetFocus(set, children) | |
| end | |
| function XCombo:IsFocused(include_children) | |
| return self.idEdit:IsFocused(include_children) | |
| end | |
| function XCombo:OnKillFocus(new_focus) | |
| if not (new_focus and new_focus:IsWithin(self.popup)) then | |
| self:CloseCombo() | |
| end | |
| end | |
| function XCombo:CloseCombo() | |
| local popup = self.popup | |
| if popup and popup.window_state == "open" then | |
| self.idEdit:ClearSelection() | |
| popup:Close() | |
| end | |
| if self.RefreshItemsOnOpen then | |
| self.Items = nil | |
| end | |
| end | |
| function XCombo:LoadMRUList() | |
| local mru_id = self.MRUStorageId | |
| if mru_id == "" or self.mru_list then | |
| return | |
| end | |
| self.mru_list = {} | |
| LocalStorage.XComboMRU = LocalStorage.XComboMRU or {} | |
| local mru_data = LocalStorage.XComboMRU[mru_id] or empty_table | |
| if next(mru_data) then | |
| -- gather all valid items by id to filter out the valid MRU entries | |
| local items_by_id = {} | |
| for i, item in ipairs(self:ResolveItems()) do | |
| items_by_id[ItemId(item)] = item | |
| end | |
| mru_data = table.ifilter(mru_data, function(idx, id) return items_by_id[id] end) | |
| self.mru_list = table.map(mru_data, function(id) return items_by_id[id] end) | |
| end | |
| end | |
| function XCombo:UpdateMRUList() | |
| if not self.mru_list or not self.mru_value_changed then return end | |
| local item_id = self.value | |
| local list = table.map(self.mru_list, function(item) return ItemId(item) end) | |
| table.remove_value(list, item_id) | |
| table.insert(list, 1, item_id) | |
| if #list > self.MRUCount then | |
| table.remove(list) | |
| end | |
| LocalStorage.XComboMRU[self.MRUStorageId] = list | |
| SaveLocalStorageDelayed() | |
| self.mru_value_changed = nil | |
| self.mru_list = nil | |
| end | |
| function XCombo:GetCurrentComboItems(mode) | |
| local recently_used | |
| local items, extra_items = {}, {} | |
| local selected_item = false | |
| local translate = self:GetTranslate() | |
| local prefix_lower = string.trim_spaces(string.lower(StringText(self:GetText(), translate))) | |
| for i, item in ipairs(self:ResolveItems()) do | |
| local itemText = ItemText(item) | |
| itemText = string.lower(StringText(itemText, translate)) | |
| local match = itemText:starts_with(prefix_lower) | |
| if mode ~= "suggest" or match then | |
| items[#items + 1] = item | |
| elseif itemText:find(prefix_lower, 1, true) then | |
| extra_items[#extra_items + 1] = item | |
| end | |
| if match and not selected_item then | |
| selected_item = item | |
| end | |
| end | |
| -- if not searching, put most recently used entries on top | |
| if prefix_lower == "" or mode ~= "suggest" then | |
| self:LoadMRUList() | |
| if next(self.mru_list) then | |
| assert(#extra_items == 0) | |
| extra_items = items | |
| items = table.copy(self.mru_list) | |
| recently_used = true | |
| end | |
| end | |
| return items, extra_items, selected_item, recently_used | |
| end | |
| function XCombo:OpenCombo(mode) | |
| if not self.enabled then return end | |
| if OpenXCombo then | |
| OpenXCombo:CloseCombo() | |
| if not mode then return end | |
| end | |
| self:SetFocus() | |
| if not self.Items then | |
| self.pending_input = mode | |
| self.pending_input_type = "opencombo" | |
| self:FetchItemsAndValidate() | |
| return | |
| end | |
| local items, extra_items, selected_item, recently_used = self:GetCurrentComboItems(mode) | |
| local sep_idx | |
| if extra_items and #extra_items > 0 then | |
| sep_idx = #items | |
| table.iappend(items, extra_items) | |
| end | |
| if #items == 0 then | |
| return | |
| end | |
| -- open popup | |
| local popup = XPopupList:new({ | |
| AutoFocus = false, | |
| DrawOnTop = true, | |
| }, self.desktop:GetModalWindow() or self.desktop) | |
| -- popup should have the same scale as the combo | |
| popup:SetScaleModifier(self.scale) | |
| popup:SetOutsideScale(point(1000, 1000)) | |
| local translate = self:GetTranslate() | |
| local virtual_items = self.VirtualItems | |
| for i, item in ipairs(items) do | |
| local context = SubContext(self.context, { | |
| idx = i, | |
| dimmed = not recently_used and sep_idx and i > sep_idx, | |
| combo = self, | |
| popup = popup, | |
| item = item, | |
| translate = translate, | |
| on_press = function(self) | |
| local combo = self.context.combo | |
| local popup = self.context.popup | |
| if combo:GetEnabled() then | |
| local value = combo.value | |
| combo:SetValue(ItemId(self.context.item)) | |
| if value ~= combo.value then | |
| combo.mru_value_changed = true -- means we will update MRU list upon deleting the control | |
| end | |
| end | |
| if popup.window_state ~= "destroying" then | |
| combo:CloseCombo() | |
| end | |
| end, | |
| on_alt_press = self.OnItemRightClicked ~= empty_func and function(self) | |
| local combo = self.context.combo | |
| combo:OnItemRightClicked(ItemId(self.context.item)) | |
| end | |
| }) | |
| local entry = virtual_items and | |
| NewXVirtualContent(popup.idContainer, context, self.ListItemTemplate) or | |
| XTemplateSpawn(self.ListItemTemplate, popup.idContainer, context) | |
| if not recently_used then | |
| if mode == "select" then | |
| if self.value == ItemId(item) then | |
| entry:SetFocus() | |
| popup.idContainer:ScrollIntoView(entry) | |
| end | |
| elseif selected_item == item then | |
| popup.idContainer:ScrollIntoView(entry) | |
| end | |
| end | |
| if i == sep_idx then | |
| XWindow:new({ Background = RGBA(0, 0, 0, 196), MinHeight = 1, Margins = box(3, 0, 3, 0) }, popup.idContainer) | |
| end | |
| end | |
| popup.idContainer:SetBackground(self.PopupBackground) | |
| popup:SetAnchor(self.box) | |
| popup:SetAnchorType("drop") | |
| popup:SetMaxItems(self.MaxItems) | |
| popup.Close = function(...) | |
| OpenXCombo = false | |
| self.popup = false | |
| XPopupList.Close(...) | |
| end | |
| popup:Open() | |
| popup.popup_parent = self | |
| Msg("XWindowRecreated", popup) | |
| if self.AutoSelectAll and not mode then | |
| self.idEdit:SelectAll() | |
| end | |
| OpenXCombo = self | |
| self.popup = popup | |
| return popup | |
| end | |
| function XCombo:FetchItemsAndValidate() | |
| assert(not self.Items) | |
| if self:IsThreadRunning("FetchItems") then | |
| return | |
| end | |
| self:CreateThread("FetchItems", function() | |
| self.Items = self:OnRequestItems() -- might sleep | |
| if self.window_state == "destroying" then | |
| return | |
| end | |
| local focused = self:IsFocused() | |
| if self.pending_input_type == "value" then | |
| self:SetValue(self.pending_input) | |
| if self.RefreshItemsOnOpen then | |
| self.Items = nil | |
| end | |
| elseif self.pending_input_type == "text" and not focused then | |
| self:TextChanged(self.pending_input) | |
| elseif self.pending_input_type == "opencombo" and (focused or self.desktop.keyboard_focus == self) then | |
| self:OpenCombo(self.pending_input) | |
| else | |
| self:SetValue(self:GetValue()) -- revalidate current value | |
| end | |
| self.pending_input = false | |
| self.pending_input_type = false | |
| end) | |
| end | |
| function XCombo:OnRequestItems() | |
| return {} | |
| end | |