|
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), |
|
} |
|
|
|
|
|
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, |
|
|
|
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 |
|
|
|
|
|
|
|
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()) |
|
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") |
|
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 |
|
|
|
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 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 |
|
|
|
|
|
local popup = XPopupList:new({ |
|
AutoFocus = false, |
|
DrawOnTop = true, |
|
}, self.desktop:GetModalWindow() or self.desktop) |
|
|
|
|
|
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 |
|
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() |
|
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()) |
|
end |
|
self.pending_input = false |
|
self.pending_input_type = false |
|
end) |
|
end |
|
|
|
function XCombo:OnRequestItems() |
|
return {} |
|
end |
|
|