Download CommonLua/Ged/GedPropEditors.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 180 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Ged/GedPropEditors.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Ged/GedPropEditors.lua
-
curl -L -o GedPropEditors.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Ged/GedPropEditors.lua
180 kB
| ----- Handling T values for Ged <-- search for this for more information | |
| -- value from the game => a T for an edit control | |
| function GedPropValueToT(value) | |
| return type(value) == "table" and setmetatable(table.copy(value), TMeta) or "" | |
| end | |
| -- a T from an edit control => value for the game | |
| function GedTToPropValue(value, default) | |
| assert(not default or default == "" or not IsT(default)) | |
| if value == "" then | |
| return type(default) == "table" and "" or default | |
| end | |
| assert(#value == 2) -- must have a loc ID generated by the edit field | |
| value[2] = ReplaceNonStandardCharacters(value[2]) | |
| if type(default) == "table" and #default == 2 and type(default[1]) == "number" then -- is the default a T value with an ID? | |
| -- use the loc ID of the default if the text is the same as the default | |
| if value[2] == default[2] then | |
| return table.raw_copy(default) | |
| end | |
| -- change the loc ID if editing the default value | |
| if value[1] == default[1] then | |
| return { RandomLocId(), value[2] } | |
| end | |
| end | |
| return table.raw_copy(value) -- strip T metatables (they can't pass through sockets and cause an assert) | |
| end | |
| ----- GedPropEditor - prop editors base class | |
| GedPropEditors = {} | |
| GedPropEditors["buttons"] = "GedPropEditor" | |
| DefineClass.GedPropEditor = { | |
| __parents = { "XContextControl" }, | |
| IdNode = true, | |
| LayoutHSpacing = 2, | |
| LayoutVSpacing = 2, | |
| FoldWhenHidden = true, | |
| RolloverAnchor = "left", | |
| RolloverTemplate = "GedPropRollover", | |
| SelectionBackground = RGB(204, 232, 255), | |
| BorderColor = RGBA(0, 0, 0, 0), | |
| BorderWidth = 2, | |
| prop_meta = false, | |
| parent_obj_id = false, | |
| panel = false, | |
| obj = false, | |
| last_set_value = false, | |
| selectable = true, | |
| selected = false, | |
| highlight_search_match = false, | |
| accept_os_clipboard_paste = false, | |
| } | |
| function GedPropEditor:ReassignFocusOrders(x, y) | |
| return y + 1 | |
| end | |
| function GedPropEditor:QueueReassignFocusOrders() | |
| local obj = GetParentOfKind(self, "GedPropPanel") | |
| if obj then | |
| obj:QueueReassignFocusOrders() | |
| end | |
| end | |
| function GedPropEditor:SetSelected(selected) | |
| self.selected = selected | |
| self:Invalidate() | |
| end | |
| function GedPropEditor:CalcBackground() | |
| if self.enabled and self.selected then | |
| return self.SelectionBackground | |
| end | |
| return XContextControl.CalcBackground(self) | |
| end | |
| function GedPropEditor:SetHighlightSearchMatch(value) | |
| self.highlight_search_match = value | |
| end | |
| function GedPropEditor:UpdatePropertyNames(internal, prop_name) | |
| local prop_meta = self.prop_meta | |
| local prop_name = prop_name or (internal and prop_meta.id or (prop_meta.name or prop_meta.id)) | |
| local prefix, suffix = "", "" | |
| if prop_meta.dlc_name then | |
| prefix = "<style GedHighlight>["..prop_meta.dlc_name.."]</style> " | |
| end | |
| if prop_meta.scale_name then | |
| suffix = " ("..prop_meta.scale_name..")" | |
| end | |
| if self.highlight_search_match then | |
| prop_name = GedPanelBase.MatchMark .. prop_name | |
| end | |
| local rollover | |
| local editor = prop_meta.editor | |
| if self.panel.ShowUnusedPropertyWarnings and not PlaceholderProperties[editor] then | |
| local prop_stats = self.panel:Obj("root|prop_stats") | |
| if prop_stats and prop_stats[prop_meta.id] then | |
| local used_in = prop_stats[prop_meta.id] | |
| if used_in == 0 then | |
| prefix = "<image CommonAssets/UI/Ged/warning.tga 900 127 127 127> " .. prefix | |
| rollover = "<style GedHighlight>Property is default for all presets." | |
| else -- only used one time, see GedPresetPropertyUsageStats | |
| prefix = "<image CommonAssets/UI/Ged/warning.tga 900 127 180 127> " .. prefix | |
| rollover = "<style GedHighlight>Property is only set in " .. used_in .. "." | |
| end | |
| end | |
| end | |
| self.idLabel:SetText(prefix..prop_name..suffix) | |
| if editor == "buttons" or not PlaceholderProperties[editor] then | |
| self:SetRolloverText(prop_meta.help and rollover and prop_meta.help.."\n\n"..rollover or prop_meta.help or rollover or false) | |
| end | |
| end | |
| function GedPropEditor:ShouldShowButtonForFunc(func_name) | |
| return self.panel:ShouldShowButtonForFunc(func_name) | |
| end | |
| function GedPropEditor:Init(parent, context, prop_meta) | |
| self.prop_meta = prop_meta | |
| self.RolloverText = prop_meta.help or nil | |
| self.RolloverAnchor = "smart" | |
| local win = XWindow:new({ | |
| Id = "idLabelHost", | |
| Dock = prop_meta.name_on_top and "top" or "left", | |
| FoldWhenHidden = true, | |
| }, self) | |
| XText:new({ | |
| Id = "idLabel", | |
| Dock = "left", | |
| VAlign = "center", | |
| MinWidth = 150, | |
| }, win) | |
| XTextButton:new({ | |
| Id = "idResetToDefault", | |
| Dock = "right", | |
| VAlign = "center", | |
| Text = "x", | |
| MaxWidth = 20, | |
| MaxHeight = 16, | |
| LayoutHSpacing = 0, | |
| Padding = box(1, 1, 1, 1), | |
| Background = RGBA(0, 0, 0, 0), | |
| RolloverBackground = RGB(204, 232, 255), | |
| PressedBackground = RGB(121, 189, 241), | |
| OnPress = function() | |
| self.panel:SetPanelFocused() -- focused prop editors may deny value updates | |
| self:SetProp(nil, "force") | |
| end | |
| }, win) | |
| win:SetVisibleInstant(not prop_meta.hide_name) | |
| local editor = prop_meta.editor | |
| if editor == "buttons" then | |
| win:SetDock("ignore") | |
| win:SetVisible(false) | |
| end | |
| if self.prop_meta.buttons then | |
| local buttons_host = XWindow:new({ | |
| Id = "idButtonsHost", | |
| Dock = PlaceholderProperties[editor] and "bottom" or "right", | |
| HAlign = "center", | |
| LayoutMethod = "HWrap", | |
| LayoutHSpacing = 2, | |
| Padding = box(2, 1, 0, 0), | |
| }, self) | |
| for _, data in ipairs(prop_meta.buttons or empty_table) do | |
| if self:ShouldShowButtonForFunc(data.func) then | |
| local button | |
| if data.toggle then | |
| button = XTemplateSpawn("GedToolbarToggleButtonSmall", buttons_host) | |
| button:SetIcon(data.icon) | |
| button:SetToggled(data.toggled) | |
| elseif data.icon then | |
| button = XTemplateSpawn("GedToolbarButtonSmall", buttons_host) | |
| button:SetIcon(data.icon) | |
| else | |
| button = XTemplateSpawn("GedPropertyButton", buttons_host) | |
| button:SetText(data.name) | |
| end | |
| if data.icon_scale then | |
| local scale = data.icon_scale * 10 | |
| button:SetIconScale(point(scale, scale)) | |
| end | |
| button.OnPress = function(button) | |
| button:SetFocus() | |
| self.panel:Op("GedPropEditorButton", self.panel.context, self.panel.RootObjectBindName or "root", | |
| prop_meta.id, data.name, data.func, data.param) | |
| button:SetFocus(false) | |
| end | |
| button:SetRolloverText(data.rollover or "") | |
| button:SetDock(false) | |
| end | |
| end | |
| end | |
| end | |
| function GedPropEditor:GetProp() | |
| local obj = self.panel:Obj(self.obj) | |
| local value = obj and obj[self.prop_meta.id] | |
| if value == nil then | |
| value = self.prop_meta.default | |
| end | |
| return value | |
| end | |
| function GedPropEditor:SetProp(value, force, slider_drag_id) | |
| if self.prop_meta.read_only or self.panel.prop_update_in_progress then return end | |
| if value == Undefined() then return end | |
| if value == nil and not force then return end | |
| -- fixes double-set scenarios when editing SortKey with multiple selection, and Preset.OnEditorSetProperty | |
| -- changes the selection after sorting presets, resulting in setting the same value to totally different objects | |
| local lua_value = ValueToLuaCode(value) | |
| if self.last_set_value == lua_value then return end | |
| self.last_set_value = lua_value | |
| LaunchRealTimeThread(function(self, value) | |
| local err = self.panel:RemoteSetProperty(self.obj, self.prop_meta.id, value, self.parent_obj_id, slider_drag_id) | |
| if rawget(self, "idResetToDefault") then | |
| if err then | |
| self.idResetToDefault:SetVisible(true) | |
| self.idResetToDefault:SetTextStyle("GedError") | |
| else | |
| self.idResetToDefault:SetVisible(value ~= nil and self.prop_meta.default ~= nil and value ~= self.prop_meta.default and not self.prop_meta.read_only) | |
| end | |
| end | |
| self:SetPropResult(err) | |
| end, self, value) | |
| end | |
| function GedPropEditor:OnKillFocus() | |
| if not self.prop_meta.read_only then | |
| self:SendValueToGame() | |
| end | |
| end | |
| function GedPropEditor:SendValueToGame() | |
| end | |
| function GedPropEditor:UpdateValue() | |
| local value = self:GetProp() | |
| if self:HasMember("idResetToDefault") then | |
| self.idResetToDefault:SetVisible(value ~= nil and self.prop_meta.default ~= nil and value ~= self.prop_meta.default and not self.prop_meta.read_only) | |
| end | |
| self.last_set_value = ValueToLuaCode(value) | |
| Msg("GedPropertyUpdated", self) | |
| end | |
| function GedPropEditor:SetPropResult(err) | |
| if self.window_state == "destroying" then return end -- can be invoked from OnKillFocus when a prop editor is being destroyed | |
| if rawget(self, "idError") then | |
| self.idError:Close() | |
| end | |
| if err and err ~= "" then | |
| XLabel:new({ | |
| Id = "idError", | |
| Dock = "bottom", | |
| ZOrder = -1, | |
| TextStyle = "GedError", | |
| }, self):Open() | |
| self.idError:SetText(err) | |
| end | |
| end | |
| local function search_display_text(text, filter) | |
| assert(text == "" or not IsT(text)) | |
| text = tostring(text):gsub("<[^>]+>", "") | |
| text = string.lower(text) | |
| return text:find(filter, 1, true) | |
| end | |
| local function get_children_of_classes(win, results, ...) | |
| if not results then | |
| results = {} | |
| end | |
| for _, child in ipairs(win) do | |
| if IsKindOfClasses(child, ...) then | |
| table.insert(results, child) | |
| end | |
| get_children_of_classes(child, results, ...) | |
| end | |
| return results | |
| end | |
| function GedPropEditor:FindText(search_text, highlight_text) | |
| -- looks in all child controls of type XText or XEditableText | |
| local text_controls = get_children_of_classes(self, nil, "XText", "XEditableText") | |
| if #text_controls == 0 then | |
| return true | |
| end | |
| local found | |
| for _, win in ipairs(text_controls) do | |
| if search_display_text(win.text, search_text) then | |
| found = true | |
| end | |
| if IsKindOf(win, "XTextEditor") then | |
| local plugin = win:FindPluginOfKind("XHighlightTextPlugin") | |
| if not plugin then | |
| plugin = XHighlightTextPlugin:new() | |
| win:AddPlugin(plugin) | |
| end | |
| plugin.highlighted_text = highlight_text | |
| plugin.ignore_case = true | |
| win:Invalidate() | |
| end | |
| end | |
| return found | |
| end | |
| function GedPropEditor:HighlightAndSelect(text) | |
| local focus = self:GetRelativeFocus(point(0, 0), "next") or self | |
| if focus then | |
| -- select the highlighted text in the first child edit control | |
| local text_controls = get_children_of_classes(self, nil, "XEditableText") | |
| if #text_controls == 0 then | |
| if text then | |
| focus:SetFocus() | |
| end | |
| return focus | |
| end | |
| -- this needs to be called after desktop layout (the edit control isn't properly initialized before that) | |
| CreateRealTimeThread(function(text_controls, focus) | |
| if self.window_state == "destroying" then return end | |
| for _, ctrl in ipairs(text_controls) do | |
| if text and ctrl:SelectFirstOccurence(text, "ignore_case") then | |
| ctrl:SetFocus() | |
| return | |
| end | |
| ctrl:ClearSelection() | |
| end | |
| if text then | |
| focus:SetFocus() | |
| end | |
| end, text_controls, focus) | |
| end | |
| return focus | |
| end | |
| function GedPropEditor:DetachForReuse() | |
| self:SetParent(false) | |
| self.last_set_value = false | |
| end | |
| function GedPropEditor:OnShortcut(shortcut, source, ...) | |
| if shortcut == "Escape" and not self.prop_meta.read_only and self.SendValueToGame ~= GedPropEditor.SendValueToGame then | |
| -- reverts the changes from the UI, but temporarily sets the new value so the user can use "undo" to get the discarded value back | |
| self.panel:SetFocus() | |
| local value = self:GetProp() | |
| self:SendValueToGame() | |
| self:SetProp(value, "force") | |
| return "break" | |
| end | |
| end | |
| ----- GedPropScript | |
| GedPropEditors["script"] = "GedPropScript" | |
| DefineClass.GedPropScript = { | |
| __parents = { "GedPropEmbeddedObject" }, | |
| } | |
| function GedPropScript:Init(parent, context, prop_meta) | |
| local edit_button = self.idCreateItemButton | |
| edit_button:SetParent(self.idLabelHost) | |
| edit_button:SetIcon("CommonAssets/UI/Ged/explorer.tga") | |
| edit_button:SetRolloverText("Edit script") | |
| edit_button:SetZOrder(-2) | |
| edit_button.OnPress = function() | |
| self.panel.app:Op("GedCreateOrEditScript", self.panel.context, self.prop_meta.id, self.prop_meta.class) | |
| end | |
| self.idCopyButton:SetParent(self.idLabelHost) | |
| self.idCopyButton:SetZOrder(0) | |
| self.idCopyButton.OnPress = function(button) | |
| self.panel.app:Op("GedNestedObjCopy", self.panel.context, self.prop_meta.id, self.prop_meta.class) | |
| end | |
| self.idPasteButton:SetParent(self.idLabelHost) | |
| self.idPasteButton:SetZOrder(-1) | |
| self.idPasteButton.OnPress = function(button) | |
| self.panel.app:Op("GedNestedObjPaste", self.panel.context, self.prop_meta.id, self.prop_meta.class) | |
| self.panel.app:Op("GedCreateOrEditScript", self.panel.context, self.prop_meta.id, self.prop_meta.class) | |
| end | |
| self.idLabelHost:SetDock("top") | |
| XScrollArea:new({ | |
| Id = "idEditHost", | |
| IdNode = false, | |
| VScroll = "idScroll", | |
| MaxHeight = 160, | |
| BorderWidth = 1, | |
| CalcBackground = function() return self.idScript:CalcBackground() end, | |
| }, self) | |
| XText:new({ | |
| Id = "idScript", | |
| WordWrap = false, | |
| BorderWidth = 0, | |
| TextStyle = "GedScript", | |
| }, self.idEditHost) | |
| XSleekScroll:new({ | |
| Id = "idScroll", | |
| Target = "idEditHost", | |
| Dock = "right", | |
| Margins = box(2, 0, 0, 0), | |
| AutoHide = true, | |
| }, self.idEditHost) | |
| self.idScroll:SetHorizontal(false) | |
| end | |
| function GedPropScript:UpdateValue() | |
| local prop = self:GetProp() | |
| if prop == Undefined() then | |
| self.idScript:SetText("Undefined") | |
| self.idScript:SetEnabled(false) | |
| self.idCopyButton:SetVisible(false) | |
| GedPropEditor.UpdateValue(self) | |
| return | |
| end | |
| self.idScript:SetText(prop) | |
| self.idScript:SetEnabled(not prop:starts_with("empty")) | |
| self.idCopyButton:SetVisible(not prop:starts_with("empty")) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| ----- GedPropHelp | |
| GedPropEditors["help"] = "GedPropHelp" | |
| GedPropEditors["documentation"] = "GedPropHelp" | |
| DefineClass.GedPropHelp = { | |
| __parents = { "GedPropEditor" }, | |
| } | |
| function GedPropHelp:Init(parent, context, prop_meta) | |
| self.idLabelHost:SetDock("ignore") | |
| self.idLabelHost:SetVisible(false) | |
| if prop_meta.help ~= Undefined() and (prop_meta.help or "") ~= "" then | |
| XText:new({ | |
| Dock = "top", | |
| Id = "idHelp", | |
| Padding = box(2, 2, 2, 0), | |
| }, self) | |
| self.idHelp:SetText(prop_meta.help) | |
| XWindow:new({ | |
| Dock = "top", | |
| MaxHeight = 1, | |
| MinHeight = 1, | |
| Margins = box(7, 0, 7, 3), | |
| Background = RGB(128, 128, 128), | |
| }, self) | |
| end | |
| self.RolloverText = nil | |
| end | |
| ----- GedPropText | |
| GedPropEditors["text"] = "GedPropText" | |
| GedPropEditors["prop_table"] = "GedPropText" | |
| DefineClass.GedPropText = { | |
| __parents = { "GedPropEditor" }, | |
| text_value = false, | |
| update_thread = false, | |
| single_line_edit_template = "XEdit", | |
| } | |
| function GedPropText:Init(parent, context, prop_meta) | |
| local lines = prop_meta.lines or self.lines | |
| if lines then | |
| self.idLabelHost:SetDock("top") | |
| XWindow:new({ | |
| Id = "idEditHost", | |
| BorderWidth = 1, | |
| CalcBackground = function() return self.idEdit:CalcBackground() end, | |
| }, self) | |
| XMultiLineEdit:new({ | |
| Id = "idEdit", | |
| VScroll = "idScroll", | |
| MinVisibleLines = lines, | |
| MaxVisibleLines = Max(prop_meta.max_lines or self.max_lines or 10, lines), | |
| MaxLen = prop_meta.max_len, | |
| WordWrap = prop_meta.wordwrap, | |
| TextStyle = prop_meta.text_style, | |
| Filter = prop_meta.allowed_chars or ".", | |
| NewLine = "\n", | |
| BorderWidth = 0, | |
| }, self.idEditHost) | |
| XSleekScroll:new({ | |
| Id = "idScroll", | |
| Target = "idEdit", | |
| Dock = "right", | |
| Margins = box(2, 0, 0, 0), | |
| AutoHide = true, | |
| }, self.idEditHost) | |
| self.idScroll:SetHorizontal(false) | |
| else | |
| self:CreateSingleLineEditor(prop_meta) | |
| end | |
| local plugins = {} | |
| if prop_meta.translate then | |
| plugins[#plugins + 1] = "XSpellcheckPlugin" | |
| end | |
| if prop_meta.code then | |
| plugins[#plugins + 1] = "XCodeEditorPlugin" | |
| end | |
| self.idEdit:SetTranslate(prop_meta.translate or false) | |
| self.idEdit:SetPlugins(plugins) | |
| self.idEdit:SetEnabled(not prop_meta.read_only) | |
| self.idEdit:SetAutoSelectAll((prop_meta.read_only or prop_meta.auto_select_all) and not prop_meta.no_auto_select) | |
| if prop_meta.realtime_update and not prop_meta.read_only then | |
| self.update_thread = self:CreateThread("update_thread", self.UpdateThread, self) | |
| end | |
| end | |
| function GedPropText:CreateSingleLineEditor(prop_meta) | |
| return XEdit:new({ | |
| Id = "idEdit", | |
| VAlign = "center", | |
| TextStyle = prop_meta.text_style, | |
| Filter = prop_meta.allowed_chars or ".", | |
| }, self) | |
| end | |
| function GedPropText:ReassignFocusOrders(x, y) | |
| self.idEdit:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropText:UpdateThread() | |
| while true do | |
| Sleep(250) | |
| if (self.idEdit:IsFocused() and self.idEdit:GetText() ~= self.text_value) then | |
| self:SendValueToGame() | |
| end | |
| end | |
| end | |
| function GedPropText:UpdateValue() | |
| if not (self.idEdit:IsFocused() and self.idEdit:GetText() ~= self.text_value) then | |
| local prop = self:GetProp() | |
| local translate = self.prop_meta.translate | |
| local text | |
| if prop == Undefined() then | |
| text = "" | |
| self.idEdit:SetHint(translate and Untranslated("Undefined") or "Undefined") | |
| elseif prop == false then | |
| text = "" | |
| self.idEdit:SetHint(translate and Untranslated("false") or "false") | |
| else | |
| text = self:ConvertToText(prop) or "" | |
| self.idEdit:SetHint("") | |
| end | |
| self.text_value = text | |
| self.idEdit:SetText(text) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropText:OnShortcut(shortcut, source, ...) | |
| if (shortcut == "Enter" or shortcut == "Ctrl-Enter") and not self.prop_meta.read_only then -- set | |
| self:SendValueToGame() | |
| return "break" | |
| end | |
| return GedPropEditor.OnShortcut(self, shortcut, source, ...) | |
| end | |
| function GedPropText:SendValueToGame() | |
| self:SetValueFromText() | |
| end | |
| function GedPropText:SetValueFromText(no_text_update) | |
| local text = self.idEdit:GetText() | |
| if type(text) == "string" and self.prop_meta.trim_spaces ~= false and string.trim_spaces(text) ~= text then | |
| text = string.trim_spaces(text) | |
| if rawget(self, "idEdit") and not no_text_update then | |
| self.idEdit:SetText(text) | |
| end | |
| end | |
| if text ~= self.text_value then | |
| local value, is_invalid, recalc_text = self:ConvertFromText(text) | |
| if is_invalid then | |
| return -- e.g. when a function doesn't compile, we neither update nor revert the value | |
| end | |
| if value ~= nil then | |
| if recalc_text then | |
| text = self:ConvertToText(value) | |
| end | |
| self.text_value = text | |
| self:SetProp(value) | |
| end | |
| if (value == nil or recalc_text) and rawget(self, "idEdit") and not no_text_update then | |
| self.idEdit:SetText(self.text_value) | |
| end | |
| end | |
| end | |
| function GedPropText:ConvertToText(value, prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| return prop_meta.translate and GedPropValueToT(value) or type(value) == "string" and value or "" | |
| end | |
| function GedPropText:ConvertFromText(value, prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| return prop_meta.translate and GedTToPropValue(value, prop_meta.default) or value | |
| end | |
| function GedPropText:DetachForReuse() | |
| self.text_value = false | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| ----- GedPropNumber | |
| GedPropEditors["number"] = "GedPropNumber" | |
| GedPropEditors["radius"] = "GedPropNumber" | |
| GedPropEditors["time"] = "GedPropNumber" | |
| DefineClass.GedPropNumber = { | |
| __parents = { "GedPropEditor" }, | |
| slider_drag_id = false, | |
| } | |
| function GedPropNumber:GetDisplayScale(prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| if type(prop_meta.scale) == "string" then | |
| return const.Scale[prop_meta.scale] or 1 | |
| else | |
| return prop_meta.scale or 1 | |
| end | |
| end | |
| function GedPropNumber:Init(parent, context, prop_meta) | |
| local step = prop_meta.buttons_step or prop_meta.step or self:GetDisplayScale(prop_meta) | |
| local add_func = function(multiplier) if type(self:GetProp()) == "number" then self:TrySetProp(self:GetProp() + step * multiplier, "update_scrollbar") end end | |
| local edit, top, bottom = CreateNumberEditor(self, "idEdit", add_func, | |
| function(multiplier) if type(self:GetProp()) == "number" then self:TrySetProp(self:GetProp() - step * multiplier, "update_scrollbar") end end, | |
| prop_meta.slider and "no_buttons" | |
| ) | |
| if not prop_meta.slider then | |
| top:SetEnabled(not prop_meta.read_only) | |
| bottom:SetEnabled(not prop_meta.read_only) | |
| end | |
| if prop_meta.slider then | |
| self.idNumberEditor.parent:SetDock("left") | |
| self.idEdit:SetMinWidth(50) | |
| local function scroll(multiplier) | |
| if type(self:GetProp()) == "number" then self:TrySetProp(self:GetProp() + step * multiplier, "update_scrollbar") end | |
| end | |
| XSleekScroll:new({ | |
| Id = "idScroll", | |
| Dock = "box", | |
| Margins = box(2, 2, 2, 2), | |
| Min = floatfloor((prop_meta.min or 0) / (prop_meta.float and prop_meta.step or 1)), | |
| Max = floatfloor((prop_meta.max or 0) / (prop_meta.float and prop_meta.step or 1)) + 1, -- compensate for PageSize = 1 | |
| Horizontal = true, | |
| Target = "node", | |
| StepSize = floatfloor(not prop_meta.float and prop_meta.step or 1), | |
| StartScroll = function(...) | |
| -- Disable updating parent panels while dragging (self.slider_drag_id is a number) | |
| self.slider_drag_id = AsyncRand() | |
| return XSleekScroll.StartScroll(...) | |
| end, | |
| -- Called after letting go of the slider | |
| OnCaptureLost = function(...) | |
| self:DeleteThread("scroll_update_thread") | |
| -- GED -> Game - Update "prop value" in case this would change how the object is displayed in a parent UI panel | |
| -- Update parent panels (self.slider_drag_id is not a number) | |
| self.slider_drag_id = false | |
| self:TrySetProp(self:SliderToPropValue(self.idScroll:GetScroll()), not "update_scrollbar") | |
| XSleekScroll.OnCaptureLost(...) | |
| end, | |
| OnMouseWheelForward = function() if terminal.IsKeyPressed(const.vkControl) then scroll( 1) return "break" end end, | |
| OnMouseWheelBack = function() if terminal.IsKeyPressed(const.vkControl) then scroll(-1) return "break" end end, | |
| }, self) | |
| self.idScroll:SetEnabled(not prop_meta.read_only) | |
| end | |
| if not prop_meta.read_only then | |
| GedCoordAdjuster:new({ | |
| set_coord = add_func, | |
| consume_events = false, | |
| Dock = "box", | |
| owner = self, | |
| }, self.idLabel) | |
| end | |
| self.idEdit:SetAutoSelectAll((prop_meta.read_only or prop_meta.auto_select_all) and not prop_meta.no_auto_select) | |
| self.idEdit:SetEnabled(not prop_meta.read_only) | |
| end | |
| function GedPropNumber:ReassignFocusOrders(x, y) | |
| self.idEdit:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| -- Called repeatedly while holding the slider to update the "prop value" | |
| function GedPropNumber:OnScrollTo(value) | |
| if not self:IsThreadRunning("scroll_update_thread") then | |
| self:CreateThread("scroll_update_thread", function() | |
| Sleep(75) | |
| self:TrySetProp(self:SliderToPropValue(self.idScroll:GetScroll()), not "update_scrollbar") | |
| end) | |
| end | |
| end | |
| -- Game -> GED - Update the UI with the latest "prop value" | |
| function GedPropNumber:UpdateValue() | |
| -- If the slider is being held => don't update the "slider value" | |
| local has_slider = self.prop_meta.slider | |
| if not self.idEdit:IsFocused(true) and not (has_slider and self.desktop:GetMouseCapture() == self.idScroll) then | |
| local value = self:GetProp() | |
| self:SetSliderValue(value) | |
| self:SetEditedValue(value) -- update text in the textbox | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| -- Called on entering a number manually in the textbox and hitting Enter / Ctrl-Enter | |
| function GedPropNumber:OnShortcut(shortcut, source, ...) | |
| if shortcut == "Enter" or shortcut == "Ctrl-Enter" then -- set | |
| self:TrySetProp(self:GetEditedValue(), "update_scrollbar") | |
| return "break" | |
| end | |
| return GedPropEditor.OnShortcut(self, shortcut, source, ...) | |
| end | |
| -- Called after the textbox loses focus and sends the edited text value to the game | |
| function GedPropNumber:SendValueToGame() | |
| self:TrySetProp(self:GetEditedValue(), "update_scrollbar") | |
| end | |
| -- Transforms "slider value" to "prop value" | |
| function GedPropNumber:SliderToPropValue(slider_value) | |
| if not self.prop_meta.slider then | |
| return slider_value | |
| end | |
| -- Transform only when using a slider with an exponent | |
| if self.prop_meta.exponent then | |
| local exponential_value = LinearToExponential(slider_value, self.prop_meta.exponent, self.idScroll.Min, self.idScroll.Max - 1) | |
| -- Round to step to avoid extra digits that change the size of the textbox | |
| slider_value = exponential_value / self.idScroll.StepSize * self.idScroll.StepSize | |
| end | |
| return slider_value * (self.prop_meta.float and self.prop_meta.step or 1) | |
| end | |
| -- Transforms "prop value" to "slider value" | |
| function GedPropNumber:PropToSliderValue(prop_value) | |
| -- If the value is undefined => default to min or zero | |
| if prop_value == Undefined() then | |
| prop_value = self.prop_meta.min or 0 | |
| end | |
| if not self.prop_meta.slider then | |
| return prop_value | |
| end | |
| prop_value = floatfloor(prop_value / (self.prop_meta.float and self.prop_meta.step or 1)) | |
| -- Transform only when using a slider with an exponent | |
| if self.prop_meta.exponent then | |
| prop_value = ExponentialToLinear(prop_value, self.prop_meta.exponent, self.idScroll.Min, self.idScroll.Max - 1) | |
| end | |
| return prop_value | |
| end | |
| -- GED -> Game - Performs checks and transformations and sets the "prop value" in the game | |
| function GedPropNumber:TrySetProp(prop_value, update_scrollbar) | |
| if self.window_state == "destroying" or self.prop_meta.read_only then return end | |
| if not prop_value then return end | |
| if self.prop_meta.max and prop_value > self.prop_meta.max then | |
| prop_value = self.prop_meta.max | |
| end | |
| if self.prop_meta.min and prop_value < self.prop_meta.min then | |
| prop_value = self.prop_meta.min | |
| end | |
| -- Set the "prop value" in the game | |
| self:SetProp(prop_value, not "force", self.slider_drag_id) | |
| -- Set the text in the textbox | |
| self:SetEditedValue(prop_value) | |
| if update_scrollbar then | |
| self:SetSliderValue(prop_value) | |
| end | |
| end | |
| -- Set the "slider value" and update the slider | |
| function GedPropNumber:SetSliderValue(prop_value) | |
| -- Note: We can't (reliably) get the prop_value with self:GetProp() | |
| -- because the updating thread might not have finished the update yet | |
| if type(prop_value) ~= "number" then | |
| prop_value = self.prop_meta.min or 0 | |
| end | |
| if self.prop_meta.slider then | |
| -- Transform "prop value" to "slider value" | |
| local slider_value = self:PropToSliderValue(prop_value) | |
| -- Update the slider | |
| self.idScroll:SetScroll(slider_value) | |
| end | |
| end | |
| function GedPropNumber:ToNumberEdit(value, prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| if value == Undefined() then | |
| return "", "Undefined" | |
| end | |
| local number = tonumber(value) | |
| if not number then | |
| return "", tostring(value) | |
| elseif prop_meta.float then | |
| return tostring(number), "" | |
| else | |
| local display_scale = self:GetDisplayScale(prop_meta) | |
| return FormatNumberProp(number, display_scale), "" | |
| end | |
| end | |
| function GedPropNumber:FromNumberEdit(num, prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| num = num * self:GetDisplayScale(prop_meta) | |
| return prop_meta.float and num or floatfloor(num + 0.5) | |
| end | |
| -- Edit the value in the textbox | |
| function GedPropNumber:SetEditedValue(value) | |
| local text, hint = self:ToNumberEdit(value) | |
| self.idEdit:SetHint(hint) | |
| self.idEdit:SetText(text) | |
| end | |
| function GedPropNumber:GetEditedValue() | |
| local num = self.idEdit:GetNumber() | |
| if not num then return nil end | |
| return self:FromNumberEdit(num) | |
| end | |
| function GedPropNumber:ConvertToText(value, prop_meta) | |
| local text, hint = self:ToNumberEdit(value, prop_meta) | |
| return text | |
| end | |
| function GedPropNumber:ConvertFromText(value, prop_meta) | |
| return self:FromNumberEdit(tonumber(value), prop_meta) | |
| end | |
| ----- GedPropUIImage | |
| GedPropEditors["ui_image"] = "GedPropUIImage" | |
| DefineClass.GedPropUIImage = { | |
| __parents = { "GedPropBrowse" }, | |
| } | |
| local function GetSegments(path) | |
| path = path:gsub("[\\/]+", '/') | |
| local segments = {} | |
| for segment in string.gmatch(path, '[^/]+') do | |
| table.insert(segments, segment) | |
| end | |
| return segments | |
| end | |
| local function GetRelativePath(path, base, game_path) | |
| path = GetSegments(path) | |
| base = GetSegments(base) | |
| game_path = GetSegments(game_path) | |
| for key, value in ipairs(base) do | |
| if value ~= path[key] then | |
| return false | |
| end | |
| end | |
| return table.concat(table.move(path, #base + 1, #path, #game_path + 1, game_path), '/') | |
| end | |
| function GedPropUIImage:BuildUIForPath(path) | |
| if self.prop_meta.dont_validate or self.in_mod_editor then | |
| return | |
| end | |
| -- if the source image exists, but the built UI texture is missing, invoke the build tool to create it | |
| local dir, name, ext = SplitPath(path) | |
| if io.exists("svnAssets/Source/" .. dir .. name .. ".png") and -- EXISTS in the Source folder | |
| not io.exists(dir .. name .. ".dds") and not io.exists(dir .. name .. ".tga") -- BUT NOT in the built files | |
| then | |
| local app = self.panel.app | |
| if not app:IsThreadRunning("BuildUIThread") then | |
| app:CreateThread("BuildUIThread", function() | |
| local assets_path = ConvertToOSPath("svnAssets/") | |
| local project_path = ConvertToOSPath("svnProject/") | |
| local err, exit_code = AsyncExec(string.format("cmd /c echo Running 'build UI' to include the selected UI image in the build. Please commit the resulting files! & %s/Build UI-win32", project_path)) | |
| if err then assert("Failed to build UI textures" and false) return end | |
| err, exit_code = AsyncExec(string.format("cmd /c echo Please use the SVN dialog that popped to commit the built UI image files! & TortoiseProc /command:commit /path:%s", assets_path)) | |
| if err then assert("Failed to open commit dialog" and false) end | |
| end) | |
| end | |
| end | |
| end | |
| function GedPropUIImage:GetCurrentFilePath(path) | |
| -- try opening the path of the current image, if it exists; otherwise, open the first folder path | |
| -- however, always remap UI/ to the source art folder at svnAssets/Source/UI | |
| local prop = self:GetProp() | |
| if prop and prop ~= "" and prop ~= Undefined() then | |
| path = SplitPath(prop) | |
| path = Platform.developer and path:starts_with("UI/") and "svnAssets/Source/"..path or path | |
| if not io.exists(path) then | |
| path = folders[1].game_path | |
| path = Platform.developer and path:starts_with("UI/") and "svnAssets/Source/"..path or path | |
| end | |
| path = ConvertToOSPath(path) | |
| end | |
| return path | |
| end | |
| function GedPropUIImage:GetSubFolderDest() | |
| return "Images" | |
| end | |
| function GedPropUIImage:Init(parent, context, prop_meta) | |
| local image_preview_size = prop_meta.image_preview_size or 0 | |
| local edit_container = self | |
| if image_preview_size > 0 then | |
| edit_container = XWindow:new({ Dock = "bottom", ZOrder = -1, Margins = box(0, 3, 0, 0), }, self) | |
| end | |
| XImage:new({ | |
| Id = "idImage", | |
| ImageFit = "scale-down", | |
| MaxWidth = Max(image_preview_size, 28), | |
| MaxHeight = Max(image_preview_size, 28), | |
| HandleMouse = true, | |
| FoldWhenHidden = true, | |
| }, XWindow:new({ | |
| Dock = "right" | |
| }, self)) | |
| self.idImage:SetVisible(false) | |
| self.idImage:SetRolloverTemplate("GedImageRollover") | |
| end | |
| function GedPropUIImage:UpdateValue() | |
| GedPropBrowse.UpdateValue(self) | |
| local prop = self:GetProp() | |
| if not prop or prop == Undefined() or prop == "" then | |
| self.idImage:SetVisible(false) | |
| else | |
| self.idImage:SetImage(prop) | |
| self.idImage:SetRolloverText(prop) | |
| self.idImage:SetVisible(true) | |
| end | |
| end | |
| function GedPropUIImage:SetProp(value, ...) | |
| GedPropBrowse.SetProp(self, value, ...) | |
| if value then -- value is 'nil' if clearing a property to its default value | |
| self:BuildUIForPath(value) | |
| end | |
| end | |
| ----- GedPropBrowse | |
| GedPropEditors["browse"] = "GedPropBrowse" | |
| DefineClass.GedPropBrowse = { | |
| __parents = { "GedPropEditor" }, | |
| in_mod_editor = false, | |
| } | |
| local function NormalizeGamePath(path) | |
| path = path:gsub("\\", "/") | |
| local final_path = "" | |
| for segment in path:gmatch("[^/]+") do | |
| segment = segment:trim_spaces() | |
| if #segment > 0 then | |
| final_path = final_path .. segment .. "/" | |
| end | |
| end | |
| return final_path:lower() | |
| end | |
| local function MatchFolder(path, folders) | |
| path = path or "" | |
| folders = folders or "" | |
| if path == "" or #folders == 0 then | |
| return | |
| end | |
| for _, folder in ipairs(folders) do | |
| if folder.game_path then | |
| local normalized_path = NormalizeGamePath(path) | |
| local normalized_folder = NormalizeGamePath(folder.game_path) | |
| if normalized_path:starts_with(normalized_folder, true) then | |
| return folder | |
| end | |
| end | |
| if folder.os_path then | |
| local normalized_path = ConvertToOSPath(path) | |
| local normalized_folder = ConvertToOSPath(folder.os_path) | |
| if string.starts_with(normalized_path, normalized_folder, true) then | |
| return folder | |
| end | |
| end | |
| end | |
| end | |
| local function SelectGamePath(editor, prop_meta, folder, folders, initial) | |
| local path = editor:OpenBrowseDialog(folder.os_path, prop_meta.filter or "", not prop_meta.allow_missing, false, initial, folders) | |
| if path then | |
| local converted_path = ConvertFromOSPath(path, folder.game_path) | |
| -- 'converted_path' is nil if 'path' is not in 'folder.game_path' | |
| if path ~= converted_path then | |
| return converted_path | |
| else | |
| local relative = GetRelativePath(converted_path, folder.os_path, folder.game_path) | |
| if relative then | |
| return relative | |
| end | |
| for _, folder in ipairs(folders) do | |
| local relative = folder.game_path and GetRelativePath(converted_path, folder.os_path, folder.game_path) | |
| if relative then | |
| return relative | |
| end | |
| end | |
| if prop_meta.dont_validate then | |
| return path | |
| end | |
| end | |
| end | |
| return false | |
| end | |
| local function SelectOSPath(editor, prop_meta, folder, folders, initial) | |
| local path = editor:OpenBrowseDialog(folder.os_path, prop_meta.filter or "", not prop_meta.allow_missing, false, initial, folders) | |
| local path, err = ConvertToOSPath(path) | |
| local folder_os_path, err2 = ConvertToOSPath(folder.os_path) | |
| if not (err or err2) and (prop_meta.dont_validate or path and string.starts_with(path, folder_os_path, true)) then | |
| return path | |
| elseif not path then | |
| return false | |
| end | |
| end | |
| function GedPropBrowse:Init(parent, context, prop_meta) | |
| self.in_mod_editor = g_GedApp.AppId == "ModEditor" | |
| local folders = self:GetPropMetaFolder() | |
| if not prop_meta.read_only and (#folders > 0 or prop_meta.os_path) then | |
| local buttonswin = XWindow:new({ | |
| Id = "idButtonsHost", | |
| Dock = "right", | |
| HAlign = "center", | |
| LayoutMethod = "HList", | |
| LayoutHSpacing = 2, | |
| Padding = box(2, 0, 0, 0), | |
| }, self) | |
| XTextButton:new({ | |
| Id = "idButtonBrowse", | |
| Text = self.in_mod_editor and not prop_meta.os_path and "Import" or "...", | |
| Enabled = not prop_meta.read_only, | |
| Dock = "right", | |
| VAlign = "stretch", | |
| Margins = box(0, 1, 0, 1), | |
| MaxWidth = 50, | |
| BorderWidth = 1, | |
| BorderHeight = 1, | |
| Background = RGB(220, 220, 220), | |
| RolloverBackground = RGB(255, 255, 255), | |
| RolloverBorderColor = RGB(0, 0, 0), | |
| PressedBackground = RGB(220, 220, 255), | |
| PressedBorderColor = RGB(0, 0, 0), | |
| OnPress = function(button, gamepad) | |
| self.desktop:SetKeyboardFocus(false) | |
| local prop = self:GetProp() or "" | |
| if prop == Undefined() then | |
| prop = "" | |
| end | |
| local path, name, ext = SplitPath(prop) | |
| if prop_meta.os_path or self.in_mod_editor then | |
| if (path or "") == "" then | |
| if type(prop_meta.os_path) == "string" then | |
| path = ConvertToOSPath(prop_meta.os_path) | |
| else | |
| path = ConvertToOSPath(".") | |
| end | |
| end | |
| path = self:OpenBrowseDialog(path, prop_meta.filter or "", not prop_meta.allow_missing) | |
| if path then | |
| self:SetProp(path) | |
| end | |
| else | |
| local folder = (path or "") ~= "" and MatchFolder(path, folders) or folders[1] | |
| local fn = folder.game_path and SelectGamePath or SelectOSPath | |
| path = fn(self, prop_meta, folder, folders, name .. ext) | |
| CreateRealTimeThread(function() | |
| Sleep(10) -- workaround for the SystemInactivate when the OS window is closed resetting the property value back | |
| if self.window_state ~= "destroying" then | |
| if path then | |
| self:SetProp(path) | |
| elseif path ~= false then | |
| self:SetPropResult("File is not in any of: " .. (folder.game_path or folder.os_path)) | |
| end | |
| end | |
| end) | |
| end | |
| end, | |
| Clip = "self", | |
| }, buttonswin) | |
| end | |
| XEdit:new({ | |
| Id = "idEdit", | |
| VAlign = "center", | |
| OnShortcut = function(edit, shortcut, ...) | |
| if (shortcut == "Enter" or shortcut == "Ctrl-Enter") and not self.prop_meta.read_only then | |
| self:SendValueToGame() | |
| return "break" | |
| end | |
| return XEdit.OnShortcut(edit, shortcut, ...) | |
| end, | |
| }, self):SetEnabled(not prop_meta.read_only) | |
| local image_preview_size = prop_meta.image_preview_size or 0 | |
| local image_preview = image_preview_size > 0 | |
| XImage:new({ | |
| Id = "idImage", | |
| ImageFit = "smallest", | |
| MaxWidth = image_preview_size, | |
| MaxHeight = image_preview_size, | |
| Dock = image_preview and "right" or "ignore" | |
| }, XWindow:new({ | |
| Dock = "bottom" | |
| }, self)) | |
| self.idImage:SetVisible(image_preview) | |
| end | |
| function GedPropBrowse:GetCurrentFilePath(path) | |
| return path | |
| end | |
| function GedPropBrowse:ReassignFocusOrders(x, y) | |
| self.idEdit:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropBrowse:GetSubFolderDest() | |
| return self.prop_meta.mod_dst or "" | |
| end | |
| function GedPropBrowse:OpenBrowseDialog(path, filter, exists, multiple, initial_file) | |
| if (filter or "") == "" then | |
| filter = "All files|*.*" | |
| end | |
| if self.in_mod_editor and not io.exists(path or "") then | |
| path = g_GedApp.mod_os_path | |
| else | |
| path = self:GetCurrentFilePath(path) | |
| end | |
| local file_path = OpenBrowseDialog(path, filter, exists, multiple, initial_file) | |
| if file_path then | |
| if self.in_mod_editor and not self.prop_meta.os_path then | |
| local mod_path = ConvertToOSPath(g_GedApp.mod_os_path) | |
| file_path = ConvertToOSPath(file_path) | |
| local is_folder = IsFolder(file_path) | |
| if not file_path:starts_with(mod_path) and not is_folder then | |
| -- copy file inside the mod folder and use the new path | |
| local dir, file, ext = SplitPath(file_path) | |
| local org_file = file | |
| local dst_path = SlashTerminate(mod_path .. self:GetSubFolderDest()) | |
| AsyncCreatePath(dst_path) | |
| --find unique file name | |
| local new_file_path, i | |
| while true do | |
| new_file_path = dst_path .. file .. ext | |
| if io.exists(new_file_path) then | |
| i = (i or 1) + 1 | |
| file = string.format("%s %d", org_file, i) | |
| else | |
| break | |
| end | |
| end | |
| local err = AsyncCopyFile(file_path, new_file_path, "raw") | |
| if err then | |
| self:SetPropResult("Failed to import the file: " .. err) | |
| return "" | |
| end | |
| file_path = new_file_path | |
| end | |
| file_path = GetRelativePath(file_path, mod_path, g_GedApp.mod_content_path) | |
| end | |
| end | |
| return file_path | |
| end | |
| function GedPropBrowse:UpdateValue() | |
| GedPropEditor.UpdateValue(self) | |
| local prop = self:GetProp() | |
| if prop == Undefined() then | |
| self.idEdit:SetHint("Undefined") | |
| self.idEdit:SetText("") | |
| elseif not prop then | |
| self.idEdit:SetHint("false") | |
| self.idEdit:SetText("") | |
| else | |
| self.idEdit:SetText(prop) | |
| end | |
| end | |
| function GedPropBrowse:GetPropMetaFolder() | |
| return self.prop_meta.folder or empty_table | |
| end | |
| function GedPropBrowse:SendValueToGame() | |
| local path = self.idEdit:GetText() | |
| if (path or "") == "" then return end | |
| local folders = self:GetPropMetaFolder() | |
| if next(folders) and not self.prop_meta.dont_validate and not MatchFolder(path, folders) then | |
| local serialize_fn = function(folder) return folder.game_path or folder.os_path end | |
| local paths = table.concat(table.map(folders, serialize_fn), ", ") | |
| self:SetPropResult("File is not in any of: " .. paths) | |
| else | |
| self:SetProp(path) | |
| end | |
| end | |
| function GedPropBrowse:SetProp(value, ...) | |
| local ext = self.prop_meta.force_extension | |
| if value and ext then | |
| local path, filename = SplitPath(value) | |
| local name = path .. filename | |
| value = ext == "" and name or ext:starts_with(".") and name..ext or name.."."..ext | |
| end | |
| GedPropEditor.SetProp(self, value, ...) | |
| end | |
| ----- GedPropPoint | |
| DefineClass.XCoordAdjuster = { | |
| __parents = { "XWindow" }, | |
| HandleKeyboard = true, | |
| HandleMouse = true, | |
| set_coord = empty_func, | |
| consume_events = "break", | |
| OnStartAdjustment = empty_func, | |
| is_adjusting = false, | |
| original_prop = false, | |
| last_cursor_pos = false, | |
| buffered_cursor_pos = false, | |
| } | |
| function XCoordAdjuster:Done() | |
| self:CancelAdjustment() | |
| end | |
| function XCoordAdjuster:OnMouseButtonDown(screen_point, button) | |
| if button == "R" and self.is_adjusting then | |
| self:CancelAdjustment() | |
| return self.consume_events | |
| end | |
| if button == "L" then | |
| self.is_adjusting = true | |
| self.last_cursor_pos = screen_point | |
| self.desktop:SetMouseCapture(self) | |
| self:OnStartAdjustment() | |
| return self.consume_events | |
| end | |
| return XWindow.OnMouseButtonDown(self, screen_point, button) | |
| end | |
| function XCoordAdjuster:OnMouseButtonUp(screen_point, button) | |
| if self.is_adjusting then | |
| self.is_adjusting = nil | |
| self.last_cursor_pos = nil | |
| self.buffered_cursor_pos = nil | |
| self.desktop:SetMouseCapture() | |
| return "break" | |
| end | |
| return XWindow.OnMouseButtonUp(self, screen_point, button) | |
| end | |
| function XCoordAdjuster:OnMousePos(screen_point, button) | |
| if self.is_adjusting then | |
| if terminal.IsKeyPressed(const.vkEsc) then | |
| self:CancelAdjustment() | |
| return self.consume_events | |
| end | |
| self.buffered_cursor_pos = screen_point | |
| if not self:IsThreadRunning("adjust_update_thread") then | |
| self:CreateThread("adjust_update_thread", function() | |
| Sleep(75) | |
| if not self.buffered_cursor_pos then return end | |
| local diff = self.buffered_cursor_pos:x() - self.last_cursor_pos:x() + self.buffered_cursor_pos:y() - self.last_cursor_pos:y() | |
| self.set_coord(diff) | |
| self.last_cursor_pos = self.buffered_cursor_pos | |
| self.buffered_cursor_pos = nil | |
| end) | |
| end | |
| return self.consume_events | |
| end | |
| return XWindow.OnMousePos(self, screen_point, button) | |
| end | |
| function XCoordAdjuster:OnKbdKeyDown(key, ...) | |
| if self.is_adjusting and key == const.vkEsc then | |
| self:CancelAdjustment() | |
| return self.consume_events | |
| end | |
| return XWindow.OnKbdKeyDown(self, key, ...) | |
| end | |
| function XCoordAdjuster:CancelAdjustment() | |
| if self.is_adjusting then | |
| return self:OnMouseButtonUp(self.cursor_start_pos, 'L') | |
| end | |
| end | |
| function XCoordAdjuster:OnCaptureLost(...) | |
| if self.is_adjusting then | |
| self:CancelAdjustment() | |
| end | |
| return XWindow.OnCaptureLost(self, ...) | |
| end | |
| DefineClass.GedCoordAdjuster = { | |
| __parents = { "XCoordAdjuster" }, | |
| owner = false, | |
| } | |
| function GedCoordAdjuster:OnStartAdjustment(...) | |
| if self.owner then | |
| -- Disable updating parent panels while adjusting (self.slider_drag_id is a number) | |
| self.owner.slider_drag_id = AsyncRand() | |
| end | |
| return XCoordAdjuster.OnStartAdjustment(...) | |
| end | |
| function GedCoordAdjuster:OnCaptureLost(...) | |
| if self.owner then | |
| self.owner.slider_drag_id = false | |
| end | |
| return XCoordAdjuster.OnCaptureLost(self, ...) | |
| end | |
| DefineClass.GedCoordEditor = { | |
| __parents = { "GedPropEditor" }, | |
| text_value = false, | |
| update_thread = false, | |
| coords = false, | |
| hide_arrows = false, | |
| hide_icons = false, | |
| lock_ratio = false, | |
| accept_os_clipboard_paste = true, | |
| GatherCoords = empty_func, | |
| } | |
| function GedCoordEditor:ReassignFocusOrders(x, y) | |
| for i, coord in ipairs(self.coords) do | |
| coord:SetFocusOrder(point(x + i - 1, y)) | |
| end | |
| return y + 1 | |
| end | |
| function GedCoordEditor:UpdateThread() | |
| while true do | |
| Sleep(250) | |
| if (self.idEdit:IsFocused() and self.idEdit:GetText() ~= self.text_value) then | |
| self:SendValueToGame() | |
| end | |
| end | |
| end | |
| function GedCoordEditor:GetEditText() | |
| local coords = self.coords | |
| local len = #coords | |
| local text = "" | |
| for i = 1, len do | |
| text = text .. coords[i]:GetText() | |
| if i ~= len then | |
| text = text .. "," | |
| end | |
| end | |
| return text | |
| end | |
| function GedCoordEditor:SetEditText(text) | |
| local coord_iter = string.gmatch(text or "", "[^,]+") | |
| for _, coord in ipairs(self.coords) do | |
| local coord_text = coord_iter() | |
| coord:SetText(coord_text and coord_text:trim_spaces() or "") | |
| end | |
| end | |
| function GedCoordEditor:UpdateValue() | |
| if not (self.idEdit:IsFocused() and self:GetEditText() ~= self.text_value) then | |
| local prop = self:GetProp() | |
| local text | |
| if prop == Undefined() then | |
| text = "" | |
| -- TODO: Hints (false/Undefined) | |
| elseif prop == false then | |
| text = "" | |
| else | |
| text = self:ConvertToText(prop) or "" | |
| end | |
| self.text_value = text | |
| self:SetEditText(text) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedCoordEditor:OnShortcut(shortcut, source, ...) | |
| if (shortcut == "Enter" or shortcut == "Ctrl-Enter") and not self.prop_meta.read_only then -- set | |
| self:SendValueToGame() | |
| return "break" | |
| end | |
| return GedPropEditor.OnShortcut(self, shortcut, source, ...) | |
| end | |
| function GedCoordEditor:SendValueToGame() | |
| self:SetValueFromText() | |
| end | |
| local mock_prop_getter = { | |
| prop_value = false, | |
| GetProp = function(self) | |
| return self.prop_value | |
| end | |
| } | |
| function GedCoordEditor:SetValueFromText(no_text_update) | |
| local text = self:GetEditText() | |
| if type(text) == "string" and self.prop_meta.trim_spaces ~= false and string.trim_spaces(text) ~= text then | |
| text = string.trim_spaces(text) | |
| if rawget(self, "idEdit") and not no_text_update then | |
| self:SetEditText(text) | |
| end | |
| end | |
| if text ~= self.text_value then | |
| local value, is_invalid, recalc_text = self:ConvertFromText(text) | |
| if is_invalid then | |
| return -- e.g. when a function doesn't compile, we neither update nor revert the value | |
| end | |
| if value ~= nil then | |
| if recalc_text then | |
| text = self:ConvertToText(value) | |
| end | |
| self.text_value = text | |
| local added_value | |
| if self.lock_ratio then | |
| local old_value = self:GetProp() | |
| local diff_coord, diff_value | |
| for _, coord in ipairs(self.coords) do | |
| mock_prop_getter.prop_value = old_value | |
| local old_coord_value = coord.getter(mock_prop_getter) | |
| mock_prop_getter.prop_value = value | |
| local new_coord_value = coord.getter(mock_prop_getter) | |
| if old_coord_value ~= new_coord_value then | |
| if diff_coord then | |
| diff_coord = nil | |
| break | |
| else | |
| diff_coord = coord | |
| diff_value = new_coord_value - old_coord_value | |
| end | |
| end | |
| end | |
| if diff_coord then | |
| self:AddCoordValue(diff_coord, diff_value) | |
| added_value = true | |
| end | |
| end | |
| if not added_value then | |
| self:SetProp(value) | |
| end | |
| end | |
| if (value == nil or recalc_text) and rawget(self, "idEdit") and not no_text_update then | |
| self:SetEditText(self.text_value) | |
| end | |
| end | |
| end | |
| function GedCoordEditor:DetachForReuse() | |
| self.text_value = false | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| local function CreatePointNumberEditor(self, label, gridx, padding_left, prop_meta, no_buttons, adder, getter) | |
| local step = prop_meta.buttons_step or prop_meta.step or self.display_scale or 1 | |
| local edit = CreateNumberEditor(self.idEdit, "idNumEdit", | |
| function(multiplier) self:AddCoordValue(edit, step * multiplier) end, | |
| function(multiplier) self:AddCoordValue(edit, -step * multiplier) end, | |
| no_buttons or prop_meta.read_only | |
| ) | |
| edit:SetDock() | |
| edit:SetEnabled(not prop_meta.read_only) | |
| edit.adder = adder | |
| edit.getter = getter | |
| edit.parent:SetDock() | |
| edit.parent:SetGridX(gridx) | |
| if label and not self.hide_icons then | |
| local txt = XText:new({ | |
| Dock = "left", | |
| TextStyle = prop_meta.text_style, | |
| Padding = box(padding_left, 2, 4, 2), | |
| HandleKeyboard = false, | |
| HandleMouse = false, | |
| Translate = IsT(label), | |
| ChildrenHandleMouse = true, | |
| FoldWhenHidden = true, | |
| }, edit.parent) | |
| edit.txt = txt | |
| txt:SetText(label) | |
| txt:SetEnabled(not prop_meta.read_only) | |
| if not prop_meta.read_only then | |
| GedCoordAdjuster:new({ | |
| set_coord = function(multiplier) self:AddCoordValue(edit, step * multiplier) end, | |
| owner = self, | |
| }, txt) | |
| end | |
| end | |
| return edit | |
| end | |
| function GedCoordEditor:Init(parent, context, prop_meta) | |
| XWindow:new({ | |
| Id = "idEdit", | |
| LayoutMethod = "Grid", | |
| UniformColumnWidth = true, | |
| HandleMouse = false, | |
| OnBoxChanged = function(edit) | |
| local coords = self.coords | |
| local column_cnt = self.column_cnt or #coords | |
| if column_cnt == 0 then return end | |
| local show_arrows = not self.hide_arrows and edit.content_measure_width > 100 * column_cnt | |
| local show_icon = not self.hide_icons and edit.content_measure_width > 60 * column_cnt | |
| for _, coord in ipairs(coords) do | |
| if coord.top_btn then | |
| coord.top_btn:SetVisible(show_arrows) | |
| end | |
| if coord.bottom_btn then | |
| coord.bottom_btn:SetVisible(show_arrows) | |
| end | |
| if coord.icon then | |
| coord.icon:SetVisible(show_icon) | |
| end | |
| if coord.txt then | |
| coord.txt:SetVisible(show_icon) | |
| end | |
| end | |
| end, | |
| }, self) | |
| if prop_meta.realtime_update and not prop_meta.read_only then | |
| self.update_thread = self:CreateThread("update_thread", self.UpdateThread, self) | |
| end | |
| local coords = {} | |
| self:GatherCoords(coords, prop_meta) | |
| self.coords = coords | |
| if prop_meta.lock_ratio then | |
| local button = XTemplateSpawn("GedToolbarToggleButtonSmall", self) | |
| button.OnChange = function(_, toggled) | |
| self.lock_ratio = toggled | |
| end | |
| button:SetIcon("CommonAssets/UI/Ged/link.tga") | |
| button:SetToggled(not prop_meta.read_only) | |
| button:SetBorderWidth(0) | |
| button:SetRolloverText("Lock Property Ratio") | |
| button:SetEnabled(not prop_meta.read_only) | |
| end | |
| end | |
| function GedCoordEditor:AddCoordValue(coord, num) | |
| if self.window_state == "destroying" or self.prop_meta.read_only then return end | |
| if not coord or not num then return end | |
| local coord_value = coord.getter(self) | |
| local prop_value = coord.adder(self, num) | |
| if not prop_value then return end | |
| if not self.lock_ratio then | |
| return self:SetProp(prop_value, not "force", self.slider_drag_id) | |
| end | |
| if coord_value == 0 then return end | |
| local coord_idx = table.find(self.coords, coord) | |
| if not coord_idx then return end | |
| mock_prop_getter.prop_value = prop_value | |
| for i, cur_coord in ipairs(self.coords) do | |
| if i ~= coord_idx then | |
| local cur_value = cur_coord.getter(self) | |
| local ratio = MulDivRound(cur_value, 100, coord_value) | |
| prop_value = cur_coord.adder(mock_prop_getter, MulDivRound(num, ratio, 100)) | |
| end | |
| end | |
| return self:SetProp(prop_value, not "force", self.slider_drag_id) | |
| end | |
| function GedCoordEditor:GetDisplayScale(prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| local scale = prop_meta.scale | |
| if type(scale) == "string" then | |
| scale = const.Scale[scale] | |
| end | |
| if IsPoint(scale) then | |
| local x, y, z = scale:xyz() | |
| return x, y, z or 1 | |
| end | |
| scale = scale or 1 | |
| return scale, scale, scale | |
| end | |
| GedPropEditors["point"] = "GedPropPoint" | |
| DefineClass.GedPropPoint = { | |
| __parents = { "GedCoordEditor" }, | |
| _2D = false, | |
| } | |
| function GedPropPoint:GatherCoords(coords, prop_meta) | |
| coords[#coords + 1] = CreatePointNumberEditor(self, "X", 1, 0, prop_meta, self.hide_arrows, function(self, num) | |
| return self:GetProp():AddX(num) | |
| end, function(self) | |
| return self:GetProp():x() | |
| end) | |
| coords[#coords + 1] = CreatePointNumberEditor(self, "Y", 2, 4, prop_meta, self.hide_arrows, function(self, num) | |
| return self:GetProp():AddY(num) | |
| end, function(self) | |
| return self:GetProp():y() | |
| end) | |
| if not self._2D then | |
| coords[#coords + 1] = CreatePointNumberEditor(self, "Z", 3, 4, prop_meta, self.hide_arrows, function(self, num) | |
| return self:GetProp():AddZ(num) | |
| end, function(self) | |
| return self:GetProp():z() | |
| end) | |
| end | |
| end | |
| function GedPropPoint:GetMinMax() | |
| local min = self.prop_meta.min | |
| local minx, miny, minz = min, min, min | |
| if IsPoint(min) then | |
| minx, miny, minz = min:xyz() | |
| end | |
| local max = self.prop_meta.max | |
| local maxx, maxy, maxz = max, max, max | |
| if IsPoint(max) then | |
| maxx, maxy, maxz = max:xyz() | |
| end | |
| return minx or min_int, miny or min_int, minz or min_int, maxx or max_int, maxy or max_int, maxz or max_int | |
| end | |
| function GedPropPoint:ConvertToText(value, prop_meta) | |
| local result = "" | |
| if IsPoint(value) then | |
| local x, y, z = value:xyz() | |
| local sx, sy, sz = self:GetDisplayScale(prop_meta) | |
| result = FormatNumberProp(x, sx) .. ", " .. FormatNumberProp(y, sy) | |
| if z then | |
| result = result .. ", " .. FormatNumberProp(z, sz) | |
| end | |
| end | |
| return result | |
| end | |
| function GedPropPoint:ApplyScale(x, y, z) | |
| local sx, sy, sz = self:GetDisplayScale() | |
| x = floatfloor(x * sx + 0.5) | |
| y = floatfloor(y * sy + 0.5) | |
| z = z and floatfloor(z * sz + 0.5) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetMinMax() | |
| local x0, y0, z0 = x, y, z | |
| x = Clamp(x, minx, maxx) | |
| y = Clamp(y, miny, maxy) | |
| z = z and Clamp(z, minz, maxz) | |
| local changed = x0 ~= x or y0 ~= y or z0 ~= z | |
| return x, y, z, changed | |
| end | |
| function GedPropPoint:ConvertFromText(value) | |
| local changed | |
| local x, y, z = value:match("^([^,]+),([^,]+),([^,]+)") | |
| if not x or not y then | |
| x, y = value:match("^([^,]+),([^,]+)") | |
| end | |
| x, y, z = tonumber(x), tonumber(y), tonumber(z) | |
| if x and y then | |
| x, y, z, changed = self:ApplyScale(x, y, z) | |
| return z and point(x, y, z) or point(x, y), nil, changed | |
| end | |
| end | |
| GedPropEditors["point2d"] = "GedPropPoint2D" | |
| DefineClass.GedPropPoint2D = { | |
| __parents = { "GedPropPoint" }, | |
| _2D = true, | |
| } | |
| ----- GedPropBox | |
| local function CreatePointNumberIcon(edit, prop_meta, icon, rollover_text, padding_left) | |
| local image_preview_size = prop_meta.image_preview_size or 0 | |
| local img = XImage:new({ | |
| Id = "idImage", | |
| Dock = "left", | |
| Image = icon, | |
| ImageColor = RGBA(180, 180, 180, 255), | |
| ImageFit = "scale-down", | |
| MaxWidth = Max(image_preview_size, 28), | |
| MaxHeight = Max(image_preview_size, 28), | |
| HandleMouse = true, | |
| FoldWhenHidden = true, | |
| Padding = box(padding_left, 0, 2, 0), | |
| RolloverTemplate = "GedPropRollover", | |
| RolloverText = rollover_text, | |
| RolloverAnchor = "center-top", | |
| }, edit.parent) | |
| img:SetEnabled(not prop_meta.read_only) | |
| return img | |
| end | |
| local function CreateRectPropNumberEditor(self, coords, prop_meta, padding_left, adder, getter) | |
| local idx = #coords + 1 | |
| local label = table.get(self.labels, idx) | |
| local text_padding_left = padding_left or (idx == 1 and 0 or 4) | |
| local coord_editor = CreatePointNumberEditor(self, label, idx, text_padding_left, prop_meta, self.hide_arrows, adder, getter) | |
| coords[idx] = coord_editor | |
| local icon = table.get(self.icons, idx) | |
| if icon then | |
| local rollover_text = table.get(self.rollover_texts, idx) | |
| local icon_padding_left = padding_left or (idx == 1 and 0 or 2) | |
| local icon_window = CreatePointNumberIcon(coords[idx], prop_meta, icon, rollover_text, icon_padding_left) | |
| coord_editor.icon = icon_window | |
| if not prop_meta.read_only then | |
| local step = prop_meta.buttons_step or prop_meta.step or self.display_scale or 1 | |
| GedCoordAdjuster:new({ | |
| set_coord = function(multiplier) self:AddCoordValue(coord_editor, step * multiplier) end, | |
| owner = self, | |
| }, icon_window) | |
| end | |
| end | |
| return coord_editor | |
| end | |
| GedPropEditors["box"] = "GedPropBox" | |
| DefineClass.GedPropBox = { | |
| __parents = { "GedPropPoint" }, | |
| labels = { Untranslated("Min<right>X"), "Y", "Z", Untranslated("Max<right>X"), "Y", "Z" }, | |
| column_cnt = 3, | |
| } | |
| -- TODO: handle 2d boxes as well | |
| function GedPropBox:GatherCoords(coords, prop_meta) | |
| local minx_edit = CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx + num, miny, minz, maxx, maxy, maxz) | |
| end, function(self) | |
| return self:GetProp():minx() | |
| end) | |
| minx_edit.txt:SetMinWidth(43) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx, miny + num, minz, maxx, maxy, maxz) | |
| end, function(self) | |
| return self:GetProp():miny() | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx, miny, minz + num, maxx, maxy, maxz) | |
| end, function(self) | |
| return self:GetProp():minz() | |
| end) | |
| local maxx_edit = CreateRectPropNumberEditor(self, coords, prop_meta, 0, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx, miny, minz, maxx + num, maxy, maxz) | |
| end, function(self) | |
| return self:GetProp():maxx() | |
| end) | |
| maxx_edit.txt:SetMinWidth(43) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx, miny, minz, maxx, maxy + num, maxz) | |
| end, function(self) | |
| return self:GetProp():maxy() | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local minx, miny, minz, maxx, maxy, maxz = self:GetProp():xyzxyz() | |
| return box(minx, miny, minz, maxx, maxy, maxz + num) | |
| end, function(self) | |
| return self:GetProp():maxz() | |
| end) | |
| for i = 4, 6 do | |
| local coord = coords[i].parent | |
| coord:SetGridX(i - 3) | |
| coord:SetGridY(2) | |
| end | |
| end | |
| function GedPropBox:ConvertToText(value, prop_meta) | |
| if not value then | |
| return "" | |
| end | |
| local sx, sy, sz = self:GetDisplayScale(prop_meta) | |
| local boxMin = FormatNumberProp(value:minx(), sx) .. ", " .. FormatNumberProp(value:miny(), sx) | |
| local boxMax = FormatNumberProp(value:maxx(), sy) .. ", " .. FormatNumberProp(value:maxy(), sy) | |
| if value:IsValidZ() then | |
| boxMin = boxMin .. ", " .. FormatNumberProp(value:minz(), sz) | |
| boxMax = boxMax .. ", " .. FormatNumberProp(value:maxz(), sz) | |
| end | |
| return boxMin .. ", " .. boxMax | |
| end | |
| function GedPropBox:ConvertFromText(value) | |
| if value == "" then | |
| return | |
| end | |
| local changed1, changed2 | |
| local minx, miny, minz, maxx, maxy, maxz = value:match("^([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)") | |
| minx, miny, minz, maxx, maxy, maxz = tonumber(minx), tonumber(miny), tonumber(minz), tonumber(maxx), tonumber(maxy), tonumber(maxz) | |
| if minx and miny and minz and maxx and maxy and maxz then | |
| minx, miny, minz, changed1 = self:ApplyScale(minx, miny, minz) | |
| maxx, maxy, maxz, changed2 = self:ApplyScale(maxx, maxy, maxz) | |
| return box(minx, miny, minz, maxx, maxy, maxz), nil, changed1 or changed2 | |
| end | |
| minx, miny, maxx, maxy = value:match("^([^,]+),([^,]+),([^,]+),([^,]+)") | |
| minx, miny, maxx, maxy = tonumber(minx), tonumber(miny), tonumber(maxx), tonumber(maxy) | |
| if minx and miny and maxx and maxy then | |
| minx, miny, minz, changed1 = self:ApplyScale(minx, miny) | |
| maxx, maxy, maxz, changed2 = self:ApplyScale(maxx, maxy) | |
| return box(minx, miny, maxx, maxy), nil, changed1 or changed2 | |
| end | |
| end | |
| ----- GedPropRect | |
| GedPropEditors["rect"] = "GedPropRect" | |
| DefineClass.GedPropRect = { | |
| __parents = { "GedCoordEditor" }, | |
| labels = { "X1", "Y1", "X2", "Y2" }, | |
| icons = false, | |
| rollover_texts = false, | |
| } | |
| function GedPropRect:GatherCoords(coords, prop_meta) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local l, u, r, b = self:GetProp():xyxy() | |
| return box(l + num, u, r, b) | |
| end, function(self) | |
| return self:GetProp():minx() | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local l, u, r, b = self:GetProp():xyxy() | |
| return box(l, u + num, r, b) | |
| end, function(self) | |
| return self:GetProp():miny() | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local l, u, r, b = self:GetProp():xyxy() | |
| return box(l, u, r + num, b) | |
| end, function(self) | |
| return self:GetProp():maxx() | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local l, u, r, b = self:GetProp():xyxy() | |
| return box(l, u, r, b + num) | |
| end, function(self) | |
| return self:GetProp():maxy() | |
| end) | |
| end | |
| function GedPropRect:ConvertToText(value) | |
| if not value then return "" end | |
| local boxMin = value:minx() .. ", " .. value:miny() | |
| local boxMax = value:maxx() .. ", " .. value:maxy() | |
| return boxMin .. ", " .. boxMax | |
| end | |
| function GedPropRect:ConvertFromText(value) | |
| local minx, miny, maxx, maxy = value:match("^([^,]+),([^,]+),([^,]+),([^,]+)") | |
| minx, miny, maxx, maxy = tonumber(minx), tonumber(miny), tonumber(maxx), tonumber(maxy) | |
| if minx and miny and maxx and maxy then | |
| return box(minx, miny, maxx, maxy) | |
| end | |
| end | |
| GedPropEditors["margins"] = "GedPropMargins" | |
| DefineClass.GedPropMargins = { | |
| __parents = { "GedPropRect" }, | |
| labels = false, | |
| icons = { | |
| "CommonAssets/UI/Controls/MarginPadding/Margin_Left", | |
| "CommonAssets/UI/Controls/MarginPadding/Margin_Top", | |
| "CommonAssets/UI/Controls/MarginPadding/Margin_Right", | |
| "CommonAssets/UI/Controls/MarginPadding/Margin_Bottom", | |
| }, | |
| rollover_texts = { | |
| "Margin Left", | |
| "Margin Top", | |
| "Margin Right", | |
| "Margin Bottom", | |
| } | |
| } | |
| GedPropEditors["padding"] = "GedPropPadding" | |
| DefineClass.GedPropPadding = { | |
| __parents = { "GedPropRect" }, | |
| labels = false, | |
| icons = { | |
| "CommonAssets/UI/Controls/MarginPadding/Padding_Left", | |
| "CommonAssets/UI/Controls/MarginPadding/Padding_Top", | |
| "CommonAssets/UI/Controls/MarginPadding/Padding_Right", | |
| "CommonAssets/UI/Controls/MarginPadding/Padding_Bottom", | |
| }, | |
| rollover_texts = { | |
| "Padding Left", | |
| "Padding Top", | |
| "Padding Right", | |
| "Padding Bottom", | |
| } | |
| } | |
| ----- GedPropBool | |
| GedPropEditors["bool"] = "GedPropBool" | |
| DefineClass.GedPropBool = { | |
| __parents = { "GedPropEditor" }, | |
| HAlign = "left", | |
| } | |
| function GedPropBool:Init(parent, context, prop_meta) | |
| XCheckButton:new({ | |
| Id = "idCheck", | |
| Icon = "CommonAssets/UI/check-threestate-40.tga", | |
| IconRows = 3, | |
| OnChange = function(control, check) | |
| self:SetProp(check) | |
| end, | |
| OnPress = function(self) | |
| self:SetFocus() | |
| local row = self.IconRow + 1 | |
| if row > 2 then | |
| row = 1 | |
| end | |
| self:SetIconRow(row) | |
| self:OnRowChange(row) | |
| end, | |
| OnRowChange = function(self, row) | |
| if row ~= 3 then | |
| XCheckButton.OnRowChange(self, row) | |
| end | |
| end, | |
| Background = RGB(240, 240, 240), | |
| BorderWidth = 1, | |
| BorderColor = RGBA(0, 0, 0, 0), | |
| DisabledBorderColor = RGBA(0, 0, 0, 0) | |
| }, self) | |
| self.idCheck:SetEnabled(not prop_meta.read_only) | |
| end | |
| function GedPropBool:ReassignFocusOrders(x, y) | |
| self.idCheck:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropBool:UpdateValue() | |
| local prop = self:GetProp() | |
| local icon_row | |
| if prop == true then | |
| icon_row = 2 | |
| elseif not prop then | |
| icon_row = 1 | |
| else | |
| icon_row = 3 -- Undefined() | |
| end | |
| self.idCheck:SetIconRow(icon_row) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| ----- GedPropCombo | |
| GedPropEditors["combo"] = "GedPropCombo" | |
| GedPropEditors["choice"] = "GedPropCombo" | |
| GedPropEditors["dropdownlist"] = "GedPropCombo" -- compatibility | |
| DefineClass.GedPropCombo = { | |
| __parents = { "GedPropEditor" }, | |
| last_value = false, | |
| } | |
| function GedPropCombo:Init(parent, context, prop_meta) | |
| XCombo:new({ | |
| Id = "idCombo", | |
| Items = false, | |
| RefreshItemsOnOpen = true, | |
| DefaultValue = prop_meta.default or "", | |
| ArbitraryValue = prop_meta.editor == "combo", | |
| OnValueChanged = function(combo, value) | |
| self:ComboValueChanged(value) | |
| end, | |
| OnRequestItems = function(combo) | |
| return self.panel.connection:Call("rfnGetPropItems", self.panel.context, self.prop_meta.id) | |
| end, | |
| OnMouseWheelForward = function(combo) | |
| if terminal.IsKeyPressed(const.vkControl) then | |
| self:IncrementValue(true) | |
| return "break" | |
| end | |
| end, | |
| OnMouseWheelBack = function(combo) | |
| if terminal.IsKeyPressed(const.vkControl) then | |
| self:IncrementValue(false) | |
| return "break" | |
| end | |
| end, | |
| MRUStorageId = prop_meta.mru_storage_id, | |
| MRUCount = prop_meta.show_recent_items, | |
| VirtualItems = true, | |
| }, self) | |
| if prop_meta.items_allow_tags then | |
| self.idCombo:SetListItemTemplate(GetDarkModeSetting() and "XComboXTextListItemDark" or "XComboXTextListItemLight") | |
| end | |
| self.idCombo:SetEnabled(not prop_meta.read_only) | |
| end | |
| function GedPropCombo:IncrementValue(up) | |
| if self:GetThread("IncrementValue") then return end | |
| self:CreateThread("IncrementValue", function(self, up) | |
| local items = self.idCombo:OnRequestItems() -- might sleep | |
| local current_item = self.idCombo.value | |
| local idx = table.find(items, current_item) or 1 | |
| local add = up and -1 or 1 | |
| local next_idx = 1 + ((idx-1) + add) % #items | |
| local next_item = items[next_idx] | |
| self:ComboValueChanged(next_item) | |
| end, self, up) | |
| end | |
| function GedPropCombo:ReassignFocusOrders(x, y) | |
| self.idCombo:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropCombo:UpdateValue() | |
| local combo = self.idCombo | |
| combo.Items = false -- list of items might have changed, must be refetched | |
| if not (combo:IsFocused() and combo.ArbitraryValue and combo:GetValue() ~= combo:GetText()) then | |
| self.last_value = self:GetProp() | |
| local combo_value = self.last_value | |
| self.idCombo:SetValue(combo_value, true) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropCombo:ComboValueChanged(value) | |
| if type(value) == "string" and self.prop_meta.trim_spaces ~= false and string.trim_spaces(value) ~= value then | |
| local items = self.idCombo.Items | |
| local is_new = not table.find(items, value) | |
| if is_new then | |
| value = string.trim_spaces(value) | |
| self.idCombo:SetValue(value) | |
| end | |
| end | |
| if self.last_value ~= value then | |
| self.last_value = value | |
| self:SetProp(value) | |
| end | |
| end | |
| function GedPropCombo:ConvertToText(value) | |
| return type(value) == "string" and value or "" | |
| end | |
| function GedPropCombo:ConvertFromText(value) | |
| return value | |
| end | |
| function GedPropCombo:DetachForReuse() | |
| self.idCombo:UpdateMRUList() | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| ----- GedPropExpr | |
| GedPropEditors["expression"] = "GedPropExpr" | |
| DefineClass.GedPropExpr = { | |
| __parents = { "GedPropFunc" }, | |
| max_lines = 10, | |
| } | |
| function GedPropExpr:Init(parent, context, prop_meta) | |
| if self.RolloverText == "" then | |
| self.RolloverText = string.format("function %s(%s)", prop_meta.id, prop_meta.params or "self") | |
| end | |
| end | |
| function GedPropExpr:Compile(code) | |
| local f, err = CompileExpression(self.prop_meta.id, self.prop_meta.params or "self", code) | |
| return err | |
| end | |
| ----- GedPropFunc | |
| GedPropEditors["func"] = "GedPropFunc" | |
| DefineClass.GedPropFunc = { | |
| __parents = { "GedPropText" }, | |
| lines = 1, | |
| max_lines = 20, | |
| } | |
| local code_edit_plugin = { "XCodeEditorPlugin" } | |
| function GedPropFunc:UpdatePropertyNames(internal) | |
| local prop_meta = self.prop_meta | |
| local prop_name = internal and prop_meta.id or (prop_meta.name or prop_meta.id) | |
| prop_name = string.format("function %s(%s)", prop_name, prop_meta.params or "self") | |
| GedPropText.UpdatePropertyNames(self, internal, prop_name) | |
| end | |
| function GedPropFunc:TextEqualTo(value) | |
| return self.idEdit:GetText():trim_spaces() == (value or ""):trim_spaces() | |
| end | |
| function GedPropFunc:Init(parent, context, prop_meta) | |
| local prop_name = prop_meta.name or prop_meta.id | |
| self.idLabel:SetTranslate(false) | |
| self.idLabel:SetText(string.format("function %s(%s)", prop_meta.name or prop_meta.id, prop_meta.params or "self")) | |
| self.idEdit.OnTextChanged = function(edit) | |
| -- clear error & set up a delayed update | |
| self:UpdateCodeEditorUI() | |
| self:DeleteThread("set_value") | |
| self:CreateThread("set_value", function() | |
| Sleep(1500) | |
| self:UpdateCompilationError(self.idEdit:GetText()) | |
| end) | |
| end | |
| self.idEdit:SetPlugins(code_edit_plugin) | |
| self.idEdit:SetWordWrap(false) -- word wrap causes errors to be displayed on the wrong line | |
| self.idEdit:SetFoldWhenHidden(true) | |
| self.idEdit:SetAutoSelectAll((prop_meta.read_only or prop_meta.auto_select_all) and not prop_meta.no_auto_select) | |
| local collapse_button = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idLabelHost) | |
| collapse_button:SetId("idCollapseButton") | |
| collapse_button:SetIcon("CommonAssets/UI/Ged/collapse.tga") | |
| collapse_button:SetRolloverText("Expand / collapse") | |
| collapse_button.OnPress = function(button) | |
| self.idEdit:SetVisible(not self.idEdit:GetVisible()) | |
| button:SetToggled(not button:GetToggled()) | |
| end | |
| local live_panel = XWindow:new({ | |
| Id = "idLivePanel", | |
| Dock = "bottom", | |
| Padding = box(2, 2, 2, 2), | |
| FoldWhenHidden = true, | |
| }, self) | |
| XText:new({ | |
| Dock = "left", | |
| TextStyle = "GedHighlight", | |
| Padding = box(2, 2, 2, 2), | |
| }, live_panel):SetText("Changes not sent to the game yet") | |
| XTextButton:new({ | |
| Dock = "right", | |
| Padding = box(2, 2, 2, 2), | |
| Padding = box(2, 0, 2, 0), | |
| BorderWidth = 1, | |
| VAlign = "center", | |
| LayoutMethod = "VList", | |
| OnPress = function() self:SendValueToGame() end, | |
| }, live_panel):SetText("Send now (Ctrl-Enter)") | |
| live_panel:SetVisible(false) | |
| end | |
| function GedPropFunc:SendValueToGame() | |
| self:SetValueFromText("no_text_updates") | |
| if self.window_state == "destroying" then return end | |
| self.idLivePanel:SetVisible(false) | |
| end | |
| function GedPropFunc:Compile(code) | |
| local f, err = CompileFunc(self.prop_meta.id, self.prop_meta.params or "self", code) | |
| return err | |
| end | |
| function GedPropFunc:UpdateCodeEditorUI(line, msg) | |
| local edit = self.idEdit | |
| local plugin = edit:FindPluginOfKind("XCodeEditorPlugin") | |
| plugin:SetDimText(self:TextEqualTo(self.prop_meta.default)) | |
| plugin:SetError(line, msg) | |
| edit:InvalidateMeasure() | |
| edit:Invalidate() | |
| local value = self:GetProp() | |
| self.idLivePanel:SetVisible(type(value) ~= "string" or not self:TextEqualTo(value)) | |
| end | |
| function GedPropFunc:UpdateCompilationError(code) | |
| if self.window_state == "destroying" then return end | |
| local err = self:Compile(code) | |
| local src, line, msg | |
| if err then | |
| src, line, msg = err:match("^([^:]+):([^:]+):(.+)$") | |
| line = tonumber(line) | |
| local edit = self.idEdit | |
| if not edit:IsFocused() then | |
| edit:SetCursor(line, 0) -- scroll the error into the view | |
| end | |
| end | |
| self:UpdateCodeEditorUI(line, msg) | |
| end | |
| function GedPropFunc:ConvertFromText(code, prop_meta) | |
| if prop_meta then | |
| code = ValueToLuaCode(code) | |
| end | |
| code = code:match("^(.-)%s*$") or code -- remove trailing whitespace | |
| if not prop_meta then | |
| self:UpdateCompilationError(code) | |
| end | |
| return code | |
| end | |
| GedPropFunc.ConvertToText = GedPropFunc.ConvertFromText | |
| function GedPropFunc:DetachForReuse() | |
| self.idEdit:SetPlugins(code_edit_plugin) | |
| GedPropText.DetachForReuse(self) | |
| end | |
| function GedPropFunc:UpdateValue(initial) | |
| -- don't set the text and reset the cursor due to whitespace changes | |
| -- (does happen when the game sends the source code back after an edit) | |
| local value = self:GetProp() | |
| if initial or type(value) ~= "string" or not self:TextEqualTo(value) then | |
| GedPropText.UpdateValue(self) | |
| self.idEdit:FindPluginOfKind("XCodeEditorPlugin"):SetDimText(self:TextEqualTo(self.prop_meta.default)) | |
| end | |
| if value == "" or value == false or value == Undefined() or (type(value) == "string" and not value:find("\n", 1, true)) then | |
| self.idEdit:SetVisible(true) | |
| self.idCollapseButton:SetVisible(false) | |
| self.idCollapseButton:SetToggled(false) | |
| else | |
| self.idCollapseButton:SetVisible(true) | |
| end | |
| end | |
| ----- ColorEditor helper class | |
| DefineClass.ColorEditor = { | |
| __parents = { "XWindow" }, | |
| properties = { | |
| { id = "CopyPaste", editor = "bool", default = true, } , | |
| { id = "Value", editor = "color", default = RGB(0,0,0) }, | |
| { id = "ReadOnly", editor = "bool", default = false, }, | |
| { id = "Alpha", editor = "bool", default = true, } | |
| }, | |
| IdNode = true, | |
| picker = false, | |
| editor_id = false, | |
| } | |
| function ColorEditor:OnColorChanged(color) | |
| end | |
| function ColorEditor:OnEditingDone() | |
| end | |
| function ColorEditor:SetValue(color, dont_notify) | |
| if self.window_state == "destroying" then return end | |
| color = color or 0 | |
| if color == Undefined() then | |
| color = 0 | |
| self.idColorText:SetText("<color 128 128 128>Undefined") | |
| else | |
| self.idColorText:SetText("") | |
| end | |
| if not self.Alpha then | |
| color = RGB(GetRGB(color)) | |
| end | |
| self.idColor:SetBackground(color) | |
| if self.Value ~= color then | |
| self.Value = color | |
| --if self.picker then -- Do not update the picker, as RGBs are 1/ With lower precision, 2/ A few ms older than the current value | |
| -- self.picker:SetColor(self.Value) | |
| --end | |
| if not dont_notify then | |
| self:OnColorChanged(color) | |
| end | |
| end | |
| end | |
| function ColorEditor:Init(parent, context, prop_meta) | |
| if self.CopyPaste then | |
| local paste_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| paste_button:SetIcon("CommonAssets/UI/Ged/paste.tga") | |
| paste_button:SetDock("right") | |
| paste_button:SetRolloverText("Paste") | |
| paste_button.OnPress = function(button) | |
| button:SetFocus() | |
| local value = self:ConvertFromText(GetFromClipboard(1024)) | |
| if value then | |
| self:SetValue(value) | |
| self:OnColorChanged(value) | |
| end | |
| button:SetFocus(false) | |
| end | |
| paste_button.IconScale = point(500, 500) | |
| local copy_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| copy_button:SetIcon("CommonAssets/UI/Ged/copy.tga") | |
| copy_button:SetDock("right") | |
| copy_button:SetRolloverText("Copy") | |
| copy_button.OnPress = function(b) | |
| local color = self.Value | |
| CopyToClipboard(self:ConvertToText(color)) | |
| end | |
| copy_button.IconScale = point(500, 500) | |
| end | |
| XWindow:new({ | |
| Id = "idContainer", | |
| Padding = box(1, 1, 1, 1), | |
| BorderWidth = 1, | |
| }, self) | |
| XFrame:new({ | |
| Id = "idColorBox", | |
| BorderColor = RGB(0, 0, 0), | |
| Image = "CommonAssets/UI/checker-pattern-40.tga", | |
| IdNode = false, | |
| TileFrame = true, | |
| ImageScale = point(450, 450) | |
| }, self.idContainer) | |
| XWindow:new({ | |
| Id = "idColor", | |
| IdNode = false, | |
| HandleMouse = true, | |
| OnMouseButtonUp = function(image, pt, button) | |
| if button == "L" then | |
| self:ToggleCombo() | |
| end | |
| end, | |
| OnMouseButtonDown = function(image, pt, button) | |
| if button == "L" then | |
| return "break" | |
| end | |
| end, | |
| }, self.idColorBox) | |
| XText:new({ Id = "idColorText", HandleMouse = false }, self.idColorBox) | |
| local combo_button = XComboButton:new({ | |
| OnPress = function(button) | |
| button:SetFocus() | |
| self:ToggleCombo() | |
| button:SetFocus(false) | |
| end, | |
| }, self.idContainer) | |
| if self.ReadOnly then | |
| combo_button:SetEnabled(not self.ReadOnly) | |
| end | |
| assert(self.editor_id) | |
| local active_popup = rawget(rawget(_G, "g_GedApp") or self.desktop, "idColorPicker") | |
| if active_popup and active_popup.owner_color_editor.editor_id == self.editor_id then | |
| active_popup.owner_color_editor = self | |
| end | |
| end | |
| function ColorEditor:ToggleCombo() | |
| if self:CloseColorPicker() then return end | |
| if self.ReadOnly then return end | |
| local popup = XPopup:new({ | |
| Id = "idColorPicker", | |
| owner_color_editor = self, | |
| OnMouseButtonUp = function(self, pt, button) | |
| if button == "L" then | |
| if not self:MouseInWindow(pt) then | |
| self:Close() | |
| end | |
| return "break" | |
| elseif button == "R" then | |
| self:Close() | |
| return "break" | |
| end | |
| end, | |
| DrawOnTop = true, | |
| }, rawget(_G, "g_GedApp") or self.desktop) | |
| self.picker = XColorPicker:new({ | |
| OnColorChanged = function(picker, color) | |
| popup.owner_color_editor:SetValue(color) | |
| end, | |
| AdditionalComponent = self.Alpha and "alpha" or "none", | |
| RolloverMode = function() | |
| local prop_editor = popup.owner_color_editor.parent | |
| prop_editor.panel.app:Send("GedRpcColorPickerRollover", prop_editor.panel.context, prop_editor.prop_meta.id) | |
| popup.owner_color_editor:CloseColorPicker() | |
| end, | |
| }, popup) | |
| popup.Close = function(popup) | |
| self:SetValue(self.picker:GetColor()) | |
| self:OnEditingDone() | |
| self.picker = false | |
| XPopup.Close(popup) | |
| end | |
| popup.OnShortcut = function(self, shortcut, ...) | |
| if shortcut == "Escape" then | |
| self:Close() | |
| return "break" | |
| end | |
| return XPopup.OnShortcut(self, shortcut, ...) | |
| end | |
| self.picker:SetColor(self.Value) | |
| popup:SetAnchor(self.idContainer.box) | |
| popup:SetAnchorType("drop") | |
| popup:Open() | |
| popup:SetModal() | |
| popup:SetFocus() | |
| local prop_editor = popup.owner_color_editor.parent | |
| prop_editor.panel.app:UpdateChildrenDarkMode(popup) | |
| end | |
| function ColorEditor:CloseColorPicker() | |
| local popup = rawget(rawget(_G, "g_GedApp") or self.desktop, "idColorPicker") | |
| if popup then | |
| popup:Close() | |
| return true | |
| end | |
| end | |
| function ColorEditor:ConvertToText(value) | |
| local r, g, b, a = GetRGBA(value) | |
| return r .. ", " .. g .. ", " .. b .. ", " .. a | |
| end | |
| function ColorEditor:ConvertFromText(value) | |
| return ConvertColorFromText(value) | |
| end | |
| ----- GedPropColor | |
| GedPropEditors["color"] = "GedPropColor" | |
| DefineClass.GedPropColor = { | |
| __parents = { "GedPropEditor" }, | |
| accept_os_clipboard_paste = true, | |
| } | |
| function GedPropColor:Init(parent, context, prop_meta) | |
| local last_update = now() | |
| ColorEditor:new({ | |
| Id = "idColorEditor", | |
| editor_id = prop_meta.id .. self.panel.context, | |
| OnColorChanged = function(editor, color) | |
| if now() - last_update >= 100 then | |
| self:SetProp(color) | |
| last_update = now() | |
| end | |
| self:DeleteThread("update_thread") | |
| self:CreateThread("update_thread", function() | |
| Sleep(100) | |
| self:SetProp(color) | |
| end) | |
| end, | |
| OnEditingDone = function(editor, color) | |
| self:DeleteThread("update_thread") | |
| end, | |
| Alpha = prop_meta.alpha, | |
| ReadOnly = prop_meta.read_only and true, | |
| }, self) | |
| end | |
| function GedPropColor:ReassignFocusOrders(x, y) | |
| return y + 1 | |
| end | |
| function GedPropColor:UpdateValue() | |
| self.idColorEditor:SetValue(self:GetProp(), "dont_notify") | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropColor:ConvertToText(value) | |
| return ColorEditor:ConvertToText(value) | |
| end | |
| function GedPropColor:ConvertFromText(text) | |
| return ColorEditor:ConvertFromText(text) | |
| end | |
| ----- GedPropEditorWithSubeditors -- helper to create composite editors | |
| DefineClass.GedPropEditorWithSubeditors = { | |
| __parents = { "GedPropEditor" }, | |
| editors = false, | |
| subeditor_container = false, | |
| } | |
| function GedPropEditorWithSubeditors:Init(parent, context, prop_meta) | |
| self.subeditor_container = XWindow:new({ | |
| LayoutMethod = "VList", | |
| Margins = box(0, 2, 0, 0), | |
| BorderWidth = 1, | |
| Padding = box(2, 2, 2, 2), | |
| }, self) | |
| end | |
| function GedPropEditorWithSubeditors:MakeSubEditor(parent, class, meta) | |
| parent = parent or self.subeditor_container | |
| assert(IsKindOf(class, "GedPropEditor")) | |
| local win = class:new({ | |
| SetProp = function (win, value) | |
| if not value then value = meta.default end | |
| win.subeditor_value = value | |
| self:TrySetProp() | |
| end, | |
| GetProp = function(win, new_focus) | |
| return win.subeditor_value | |
| end, | |
| panel = self.panel, | |
| subeditor_value = meta.default, | |
| }, parent, nil, meta) | |
| self.editors = self.editors or {} | |
| table.insert(self.editors, win) | |
| return win | |
| end | |
| function GedPropEditorWithSubeditors:UpdateValue() | |
| for _, editor in ipairs(self.editors) do | |
| editor:UpdateValue() | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropEditorWithSubeditors:UpdatePropertyNames(internal) | |
| for _, editor in ipairs(self.editors) do | |
| editor:UpdatePropertyNames(internal) | |
| end | |
| GedPropEditor.UpdatePropertyNames(self, internal) | |
| end | |
| ----- GedPropMaterial | |
| GedPropEditors["rgbrm"] = "GedPropMaterial" | |
| DefineClass.GedPropMaterial = { | |
| __parents = { "GedPropEditorWithSubeditors" }, | |
| color_editor = false, | |
| roughness_editor = false, | |
| metallic_editor = false, | |
| } | |
| function GedPropMaterial:Init(parent, context, prop_meta) | |
| self.idLabelHost:SetDock("top") | |
| local default = prop_meta.default | |
| local r, g, b, ro, m = GetRGBRM(default) | |
| self.color_editor = self:MakeSubEditor(false, GedPropColor, { | |
| editor = "color", | |
| alpha = false, | |
| id = "Color_" .. prop_meta.id, | |
| name = "Color", | |
| default = RGB(r, g, b), | |
| read_only = prop_meta.read_only, | |
| }) | |
| self.roughness_editor = self:MakeSubEditor(false, GedPropNumber, { | |
| max = 127, | |
| min = -128, | |
| slider = true, | |
| default = ro, | |
| id = "Roughness", | |
| editor = "number", | |
| read_only = prop_meta.read_only, | |
| }) | |
| self.metallic_editor = self:MakeSubEditor(false, GedPropNumber, { | |
| max = 127, | |
| min = -128, | |
| slider = true, | |
| default = m, | |
| id = "Metallic", | |
| editor = "number", | |
| read_only = prop_meta.read_only, | |
| }) | |
| end | |
| function GedPropMaterial:TrySetProp() | |
| local rgb, roughness, metallic = self.color_editor.subeditor_value, self.roughness_editor.subeditor_value, self.metallic_editor.subeditor_value | |
| if rgb == Undefined() then | |
| rgb = self.color_editor.prop_meta.default | |
| end | |
| if roughness == Undefined() then | |
| roughness = self.roughness_editor.prop_meta.default | |
| end | |
| if metallic == Undefined() then | |
| metallic = self.metallic_editor.prop_meta.default | |
| end | |
| local value = RGBRM(rgb, roughness, metallic) | |
| if value ~= self:GetProp() then | |
| self:SetProp(value) | |
| end | |
| end | |
| function GedPropMaterial:ReassignFocusOrders(x, y) | |
| return y + 3 | |
| end | |
| function GedPropMaterial:UpdateValue() | |
| local value = self:GetProp() | |
| local rgb, roughness, metallic | |
| if value == Undefined() then | |
| rgb = value | |
| roughness = value | |
| metallic = value | |
| else | |
| rgb, roughness, metallic = GetRGBRM3(value) | |
| end | |
| self.color_editor.subeditor_value = rgb | |
| self.roughness_editor.subeditor_value = roughness | |
| self.metallic_editor.subeditor_value = metallic | |
| -- update controls | |
| GedPropEditorWithSubeditors.UpdateValue(self) | |
| end | |
| ----- GedPropRange | |
| GedPropEditors["range"] = "GedPropRange" | |
| DefineClass.GedPropRange = { | |
| __parents = { "GedCoordEditor" }, | |
| labels = { "From", "To" }, | |
| rollovers = false, | |
| } | |
| function GedPropRange:Init(parent, context, prop_meta) | |
| if prop_meta.slider then | |
| self.hide_arrows = true | |
| self.hide_icons = true | |
| self.idEdit:SetLayoutMethod("Box") | |
| self.coords[1].parent:SetDock("left") | |
| self.coords[1]:SetMinWidth(50) | |
| self.coords[2].parent:SetDock("right") | |
| self.coords[2]:SetMinWidth(50) | |
| local function create_custom_rollover(parent, context, GetCustomAnchor) | |
| context = context or {} | |
| context.control = parent | |
| context.anchor = parent.box | |
| context.RolloverAnchor = "custom" | |
| local rollover = XTemplateSpawn("GedPropRollover", nil, context) | |
| rollover.GetCustomAnchor = GetCustomAnchor | |
| local rollover_text = rollover[1] | |
| if rollover_text then | |
| rollover_text:SetMargins(box(2, 0, 2, 0)) | |
| end | |
| rollover:SetMargins(box(0, 0, 0, 1)) | |
| rollover:Open() | |
| self.panel.app:UpdateChildrenDarkMode(rollover) | |
| self.rollovers = self.rollovers or {} | |
| self.rollovers[#self.rollovers + 1] = rollover | |
| end | |
| local function create_rollovers(scroll) | |
| if self.rollovers then return end | |
| local max_range = scroll.Max - scroll.Min | |
| local range_min_from = scroll.Scroll.from - scroll.Min | |
| local range_min_from_pct = range_min_from * 100 / max_range | |
| if range_min_from_pct > 15 then | |
| create_custom_rollover(scroll, { min = true }, function(rollover, x, y, width, height, anchor) | |
| local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(rollover.scale, rollover.Margins:xyxy()) | |
| x = anchor:minx() - width/2 - margins_x2 | |
| y = anchor:miny() - height - margins_y1 - margins_y2 | |
| return x, y, width, height | |
| end) | |
| end | |
| local range_to_max = scroll.Max - scroll.Scroll.to | |
| local range_to_max_pct = range_to_max * 100 / max_range | |
| if range_to_max_pct > 15 then | |
| create_custom_rollover(scroll, nil, function(rollover, x, y, width, height, anchor) | |
| local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(rollover.scale, rollover.Margins:xyxy()) | |
| x = anchor:maxx() - width/2 + margins_x2 | |
| y = anchor:miny() - height - margins_y1 - margins_y2 | |
| return x, y, width, height | |
| end) | |
| end | |
| local range_from_to = scroll.Scroll.to - scroll.Scroll.from | |
| local range_pct = range_from_to * 100 / max_range | |
| create_custom_rollover(scroll.idThumbLeft, nil, function(rollover, x, y, width, height, anchor) | |
| local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(rollover.scale, rollover.Margins:xyxy()) | |
| x = anchor:minx() - width/2 + margins_x2 | |
| if range_pct < 20 then | |
| x = x - width*(20-range_pct)/40 | |
| end | |
| y = anchor:miny() - height - margins_y1 - margins_y2 | |
| return x, y, width, height | |
| end) | |
| create_custom_rollover(scroll.idThumbRight, nil, function(rollover, x, y, width, height, anchor) | |
| local margins_x1, margins_y1, margins_x2, margins_y2 = ScaleXY(rollover.scale, rollover.Margins:xyxy()) | |
| x = anchor:maxx() - width/2 + margins_x2 | |
| if range_pct < 20 then | |
| x = x + width*(20-range_pct)/40 | |
| end | |
| y = anchor:miny() - height - margins_y1 - margins_y2 | |
| return x, y, width, height | |
| end) | |
| end | |
| local function close_rollovers() | |
| for _, rollover in ipairs(self.rollovers) do | |
| rollover:Close() | |
| end | |
| self.rollovers = nil | |
| end | |
| local idScroll = XRangeScroll:new({ | |
| Id = "idScroll", | |
| Dock = "box", | |
| Margins = box(2, 2, 2, 2), | |
| Min = (prop_meta.min or 0), | |
| Max = (prop_meta.max or 100), | |
| Horizontal = true, | |
| Target = "node", | |
| StepSize = prop_meta.step or 1, | |
| Scroll = prop_meta.default or nil, | |
| OnMousePos = function(scroll, ...) | |
| create_rollovers(scroll) | |
| return XRangeScroll.OnMousePos(scroll, ...) | |
| end, | |
| OnMouseLeft = function(scroll, ...) | |
| close_rollovers() | |
| return XRangeScroll.OnMouseLeft(scroll, ...) | |
| end, | |
| StartScroll = function(...) | |
| -- Disable updating parent panels while dragging (self.slider_drag_id is a number) | |
| self.slider_drag_id = AsyncRand() | |
| return XRangeScroll.StartScroll(...) | |
| end, | |
| -- Called after letting go of the slider | |
| OnCaptureLost = function(...) | |
| self:DeleteThread("scroll_update_thread") | |
| -- GED -> Game - Update "prop value" in case this would change how the object is displayed in a parent UI panel | |
| -- Update parent panels (self.slider_drag_id is not a number) | |
| self.slider_drag_id = false | |
| self:TrySetProp(self.idScroll:GetScroll(), not "update_scrollbar") | |
| close_rollovers() | |
| XRangeScroll.OnCaptureLost(...) | |
| end, | |
| MoveThumb = function(scroll) | |
| XRangeScroll.MoveThumb(scroll) | |
| if scroll.mouse_in or scroll.current_pos then | |
| close_rollovers() | |
| create_rollovers(scroll) | |
| end | |
| end, | |
| GetRolloverText = function(scroll, context) | |
| return tostring(context and context.min and scroll.Min or scroll.Max) | |
| end, | |
| }, self.idEdit) | |
| self.idScroll.idThumbLeft.GetRolloverText = function(thumb) | |
| return tostring(self.idScroll.Scroll.from) | |
| end | |
| self.idScroll.idThumbRight.GetRolloverText = function(thumb) | |
| return tostring(self.idScroll.Scroll.to) | |
| end | |
| self.idScroll = idScroll | |
| idScroll:SetEnabled(not prop_meta.read_only) | |
| end | |
| end | |
| function GedPropRange:GatherCoords(coords, prop_meta) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local prop = self:GetProp() | |
| if prop.from + num > prop.to then | |
| num = prop.to - prop.from | |
| end | |
| return range(prop.from + num, prop.to) | |
| end, function(self) | |
| return self:GetProp().from | |
| end) | |
| CreateRectPropNumberEditor(self, coords, prop_meta, nil, function(self, num) | |
| local prop = self:GetProp() | |
| if prop.from > prop.to + num then | |
| num = prop.from - prop.to | |
| end | |
| return range(prop.from, prop.to + num) | |
| end, function(self) | |
| return self:GetProp().to | |
| end) | |
| end | |
| -- Called repeatedly while holding the slider to update the "prop value" | |
| function GedPropRange:OnScrollTo(value) | |
| if not self:IsThreadRunning("scroll_update_thread") then | |
| self:CreateThread("scroll_update_thread", function() | |
| Sleep(75) | |
| self:TrySetProp(self.idScroll:GetScroll(), not "update_scrollbar") | |
| end) | |
| end | |
| end | |
| -- Game -> GED - Update the UI with the latest "prop value" | |
| function GedPropRange:UpdateValue() | |
| -- If the slider is being held => don't update the "slider value" | |
| if self.prop_meta.slider and self.desktop:GetMouseCapture() ~= self.idScroll then | |
| for _, coord in ipairs(self.coords) do | |
| if not coord:IsFocused(true) then | |
| local value = self:GetProp() | |
| self:SetSliderValue(value) | |
| --self:SetEditedValue(value) -- update text in the textbox | |
| break | |
| end | |
| end | |
| end | |
| return GedCoordEditor.UpdateValue(self) | |
| end | |
| function GedPropRange:TrySetProp(prop_value, update_scrollbar) | |
| if self.window_state == "destroying" or self.prop_meta.read_only then return end | |
| if not prop_value or prop_value.from > prop_value.to then return end | |
| self:SetProp(prop_value, not "force", self.slider_drag_id) | |
| if update_scrollbar then | |
| self:SetSliderValue(prop_value) | |
| end | |
| end | |
| function GedPropRange:SetSliderValue(prop_value) | |
| if not IsRange(prop_value) or not self.prop_meta.slider then return end | |
| self.idScroll:SetScroll(prop_value) | |
| end | |
| function GedPropRange:GetDisplayScale(prop_meta) | |
| prop_meta = prop_meta or self.prop_meta | |
| local scale = prop_meta.scale | |
| if type(scale) == "string" then | |
| scale = const.Scale[scale] | |
| end | |
| return scale or 1 | |
| end | |
| function GedPropRange:ConvertToText(value) | |
| if IsRange(value) then | |
| local display_scale = self:GetDisplayScale() | |
| return FormatNumberProp(value.from, display_scale) .. ", " .. FormatNumberProp(value.to, display_scale) | |
| end | |
| return "" | |
| end | |
| function GedPropRange:ConvertFromText(value) | |
| local from, to = value:match("^([^,]+),([^,]+)") | |
| from, to = tonumber(from), tonumber(to) | |
| if from and to and from <= to then | |
| local display_scale = self:GetDisplayScale() | |
| from = floatfloor(from * display_scale + 0.5) | |
| to = floatfloor(to * display_scale + 0.5) | |
| local min = self.prop_meta.min | |
| local max = self.prop_meta.max | |
| local changed | |
| if min and from < min then | |
| from = min | |
| changed = true | |
| end | |
| if max and to > max then | |
| to = max | |
| changed = true | |
| end | |
| return range(from, to), nil, changed | |
| end | |
| end | |
| ----- GedPropSet | |
| GedPropEditors["set"] = "GedPropSet" | |
| DefineClass.GedPropSet = { | |
| __parents = { "GedPropEditor" }, | |
| add_thread = false, | |
| } | |
| function GedPropSet:Init() | |
| local win = XWindow:new({ | |
| Dock = "box", | |
| Id = "idContainer", | |
| LayoutMethod = "HWrap", | |
| VAlign = "center", | |
| }, self) | |
| if self.prop_meta.horizontal then | |
| self.idLabelHost:SetDock("top") | |
| win:SetBorderWidth(1) | |
| end | |
| end | |
| function GedPropSet:GetItems() | |
| local items = self.prop_meta.items or empty_table | |
| if IsSet(items) or #items == 0 then | |
| items = table.keys(items, true) | |
| end | |
| return items | |
| end | |
| function GedPropSet:UpdateValue() | |
| self.idContainer:DeleteChildren() | |
| local item_keys = {} | |
| for _, item in ipairs(self:GetItems() or empty_table) do | |
| self:CreateButton(item) | |
| item_keys[type(item) == "table" and (item.value or item.id) or item] = true | |
| end | |
| for key in pairs(self:GetProp() or empty_table) do | |
| if not item_keys[key] then | |
| self:CreateButton(key, nil, "invalid_item") | |
| end | |
| end | |
| if self.prop_meta.arbitrary_value then | |
| local button = XTextButton:new({ | |
| Text = "Add", | |
| Dock = "right", | |
| FoldWhenHidden = true, | |
| BorderWidth = 1, | |
| OnPress = function(button) | |
| self.add_thread = IsValidThread(self.add_thread) or CreateRealTimeThread(function() | |
| local host = GetActionsHost(self, true) | |
| local prop_name = (self.prop_meta.name or "") ~= "" and self.prop_meta.name or "tag" | |
| local new_item, err = host:WaitUserInput("Enter a new " .. prop_name, "") | |
| if not new_item or new_item == "" then | |
| self.add_thread = nil | |
| return | |
| end | |
| local prop = table.copy(self:GetProp() or set()) | |
| prop[new_item] = true | |
| self:SetProp(prop) | |
| self:UpdateValue() | |
| self.add_thread = nil | |
| end) | |
| XTextButton.OnPress(button) | |
| end, | |
| }, self.idContainer) | |
| end | |
| Msg("XWindowRecreated", self) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| local function GetItemCount(prop) | |
| local count = 0 | |
| for _, value in pairs(prop) do | |
| if value ~= nil then | |
| count = count + 1 | |
| end | |
| end | |
| return count | |
| end | |
| local function RemoveOneItem(prop) | |
| for idx, value in pairs(prop) do | |
| if value ~= nil then | |
| prop[idx] = nil | |
| return | |
| end | |
| end | |
| end | |
| function GedPropSet:CreateButton(item, parent, invalid_item) | |
| if type(item) ~= "table" then | |
| local str = item | |
| if type(item) ~= "string" and not IsT(item) then | |
| str = tostring(item) | |
| end | |
| item = { text = str, value = item } | |
| end | |
| local max_items_in_set = self.prop_meta.max_items_in_set or 0 | |
| local button = XToggleButton:new({ | |
| Text = item.text, | |
| Toggled = self:GetProp() and self:GetProp()[item.value] and true, | |
| ToggledBackground = RGB(204, 232, 255), | |
| RolloverBackground = RGBA(0, 0, 0, 0), | |
| RolloverAnchor = "live-mouse", | |
| RolloverTemplate = "GedPropRollover", | |
| Padding = self.prop_meta.horizontal and box(0, 2, 0, 2) or box(0, 0, 0, 0), | |
| AltPress = self.prop_meta.three_state, | |
| CalcBackground = function(button) | |
| if self.prop_meta.three_state then | |
| local value | |
| local prop = self:GetProp() | |
| if prop then | |
| value = prop[item.value] | |
| end | |
| if value == nil then return RGBA(0, 0, 0, 0) end | |
| local color | |
| if button.state == "mouse-in" or button:IsFocused() then | |
| color = value and RGB(0, 128, 0) or RGB(160, 0, 0) | |
| else | |
| color = value and RGB(0, 96, 0) or RGB(128, 0, 0) | |
| end | |
| return GetDarkModeSetting() and color or color + RGB(80, 80, 80) | |
| end | |
| return XToggleButton.CalcBackground(button) | |
| end, | |
| OnPress = function(button) | |
| if self.prop_meta.three_state then | |
| button:DoToggle(nil, true, false) | |
| return | |
| end | |
| XToggleButton.OnPress(button) | |
| end, | |
| OnAltPress = function(button) | |
| if self.prop_meta.three_state then | |
| button:DoToggle(nil, false) | |
| return | |
| end | |
| XToggleButton.OnAltPress(button) | |
| end, | |
| OnChange = function(button, toggle) | |
| button:DoToggle(nil, true) | |
| end, | |
| DoToggle = function(button, val1, val2, val3) | |
| button:SetFocus() | |
| local prop = table.copy(self:GetProp() or set()) | |
| local value = prop[item.value] | |
| if value == val1 then | |
| value = val2 | |
| elseif value == val2 then | |
| value = val3 | |
| elseif val3 == nil or value == val3 then | |
| value = val1 | |
| end | |
| if max_items_in_set ~= 0 and max_items_in_set == GetItemCount(prop) then | |
| RemoveOneItem(prop) | |
| end | |
| prop[item.value] = value | |
| self:SetProp(prop) | |
| button:Invalidate() | |
| button:SetFocus(false) | |
| end, | |
| }, parent or self.idContainer) | |
| if invalid_item then | |
| XWindow:new({ | |
| Background = RGB(255, 0, 0), | |
| Dock = "bottom", | |
| MinHeight = 2, | |
| MaxHeight = 2, | |
| }, button) | |
| end | |
| button:SetEnabled(not self.prop_meta.read_only) | |
| if item.help then | |
| button:SetRolloverText(item.help) | |
| end | |
| return button | |
| end | |
| function GedPropSet:FindText(search_text, highlight_text) | |
| local found = false | |
| local buttons = get_children_of_classes(self, nil, "XToggleButton") | |
| for _, button in ipairs(buttons) do | |
| local label = button.idLabel | |
| label:HighlightText(highlight_text, XHighlightTextPlugin.HighlightColor, true) | |
| found = found or search_display_text(label.text, search_text) | |
| end | |
| return found or GedPropEditor.FindText(self, search_text, highlight_text) | |
| end | |
| ----- GedPropImage | |
| GedPropEditors["image"] = "GedPropImage" | |
| DefineClass.GedPropImage = { | |
| __parents = { "GedPropEditor" }, | |
| comp_modifier = false, | |
| } | |
| function GedPropImage:Init() | |
| XImage:new({ | |
| Id = "idImage", | |
| ImageFit = (self.prop_meta.img_width or self.prop_meta.img_height) and "largest" or "smallest", | |
| HAlign = "center", | |
| DrawContent = function(img, clip_rect) self:DrawImageContent(clip_rect) end, | |
| }, self) | |
| XLabel:new({ | |
| Id = "idErrLabel", | |
| Dock = "ignore", | |
| }, self) | |
| end | |
| function GedPropImage:UpdateValue() | |
| local prop = self:GetProp() | |
| local file_exists = prop ~= Undefined() and prop and io.exists(prop) | |
| if not file_exists and type(prop) == "string" then | |
| prop = string.gsub(prop, "\\", "/") | |
| local width, height = UIL.MeasureImage(prop) | |
| if width and height and width > 0 and height > 0 then | |
| file_exists = true | |
| end | |
| end | |
| self.idImage:SetDock(not file_exists and "ignore") | |
| self.idImage:SetVisible(file_exists) | |
| self.idErrLabel:SetDock(file_exists and "ignore" or "right") | |
| self.idErrLabel:SetVisible(not file_exists) | |
| if file_exists then | |
| self.idImage:SetImage(prop) | |
| local meta = self.prop_meta | |
| if meta.img_size and type(meta.img_size) == "number" and meta.img_size > 0 or meta.img_width or meta.img_height then | |
| self.idImage.MaxWidth = meta.img_width or meta.img_size | |
| self.idImage.MaxHeight = meta.img_height or meta.img_size | |
| end | |
| if meta.img_box and meta.img_box > 0 then | |
| self.idImage.BorderWidth = meta.img_box | |
| end | |
| if meta.img_back then | |
| self.idImage.Background = meta.img_back | |
| end | |
| else | |
| self.idErrLabel:SetText(prop ~= Undefined() and "File not found " .. prop or "Undefined") | |
| end | |
| if self.comp_modifier then | |
| self.idImage:RemoveModifier(self.comp_modifier) | |
| self.comp_modifier = false | |
| end | |
| if self.prop_meta.img_draw_alpha_only then | |
| self.comp_modifier = self.idImage:AddShaderModifier({ | |
| modifier_type = const.modShader, | |
| shader_flags = const.modImageCompAlpha | |
| }) | |
| end | |
| self.idImage:SetBaseColorMap(self.prop_meta.base_color_map) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropImage:DrawImageContent(clip_rect) | |
| XImage.DrawContent(self.idImage, clip_rect) | |
| local meta = self.prop_meta | |
| local color = meta.img_polyline_color | |
| local polylines = meta.img_polyline | |
| if polylines and color then | |
| local content_box = self.idImage.content_box | |
| local pt, sizex, sizey = content_box:min(), content_box:sizex(), content_box:sizey() | |
| for _, polyline in ipairs(polylines) do | |
| if type(polyline) ~= "table" then | |
| break | |
| end | |
| assert(#polyline >= 2) | |
| for i = 2, #polyline do | |
| local p1 = pt + point(polyline[i - 1]:x() * sizex, polyline[i - 1]:y() * sizey) / 4096 | |
| local p2 = pt + point(polyline[i]:x() * sizex, polyline[i]:y() * sizey) / 4096 | |
| UIL.DrawLine(p1, p2, color) | |
| end | |
| end | |
| end | |
| end | |
| ----- GedPropShortcut | |
| DefineClass.ShortcutPropContainer = { | |
| __parents = { "XWindow" }, | |
| IdNode = true, | |
| LayoutHSpacing = 2, | |
| LayoutVSpacing = 2, | |
| FoldWhenHidden = true, | |
| RolloverAnchor = "left", | |
| RolloverTemplate = "GedPropRollover", | |
| SelectionBackground = RGB(204, 232, 255), | |
| BorderColor = RGBA(0, 0, 0, 0), | |
| BorderWidth = 2, | |
| Label = false, | |
| ResetFunc = empty_func, | |
| } | |
| function ShortcutPropContainer:Init(parent, ResetFunc) | |
| local label_container = XWindow:new({ | |
| Id = "idLabelHost", | |
| Dock = "left", | |
| FoldWhenHidden = true, | |
| }, self) | |
| XText:new({ | |
| Id = "idLabel", | |
| Dock = "left", | |
| VAlign = "center", | |
| MinWidth = 147, | |
| }, label_container):SetText(self.Label) | |
| XTextButton:new({ | |
| Id = "idResetToDefault", | |
| Dock = "right", | |
| VAlign = "center", | |
| Text = "x", | |
| MaxWidth = 20, | |
| MaxHeight = 16, | |
| LayoutHSpacing = 0, | |
| Padding = box(1, 1, 1, 1), | |
| Background = RGBA(0, 0, 0, 0), | |
| RolloverBackground = RGB(204, 232, 255), | |
| PressedBackground = RGB(121, 189, 241), | |
| OnPress = function() | |
| return self:ResetFunc() | |
| end | |
| }, label_container) | |
| end | |
| DefineClass.ShortcutEditor = { | |
| __parents = { "XPopup" }, | |
| handle_keyboard = true, | |
| handle_mouse = true, | |
| handle_gamepad = false, | |
| last_value = false, | |
| IdNode = true, | |
| editor_id = false, | |
| owner = false, | |
| } | |
| local keyboard_modifiers = { "Ctrl", "Alt", "Shift" } -- synced with KbdShortcut and MouseShortcut | |
| local gamepad_modifiers = { "LeftTrigger", "RightTrigger", "LeftShoulder", "RightShoulder"} -- synced with XInputShortcut | |
| function ShortcutEditor:Init(parent, context, prop_meta) | |
| local container = XControl:new({ | |
| Id = "idContainer", | |
| Padding = box(1, 1, 1, 1), | |
| BorderWidth = 1, | |
| LayoutMethod = "VList", | |
| OnMouseButtonDown = function(self, pos, button) | |
| if button == "L" then | |
| self:SetFocus() | |
| end | |
| return XWindow.OnMouseButtonDown(self, pos, button) | |
| end, | |
| }, self) | |
| local keycode_prop = ShortcutPropContainer:new({ | |
| Id = "idKeycode", | |
| Label = "Keycode", | |
| ResetFunc = function(self) | |
| local combo = self.idCombo | |
| return combo:SetValue(combo.DefaultValue) | |
| end, | |
| }, container) | |
| local keys = { "" } | |
| if self.handle_keyboard then | |
| for _, key in ipairs(table.values(VKStrNames)) do | |
| keys[#keys + 1] = key | |
| end | |
| end | |
| if self.handle_mouse then | |
| for _, mouse_button in ipairs(table.keys(MouseButtonNames, true)) do | |
| keys[#keys + 1] = mouse_button | |
| end | |
| end | |
| if self.handle_gamepad then | |
| for _, button in ipairs(XInput.Buttons) do | |
| keys[#keys + 1] = button | |
| end | |
| for _, button in ipairs(XInput.AnalogsAsButtons) do | |
| keys[#keys + 1] = button | |
| end | |
| for _, button in ipairs(XInput.ThumbsAsButtons) do | |
| keys[#keys + 1] = button | |
| end | |
| for _, button in ipairs(XInput.LeftThumbDirectionButtons) do | |
| keys[#keys + 1] = button | |
| end | |
| for _, button in ipairs(XInput.RightThumbDirectionButtons) do | |
| keys[#keys + 1] = button | |
| end | |
| end | |
| XCombo:new({ | |
| Id = "idCombo", | |
| RefreshItemsOnOpen = true, | |
| DefaultValue = "", | |
| ArbitraryValue = false, | |
| OnValueChanged = function(combo, value) | |
| return self:SetTextFromShortcutUI() | |
| end, | |
| OnRequestItems = function(combo) | |
| return keys | |
| end, | |
| VirtualItems = true, | |
| MRUStorageId = prop_meta.mru_storage_id, | |
| MRUCount = prop_meta.show_recent_items, | |
| }, keycode_prop) | |
| local listen_button = XToggleButton:new({ | |
| IdNode = true, | |
| Id = "idInputListener", | |
| BorderWidth = 1, | |
| Margins = box(2, 1, 2, 1), | |
| Dock = "right", | |
| VAlign = "center", | |
| TextHAlign = true, | |
| Text = "Listen", | |
| MinWidth = 70, | |
| FocusedBorderColor = RGB(128, 128, 128), | |
| DisabledBorderColor = RGB(128, 128, 128), | |
| RolloverBorderColor = RGB(128, 128, 128), | |
| PressedBorderColor = RGB(128, 128, 128), | |
| OnShortcut = function(button, shortcut) | |
| if shortcut:starts_with("-") then return end | |
| if self.handle_gamepad and not shortcut:starts_with("+") then return end | |
| if shortcut:starts_with("+") then | |
| shortcut = shortcut:sub(2, #shortcut) | |
| end | |
| self:SetShortcutUIFromText(shortcut) | |
| self:SetTextFromShortcutUI() | |
| return "break" | |
| end, | |
| OnMouseButtonDown = function(button, pt, mouse_button) | |
| if mouse_button == "L" and button.Toggled and not button:MouseInWindow(pt) then | |
| button:SetToggled(false) | |
| return "break" | |
| end | |
| return XToggleButton.OnMouseButtonDown(button, pt, mouse_button) | |
| end, | |
| OnChange = function(button, toggled) | |
| button:SetFocus(toggled) | |
| button:SetText(toggled and "Listening" or "Listen") | |
| end, | |
| SetText = function(button, text) | |
| self.Text = text | |
| return button.idLabel:SetText(text) | |
| end, | |
| }, keycode_prop) | |
| listen_button.idLabel:SetDock("box") | |
| listen_button.ToggledBackground = RGB(20, 109, 171) | |
| local modifiers = self.handle_gamepad and gamepad_modifiers or keyboard_modifiers | |
| local active_modifiers = { cnt = #modifiers } | |
| self.Modifiers = active_modifiers | |
| local modifiers_prop = ShortcutPropContainer:new({ | |
| Id = "idModifiers", | |
| Label = "Modifiers", | |
| LayoutMethod = "HWrap", | |
| ResetFunc = function(self) | |
| for _, modifier in ipairs(modifiers) do | |
| local modifier_button = self[modifier] | |
| if modifier_button then | |
| modifier_button:SetToggled(false) | |
| end | |
| end | |
| end | |
| }, container) | |
| for i, modifier in ipairs(modifiers) do | |
| XToggleButton:new({ | |
| Id = modifier, | |
| Text = modifier, | |
| ToggledBackground = RGB(204, 232, 255), | |
| RolloverBackground = RGBA(0, 0, 0, 0), | |
| RolloverAnchor = "live-mouse", | |
| RolloverTemplate = "GedPropRollover", | |
| Padding = box(0, 2, 0, 2), | |
| OnChange = function(button, toggled) | |
| self.Modifiers[i] = toggled and modifier or nil | |
| return self:SetTextFromShortcutUI() | |
| end, | |
| }, modifiers_prop) | |
| end | |
| local interaction_prop = ShortcutPropContainer:new({ | |
| Id = "idInteraction", | |
| Label = "Interaction", | |
| ResetFunc = function(self) | |
| local combo = self.idCombo | |
| return combo:SetValue(combo.DefaultValue) | |
| end | |
| }, container) | |
| local interactions | |
| if self.handle_gamepad then | |
| interactions = { | |
| "Held", | |
| "Pressed +", | |
| "Released -" | |
| } | |
| else | |
| interactions = { | |
| "Pressed", | |
| "Released -" | |
| } | |
| end | |
| XCombo:new({ | |
| Id = "idCombo", | |
| RefreshItemsOnOpen = true, | |
| DefaultValue = interactions[1], | |
| ArbitraryValue = false, | |
| OnValueChanged = function(combo, value) | |
| return self:SetTextFromShortcutUI() | |
| end, | |
| OnRequestItems = function(combo) | |
| return interactions | |
| end, | |
| VirtualItems = true, | |
| MRUStorageId = prop_meta.mru_storage_id, | |
| MRUCount = prop_meta.show_recent_items, | |
| }, interaction_prop) | |
| self:SetShortcutUIFromText(self.owner:GetProp()) | |
| self:SetTextFromShortcutUI() | |
| end | |
| function ShortcutEditor:OnMouseButtonDown(pt, button) | |
| if button == "L" then | |
| if not self:MouseInWindow(pt) then | |
| self:Close() | |
| end | |
| return "break" | |
| end | |
| end | |
| function ShortcutEditor:SetShortcutUIFromText(text) | |
| if not text then return end | |
| local container = self.idContainer | |
| local interation_combo = container.idInteraction.idCombo | |
| if text:starts_with("+") then | |
| interation_combo:SetValue("Pressed +") | |
| text = text:sub(2, #text) | |
| elseif text:starts_with("-") then | |
| interation_combo:SetValue("Released -") | |
| text = text:sub(2, #text) | |
| end | |
| local modifier_buttons = container.idModifiers | |
| local modifiers = self.handle_gamepad and gamepad_modifiers or keyboard_modifiers | |
| for _, modifier in ipairs(modifiers) do | |
| local modifier_button = modifier_buttons[modifier] | |
| if modifier_button then | |
| modifier_button:SetToggled(false) | |
| end | |
| end | |
| local keycode_combo = container.idKeycode.idCombo | |
| local keys = text:split('-') | |
| local key_cnt = #keys | |
| for i = 1, key_cnt do | |
| local key = keys[i] | |
| if i < key_cnt or table.find(modifiers, key) then | |
| local modifier_button = modifier_buttons[key] | |
| if modifier_button then | |
| modifier_button:SetToggled(true) | |
| end | |
| else | |
| keycode_combo:SetValue(key, "do_not_validate") | |
| end | |
| end | |
| end | |
| function ShortcutEditor:SetTextFromShortcutUI() | |
| if self:IsThreadRunning("set_text_from_ui_thread") then return end | |
| self:CreateThread("set_text_from_ui_thread", function() | |
| local container = self.idContainer | |
| local interaction = container.idInteraction | |
| local interation_combo = interaction.idCombo | |
| local interaction_value = interation_combo:GetValue() | |
| local shortcut_text = "" | |
| if interaction_value == "Pressed +" then | |
| shortcut_text = shortcut_text .. "+" | |
| elseif interaction_value == "Released -" then | |
| shortcut_text = shortcut_text .. "-" | |
| end | |
| interaction.idResetToDefault:SetVisibleInstant(interaction_value ~= interation_combo.DefaultValue) | |
| local has_modifiers = false | |
| local modifiers = self.Modifiers | |
| for i = 1, modifiers.cnt do | |
| if modifiers[i] then | |
| shortcut_text = shortcut_text .. modifiers[i] .. "-" | |
| has_modifiers = true | |
| end | |
| end | |
| container.idModifiers.idResetToDefault:SetVisibleInstant(has_modifiers) | |
| local keycode = container.idKeycode | |
| local keycode_combo = keycode.idCombo | |
| local keycode_value = keycode_combo:GetValue() | |
| keycode.idResetToDefault:SetVisibleInstant(keycode_value ~= keycode_combo.DefaultValue) | |
| if #keycode_value > 0 then | |
| shortcut_text = shortcut_text .. keycode_value | |
| elseif has_modifiers then | |
| shortcut_text = shortcut_text:sub(1, #shortcut_text - 1) | |
| end | |
| if self.last_value ~= shortcut_text then | |
| self.last_value = shortcut_text | |
| self.owner:SetProp(shortcut_text) | |
| end | |
| end) | |
| end | |
| GedPropEditors["shortcut"] = "GedPropShortcut" | |
| DefineClass.GedPropShortcut = { | |
| __parents = { "GedPropText" }, | |
| single_line_edit_template = "XCombo", | |
| shortcut_type = "keyboard&mouse", | |
| } | |
| function GedPropShortcut:Init(parent, context, prop_meta) | |
| self.shortcut_type = prop_meta.shortcut_type or self.shortcut_type | |
| end | |
| function GedPropShortcut:CreateSingleLineEditor(prop_meta) | |
| local combo = XCombo:new({ | |
| ArbitraryValue = false, | |
| OpenCombo = function(combo, mode) | |
| if mode == "suggest" then return end | |
| local active_popup = rawget(rawget(_G, "g_GedApp") or self.desktop, "idShortcutEditor") | |
| if active_popup then | |
| active_popup.owner = self | |
| else | |
| self:OpenShortcutEditor() | |
| end | |
| end | |
| }, self) | |
| local idEdit = combo.idEdit | |
| idEdit:SetVAlign("center") | |
| idEdit:SetTextStyle(prop_meta.text_style) | |
| idEdit:SetFilter(prop_meta.allowed_chars or ".") | |
| self.idEdit = idEdit | |
| return idEdit | |
| end | |
| function GedPropShortcut:OpenShortcutEditor() | |
| if self:CloseShortcutEditor() then return end | |
| if self.ReadOnly then return end | |
| local context = self.context | |
| local prop_meta = self.prop_meta | |
| -- TODO: Handle ActionMouseBindable and ActionBindSingleKey | |
| local shortcut_type = self.shortcut_type | |
| local editor_popup = ShortcutEditor:new({ | |
| Id = "idShortcutEditor", | |
| editor_id = prop_meta.id .. self.panel.context, | |
| ReadOnly = prop_meta.read_only, | |
| DrawOnTop = true, | |
| handle_keyboard = shortcut_type == "keyboard&mouse" or shortcut_type == "keyboard", | |
| handle_mouse = shortcut_type == "keyboard&mouse", | |
| handle_gamepad = shortcut_type == "gamepad", | |
| owner = self, | |
| }, rawget(_G, "g_GedApp") or self.desktop, context, prop_meta) | |
| editor_popup:SetAnchor(self.box) | |
| editor_popup:SetAnchorType("drop") | |
| editor_popup:Open() | |
| editor_popup:SetModal() | |
| editor_popup:SetFocus() | |
| self.panel.app:UpdateChildrenDarkMode(editor_popup) | |
| end | |
| function GedPropShortcut:CloseShortcutEditor() | |
| local editor_popup = rawget(rawget(_G, "g_GedApp") or self.desktop, "idShortcutEditor") | |
| if editor_popup then | |
| editor_popup:Close() | |
| return true | |
| end | |
| end | |
| ----- GedPropFlags | |
| local function LimitIntToSize(value, size) | |
| local mask = 0 | |
| for i = 1, size do | |
| mask = shift(mask, 1) | |
| mask = bor(mask, 1) | |
| end | |
| return band(value, mask) | |
| end | |
| -- Sorts flags - named flags are first alphabetically, then unnamed flags alphabetically | |
| local function sort_flags(a, b) | |
| -- shape of a/b: { name, id, value } | |
| local a_noname = string.starts_with(a.name, "bit") | |
| local b_noname = string.starts_with(b.name, "bit") | |
| if a_noname and not b_noname then | |
| return false -- b before a | |
| elseif not a_noname and b_noname then | |
| return true -- a before b | |
| else | |
| return a.name < b.name | |
| end | |
| end | |
| GedPropEditors["flags"] = "GedPropFlags" | |
| DefineClass.GedPropFlags = { | |
| __parents = { "GedPropEditor" }, | |
| } | |
| function GedPropFlags:Init() | |
| XCheckButtonCombo:new({ | |
| Id = "idCombo", | |
| Items = function(check_combo) return self:GetComboItems() end, | |
| Editable = not self.prop_meta.read_only, | |
| OnCheckButtonChanged = function(checkbox, id, value) | |
| local items = self:GetComboItems() | |
| local item_idx = table.find(items, "id", id) | |
| assert(item_idx, "Flag not found in items") | |
| if not item_idx then return end | |
| local flag_bit = items[item_idx] and items[item_idx].flag_bit | |
| local flags = self:GetProp() | |
| if not value then | |
| flags = band(flags, bnot(shift(1, flag_bit - 1))) | |
| else | |
| flags = bor(flags, shift(1, flag_bit - 1)) | |
| end | |
| self:SetProp(LimitIntToSize(flags, self.prop_meta.size)) | |
| self:UpdateValue() | |
| end | |
| }, self) | |
| end | |
| function GedPropFlags:GetComboItems(flags, flag_names) | |
| local items = {} | |
| local flags = self:GetProp() | |
| local flag_names = self.prop_meta.items | |
| for i = 1, self.prop_meta.size do | |
| local name = flag_names[i] | |
| local read_only = false | |
| if type(name) == "table" then | |
| read_only = name.read_only | |
| name = name.name | |
| end | |
| if not name then | |
| name = "bit " .. i | |
| end | |
| local value = band(flags, shift(1, i - 1)) ~= 0 | |
| table.insert(items, { id = name, flag_bit = i, name = name, read_only = read_only or flags == Undefined() or self.prop_meta.read_only, value = value }) | |
| end | |
| table.sort(items, sort_flags) | |
| return items | |
| end | |
| function GedPropFlags:UpdateValue() | |
| GedPropEditor.UpdateValue(self) | |
| local flags = self:GetProp() | |
| if flags == Undefined() then | |
| self.idCombo:SetText("Undefined") | |
| return | |
| end | |
| local items = self:GetComboItems(flags) | |
| local filtered_items = {} | |
| for idx = 1, self.prop_meta.size do | |
| if band(flags, shift(1, idx - 1)) ~= 0 then | |
| local item_idx = table.find(items, "flag_bit", idx) | |
| if item_idx then | |
| table.insert(filtered_items, items[item_idx].name) | |
| end | |
| end | |
| end | |
| table.sort(filtered_items) | |
| self.idCombo:SetText(string.format("0x%x", flags) .. " : " .. table.concat(filtered_items, " | ")) | |
| end | |
| ----- GedPropEmpty | |
| GedPropEditors["empty"] = "GedPropEmpty" | |
| DefineClass.GedPropEmpty = { | |
| __parents = { "GedPropEditor" }, | |
| } | |
| function GedPropEmpty:Init() | |
| XLabel:new({ | |
| Id = "idLabel", | |
| Dock = "box", | |
| }, self):SetText("Undefined") | |
| end | |
| ----- GedPropGrid | |
| if FirstLoad then | |
| GridToUnload = {} | |
| GridPreviewIdx = 1 | |
| end | |
| GedPropEditors["grid"] = "GedPropGrid" | |
| DefineClass.GedPropGrid = { | |
| __parents = { "GedPropEditor" }, | |
| grid = false, | |
| grid_img = "", | |
| grid_hash = false, | |
| grid_size = false, | |
| grid_offset = false, | |
| max_size = 0, | |
| color = false, | |
| invalid_value = false, | |
| dont_normalize = false, | |
| } | |
| function GedPropGrid:Done() | |
| self:SetGridImage(false) | |
| end | |
| function GedPropGrid:Init() | |
| XLabel:new({ | |
| Id = "idNoData", | |
| Dock = "box", | |
| }, self):SetText("No Grid Data") | |
| local image = XImage:new({ | |
| Id = "idImage", | |
| Dock = "box", | |
| ImageFit = "stretch", | |
| VAlign = "center", | |
| HAlign = "left", | |
| HandleMouse = true, | |
| RolloverAnchor = "live-mouse", | |
| RolloverTemplate = "GedPropRollover", | |
| Measure = function(self, width, height) return UIL.MeasureImage(self.Image) end | |
| }, self) | |
| image.OnMousePos = function(image, pt) | |
| XRecreateRolloverWindow(image) | |
| end | |
| image.GetRolloverText = function(image) | |
| local grid = self.grid | |
| if not grid then | |
| return | |
| end | |
| local pt = terminal.GetMousePos() | |
| local box = image.content_box | |
| if not pt:InBox(box) then | |
| return "" | |
| end | |
| local rw, rh = self.grid_size:xy() | |
| local gw, gh = grid:size() | |
| local bw, bh = box:sizexyz() | |
| local bx, by = (pt - box:min()):xy() | |
| local gx = MulDivTrunc(gw, bx, bw) | |
| local gy = MulDivTrunc(gh, by, bh) | |
| local rx = MulDivTrunc(rw, bx, bw) | |
| local ry = MulDivTrunc(rh, by, bh) | |
| local scale = 1000 | |
| local v = GridGet(grid, gx, gy, scale) | |
| local sign = v < 0 and "-" or "" | |
| v = abs(v) | |
| local a, b = v / scale, v % scale | |
| local bs | |
| if b < 1 then | |
| bs = "" | |
| elseif b < 10 then | |
| bs = ".00" .. b | |
| elseif b < 100 then | |
| bs = ".0" .. b | |
| else | |
| bs = "." .. b | |
| end | |
| return string.format("(%i, %i) %s%d%s", rx, ry, sign, a, bs) | |
| end | |
| end | |
| function GedPropGrid:SetGridImage(img) | |
| local frame = GetRenderFrame() | |
| for unload_img, unload_frame in pairs(GridToUnload) do | |
| if frame - unload_frame > 5 and UIL.IsImageReady(unload_img) then | |
| local err = AsyncFileDelete(unload_img) | |
| GridToUnload[unload_img] = nil | |
| end | |
| end | |
| img = img or "" | |
| local prev_img = self.grid_img or "" | |
| if prev_img ~= "" then | |
| UIL.UnloadImage(prev_img) | |
| GridToUnload[prev_img] = frame | |
| end | |
| self.idImage:SetImage(img) | |
| self.grid_img = img | |
| end | |
| function GedPropGrid:UpdateValue() | |
| local prop_value = self:GetProp() | |
| local grid_str, grid_w, grid_h | |
| if type(prop_value) == "table" then | |
| grid_str, grid_w, grid_h = table.unpack(prop_value) | |
| else | |
| grid_str = prop_value | |
| end | |
| local hasgrid = type(grid_str) == "string" and grid_str ~= "" | |
| self.idImage:SetDock(hasgrid and "box" or "ignore") | |
| self.idNoData:SetDock(hasgrid and "ignore" or "box") | |
| self.idImage:SetVisible(hasgrid) | |
| self.idNoData:SetVisible(not hasgrid) | |
| if not hasgrid then | |
| self.grid = false | |
| return | |
| end | |
| local update | |
| local grid = self.grid | |
| local force_read_grid | |
| local grid_offset = self.prop_meta.grid_offset or false | |
| if grid_offset ~= self.grid_offset then | |
| force_read_grid = true | |
| self.grid_offset = grid_offset | |
| end | |
| local grid_hash = xxhash(grid_str) | |
| if not grid or force_read_grid or grid_hash ~= self.grid_hash then | |
| update = true | |
| self.grid_hash = grid_hash | |
| grid = GridReadStr(grid_str) | |
| if not grid then | |
| self.grid = false | |
| return | |
| end | |
| grid = GridRepack(grid, "F") | |
| if grid_offset then | |
| GridAdd(grid, grid_offset) | |
| end | |
| self.grid = grid | |
| end | |
| self.grid_size = grid_w and grid_h and point(grid_w, grid_h) or point(grid:size()) | |
| local color = self.prop_meta.color or false | |
| if color ~= self.color then | |
| update = true | |
| self.color = color | |
| end | |
| local invalid_value = self.prop_meta.invalid_value or false | |
| if invalid_value ~= self.invalid_value then | |
| update = true | |
| self.invalid_value = invalid_value | |
| end | |
| local dont_normalize = self.prop_meta.dont_normalize or false | |
| if dont_normalize ~= self.dont_normalize then | |
| update = true | |
| self.dont_normalize = dont_normalize | |
| end | |
| local min_size = self.prop_meta.min or 0 | |
| local max_size = self.prop_meta.max or 512 | |
| if self.max_size ~= max_size then | |
| update = true | |
| self.max_size = max_size | |
| end | |
| local w, h = grid:size() | |
| if update then | |
| local size = Max(w, h) | |
| if not color then | |
| while true do | |
| if size < 2 * max_size then | |
| break | |
| end | |
| w, h = w / 2, h / 2 | |
| grid = GridResample(grid, w, h) | |
| end | |
| elseif size > max_size then | |
| grid = GridResample(grid, w * max_size / size, h * max_size / size, false) | |
| end | |
| GridPreviewIdx = GridPreviewIdx + 1 | |
| local new_img = "memoryfs/grid_" .. GridPreviewIdx .. ".tga" | |
| -- Prepare image for exporting to TGA: | |
| local color_fmt = color and "color" or "gray24" | |
| local normalize = not dont_normalize | |
| local err = GridToImage(new_img, grid, color_fmt, invalid_value, normalize) | |
| if err then | |
| assert(false, 'Failed to export tga: ' .. err) | |
| new_img = "" | |
| end | |
| self:SetGridImage(new_img) | |
| end | |
| local frame = self.prop_meta.frame or 0 | |
| self.idImage.BorderWidth = frame | |
| self.idImage.BorderHeight = frame | |
| if min_size > 0 then | |
| local s = Min(w, h) | |
| self.idImage.MinWidth = min_size * w / s | |
| self.idImage.MinHeight = min_size * h / s | |
| end | |
| if max_size > min_size then | |
| local s = Max(w, h) | |
| self.idImage.MaxWidth = max_size * w / s | |
| self.idImage.MaxHeight = max_size * h / s | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| ----- GedPropPresetId | |
| GedPropEditors["preset_id"] = "GedPropPresetId" | |
| DefineClass.GedPropPresetId = { | |
| __parents = { "GedPropEditor" } | |
| } | |
| function GedPropPresetId:Init() | |
| local prop_meta = self.prop_meta | |
| local create_button, open_button | |
| if not prop_meta.read_only and self:ShouldShowButtonForFunc("GedOpPresetIdNewInstance") then | |
| create_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| create_button:SetIcon("CommonAssets/UI/Ged/plus-one.tga") | |
| create_button:SetRolloverText(string.format("New %s", prop_meta.preset_class)) | |
| create_button.OnPress = function(button) | |
| button:SetFocus() | |
| self.panel.app:Op("GedOpPresetIdNewInstance", self.panel.context, prop_meta.id, prop_meta.preset_class) | |
| self.idCombo.Items = false -- let it fetch them again | |
| button:SetFocus(false) | |
| end | |
| end | |
| if self:ShouldShowButtonForFunc("GedRpcEditPreset") then | |
| open_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| open_button:SetIcon("CommonAssets/UI/Ged/explorer.tga") | |
| open_button:SetRolloverText("Open Preset Editor") | |
| open_button.OnPress = function(button) | |
| button:SetFocus() | |
| self.panel.app:Send("GedRpcEditPreset", self.panel.context, prop_meta.id) | |
| button:SetFocus(false) | |
| end | |
| end | |
| XCombo:new({ | |
| Id = "idCombo", | |
| Items = false, | |
| RefreshItemsOnOpen = true, | |
| DefaultValue = prop_meta.default or "", | |
| ArbitraryValue = false, | |
| OnValueChanged = function(combo, value) | |
| self:SetProp(value) | |
| end, | |
| OnRequestItems = function(combo) | |
| return self.panel.connection:Call("rfnGetPresetItems", self.panel.context, self.prop_meta.id) | |
| end, | |
| MRUStorageId = prop_meta.mru_storage_id, | |
| MRUCount = prop_meta.show_recent_items, | |
| VirtualItems = true, | |
| }, self) | |
| self.idCombo:SetEnabled(not prop_meta.read_only) | |
| if prop_meta.editor_preview then | |
| GedTextPanel:new({ | |
| Title = "", | |
| Dock = "bottom", | |
| ZOrder = -1, | |
| Format = prop_meta.editor_preview, | |
| }, self):SetContext(prop_meta.id..".ReferencedPreset") | |
| end | |
| -- GoldMaster (Mod Editor) does not support the additional features | |
| if not Platform.developer then | |
| if open_button then | |
| open_button:SetVisible(false) | |
| open_button:SetDock("ignore") | |
| end | |
| if create_button then | |
| create_button:SetVisible(false) | |
| create_button:SetDock("ignore") | |
| end | |
| end | |
| end | |
| function GedPropPresetId:UpdateValue() | |
| local combo = self.idCombo | |
| combo.Items = false -- list of items might have changed, must be refetched | |
| if not (combo:IsFocused() and combo.ArbitraryValue and combo:GetValue() ~= combo:GetText()) then | |
| self.last_value = self:GetProp() | |
| combo:SetValue(self.last_value, true) | |
| end | |
| if self.prop_meta.editor_preview then | |
| self.panel.app:Send("GedRpcBindPreset", self.prop_meta.id..".ReferencedPreset", self.panel.context, self.prop_meta.id) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropPresetId:ReassignFocusOrders(x, y) | |
| self.idCombo:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropPresetId:DetachForReuse() | |
| self.idCombo:UpdateMRUList() | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| ----- GedPropEmbeddedObject (base class for the nested_obj/nested_list/script editors) | |
| DefineClass.GedPropEmbeddedObject = { | |
| __parents = { "GedPropEditor" }, | |
| } | |
| function GedPropEmbeddedObject:Init(parent, context, prop_meta) | |
| local create_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| create_button:SetId("idCreateItemButton") | |
| create_button:SetIcon("CommonAssets/UI/Ged/new.tga") | |
| create_button:SetRolloverText("Create Item") | |
| create_button:SetEnabled(not prop_meta.read_only) | |
| local paste_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| paste_button:SetId("idPasteButton") | |
| paste_button:SetIcon("CommonAssets/UI/Ged/paste.tga") | |
| paste_button:SetRolloverText("Paste") | |
| paste_button:SetEnabled(not prop_meta.read_only) | |
| local copy_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| copy_button:SetId("idCopyButton") | |
| copy_button:SetIcon("CommonAssets/UI/Ged/copy.tga") | |
| copy_button:SetRolloverText("Copy") | |
| if self:HasMember("idButtonsHost") then | |
| self.idButtonsHost:SetDock("right") | |
| self.idButtonsHost:SetZOrder(-1) | |
| end | |
| if prop_meta.editor ~= "script" then | |
| XText:new({ | |
| Id = "idValueText", | |
| VAlign = "center", | |
| MaxHeight = 24, | |
| HandleMouse = false, | |
| }, self) | |
| end | |
| end | |
| function GedPropEmbeddedObject:ReassignFocusOrders(x, y) | |
| self.idCreateItemButton:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| ----- GedPropNestedObj | |
| GedPropEditors["nested_obj"] = "GedPropNestedObj" | |
| GedPropEditors["property_array"] = "GedPropNestedObj" | |
| DefineClass.GedPropNestedObj = { | |
| __parents = { "GedPropEmbeddedObject", "GedPanelBase" }, | |
| } | |
| function GedPropNestedObj:Init(parent, context, prop_meta) | |
| self.idCreateItemButton.OnPress = function(button) | |
| if prop_meta.editor == "property_array" then | |
| if self.window_state == "destroying" then return end | |
| button:SetFocus() | |
| self.app:Op("GedCreateNestedObj", self.panel.context, self.prop_meta.id, "GedDynamicProps") | |
| self.idPropPanel.expanded = true | |
| button:SetFocus(false) | |
| return | |
| end | |
| CreateRealTimeThread(function() | |
| local items = self.app:Call("GedGetNestedClassItems", self.panel.context, self.prop_meta.id) | |
| local title = string.format("New %s object", prop_meta.base_class) | |
| GedOpenCreateItemPopup(self, title, items, button, function(class) | |
| if self.window_state == "destroying" then return end | |
| self.app:Op("GedCreateNestedObj", self.panel.context, self.prop_meta.id, class) | |
| self.idPropPanel.expanded = true | |
| end) | |
| end) | |
| end | |
| self.idCreateItemButton:SetFoldWhenHidden(true) | |
| self.idCopyButton.OnPress = function(button) | |
| self.panel:SetFocus() | |
| self.app:Op("GedNestedObjCopy", self.panel.context, self.prop_meta.id, self.prop_meta.base_class) | |
| end | |
| self.idPasteButton.OnPress = function(button) | |
| self.panel:SetFocus() | |
| self.app:Op("GedNestedObjPaste", self.panel.context, self.prop_meta.id, self.prop_meta.base_class) | |
| end | |
| GedPropPanel:new({ | |
| Id = "idPropPanel", | |
| Dock = "bottom", | |
| ZOrder = -1, | |
| Margins = box(10, 0, 0, 0), | |
| FoldWhenHidden = true, | |
| Embedded = true, | |
| Collapsible = true, | |
| HideFirstCategory = true, | |
| RootObjectBindName = self.panel.RootObjectBindName, | |
| Title = prop_meta.editor == "property_array" and "<empty>" or prop_meta.format or "<EditorView>", | |
| ActionsClass = "PropertyObject", | |
| Copy = "GedOpPropertyCopy", | |
| Paste = "GedOpPropertyPaste", | |
| StartsExpanded = prop_meta.auto_expand or false, | |
| SuppressProps = prop_meta.suppress_props, | |
| read_only = prop_meta.read_only, | |
| }, self) | |
| end | |
| function GedPropNestedObj:DetachForReuse() | |
| self:UnbindViews() | |
| self.idPropPanel:UnbindViews() | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| function GedPropNestedObj:OnContextUpdate(context, view) | |
| GedPanelBase.OnContextUpdate(self, context, view) | |
| if view == nil then | |
| self.idPropPanel:SetContext(self.context) | |
| end | |
| end | |
| function GedPropNestedObj:UpdateValue() | |
| local value = self:GetProp() | |
| self.idValueText:SetText(value == Undefined() and "(undefined)" or "") | |
| value = value ~= Undefined() and value | |
| self.idPropPanel:SetVisible(value) | |
| self.idCreateItemButton:SetVisible(not value) | |
| self.connection:BindObj(self.context, { self.panel.context, "prop:" .. self.prop_meta.id }) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropNestedObj:ReassignFocusOrders(x, y) | |
| y = GedPropEmbeddedObject.ReassignFocusOrders(self, x, y) | |
| y = self.idPropPanel:ReassignFocusOrders(x, y) | |
| return y | |
| end | |
| function GedPropNestedObj:UpdatePropertyNames(internal) | |
| self.idPropPanel:UpdatePropertyNames(internal) | |
| GedPropEditor.UpdatePropertyNames(self, internal) | |
| end | |
| ----- GedPropNestedList | |
| DefineClass.GedPropListDragAndDrop = { | |
| __parents = { "GedDragAndDrop", }, | |
| prop_parent_class = "GedPropEditor", | |
| drop_target_container_class = "XList", | |
| List = false, | |
| Item = false, | |
| } | |
| function GedPropListDragAndDrop:Init() | |
| XImage:new({ | |
| HAlign = "center", | |
| VAlign = "center", | |
| Image = "CommonAssets/UI/Ged/dragndrop.tga", | |
| ImageFit = "scale_down", | |
| ImageScale = point(700, 700), | |
| ImageColor = GetDarkModeSetting() and RGB(210, 210, 210) or RGB(0, 0, 0), | |
| }, self) | |
| end | |
| function GedPropListDragAndDrop:GetMouseCursor() | |
| if not self.enabled then return end | |
| if self.drag_win and self:GetDragAndDropError() then | |
| return "CommonAssets/UI/ErrorCursor.tga" | |
| else | |
| return "CommonAssets/UI/HandCursor.tga" | |
| end | |
| end | |
| function GedPropListDragAndDrop:ForEachSelectedDragTargetChild(func, ...) | |
| local prop_list = GetParentOfKind(ged_drag_target, self.prop_parent_class) | |
| if not prop_list then return end | |
| local selection = ged_drag_target:GetSelection() | |
| for _, idx in ipairs(selection) do | |
| local selected_child = ged_drag_target[idx] | |
| if func(selected_child, ...) == "break" then | |
| return "break" | |
| end | |
| end | |
| end | |
| function GedPropListDragAndDrop:GetGedDragTarget() | |
| return self.List | |
| end | |
| function GedPropListDragAndDrop:ForEachDragTargetChild(func, ...) | |
| if not IsKindOf(ged_drag_target, self.drop_target_container_class) then return end | |
| for _, child in ipairs(ged_drag_target) do | |
| if func(child, ...) == "break" then | |
| return "break" | |
| end | |
| end | |
| end | |
| DefineClass.GedNestedPropDragAndDrop = { | |
| __parents = { "GedPropListDragAndDrop", }, | |
| prop_parent_class = "GedPropNestedList", | |
| } | |
| function GedNestedPropDragAndDrop:GetDragWindowTextControl(control) | |
| return control.idTitle | |
| end | |
| function GedNestedPropDragAndDrop:OnDrop(drag_win, pt, drag_source_win) | |
| if self:GetDragAndDropError() then return end | |
| local prop_parent = GetParentOfKind(ged_drop_target, self.prop_parent_class) | |
| local sel = prop_parent.idList:GetSelection() | |
| local last_time = prop_parent.last_move_items_time | |
| -- don't allow multiple ops to reach the game before they get the chance to get executed | |
| if GetPreciseTicks() > last_time + 350 then | |
| prop_parent:CollapseUnselected() | |
| local op = gedListDragAndDropOps[ged_drop_type] | |
| prop_parent.app:Op(op, prop_parent.context, sel, table.find(prop_parent.idList, ged_drop_target)) | |
| prop_parent.app:Send("GedNotifyPropertyChanged", prop_parent.panel.context, prop_parent.prop_meta.id) | |
| prop_parent.last_move_items_time = GetPreciseTicks() | |
| prop_parent.last_move_items_selection = sel | |
| end | |
| end | |
| DefineClass.GedNestedPropPanel = { | |
| __parents = { "GedPropPanel", "XListItem" }, | |
| SelectionBackground = RGB(255, 255, 255), | |
| Embedded = true, | |
| Collapsible = true, | |
| HideFirstCategory = true, | |
| expanded = false, | |
| prop_id = false, | |
| item_addr = false, | |
| list_context = false, | |
| parent_obj_context = false, | |
| selection_mark = false, | |
| } | |
| GedNestedPropPanel.OnMouseButtonDown = XControl.OnMouseButtonDown | |
| function GedNestedPropPanel:InitControls() | |
| GedPropPanel.InitControls(self) | |
| local read_only = GetParentOfKind(self, "GedPropNestedList").prop_meta.read_only | |
| if self:ShouldShowButtonForFunc("GedOpListDeleteItem") then | |
| local delete_button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer) | |
| delete_button:SetIcon("CommonAssets/UI/Ged/delete.tga") | |
| delete_button:SetRolloverText("Delete") | |
| delete_button.OnPress = function(button) | |
| local idx = table.find(self.parent, self) | |
| button:SetFocus() | |
| self.app:Op("GedOpListDeleteItem", self.list_context, idx) | |
| self.app:Send("GedNotifyPropertyChanged", self.parent_obj_context, self.prop_id) | |
| button:SetFocus(false) | |
| end | |
| delete_button:SetEnabled(not read_only) | |
| end | |
| if self:ShouldShowButtonForFunc("GedOpListDuplicate") then | |
| local duplicate_button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer) | |
| duplicate_button:SetIcon("CommonAssets/UI/Ged/duplicate.tga") | |
| duplicate_button:SetRolloverText("Duplicate") | |
| duplicate_button.OnPress = function(button) | |
| local idx = table.find(self.parent, self) | |
| button:SetFocus() | |
| self.app:Op("GedOpListDuplicate", self.list_context, { idx }) | |
| self.app:Send("GedNotifyPropertyChanged", self.parent_obj_context, self.prop_id) | |
| GetParentOfKind(self, "GedPropNestedList").new_item_idx = idx + 1 | |
| button:SetFocus(false) | |
| end | |
| duplicate_button:SetEnabled(not read_only) | |
| end | |
| self.selection_mark = XWindow:new({ MinWidth = 3, Dock = "left", ZOrder = 0, HandleMouse = true }, self) | |
| GedNestedPropDragAndDrop:new({ | |
| Id = "idDragAndDrop", | |
| Dock = "left", | |
| Padding = box(2, 1, 2, 1), | |
| List = GetParentOfKind(self, "XList"), | |
| Item = self, | |
| ZOrder = -1, | |
| }, self) | |
| self.idDragAndDrop:SetEnabled(not read_only) | |
| -- find our GedPropPanel parent, and create a thread there to highlight the currently hovered GetNestedPropPanel | |
| local current_list = GetParentOfKind(self, "GedPropNestedList") | |
| local prop_panel = GetParentOfKind(self, "GedPropPanel") | |
| repeat | |
| prop_panel = GetParentOfKind(prop_panel.parent, "GedPropPanel") | |
| until prop_panel.class == "GedPropPanel" | |
| if not prop_panel:IsThreadRunning("NestedItemRolloverThread") then | |
| prop_panel:CreateThread("NestedItemRolloverThread", function(self) | |
| local last_rollover | |
| while true do | |
| local pt = terminal.GetMousePos() | |
| local list = GetParentOfKind(terminal.desktop:GetMouseTarget(pt), "GedPropNestedList") | |
| list = list and list.idList | |
| local rollover = list and list[list:GetItemAt(pt)] | |
| if rollover ~= last_rollover then | |
| if rollover and not rollover.selected then | |
| rollover.selection_mark:SetBackground(GetDarkModeSetting() and RGB(128, 128, 128) or RGB(180, 180, 180)) | |
| end | |
| if last_rollover and not last_rollover.selected then | |
| last_rollover.selection_mark:SetBackground(0) | |
| end | |
| if last_rollover then | |
| last_rollover.RolloverTemplate = nil | |
| last_rollover.RolloverText = nil | |
| last_rollover.RolloverAnchor = nil | |
| end | |
| if rollover and list:GetSelection() and #list:GetSelection() == 1 and list:GetItemCount() > 1 then | |
| rollover.RolloverTemplate = "GedPropRollover" | |
| rollover.RolloverText = "Hold Ctrl or Shift to select multiple list elements." | |
| rollover.RolloverAnchor = "bottom" | |
| end | |
| last_rollover = rollover | |
| end | |
| Sleep(50) | |
| end | |
| end, prop_panel) | |
| end | |
| Msg("XWindowRecreated", self) | |
| end | |
| function GedNestedPropPanel:OnSetFocus() | |
| self.parent:SetSelection(table.find(self.parent, self)) | |
| end | |
| function GedNestedPropPanel:CalcBackground() | |
| return XContextControl.CalcBackground(self) -- don't change background when selected | |
| end | |
| function GedNestedPropPanel:SetSelected(selected) | |
| if self.selected ~= selected then | |
| self.selected = selected | |
| self.selection_mark:SetBackground(selected and (GetDarkModeSetting() and RGB(180, 180, 180) or RGB(128, 128, 128)) or 0) | |
| if selected then | |
| -- unselect items in all other nested lists in the same properties panel | |
| local current_list = GetParentOfKind(self, "GedPropNestedList") | |
| local prop_panel = GetParentOfKind(self, "GedPropPanel") | |
| repeat | |
| prop_panel = GetParentOfKind(prop_panel.parent, "GedPropPanel") | |
| until prop_panel.class == "GedPropPanel" | |
| for _, list in ipairs(get_children_of_classes(prop_panel, nil, "GedPropNestedList")) do | |
| if list ~= current_list then | |
| list.idList:SetSelection(false) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| GedPropEditors["nested_list"] = "GedPropNestedList" | |
| DefineClass.GedPropNestedList = { | |
| __parents = { "GedPropEmbeddedObject", "GedPanelBase" }, | |
| Interactive = true, | |
| new_item_idx = false, | |
| last_move_items_time = 0, | |
| last_move_items_selection = empty_table, | |
| table_addr = false, | |
| } | |
| function GedPropNestedList:OnMouseButtonDoubleClick(_, button) | |
| if button == "L" then | |
| local expanded = true | |
| for _, panel in ipairs(self.idList or empty_table) do | |
| expanded = expanded and panel.expanded | |
| end | |
| for _, panel in ipairs(self.idList or empty_table) do | |
| panel:Expand(not expanded) | |
| end | |
| return "break" | |
| end | |
| end | |
| function GedPropNestedList:Init(parent, context, prop_meta) | |
| self.idCreateItemButton.OnPress = function(button) | |
| CreateRealTimeThread(function() | |
| local items = self.app:Call("GedGetNestedClassItems", self.panel.context, self.prop_meta.id) | |
| local title = string.format("New %s element", prop_meta.base_class) | |
| GedOpenCreateItemPopup(self, title, items, button, function(class) | |
| if self.window_state == "destroying" then return end | |
| local parent_context = self.panel.context | |
| self.app:Op("GedOpNestedListNewItem", parent_context, parent_context, prop_meta.id, self.idList:GetFocusedItem(), class) | |
| self.new_item_idx = (self.idList:GetFocusedItem() or #self.idList) + 1 | |
| self:CollapseUnselected() | |
| end) | |
| end) | |
| end | |
| self.idCopyButton:SetRolloverText("Copy all") | |
| self.idCopyButton.OnPress = function(button) | |
| self.panel:SetFocus() | |
| self.app:Op("GedNestedListCopy", self.context, self.prop_meta.base_class) | |
| end | |
| self.idPasteButton:SetRolloverText("Paste over") | |
| self.idPasteButton.OnPress = function(button) | |
| self.panel:SetFocus() | |
| self.app:Op("GedNestedListPaste", self.panel.context, self.prop_meta.id, self.prop_meta.base_class) | |
| end | |
| local movedown_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| movedown_button:SetId("idMoveDown") | |
| movedown_button:SetIcon("CommonAssets/UI/Ged/down.tga") | |
| movedown_button:SetRolloverText("Move item(s) down") | |
| movedown_button.OnPress = function(button) | |
| local idx, sel = self.idList:GetFocusedItem(), self.idList:GetSelection() | |
| local last_time, last_sel = self.last_move_items_time, self.last_move_items_selection | |
| if idx then | |
| -- don't allow multiple ops to reach the game before they get the chance to get executed | |
| if GetPreciseTicks() > last_time + 350 or not table.iequal(last_sel, sel) then | |
| button:SetFocus() | |
| self:CollapseUnselected() | |
| self.app:Op("GedOpListMoveDown", context, sel) | |
| self.app:Send("GedNotifyPropertyChanged", self.panel.context, prop_meta.id) | |
| button:SetFocus(false) | |
| self.last_move_items_time, self.last_move_items_selection = GetPreciseTicks(), sel | |
| end | |
| else | |
| CreateMessageBox(nil, Untranslated("Info"), Untranslated("Please select the item(s) you would like to move.")) | |
| end | |
| end | |
| movedown_button:SetEnabled(not prop_meta.read_only) | |
| movedown_button:SetVisibleInstant(false) | |
| local moveup_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| moveup_button:SetId("idMoveUp") | |
| moveup_button:SetIcon("CommonAssets/UI/Ged/up.tga") | |
| moveup_button:SetRolloverText("Move item(s) up") | |
| moveup_button.OnPress = function(button) | |
| local idx, sel = self.idList:GetFocusedItem(), self.idList:GetSelection() | |
| local last_time, last_sel = self.last_move_items_time, self.last_move_items_selection | |
| if idx then | |
| -- don't allow multiple ops to reach the game before they get the chance to get executed | |
| if GetPreciseTicks() > last_time + 350 or not table.iequal(last_sel, sel) then | |
| button:SetFocus() | |
| self:CollapseUnselected() | |
| self.app:Op("GedOpListMoveUp", context, sel) | |
| self.app:Send("GedNotifyPropertyChanged", self.panel.context, prop_meta.id) | |
| button:SetFocus(false) | |
| self.last_move_items_time, self.last_move_items_selection = GetPreciseTicks(), sel | |
| end | |
| else | |
| CreateMessageBox(nil, Untranslated("Info"), Untranslated("Please select the item(s) you would like to move.")) | |
| end | |
| end | |
| moveup_button:SetEnabled(not prop_meta.read_only) | |
| moveup_button:SetVisibleInstant(false) | |
| XList:new({ | |
| Id = "idList", | |
| Dock = "bottom", | |
| ZOrder = -1, | |
| Margins = box(0, 0, 2, 0), | |
| BorderWidth = 0, | |
| FoldWhenHidden = true, | |
| Visible = false, | |
| MultipleSelection = true, | |
| }, self) | |
| self.idList.OnShortcut = function(list, shortcut, source, ...) | |
| if shortcut == "Ctrl-C" then | |
| self.app:Op("GedOpListCopy", self.context, list:GetSelection(), prop_meta.base_class) | |
| return "break" | |
| elseif shortcut == "Ctrl-X" then | |
| self.app:Op("GedOpListCut", self.context, list:GetSelection(), prop_meta.base_class) | |
| return "break" | |
| elseif shortcut == "Ctrl-V" then | |
| self.app:Op("GedOpListPaste", self.context, list:GetSelection(), prop_meta.base_class) | |
| return "break" | |
| end | |
| end | |
| self.idList.OnSelection = function(list, selected) | |
| self.idMoveUp:SetVisibleInstant(selected) | |
| self.idMoveDown:SetVisibleInstant(selected) | |
| end | |
| end | |
| function GedPropNestedList:DetachForReuse() | |
| self:UnbindViews() | |
| for _, prop_panel in ipairs(self.idList) do | |
| prop_panel:UnbindViews() | |
| end | |
| self.idList:Clear() | |
| self.table_addr = false | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| function GedPropNestedList:CollapseUnselected() | |
| local list = self.idList | |
| for i, win in pairs(list) do | |
| if IsKindOf(win, "GedNestedPropPanel") and not win.selected and win.context then | |
| win:Expand(false) | |
| end | |
| end | |
| end | |
| function GedPropNestedList:GetSelection() | |
| local selection = self.idList:GetSelection() | |
| if not selection then return end | |
| return selection[1], selection | |
| end | |
| function GedPropNestedList:SetSelection(selection, multiple_selection) | |
| self.idList:SetSelection(multiple_selection or selection) | |
| end | |
| function GedPropNestedList:ReassignFocusOrders(x, y) | |
| y = GedPropEmbeddedObject.ReassignFocusOrders(self, x, y) | |
| for _, prop_panel in ipairs(self.idList) do | |
| y = prop_panel:ReassignFocusOrders(x, y) | |
| end | |
| return y | |
| end | |
| function GedPropNestedList:UpdateValue() | |
| GedPropEmbeddedObject.UpdateValue(self) | |
| local item_data = self:GetProp() | |
| local prop_meta = self.prop_meta | |
| local list = self.idList | |
| if item_data == Undefined() then | |
| list:Clear() | |
| self.idValueText:SetText("(undefined)") | |
| return | |
| end | |
| if (item_data and item_data.table_addr or false) ~= self.table_addr then | |
| self.connection:BindObj(self.context, { self.panel.context, "prop:" .. prop_meta.id }) | |
| self.table_addr = item_data.table_addr | |
| end | |
| -- detach old item panels to reuse them if they stayed the same (are displaying the same object) | |
| local old_panels = {} | |
| local old_sel = table.map(list:GetSelection(), function(idx) return list[idx].item_addr end) | |
| for i = #list, 1, -1 do | |
| local win = list[i] | |
| if not win.Dock or win.Dock == "ignore" then | |
| table.insert(old_panels, win) | |
| win:SetFocused(false) | |
| win:SetSelected(false) | |
| win:SetParent(false) | |
| end | |
| end | |
| -- update list, reusing old panel where the object addresses match | |
| local binding_map = {} | |
| for idx, addr in ipairs(type(item_data) == "table" and item_data) do | |
| local item_context = self.context .. "." .. addr | |
| local panel = table.find_value(old_panels, "item_addr", addr) | |
| if panel then | |
| table.remove_value(old_panels, panel) | |
| panel:SetParent(list) | |
| else | |
| local panel = GedNestedPropPanel:new({ | |
| Title = prop_meta.format or "<EditorView>", | |
| RootObjectBindName = self.panel.RootObjectBindName, | |
| StartsExpanded = prop_meta.auto_expand or self.new_item_idx and idx == self.new_item_idx, | |
| prop_id = prop_meta.id, | |
| read_only = prop_meta.read_only, | |
| item_addr = addr, | |
| list_context = self.context, | |
| parent_obj_context = self.panel.context, | |
| }, self.idList, item_context) | |
| panel:Open() | |
| self.connection:BindObj(item_context, { self.context, idx }) | |
| end | |
| end | |
| for _, panel in ipairs(old_panels) do | |
| panel:delete() | |
| end | |
| -- setup selection, etc. | |
| list:SetVisible(#item_data > 0) | |
| if self.new_item_idx then | |
| list:SetFocus(true) | |
| list:SetSelection(self.new_item_idx) | |
| self.new_item_idx = false | |
| else | |
| local sel = {} | |
| for _, addr in ipairs(old_sel) do | |
| local new_idx = table.findfirst(list, function(idx, item) return item.item_addr == addr end) | |
| if new_idx then | |
| table.insert(sel, new_idx) | |
| end | |
| end | |
| list:SetSelection(sel) | |
| end | |
| self:QueueReassignFocusOrders() | |
| self.idValueText:SetText(string.format("(%d objects)", #item_data)) | |
| end | |
| function GedPropNestedList:UpdatePropertyNames(internal) | |
| for _, prop_panel in ipairs(self.idList) do | |
| prop_panel:UpdatePropertyNames(internal) | |
| end | |
| GedPropEditor.UpdatePropertyNames(self, internal) | |
| end | |
| ----- GedPropLinkedPresets | |
| GedPropEditors["linked_presets"] = "GedPropLinkedPresets" | |
| -- caption comes from prop_meta.help; Create/Delete buttons come from prop_meta.buttons (both set in Ged.lua) | |
| DefineClass.GedPropLinkedPresets = { | |
| __parents = { "GedPropHelp" }, | |
| LayoutMethod = "VList", | |
| } | |
| function GedPropLinkedPresets:CalcBackground() | |
| local base = GetDarkModeSetting() and RGB(32, 32, 32) or RGB(240, 240, 240) | |
| return InterpolateRGB(base, TextStyles.GedHighlight.TextColor, 1, 3) | |
| end | |
| function GedPropLinkedPresets:UpdateValue(initial) | |
| if not initial then return end | |
| local prop_meta = self.prop_meta | |
| for _, class in ipairs(prop_meta.preset_classes) do | |
| local suppress_props = {} | |
| for prop, value in pairs(prop_meta.suppress_props) do | |
| suppress_props[prop] = suppress_props[prop] or (value == true) | |
| end | |
| for prop, value in pairs(prop_meta.suppress_props[class]) do | |
| suppress_props[prop] = suppress_props[prop] or (value == true) | |
| end | |
| local panel_context = self.panel.context .. "." .. class | |
| local panel = GedPropPanel:new({ | |
| preset_class = class, | |
| FoldWhenHidden = true, | |
| Embedded = true, | |
| Collapsible = true, | |
| HideFirstCategory = true, | |
| RootObjectBindName = self.panel.RootObjectBindName, | |
| Title = "<style GedHighlight>" .. class, | |
| ActionsClass = "PropertyObject", | |
| Copy = "GedOpPropertyCopy", | |
| Paste = "GedOpPropertyPaste", | |
| SuppressProps = suppress_props, | |
| }, self, panel_context) | |
| panel:SetVisible(false) | |
| panel:Open() | |
| if self:ShouldShowButtonForFunc("GedRpcEditPreset") then | |
| local open_button = XTemplateSpawn("GedToolbarButtonSmall", panel.idTitleContainer) | |
| open_button:SetIcon("CommonAssets/UI/Ged/explorer.tga") | |
| open_button:SetRolloverText("Open Preset Editor") | |
| open_button.OnPress = function(button) | |
| button:SetFocus() | |
| panel.app:Send("GedRpcEditPreset", panel.context) | |
| button:SetFocus(false) | |
| end | |
| open_button:Open() | |
| end | |
| self.panel.app:Call("GedRpcBindLinkedPreset", panel_context, self.panel.context, class) | |
| end | |
| -- GedPropHelp.UpdateValue(self) -- no need to call, this type of property is virtual and has no value | |
| end | |
| function GedPropLinkedPresets:DetachForReuse() | |
| for i = #self, 1, -1 do | |
| local win = self[i] | |
| if IsKindOf(win, "GedPropPanel") then | |
| win:UnbindViews() | |
| win:delete() | |
| end | |
| end | |
| GedPropHelp.DetachForReuse(self) | |
| end | |
| ----- GedPropPrimitiveList base class | |
| -- Override ConvertToText and ConvertFromText to validate and display values | |
| DefineClass.GedPropPrimitiveListDragAndDrop = { | |
| __parents = { "GedPropListDragAndDrop" }, | |
| prop_parent_class = "GedPropPrimitiveList", | |
| } | |
| function GedPropPrimitiveListDragAndDrop:GetDragWindowTextControl(control) | |
| return control.idEdit or control.idCombo and control.idCombo.idEdit | |
| end | |
| function GedPropPrimitiveListDragAndDrop:OnDrop(drag_win, pt, drag_source_win) | |
| if self:GetDragAndDropError() then return end | |
| local list = ged_drag_target | |
| local prop_list = GetParentOfKind(list, self.prop_parent_class) | |
| local sel = list:GetSelection() | |
| if #sel == 0 then return end | |
| local sel_map = {} | |
| for _, sel_idx in ipairs(sel) do | |
| sel_map[sel_idx] = true | |
| end | |
| local drop_target_idx = table.find(list, ged_drop_target) | |
| assert(drop_target_idx) | |
| if not drop_target_idx then return end | |
| if ged_drop_type == "Down" then | |
| if sel[1] > drop_target_idx then | |
| drop_target_idx = Min(#prop_list.list_values, drop_target_idx + 1) | |
| end | |
| elseif sel[1] < drop_target_idx then | |
| drop_target_idx = Max(1, drop_target_idx - 1) | |
| end | |
| local values = {} | |
| local i = 1 | |
| local values_cnt = 0 | |
| for _, value in ipairs(prop_list.list_values) do | |
| if values_cnt == drop_target_idx - 1 then | |
| break | |
| end | |
| if not sel_map[i] then | |
| values_cnt = values_cnt + 1 | |
| values[values_cnt] = value | |
| end | |
| i = i + 1 | |
| end | |
| local new_selection = {} | |
| for _, sel_idx in ipairs(sel) do | |
| values_cnt = values_cnt + 1 | |
| values[values_cnt] = prop_list.list_values[sel_idx] | |
| new_selection[#new_selection + 1] = values_cnt | |
| end | |
| for j = i, #prop_list.list_values do | |
| if not sel_map[j] then | |
| values_cnt = values_cnt + 1 | |
| values[values_cnt] = prop_list.list_values[j] | |
| end | |
| end | |
| prop_list.list_values = values | |
| prop_list:SetProp(values) | |
| prop_list:UpdateControls() | |
| list:SetSelection(new_selection) | |
| 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 ItemId(item) | |
| if type(item) == "table" then | |
| return item.id or (item.value ~= nil and item.value) | |
| end | |
| return item | |
| end | |
| DefineClass.GedPropPrimitiveList = { | |
| __parents = { "GedPropEditor" }, | |
| Translate = false, | |
| list_values = false, | |
| new_item_default = false, | |
| choice_items = false, | |
| choice_items_status = false, | |
| choice_items_fetch_time = false, | |
| } | |
| function GedPropPrimitiveList:Init(parent, context, prop_meta) | |
| self.idLabelHost:SetDock("top") | |
| local add_button = XTemplateSpawn("GedToolbarButtonSmall", self.idLabelHost) | |
| add_button:SetIcon("CommonAssets/UI/Ged/new.tga") | |
| add_button:SetRolloverText("Add new") | |
| add_button:SetId("idNewElement") | |
| add_button:SetEnabled(not prop_meta.read_only) | |
| add_button.OnPress = function(button) | |
| local focus = terminal.desktop.keyboard_focus | |
| button:SetFocus() | |
| if focus and focus ~= self.idContainer and focus:IsWithin(self.idContainer) then | |
| -- search for item element so we can find its index | |
| while focus.parent ~= self.idContainer do | |
| focus = focus.parent | |
| end | |
| self:NewElement(table.find(self.idContainer, focus)) | |
| button:SetFocus(false) | |
| return | |
| end | |
| self:NewElement(table.max(self.idContainer:GetSelection())) | |
| button:SetFocus(false) | |
| end | |
| local move_down = XTemplateSpawn("GedToolbarButtonSmall", self.idLabelHost) | |
| move_down:SetIcon("CommonAssets/UI/Ged/down.tga") | |
| move_down:SetRolloverText("Move down\n(select an item first)") | |
| move_down:SetId("idDown") | |
| move_down:SetEnabled(false) | |
| move_down.OnPress = function(button) | |
| local sel = table.copy(self.idContainer:GetSelection()) | |
| local values = table.copy(self.list_values) | |
| local focus | |
| for i = #values - 1, 1, -1 do | |
| local idx = table.find(sel, i) | |
| if idx and not table.find(sel, i + 1) then | |
| values[i], values[i + 1] = values[i + 1], values[i] | |
| sel[idx] = i + 1 | |
| if self.idContainer[i]:IsFocused(true) then | |
| self.idContainer[i]:SetFocus(false) | |
| focus = i + 1 | |
| end | |
| end | |
| end | |
| self.list_values = values | |
| self:SetProp(values) | |
| self:UpdateControls() | |
| if focus then | |
| self.idContainer[focus][1]:SetFocus() | |
| end | |
| self.idContainer:SetSelection(sel) | |
| end | |
| local move_up = XTemplateSpawn("GedToolbarButtonSmall", self.idLabelHost) | |
| move_up:SetIcon("CommonAssets/UI/Ged/up.tga") | |
| move_up:SetRolloverText("Move up\n(select an item first)") | |
| move_up:SetId("idUp") | |
| move_up:SetEnabled(false) | |
| move_up.OnPress = function(button) | |
| local sel = table.copy(self.idContainer:GetSelection()) | |
| local values = table.copy(self.list_values) | |
| local focus | |
| for i = 2, #values do | |
| local idx = table.find(sel, i) | |
| if idx and not table.find(sel, i - 1) then | |
| values[i], values[i - 1] = values[i - 1], values[i] | |
| sel[idx] = i - 1 | |
| if self.idContainer[i]:IsFocused(true) then | |
| self.idContainer[i]:SetFocus(false) | |
| focus = i - 1 | |
| end | |
| end | |
| end | |
| self.list_values = values | |
| self:SetProp(values) | |
| self:UpdateControls() | |
| if focus then | |
| self.idContainer[focus][1]:SetFocus() | |
| end | |
| self.idContainer:SetSelection(sel) | |
| end | |
| XList:new({ | |
| Id = "idContainer", | |
| Dock = "bottom", | |
| BorderWidth = 0, | |
| MultipleSelection = true, | |
| OnSelection = function(list, focused_item, selection) | |
| self.idUp:SetEnabled(next(selection) and not prop_meta.read_only) | |
| self.idDown:SetEnabled(next(selection) and not prop_meta.read_only) | |
| end | |
| }, self) | |
| self:WithItems(function(items) | |
| self:ValidatePropMetaAndInitDefault() | |
| end) | |
| end | |
| function GedPropPrimitiveList:DetachForReuse() | |
| self.idContainer:SetSelection(false) | |
| for _, item in ipairs(self.idContainer) do | |
| if rawget(item, "idCombo") then | |
| item.idCombo:UpdateMRUList() | |
| end | |
| end | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| function GedPropPrimitiveList:WithItems(f) | |
| if self.choice_items_status == "fetched" then | |
| f(self.choice_items) | |
| else | |
| CreateRealTimeThread(function() | |
| self:WaitForItems() | |
| if self.window_state ~= "destroying" then | |
| f(self.choice_items) | |
| end | |
| end) | |
| end | |
| end | |
| function GedPropPrimitiveList:WaitForItems() | |
| assert(CanYield()) | |
| if self.choice_items_status == false or self.choice_items_status == "fetched" and self.choice_items_fetch_time ~= RealTime() then | |
| self.choice_items_status = { } | |
| CreateRealTimeThread(function() | |
| local old_status = self.choice_items_status | |
| repeat | |
| self.choice_items = self:GetChoiceItems() | |
| until self.choice_items ~= "timeout" | |
| self.choice_items_status = "fetched" | |
| self.choice_items_fetch_time = RealTime() | |
| for _, thread in ipairs(old_status) do | |
| Wakeup(thread) | |
| end | |
| end) | |
| elseif self.choice_items_status == "fetched" then | |
| return self.choice_items | |
| else | |
| assert( type(self.choice_items_status) == "table" ) | |
| end | |
| table.insert(self.choice_items_status, CurrentThread()) | |
| WaitWakeup() | |
| return self.choice_items | |
| end | |
| function GedPropPrimitiveList:ReassignFocusOrders(x, y) | |
| self.idUp:SetFocusOrder(point(x, y)) | |
| y = y + 1 | |
| self.idDown:SetFocusOrder(point(x, y)) | |
| y = y + 1 | |
| self.idNewElement:SetFocusOrder(point(x, y)) | |
| y = y + 1 | |
| local container = self.idContainer | |
| for _, edit in ipairs(container) do | |
| edit:SetFocusOrder(point(x, y)) | |
| y = y + 1 | |
| end | |
| return y | |
| end | |
| function GedPropPrimitiveList:ValidatePropMetaAndInitDefault() | |
| assert(self.choice_items_status == "fetched") | |
| self:SetPropResult("") | |
| -- Validate choice items | |
| local choice_items = self.choice_items | |
| if choice_items then | |
| for _, item in ipairs(choice_items) do | |
| self:CheckUpdateError(self:ValidateValue(item.value), "'items' contains invalid entries.") | |
| end | |
| end | |
| -- Validate and cache item_default | |
| local item_default = self.prop_meta.item_default or self:DefaultValue() | |
| if item_default and choice_items then | |
| local value_default = choice_items and choice_items[1] and choice_items[1].value or item_default | |
| self.new_item_default = value_default | |
| else | |
| self.new_item_default = item_default | |
| end | |
| self:CheckUpdateError(self:ValidateValue(self.new_item_default), "Invalid 'item_default'") | |
| end | |
| function GedPropPrimitiveList:ValidateWeights() | |
| if #self.list_values == 0 then | |
| return true | |
| end | |
| local weight_sum = 0 | |
| for _, item in ipairs(self.list_values) do | |
| local _, weight = self:ResolveItem(item) | |
| if weight then | |
| weight_sum = weight_sum + weight | |
| end | |
| end | |
| return weight_sum > 0 | |
| end | |
| function GedPropPrimitiveList:CheckWeightsError() | |
| if self.prop_meta and self.prop_meta.weights then | |
| self:CheckUpdateError(self:ValidateWeights(), "The sum of all weights has to be more than zero") | |
| end | |
| end | |
| function GedPropPrimitiveList:CheckUpdateError(expr, err) | |
| if not expr then | |
| self:SetPropResult(err) | |
| end | |
| end | |
| function GedPropPrimitiveList:NewElement(idx, copy_item_value) | |
| local max_items = self.prop_meta.max_items or -1 | |
| if max_items ~= -1 and #self.list_values >= max_items then | |
| return self:SetPropResult("Can not add more than "..tostring(max_items).." items to the list.") | |
| end | |
| self:WithItems(function() | |
| local list_values = self.list_values or {} | |
| local new_item_value = copy_item_value and list_values[idx] or self.new_item_default | |
| idx = (idx or #list_values) + 1 | |
| local new_item = self:WrapValue(new_item_value) | |
| table.insert(list_values, idx, new_item) | |
| self.list_values = list_values | |
| self:UpdateControls() -- will spawn the new control | |
| local last = self.idContainer[idx] | |
| if last then | |
| last:SetFocus() | |
| end | |
| self:SetProp(list_values) | |
| self.idContainer:SetSelection(false) | |
| end) | |
| end | |
| function GedPropPrimitiveList:RemoveElement(idx) | |
| if self:IsFocused(true) then | |
| assert(IsKindOf(self.desktop.keyboard_focus, "XButton")) | |
| end | |
| local list_values = self.list_values or {} | |
| if #list_values >= idx then | |
| table.remove(list_values, idx) | |
| local container = self.idContainer | |
| container[idx]:delete() | |
| self:UpdateControls() | |
| self:SetProp(list_values) | |
| end | |
| local max_items = self.prop_meta.max_items or -1 | |
| if max_items ~= -1 and #self.list_values <= max_items then | |
| self:SetPropResult() | |
| end | |
| end | |
| function GedPropPrimitiveList:ResolveItem(item) | |
| local prop_meta = self.prop_meta or empty_table | |
| if not item or not prop_meta.weights then | |
| return item | |
| end | |
| local value_key = prop_meta.value_key or "value" | |
| local weight_key = prop_meta.weight_key or "weight" | |
| return item[value_key], item[weight_key] | |
| end | |
| function GedPropPrimitiveList:SetElement(idx, value) | |
| local list_values = self.list_values or {} | |
| local old_value, old_weight = self:ResolveItem(list_values[idx]) | |
| local new_value, new_weight = self:ResolveItem(value) | |
| local has_changed = old_value ~= new_value or old_weight ~= new_weight | |
| if idx and has_changed then | |
| list_values[idx] = value | |
| self.list_values = list_values | |
| self:SetProp(list_values) | |
| self:OnListItemValueChanged(idx) | |
| end | |
| end | |
| function GedPropPrimitiveList:GetChoiceItems() | |
| local items = self.prop_meta.items | |
| if not items or #items <= 0 then | |
| return false | |
| end | |
| local t = {} | |
| for key, value in ipairs(items) do | |
| -- support most popular item formats | |
| if type(value) == "table" then | |
| table.insert(t, { text = ItemText(value), value = ItemId(value) }) | |
| else | |
| table.insert(t, { text = self:ConvertToText(value), value = value }) | |
| end | |
| end | |
| return t | |
| end | |
| function GedPropPrimitiveList:WrapValue(value, weight) | |
| local prop_meta = self.prop_meta or empty_table | |
| assert(not (type(value) == "table" and not (prop_meta.editor == "T_list")), "Weighted list - Wrapping a table value") | |
| if not prop_meta.weights then | |
| return value | |
| end | |
| local value_key = prop_meta.value_key or "value" | |
| local weight_key = prop_meta.weight_key or "weight" | |
| return {[value_key] = value, [weight_key] = weight or self:DefaultWeightValue()} | |
| end | |
| function GedPropPrimitiveList:UpdateItem(container, item, value, weight_ctrl) | |
| self:SetElement( | |
| table.find(container, item), | |
| self:WrapValue(value, weight_ctrl and weight_ctrl:GetNumber()) | |
| ) | |
| end | |
| function GedPropPrimitiveList:CreateItemEditor(container, choice_items, idx) | |
| local item = XListItem:new({ | |
| BorderWidth = 0, | |
| Padding = box(2, 2, 2, 2), | |
| SelectionBackground = RGB(204, 232, 255), | |
| SetFocus = function(item, focus) | |
| if item.idEdit then | |
| return item.idEdit:SetFocus(focus) | |
| elseif item.idCombo then | |
| return item.idCombo:SetFocus(focus) | |
| end | |
| return XWindow.SetFocus(item, focus) | |
| end, | |
| SetValue = function(item, value) | |
| local actual_value, weight = self:ResolveItem(value) | |
| if weight and item.idWeightEdit then | |
| item.idWeightEdit:SetNumber(weight) | |
| end | |
| if item.idEdit then | |
| item.idEdit:SetText(self:ConvertToText(actual_value)) | |
| elseif item.idCombo then | |
| item.idCombo:SetValue(actual_value) | |
| end | |
| self:OnListItemValueChanged(idx) | |
| end, | |
| SetFocusOrder = function(item, order) | |
| if item.idEdit then | |
| item.idEdit:SetFocusOrder(order) | |
| elseif item.idCombo then | |
| item.idCombo:SetFocusOrder(order) | |
| end | |
| end, | |
| idEdit = false, | |
| idCombo = false, | |
| IdNode = true, -- capture the nodes above | |
| idWeightEdit = false | |
| }, container) | |
| local control | |
| local weight_control | |
| if self.prop_meta.weights then | |
| local weight_label = XText:new({ | |
| Id = "idWeightLabel", | |
| Dock = "right", | |
| MinWidth = 50, | |
| ZOrder = 3, -- Order in the component tree (default = 1) | |
| Margins = box(5, 0, 0, 0) | |
| }, item):SetText("Weight:") | |
| weight_control = XNumberEdit:new({ | |
| Id = "idWeightEdit", | |
| MinWidth = 40, | |
| Dock = "right", | |
| ZOrder = 2, -- Order in the component tree (default = 1) | |
| Margins = box(2, 0, 2, 0) | |
| }, item) | |
| weight_control:SetEnabled(not self.prop_meta.read_only) | |
| weight_control.OnTextChanged = function(weight_edit) | |
| self:DeleteThread("SetElementWeightThread") | |
| self:CreateThread("SetElementWeightThread", function() | |
| Sleep(250) | |
| local value = (item.idEdit and self:ConvertFromText(item.idEdit:GetText())) or (item.idCombo and item.idCombo:GetValue()) | |
| self:UpdateItem(container, item, value, weight_edit) | |
| end) | |
| end | |
| end | |
| if choice_items then | |
| control = XCombo:new({ | |
| Id = "idCombo", | |
| RefreshItemsOnOpen = true, | |
| OnRequestItems = function() return self:WaitForItems() end, | |
| DefaultValue = self.new_item_default, | |
| ArbitraryValue = self.prop_meta.arbitraty_value, | |
| OnValueChanged = function(combo, value) | |
| self:UpdateItem(container, item, value, weight_control) | |
| end, | |
| MRUStorageId = self.prop_meta.mru_storage_id, | |
| MRUCount = self.prop_meta.show_recent_items, | |
| VirtualItems = true, | |
| }, item) | |
| control:SetEnabled(not self.prop_meta.read_only) | |
| control.OnSetFocus = function(control) | |
| container:SetSelection(idx) | |
| XCombo.OnSetFocus(control) | |
| end | |
| else | |
| control = self:CreateTextEditControl(item) | |
| control:SetEnabled(not self.prop_meta.read_only) | |
| control.OnTextChanged = function(edit) | |
| self:DeleteThread("SetElementThread") | |
| self:CreateThread("SetElementThread", function() | |
| Sleep(250) | |
| local value = self:ConvertFromText(edit:GetText()) | |
| self:UpdateItem(container, item, value, weight_control) | |
| end) | |
| end | |
| control.OnKillFocus = function(edit, new_focus) | |
| local value = self:ConvertFromText(edit:GetText()) | |
| self:UpdateItem(container, item, value, weight_control) | |
| return XTextEditor.OnKillFocus(edit, new_focus) | |
| end | |
| control.OnSetFocus = function(control) | |
| container:SetSelection(idx) | |
| XTextEditor.OnSetFocus(control) | |
| end | |
| end | |
| self:CreateAdditionalButtons(item, table.find(container, item)) | |
| local delete_button = XTemplateSpawn("GedToolbarButtonSmall", item) | |
| delete_button:SetIcon("CommonAssets/UI/Ged/delete.tga") | |
| delete_button:SetRolloverText("Delete") | |
| delete_button.OnPress = function(button) | |
| button:SetFocus() | |
| self:RemoveElement(table.find(container, item)) | |
| button:SetFocus(false) | |
| end | |
| delete_button:SetEnabled(not self.prop_meta.read_only) | |
| local duplicate_button = XTemplateSpawn("GedToolbarButtonSmall", item) | |
| duplicate_button:SetIcon("CommonAssets/UI/Ged/duplicate.tga") | |
| duplicate_button:SetRolloverText("Duplicate") | |
| duplicate_button.OnPress = function(button) | |
| button:SetFocus() | |
| self:NewElement(table.find(container, item), "copy_item_value") | |
| button:SetFocus(false) | |
| end | |
| duplicate_button:SetEnabled(not self.prop_meta.read_only) | |
| if self.prop_meta.per_item_buttons then | |
| self:SpawnCustomButtons(container, item, idx) | |
| end | |
| GedPropPrimitiveListDragAndDrop:new({ | |
| Id = "idDragAndDrop", | |
| Dock = "left", | |
| Padding = box(2, 1, 2, 1), | |
| List = self.idContainer, | |
| Item = item, | |
| }, item) | |
| item.idDragAndDrop:SetEnabled(not self.prop_meta.read_only) | |
| item:Open() | |
| end | |
| function GedPropPrimitiveList:SpawnCustomButtons(container, item, idx) | |
| for _, button_props in ipairs(self.prop_meta.per_item_buttons) do | |
| local custom_button = XTemplateSpawn("GedToolbarButtonSmall", item) | |
| custom_button:SetIcon(button_props.icon) | |
| custom_button:SetRolloverText(button_props.name) | |
| custom_button.OnPress = function(button) | |
| button:SetFocus() | |
| if button_props.func then | |
| self.panel:Op("GedPropEditorButton", self.panel.context, self.panel.RootObjectBindName or "root", | |
| self.prop_meta.id, button_props.name, button_props.func, button_props.param, idx) | |
| end | |
| button:SetFocus(false) | |
| end | |
| custom_button:SetEnabled(not self.prop_meta.read_only) | |
| end | |
| end | |
| function GedPropPrimitiveList:OnListItemValueChanged(idx) | |
| self:CheckWeightsError() | |
| end | |
| function GedPropPrimitiveList:CreateTextEditControl(parent) | |
| return XEdit:new({ Id = "idEdit" }, parent) | |
| end | |
| function GedPropPrimitiveList:CreateAdditionalButtons(parent, idx) | |
| end | |
| function GedPropPrimitiveList:UpdateControls() | |
| assert(self.choice_items_status == "fetched") | |
| local list_values = self.list_values | |
| local container = self.idContainer | |
| if #list_values ~= #container then | |
| self:QueueReassignFocusOrders() | |
| end | |
| -- remove unnecessary items | |
| while #list_values < #container do | |
| container[#list_values+1]:delete() | |
| end | |
| -- spawn additional items | |
| local choice_items = self.choice_items | |
| while #list_values > #container do | |
| self:CreateItemEditor(container, choice_items, #container + 1) | |
| end | |
| -- update controls' values | |
| for idx, item in ipairs(container) do | |
| item:SetValue(list_values[idx]) | |
| end | |
| self:CheckWeightsError() | |
| Msg("XWindowRecreated", self) | |
| end | |
| function GedPropPrimitiveList:UpdateValue() | |
| local data = self:GetProp() or {} | |
| local focus = terminal.desktop.keyboard_focus | |
| if (not focus or not focus:IsWithin(self)) or #data ~= #(self.list_values or empty_table) then | |
| self.list_values = type(data) == "table" and table.copy(data) or {} | |
| self:WithItems(function() | |
| self:UpdateControls() | |
| end) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropPrimitiveList:ConvertFromText(text) | |
| return text | |
| end | |
| function GedPropPrimitiveList:ConvertToText(value) | |
| return value | |
| end | |
| function GedPropPrimitiveList:DefaultValue() | |
| return false | |
| end | |
| function GedPropPrimitiveList:DefaultWeightValue() | |
| return self.prop_meta.weight_default or 100 | |
| end | |
| function GedPropPrimitiveList:ValidateValue(value) | |
| return true | |
| end | |
| ----- GedPropPresetIdList | |
| GedPropEditors["preset_id_list"] = "GedPropPresetIdList" | |
| DefineClass.GedPropPresetIdList = { | |
| __parents = { "GedPropPrimitiveList" }, | |
| } | |
| function GedPropPresetIdList:GetChoiceItems() | |
| return self.panel.connection:Call("rfnGetPresetItems", self.panel.context, self.prop_meta.id) | |
| end | |
| function GedPropPresetIdList:CreateAdditionalButtons(parent, idx) | |
| if self:ShouldShowButtonForFunc("GedRpcEditPreset") then | |
| local open_button = XTemplateSpawn("GedToolbarButtonSmall", parent) | |
| open_button:SetIcon("CommonAssets/UI/Ged/explorer.tga") | |
| open_button:SetRolloverText("Open Preset Editor", self.prop_meta.preset_class) | |
| open_button.OnPress = function(button) | |
| button:SetFocus() | |
| self.panel.app:Send("GedRpcEditPreset", self.panel.context, self.prop_meta.id, self.list_values[idx]) | |
| button:SetFocus(false) | |
| end | |
| end | |
| if self.prop_meta.editor_preview then | |
| GedTextPanel:new({ | |
| Title = "", | |
| Dock = "bottom", | |
| ZOrder = -1, | |
| Format = self.prop_meta.editor_preview, | |
| Shorten = true, | |
| MaxHeight = 50, | |
| }, parent):SetContext(self.prop_meta.id..".ReferencedPreset"..tostring(idx)) | |
| end | |
| end | |
| function GedPropPresetIdList:OnListItemValueChanged(idx) | |
| GedPropPrimitiveList:OnListItemValueChanged(idx) | |
| if self.prop_meta.editor_preview then | |
| self.panel.app:Send("GedRpcBindPreset", self.prop_meta.id..".ReferencedPreset"..tostring(idx), self.panel.context, self.prop_meta.id, self.list_values[idx]) | |
| end | |
| end | |
| ----- GedPropNumberList | |
| GedPropEditors["number_list"] = "GedPropNumberList" | |
| DefineClass.GedPropNumberList = { | |
| __parents = { "GedPropPrimitiveList"}, | |
| } | |
| function GedPropNumberList:ConvertFromText(text) | |
| return tonumber(text) or 0 | |
| end | |
| function GedPropNumberList:ConvertToText(value) | |
| if type(value) ~= "number" then return "" end | |
| return tostring(value) | |
| end | |
| function GedPropNumberList:DefaultValue() | |
| return 0 | |
| end | |
| function GedPropNumberList:ValidateValue(value) | |
| return type(value) == "number" | |
| end | |
| ----- GedPropStringList | |
| GedPropEditors["string_list"] = "GedPropStringList" | |
| DefineClass.GedPropStringList = { | |
| __parents = { "GedPropPrimitiveList"}, | |
| } | |
| function GedPropStringList:DefaultValue() | |
| return "" | |
| end | |
| function GedPropStringList:ValidateValue(value) | |
| return type(value) == "string" | |
| end | |
| ----- GedPropTList | |
| GedPropEditors["T_list"] = "GedPropTList" | |
| DefineClass.GedPropTList = { | |
| __parents = { "GedPropPrimitiveList"}, | |
| } | |
| function GedPropTList:Init(parent, context, prop_meta) | |
| assert(not prop_meta.weights, "T_list doesn't support weights") | |
| end | |
| function GedPropTList:DefaultValue() | |
| return "" | |
| end | |
| function GedPropTList:ValidateValue(value) | |
| return type(value) == "string" | |
| end | |
| function GedPropTList:CreateTextEditControl(parent) | |
| local control = XMultiLineEdit:new({ | |
| Id = "idEdit", | |
| MinVisibleLines = 1, | |
| MaxVisibleLines = 30, | |
| Translate = true, | |
| }, parent) | |
| control:SetPlugins({ "XSpellcheckPlugin" }) | |
| return control | |
| end | |
| function GedPropTList:ConvertToText(value) | |
| return GedPropValueToT(value) | |
| end | |
| function GedPropTList:ConvertFromText(value) | |
| return GedTToPropValue(value, "") | |
| end | |
| ----- GedPropListPicker, base class for picking item(s) from a list (GedPropTextPicker / GedPropTexturePicker) | |
| DefineClass.GedPropListPicker = { | |
| __parents = {"GedPropEditor"}, | |
| } | |
| function GedPropListPicker:Init() | |
| local horizontal = self.prop_meta.horizontal | |
| if not horizontal then | |
| self.idLabelHost:SetDock("top") | |
| end | |
| self.idResetToDefault:SetVisibleInstant(false) | |
| XList:new({ | |
| Id = "idList", | |
| VScroll = "idScroll", | |
| Padding = horizontal and box(2, 1, 2, 0) or empty_box, | |
| OnSelection = function(list, selected_item, selected_items) | |
| self:SetValue(selected_items) | |
| end, | |
| MultipleSelection = self.prop_meta.multiple or false, | |
| MaxRowsVisible = self.prop_meta.horizontal and 0 or (self.prop_meta.max_rows or 1), | |
| OnDoubleClick = function(list, idx) | |
| self.panel.app:Send("GedPickerItemDoubleClicked", self.panel.context, self.prop_meta.id, ItemId(list[idx].item)) | |
| end, | |
| }, self) | |
| self:SpawnItems() | |
| if not self.prop_meta.horizontal then | |
| XSleekScroll:new({ | |
| Id = "idScroll", | |
| Target = "node", | |
| Dock = "right", | |
| Margins = box(2, 0, 0, 0), | |
| AutoHide = true, | |
| }, self.idList) | |
| end | |
| -- filtering | |
| if self.prop_meta.filter_by_prop and self.prop_meta.filter_by_prop ~= "" then | |
| local filter_editor = self.panel:LocateEditorById(self.prop_meta.filter_by_prop) | |
| filter_editor.idEdit.OnTextChanged = function(edit) | |
| self:DeleteThread("filter_thread") | |
| self:CreateThread("filter_thread", function() | |
| Sleep(150) | |
| if self.window_state ~= "destroying" then | |
| self:FilterItems() | |
| end | |
| end) | |
| end | |
| end | |
| end | |
| function GedPropListPicker:FilterItems() | |
| local filter_editor = self.panel:LocateEditorById(self.prop_meta.filter_by_prop) | |
| if not filter_editor then | |
| return false | |
| end | |
| local filter_string = filter_editor.idEdit:GetText() | |
| if (filter_string == "" or type(filter_string) ~= "string") and string.lower(filter_string) == self.last_filter_string then | |
| self.last_filter_string = filter_string | |
| return false | |
| end | |
| filter_string = string.lower(filter_string) | |
| -- Early return if the filter hasn't changed | |
| if filter_string == self.last_filter_string then | |
| return false | |
| end | |
| local fill_and_sort_cache = false | |
| if not self.sorted_items_cache then | |
| self.sorted_items_cache = {} | |
| fill_and_sort_cache = true | |
| end | |
| local starting_with = {} | |
| local idx = 1 | |
| -- Item data is in self.idList[idx].item | |
| for item_idx, item in ipairs(self.idList) do | |
| if item.Id ~= "idScroll" then | |
| local item_text_lower = ItemText(item.item):strip_tags():lower() | |
| local visible = filter_string == "" or string.find(item_text_lower, filter_string, 1, true) | |
| if filter_string ~= "" and string.starts_with(item_text_lower, filter_string) then | |
| table.insert(starting_with, item) | |
| end | |
| item:SetVisible(visible) | |
| item:SetDock(not visible and "ignore" or false) | |
| idx = idx + 1 | |
| end | |
| -- Fill items cache | |
| if fill_and_sort_cache then | |
| table.insert(self.sorted_items_cache, item) | |
| end | |
| end | |
| -- -- Reorder the items in the list | |
| -- Put items starting with the filter word first | |
| local starting_with_count = #starting_with | |
| for i = 1, starting_with_count do | |
| self.idList[i] = starting_with[i] | |
| end | |
| -- Then add the rest from the cache | |
| local cache_count = #self.sorted_items_cache | |
| local list_idx = 1 | |
| for i = 1, cache_count do | |
| -- Skip the starting_with items that were prepended to the list | |
| local skip = false | |
| for _, item in ipairs(starting_with) do | |
| if item == self.sorted_items_cache[i] then | |
| skip = true | |
| break | |
| end | |
| end | |
| if not skip then | |
| self.idList[starting_with_count + list_idx] = self.sorted_items_cache[i] | |
| list_idx = list_idx + 1 | |
| end | |
| end | |
| -- Reselect items that were selected before the reodering | |
| local selection = {} | |
| for idx, item in ipairs(self.idList) do | |
| if item.selected then | |
| table.insert(selection, idx) | |
| item:SetSelected(false) | |
| end | |
| end | |
| self.idList:SetSelection(selection, not "notify") | |
| -- Save the previous filter string | |
| self.last_filter_string = filter_string | |
| end | |
| function GedPropListPicker:DetachForReuse() | |
| -- revert changes made by Layout below, so it gets recalculated | |
| self.idList:SetMinHeight(nil) | |
| self.idList:SetMaxHeight(nil) | |
| self.idList:SetMaxRowsVisible(self.prop_meta.horizontal and 0 or (self.prop_meta.max_rows or 1)) | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| function GedPropListPicker:Layout(x, y, width, height) | |
| if not self.prop_meta.max_rows and not self.prop_meta.horizontal and self.idList.MaxHeight == XList.MaxHeight then | |
| -- size vertically up to take up all unused space | |
| local app = self.panel.app | |
| local new_height = MulDivTrunc(height + app.parent.content_box:sizey() - app.measure_height, 1000, self.scale:y()) | |
| self.idList:SetMinHeight(new_height) | |
| self.idList:SetMaxHeight(new_height) | |
| self.idList:SetMaxRowsVisible(0) | |
| end | |
| return GedPropEditor.Layout(self, x, y, width, height) | |
| end | |
| function GedPropListPicker:SetValue(selected) | |
| local texts = {} | |
| for _, idx in ipairs(selected) do | |
| -- Item data is in self.idList[idx].item | |
| texts[#texts + 1] = ItemId(self.idList[idx].item) | |
| end | |
| self:SetProp(self.prop_meta.multiple and texts or texts[1] or "") | |
| end | |
| function GedPropListPicker:UpdateValue() | |
| -- Item data is in self.idList[idx].item | |
| local ui_items = self.idList or empty_table | |
| local tables = type(ui_items[1] and ui_items[1].item) == "table" | |
| local value = self.prop_meta.multiple and self:GetProp() or { self:GetProp() } | |
| local selection = {} | |
| for _, val in ipairs(value or empty_table) do | |
| local selection_idx | |
| -- Find the index of the item with the selected value | |
| for item_idx, ui_item in ipairs(ui_items) do | |
| if ui_item.Id ~= "idScroll" then | |
| if ui_item.item == val or tables and (ui_item.item.id == val or ui_item.item.value == val) then | |
| selection_idx = item_idx | |
| break | |
| end | |
| end | |
| end | |
| if selection_idx then | |
| selection[#selection + 1] = selection_idx | |
| end | |
| end | |
| self.idList:SetSelection(selection, not "notify") | |
| self:FilterItems() | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropListPicker:SpawnItems() | |
| -- create the XListItems in self.idList here | |
| end | |
| ----- GedPropTextPicker | |
| GedPropEditors["text_picker"] = "GedPropTextPicker" | |
| DefineClass.GedPropTextPicker = { | |
| __parents = { "GedPropListPicker" }, | |
| } | |
| function GedPropTextPicker:Init() | |
| self.idList:SetLayoutMethod(self.prop_meta.horizontal and "HWrap" or "VList") | |
| end | |
| function GedPropTextPicker:SpawnItems() | |
| local list = self.idList | |
| local selectable = not self.prop_meta.read_only | |
| local has_bookmarks = self.prop_meta.bookmark_fn | |
| local font = self.prop_meta.small_font and "GedSmall" or "GedDefault" | |
| list:Clear() | |
| for _, item in ipairs(self.prop_meta.items or empty_table) do | |
| local context = { text = ItemText(item), help = item.help, font = font, selectable = selectable, documentation = item.documentation } | |
| if has_bookmarks then | |
| context.bookmarked = item.bookmarked and true or false | |
| end | |
| local control | |
| if self.prop_meta.virtual_items then | |
| control = NewXVirtualContent(list, context, "GedTextPickerItem") | |
| else | |
| control = XTemplateSpawn("GedTextPickerItem", list, context) | |
| end | |
| control.item = item | |
| end | |
| end | |
| ----- GedPropTexturePicker | |
| GedPropEditors["texture_picker"] = "GedPropTexturePicker" | |
| DefineClass.GedPropTexturePicker = { | |
| __parents = { "GedPropListPicker" }, | |
| } | |
| function GedPropTexturePicker:Init(parent, context, prop_meta) | |
| self.idList:SetLayoutMethod("HWrap") | |
| if prop_meta.alt_prop then | |
| self.idList.OnMouseButtonDown = function(list, pt, button) | |
| if button == "L" and terminal.IsKeyPressed(const.vkAlt) then | |
| local item_idx = list:GetItemAt(pt) | |
| self.panel:Op("GedSetProperty", self.panel.context, prop_meta.alt_prop, ItemId(prop_meta.items[item_idx])) | |
| return "break" | |
| end | |
| return XList.OnMouseButtonDown(list, pt, button) | |
| end | |
| end | |
| end | |
| function GedPropTexturePicker:SpawnItems() | |
| local list = self.idList | |
| local prop_meta = self.prop_meta | |
| local enabled = not prop_meta.read_only | |
| list:Clear() | |
| for _, item in ipairs(prop_meta.items or empty_table) do | |
| local listitem = XListItem:new({ | |
| RolloverText = item.help, | |
| RolloverTemplate = "GedPropRollover", | |
| RolloverBackground = RGBA(24, 123, 197, 255), | |
| FocusedBackground = RGBA(24, 123, 197, 255), | |
| Padding = box(2, 2, 2, 0), | |
| selectable = enabled, | |
| SetSelected = function(self, selected) | |
| if selected and #list.selection > 1 then | |
| local idx = table.find(list, self) | |
| local sel_idx = table.find(list.selection, idx) | |
| self.idSelectedNumber:SetText(string.format("#%d", sel_idx)) | |
| else | |
| self.idSelectedNumber:SetText("") | |
| end | |
| XListItem.SetSelected(self, selected) | |
| end, | |
| }, list) | |
| local image_parent = XWindow:new({ Dock = "top" }, listitem) | |
| local image = XImage:new({ | |
| BorderWidth = 1, | |
| ImageFit = prop_meta.thumb_height and "largest" or "smallest", | |
| MinWidth = prop_meta.thumb_size or prop_meta.thumb_width or 60, | |
| MaxWidth = prop_meta.thumb_size or prop_meta.thumb_width or 60, | |
| MinHeight = prop_meta.thumb_height, | |
| MaxHeight = prop_meta.thumb_height, | |
| BaseColorMap = prop_meta.base_color_map, | |
| }, image_parent) | |
| image:SetImage(item.image or item.value) | |
| local width, height = UIL.MeasureImage(image.Image) | |
| local new_width = MulDivRound(width, prop_meta.thumb_zoom or 100, 100) | |
| local new_starting_point = (width - new_width) / 2 | |
| image:SetImageRect(box(new_starting_point, new_starting_point, new_starting_point + new_width, new_starting_point + new_width)) | |
| if item.color then | |
| image:SetImageColor(item.color) | |
| end | |
| -- selected # text | |
| XText:new({ | |
| Id = "idSelectedNumber", | |
| TextStyle = "GedSmall", | |
| HAlign = "right", | |
| VAlign = "bottom", | |
| }, image_parent) | |
| XText:new({ | |
| Dock = "bottom", | |
| TextStyle = prop_meta.small_font and "GedSmall" or "GedDefault", | |
| MaxWidth = prop_meta.thumb_size or prop_meta.thumb_width or 60, | |
| VAlign = "center", | |
| TextHAlign = "center", | |
| Padding = box(2, 2, 2, 0), | |
| RolloverText = item.text, | |
| RolloverTemplate = "GedPropRollover", | |
| RolloverBackground = RGBA(24, 123, 197, 255), | |
| }, listitem):SetText(item.text) | |
| listitem.item = item | |
| end | |
| end | |
| ----- GedPropObjectPicker | |
| GedPropEditors["object"] = "GedPropObjectPicker" | |
| DefineClass.GedPropObjectPicker = { | |
| __parents = {"GedPropEditor"}, | |
| last_object = false, | |
| } | |
| function GedPropObjectPicker:Init(parent, context, prop_meta) | |
| XCombo:new({ | |
| Id = "idCombo", | |
| Items = false, | |
| RefreshItemsOnOpen = true, | |
| DefaultValue = self.prop_meta.default or "", | |
| ArbitraryValue = true, | |
| OnValueChanged = function(combo, value) | |
| self:ComboValueChanged(value) | |
| end, | |
| OnRequestItems = function(combo) | |
| return self.panel.connection:Call("rfnMapGetGameObjects", self.panel.context, self.prop_meta.id) | |
| end, | |
| MRUStorageId = self.prop_meta.mru_storage_id, | |
| MRUCount = self.prop_meta.show_recent_items, | |
| VirtualItems = true, | |
| }, self) | |
| self.idCombo:SetEnabled(not prop_meta.read_only) | |
| self.last_object = {} | |
| local inspect_button = XTemplateSpawn("GedToolbarButtonSmall", self) | |
| inspect_button:SetIcon("CommonAssets/UI/Ged/explorer.tga") | |
| inspect_button:SetRolloverText("Inspect Object") | |
| inspect_button.OnPress = function(button) | |
| button:SetFocus() | |
| self.panel.app:Send("GedRpcInspectObj", self.panel.context, prop_meta.id) | |
| button:SetFocus(false) | |
| end | |
| end | |
| function GedPropObjectPicker:ReassignFocusOrders(x, y) | |
| self.idCombo:SetFocusOrder(point(x, y)) | |
| return y + 1 | |
| end | |
| function GedPropObjectPicker:UpdateValue() | |
| local combo = self.idCombo | |
| combo.Items = false -- list of items might have changed, must be refetched | |
| self.last_object = self:GetProp() or {} | |
| self.idCombo:SetValueWithText(self.last_object.handle, self.last_object.text) | |
| GedPropEditor.UpdateValue(self) | |
| end | |
| function GedPropObjectPicker:ComboValueChanged(value) | |
| if type(value) == "string" and self.prop_meta.trim_spaces ~= false and string.trim_spaces(value) ~= value then | |
| value = string.trim_spaces(value) | |
| self.idCombo:SetValue(value) | |
| end | |
| if self.last_object.handle ~= value then | |
| self.last_object.handle = value | |
| self:SetProp({ handle = value }) | |
| end | |
| end | |
| function GedPropObjectPicker:DetachForReuse() | |
| self.idCombo:UpdateMRUList() | |
| GedPropEditor.DetachForReuse(self) | |
| end | |
| ----- GedPropHistogram | |
| GedPropEditors["histogram"] = "GedPropHistogram" | |
| DefineClass.GedPropHistogram = { | |
| __parents = {"GedPropEditorWithSubeditors"}, | |
| } | |
| function GedPropHistogram:Init(parent, context, prop_meta) | |
| local histogram = XHistogram:new({ | |
| Id = "idHistogram", | |
| MinWidth = 200, | |
| MinHeight = 200, | |
| }, self.subeditor_container) | |
| self.idLabelHost:SetDock("top") | |
| end | |
| function GedPropHistogram:UpdateValue() | |
| self.idHistogram:SetValue(self:GetProp()) | |
| GedPropEditorWithSubeditors.UpdateValue(self) | |
| end | |
| ----- GedPropCurvePicker | |
| GedPropEditors["packedcurve"] = "GedPropCurvePicker" | |
| GedPropEditors["curve4"] = "GedPropCurvePicker" | |
| DefineClass.GedPropCurvePicker = { | |
| __parents = {"GedPropEditorWithSubeditors"}, | |
| default_scale = 1000, | |
| graph_max_value = 10, | |
| range_editor = false, | |
| color_args = false, | |
| control_points = 4, | |
| } | |
| function GedPropCurvePicker:UpdateDynamicGraphParams() | |
| self.graph_max_value = self.range_editor.subeditor_value | |
| self.graph_max_value = Max(Min(self.graph_max_value, self.prop_meta.max_amplitude), self.prop_meta.min_amplitude) | |
| self.idCurve.DisplayScaleY = (self.prop_meta.scale or 1000) * 10 / self.graph_max_value | |
| self.idCurve.scale_texts = false | |
| end | |
| function GedPropCurvePicker:DrawGraphBackground(editor, graph_box, points) | |
| local color_args = self.color_args | |
| if not color_args then return end | |
| local step = 12 | |
| local units_per_color = 1000 / (#color_args - 2) | |
| local DrawSolidRect = UIL.DrawSolidRect | |
| for y = graph_box:miny(), graph_box:maxy(), step do | |
| local percent = 1000 - MulDivRound(y - graph_box:miny(), 1000, graph_box:sizey()) | |
| local i = percent / units_per_color | |
| local color_interp = percent % units_per_color | |
| local color = InterpolateRGB(color_args[i + 1], color_args[i + 2], color_interp, units_per_color) | |
| DrawSolidRect(box(graph_box:minx(), y, graph_box:maxx(), Min(graph_box:maxy(), y + step)), color, RGBA(0, 0, 0, 0)) | |
| end | |
| end | |
| function GedPropCurvePicker:Init(parent, context, prop_meta) | |
| prop_meta.max_amplitude = prop_meta.max_amplitude or 10 | |
| prop_meta.min_amplitude = prop_meta.min_amplitude or 10 | |
| if prop_meta.scale then | |
| prop_meta.max_amplitude = prop_meta.max_amplitude or prop_meta.scale | |
| prop_meta.min_amplitude = prop_meta.min_amplitude or prop_meta.scale | |
| end | |
| self.control_points = prop_meta.control_points or 4 | |
| local curve_editor = XCurveEditor:new({ | |
| Id = "idCurve", | |
| ControlPoints = self.control_points, | |
| MaxX = prop_meta.max_x or 1000, | |
| MinX = prop_meta.min_x or 0, | |
| MaxY = prop_meta.max or 1000, | |
| MinY = prop_meta.min or 0, | |
| DisplayScaleX = prop_meta.scale_x or 1000, | |
| DisplayScaleY = prop_meta.scale or 1000, | |
| SnapX = 1, | |
| SnapY = 1, | |
| MinWidth = 500, | |
| MinHeight = 200, | |
| Smooth = false, | |
| FixedX = prop_meta.fixedx or false, | |
| OnCurveChanged = function(editor) | |
| if not self:IsThreadRunning("scroll_update_thread") then | |
| self:CreateThread("scroll_update_thread", function() | |
| Sleep(75) | |
| self:TrySetProp() | |
| end) | |
| end | |
| end, | |
| DrawGraphBackground = function(editor, graph_box, points) | |
| self:DrawGraphBackground(editor, graph_box, points) | |
| end, | |
| ReadOnly = prop_meta.read_only and true, | |
| MinMaxRangeMode = not prop_meta.no_minmax, | |
| }, self.subeditor_container) | |
| curve_editor.GridUnitY = curve_editor:GetRange():y() / 4 | |
| curve_editor.GridUnitX = curve_editor:GetRange():x() / 4 | |
| if prop_meta.color_args and #prop_meta.color_args > 0 then | |
| self.color_args = prop_meta.color_args | |
| table.insert(self.color_args, self.color_args[#self.color_args]) | |
| end | |
| self.range_editor = self:MakeSubEditor(self.subeditor_container, GedPropNumber, { | |
| max = prop_meta.max_amplitude or 10, | |
| min = prop_meta.min_amplitude or 10, | |
| scale = 10, | |
| slider = true, | |
| default = 10, | |
| id = "range_editor", | |
| editor = "number" | |
| }) | |
| if prop_meta.max_amplitude == prop_meta.min_amplitude then | |
| self.range_editor:SetVisible(false) | |
| self.range_editor:SetDock("ignore") | |
| end | |
| self.idLabelHost:SetDock("top") | |
| end | |
| function GedPropCurvePicker:TrySetProp() | |
| local result = table.copy(self.idCurve.points) | |
| result.range_y = self.range_editor.subeditor_value | |
| result.scale = self.prop_meta.scale or result.range_y | |
| self.graph_max_value = result.range_y or 10 | |
| self:UpdateDynamicGraphParams() | |
| self:SetProp(result) | |
| end | |
| function GedPropCurvePicker:UpdateValue() | |
| GedPropEditorWithSubeditors.UpdateValue(self) | |
| local prop = self:GetProp() | |
| if not prop then return end | |
| if self.idCurve:IsFocused(true) then | |
| return | |
| end | |
| for i = 1, #prop do | |
| if IsPoint(prop[i]) then | |
| local pt = prop[i] | |
| if not pt:z() then | |
| pt = pt:SetZ(pt:y()) | |
| end | |
| self.idCurve.points[i] = pt | |
| end | |
| end | |
| self.idCurve:ValidatePoints() | |
| self.graph_max_value = prop.range_y or 10 | |
| self.range_editor.subeditor_value = self.graph_max_value | |
| self:UpdateDynamicGraphParams() | |
| end | |
| ----- GedPropPointList | |
| GedPropEditors["point_list"] = "GedPropPointList" | |
| DefineClass.GedPropPointList = { | |
| __parents = { "GedPropPrimitiveList"}, | |
| } | |
| function GedPropPointList:DefaultValue() | |
| return point30 | |
| end | |
| function GedPropPointList:ValidateValue(value) | |
| return IsPoint(value) | |
| end | |
| function GedPropPointList:ConvertFromText(value) | |
| return GedPropPoint.ConvertFromText(self, value) | |
| end | |
| function GedPropPointList:ConvertToText(value) | |
| return GedPropPoint.ConvertToText(self, value) | |
| end | |
| function GedPropPointList:ApplyScale(...) | |
| return GedPropPoint.ApplyScale(self, ...) | |
| end | |
| function GedPropPointList:GetDisplayScale(...) | |
| return GedPropPoint.GetDisplayScale(self, ...) | |
| end | |
| function GedPropPointList:GetMinMax(...) | |
| return GedPropPoint.GetMinMax(self, ...) | |
| end | |
| function GedPropPointList:UpdateValue() | |
| local data = self:GetProp() or {} | |
| if not terminal.desktop.keyboard_focus or not terminal.desktop.keyboard_focus:IsWithin(self) then | |
| self.list_values = table.map(data, function(p) return point(p:xyz()) end) | |
| self:WithItems(function() | |
| self:UpdateControls() | |
| end) | |
| end | |
| GedPropEditor.UpdateValue(self) | |
| end |