myspace / CommonLua /Ged /GedPanel.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
111 kB
local warn_mark = Untranslated("<color 255 140 0>!</color>")
local err_mark = Untranslated("<color 240 0 0>!!</color>")
local dirty_mark = Untranslated("<color 240 0 0>*</color>")
local match_mark_text = "<color 0 196 0>•</color>"
local match_mark = Untranslated(match_mark_text)
local function get_warning_msg(obj_addr)
return (Platform.ged and g_GedApp.connection:Obj("root|warnings_cache") or rawget(_G, "DiagnosticMessagesForGed") or empty_table)[obj_addr]
end
function TFormat.ged_marks(context_obj, obj_addr)
local marks = ""
-- Ged warning/error ! mark
local msg = get_warning_msg(obj_addr)
if msg then
marks = marks .. (msg[#msg] == "warning" and warn_mark or err_mark)
end
-- Ged dirty * mark
local dirty = Platform.ged and g_GedApp.connection:Obj("root|dirty_objects") or empty_table
if dirty[obj_addr] then
marks = marks .. dirty_mark
end
-- Mark for a match from "search values"
for _, panel in pairs(Platform.ged and g_GedApp.interactive_panels) do
local results = panel.search_value_results
if results and type(results[obj_addr]) == "table" then
marks = marks .. match_mark
break
end
end
return marks
end
local read_only_color = Untranslated("<color 150 150 150>")
local read_only_color_close = Untranslated("</color>")
-- <read_only(panel.context)>
-- The read_only tag is used to gray out all items in a ged panel whose context is read-only.
-- Currently this is useful to gray out a Container's sub-items (like Preset sub-items).
-- If you want to gray out only specific items that read-only in a panel whose context is NOT read-only
-- use the Format property with a construct like <if(IsReadOnly)>...</if>
function TFormat.read_only(context_obj, context_name)
local result = ""
local read_only = (Platform.ged and context_name) and g_GedApp.connection:Obj(context_name .. "|read_only")
if read_only then
result = result .. read_only_color
end
return result
end
-- </read_only(panel.context)>
-- Closes a read_only tag.
TFormat["/read_only"] = function(context_obj, context_name)
local result = ""
local read_only = (Platform.ged and context_name) and g_GedApp.connection:Obj(context_name .. "|read_only")
if read_only then
result = result .. read_only_color_close
end
return result
end
----- GedPanelBase, used by all panels and GedPropNestedList
local reCommaList = "([%w_]+)%s*,%s*"
DefineClass.GedPanelBase = {
__parents = { "XControl", "XContextWindow" },
properties = {
{ category = "Interaction", id = "Collapsible", editor = "bool", default = false, help = "Allows the panel to be collapsed and expanded." },
{ category = "Interaction", id = "StartsExpanded", editor = "bool", default = false, help = "Controls if the panel is initially collapsed or expanded." },
{ category = "Interaction", id = "ExpandedMessage", editor = "text", default = "", help = "Dimmed help message to display right-aligned in the panel's title when it is expanded." },
{ category = "Interaction", id = "EmptyMessage", editor = "text", default = "", help = "Dimmed help message to display right-aligned in the panel's title when it is empty." },
},
Embedded = false,
Interactive = false, -- interactive panels have selection and store/restore state (see GedApp)
MatchMark = match_mark_text,
focus_column = 1,
connection = false,
app = false,
}
function GedPanelBase:Init(parent, context)
assert(not context or type(context) == "string") -- context is the bind name of the object displayed
self.app = GetParentOfKind(self.parent, "GedApp")
self.connection = self.app and self.app.connection
self.app:AddPanel(self.context, self)
end
function GedPanelBase:Done()
if self.app.window_state ~= "destroying" then
self:UnbindViews()
self.app:RemovePanel(self)
end
end
function GedPanelBase:SetPanelFocused()
end
function GedPanelBase:GetSelection()
return false
end
function GedPanelBase:GetMultiSelection()
return false
end
function GedPanelBase:SetSelection(...)
end
function GedPanelBase:OnSelection(selection)
end
function GedPanelBase:TryHighlightSearchMatch()
-- no search support in GetPanelBase
end
function GedPanelBase:CancelSearch(dont_select)
-- no search support in GetPanelBase
end
function GedPanelBase:GetState()
return { selection = table.pack(self:GetSelection()) }
end
function GedPanelBase:Obj(name)
return self.connection.bound_objects[name]
end
function GedPanelBase:BindView(suffix, func_name, ...)
local name = self.context
self.connection:BindObj(name .. "|" .. suffix, name, func_name, ...)
end
function GedPanelBase:UnbindView(suffix)
local name = self.context
self.connection:UnbindObj(name .. "|" .. suffix)
end
function GedPanelBase:BindViews()
end
function GedPanelBase:UnbindViews()
local name = self.context
if name then
self.connection:UnbindObj(name)
self.connection:UnbindObj(name .. "|", "") -- unbind all views
self:SetContext(false)
end
end
function GedPanelBase:OnContextUpdate(context, view)
self:SetVisible(true)
if view == nil then
self:BindViews()
end
self.app:CheckUpdateItemTexts(view)
end
function GedPanelBase:OnSetFocus()
if self.app:SetLastFocusedPanel(self) then
self:BindSelectedObject(self:GetSelection())
end
end
function GedPanelBase:BindSelectedObject(selected_item)
end
function GedPanelBase:Op(op_name, obj, ...)
self.app:Op(op_name, obj, ...)
end
function GedPanelBase:Send(rfunc_name, ...)
self.app:Send(rfunc_name, ...)
end
function GedPanelBase:UpdateItemTexts()
-- used to refresh all texts so that their warning statuses get updated (see TFormat.diagnmsg)
end
local function get_warning_nodes(self)
-- see Preset's Warning property
local warning_data = self:Obj(self.context .. "|warning")
if type(warning_data) == "table" then
local warning_idxs = {}
for i = 3, #warning_data do
table.insert(warning_idxs, warning_data[i])
end
return warning_idxs
end
return empty_table
end
----- Ops for common actions per panel type and class
local common_action_ops = {
GedListPanel = {
None = {},
PropertyObject = {
MoveUp = "GedOpListMoveUp",
MoveDown = "GedOpListMoveDown",
Delete = "GedOpListDeleteItem",
Cut = "GedOpListCut",
Copy = "GedOpListCopy",
Paste = "GedOpListPaste",
Duplicate = "GedOpListDuplicate",
},
Object = {
Delete = "GedOpListDeleteItem",
Cut = "GedOpObjectCut",
Copy = "GedOpObjectCopy",
Paste = "GedOpObjectPaste",
Duplicate = "GedOpObjectDuplicate",
},
},
GedTreePanel = {
None = {},
PropertyObject = {
MoveUp = "GedOpTreeMoveItemUp",
MoveDown = "GedOpTreeMoveItemDown",
MoveOut = "GedOpTreeMoveItemOutwards",
MoveIn = "GedOpTreeMoveItemInwards",
Delete = "GedOpTreeDeleteItem",
Cut = "GedOpTreeCut",
Copy = "GedOpTreeCopy",
Paste = "GedOpTreePaste",
Duplicate = "GedOpTreeDuplicate",
},
Preset = {
Delete = "GedOpPresetDelete",
Cut = "GedOpPresetCut",
Copy = "GedOpPresetCopy",
Paste = "GedOpPresetPaste",
Duplicate = "GedOpPresetDuplicate",
},
},
GedPropPanel = {
None = {},
PropertyObject = {
Copy = "GedOpPropertyCopy",
Paste = "GedOpPropertyPaste",
},
},
}
----- GedPanel
local function op_readonly(self, prop_meta) return self:GetProperty(prop_meta.id) == GedDisabledOp end
local function op_noedit(self, prop_meta) return not common_action_ops[rawget(self, "__class") or self.class] end
local op_edit_button = {{
name = "Edit", func = "OpEdit",
is_hidden = function(obj, prop_meta) return obj:GetProperty(prop_meta.id) ~= GedDisabledOp end
}}
DefineClass.GedPanel = {
__parents = { "GedPanelBase" },
properties = {
{ category = "General", id = "Title", editor = "text", default = "<class>", },
{ category = "General", id = "TitleFormatFunc", editor = "text", default = "GedFormatObject", },
{ category = "General", id = "EnableSearch", editor = "bool", default = false, },
{ category = "General", id = "SearchHistory", editor = "number", default = 0 },
{ category = "General", id = "SearchValuesAvailable", editor = "bool", default = false },
{ category = "General", id = "PersistentSearch", editor = "bool", default = false },
{ category = "General", id = "Predicate", editor = "text", default = "", help = "This object member function controls whether the panel is visible" },
{ category = "General", id = "DisplayWarnings", editor = "bool", default = true, },
{ category = "Common Actions", id = "ActionsClass", editor = "choice", default = "None", no_edit = op_noedit,
items = function(self) return table.keys2(common_action_ops[rawget(self, "__class") or self.class] or { None = true }, "sorted") end },
{ category = "Common Actions", id = "MoveUp", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "MoveDown", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "MoveOut", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "MoveIn", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "Delete", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "Cut", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "Copy", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "Paste", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Common Actions", id = "Duplicate", editor = "text", default = GedDisabledOp, read_only = op_readonly, no_edit = op_noedit, buttons = op_edit_button },
{ category = "Context Menu", id = "ActionContext", editor = "text", default = "" },
{ category = "Context Menu", id = "SearchActionContexts", editor = "string_list", default = false,},
},
IdNode = true,
HAlign = "stretch",
VAlign = "stretch",
Padding = box(2, 2, 2, 2),
Background = RGB(255, 255, 255),
FocusedBackground = RGB(255, 255, 255),
MinWidth = 300,
MaxWidth = 100000,
ContainerControlClass = "XScrollArea",
HorizontalScroll = false,
Translate = false,
documentation_btn = false,
test_btn = false,
expanded = true, -- panels with Collapsible == true can be expanded/collapsed
search_popup = false,
search_values = false,
search_value_results = false,
read_only = false,
}
function GedPanel:OnXTemplateSetProperty(prop_id, old_value, ged)
if prop_id == "__class" or prop_id == "ActionsClass" then
local data = common_action_ops[self.__class]
if prop_id == "__class" then -- reset to default ops for that panel class
self.ActionsClass = data and "PropertyObject" or "None"
end
local ops = data and data[self.ActionsClass] or empty_table
for _, op in ipairs(GedCommonOps) do
self:SetProperty(op.Id, ops[op.Id] or GedDisabledOp)
end
end
end
function GedPanel:OpEdit(root, prop_id, ged, param)
self:SetProperty(prop_id, "")
ObjModified(self)
end
function GedPanel:ToggleSearch()
if self.idSearchContainer:GetVisible() then
self:CloseSearch()
else
self:OpenSearch()
end
end
function GedPanel:SaveSearchToggled(value)
local settings = self.app.settings or {}
local opened_panels = settings.opened_panels or {}
if value ~= nil then
opened_panels[self.Id] = value
end
settings.opened_panels = opened_panels
self.app.settings = settings
return opened_panels[self.Id]
end
function GedPanel:OpenSearch()
if not self:IsSearchAvailable() then return end
local search = self.idSearchContainer
if not search.visible then
search:SetDock("top")
search:SetVisible(true)
end
self.idSearchEdit:SetFocus()
self.idSearchEdit:SelectAll()
self:SaveSearchToggled(true)
end
function GedPanel:CloseSearch()
local search = self.idSearchContainer
if not search.visible then return end
search:SetDock("ignore")
search:SetVisible(false)
if self.idSearchEdit:GetText() ~= "" then
self:UpdateFilter()
end
self:SaveSearchToggled(false)
return true
end
function GedPanel:CancelSearch(dont_select)
if not self.idSearchContainer:GetVisible() then
return false
end
if self.PersistentSearch then
if self.idSearchEdit:GetText() ~= "" then
self.idSearchEdit:SetText("")
self.app.search_value_filter_text = ""
self.app:TryHighlightSearchMatchInChildPanels(self) -- clear highlights when search is canceled
self:UpdateFilter()
elseif not self.idContainer:IsFocused(true) then
if not dont_select then
self:FocusFirstEntry()
end
else
self.idSearchEdit:SetFocus()
self.idSearchEdit:SelectAll()
end
return true
else
return self:CloseSearch()
end
end
function GedPanel:UpdateFilter()
end
function GedPanel:FocusFirstEntry()
end
function GedPanel:GetFilterText()
local search = self.idSearchContainer
if not search or not search.visible then
return ""
end
return string.lower(self.idSearchEdit:GetText())
end
function GedPanel:GetHighlightText()
local app = self.app
return app.search_value_results and app.search_value_filter_text
end
function GedPanel:OnShortcut(shortcut, source, ...)
if shortcut == "Escape" and self:CancelSearch() then
return "break"
end
local app = self.app
local search_panel = app.search_value_panel
if app.search_value_results and search_panel then
if shortcut == "F4" then
search_panel:NextMatch(1)
return "break"
elseif shortcut == "Shift-F4" then
search_panel:NextMatch(-1)
return "break"
elseif shortcut == "F5" then
search_panel:StartUpdateFilterThread()
return "break"
end
end
end
function GedPanel:IsSearchAvailable()
return self.EnableSearch and not self.Embedded
end
function GedPanel:UpdateSearchVisiblity()
local enabled = self.EnableSearch and not self.Embedded
if not enabled then
self:CloseSearch()
end
self.idToggleSearch:SetVisible(enabled)
self.idToggleSearch:SetDock(enabled and "right" or "ignore")
self:UpdateSearchContextMenu(self:GetSearchActionContexts()) -- updates "Search" action
end
function GedPanel:SetSearchActionContexts(search_contexts)
if not search_contexts or type(search_contexts) ~= "table" then return end
self:UpdateSearchContextMenu(search_contexts)
end
function GedPanel:UpdateSearchContextMenu(new_contexts)
-- Attach search to the context menu
if not new_contexts or type(new_contexts) ~= "table" or #new_contexts == 0 then return end
local host = GetActionsHost(self)
if not host then return end
local search_action = host:ActionById("idSearch")
if not search_action then return end
local contexts = search_action.ActionContexts
if not contexts then return end
local old_contexts = self:GetSearchActionContexts()
if old_contexts and #old_contexts ~= 0 then
for _, old in ipairs(old_contexts) do
table.remove_entry(contexts, old)
end
end
local search_enabled = self.EnableSearch and not self.Embedded
if search_enabled then
for _, new in ipairs(new_contexts) do
table.insert(contexts, new)
end
end
self.SearchActionContexts = new_contexts
end
function GedPanel:AddToSearchHistory(text)
local settings = self.app.settings or {}
settings.search_history = settings.search_history or {}
local history_list = settings.search_history[self.Id] or {}
if text and #text:trim_spaces() > 0 then
table.remove_value(history_list, text)
table.insert(history_list, 1, text)
if #history_list > self.SearchHistory then
table.remove(history_list, #history_list)
end
end
settings.search_history[self.Id] = history_list
self.app.settings = settings
return history_list
end
function GedPanel:OpenSearchHistory(keyboard)
local popup = XPopupList:new({
LayoutMethod = "VList",
}, self.desktop)
local history = self:AddToSearchHistory(nil)
if #history > 0 then
for idx, item in ipairs(history) do
local entry = XTemplateSpawn("XComboListItem", popup.idContainer, self.context)
entry:SetFocusOrder(point(1, idx))
entry:SetFontProps(XCombo)
entry:SetText(item)
entry:SetMinHeight(entry:GetFontHeight())
entry.OnPress = function(entry)
self.idSearchEdit:SetText(item)
self:AddToSearchHistory(item)
if popup.window_state ~= "destroying" then
popup:Close()
end
end
end
else
XText:new({}, popup.idContainer):SetText("No history.")
end
popup:SetAnchor(self.idSearchContainer.box)
popup:SetAnchorType("drop")
popup:Open()
if keyboard and #history > 0 then
popup.idContainer[1]:SetFocus()
end
self.search_popup = popup
self.app:UpdateChildrenDarkMode(popup)
end
function GedPanel:InitControls()
self.expanded = self.StartsExpanded
XWindow:new({
Id = "idTitleContainer",
Dock = "ignore", -- see OnContextUpdate
Background = RGB(196, 196, 196),
HandleMouse = true,
}, self):SetVisible(false)
local search_container
if self.SearchValuesAvailable then
assert(self.PersistentSearch)
XWindow:new({
Id = "idSearchContainer",
Dock = "ignore",
}, self):SetVisible(false)
search_container = XWindow:new({
BorderWidth = 1,
}, self.idSearchContainer)
self:CreateSearchResultsPanel()
local search_toggle = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idSearchContainer)
search_toggle:SetId("idSearchValuesButton")
search_toggle:SetIcon("CommonAssets/UI/Ged/log-dataset.tga")
search_toggle:SetRolloverText(Untranslated("Toggle search in properties and sub-objects"))
search_toggle:SetMargins(box(2, 0, 0, 0))
search_toggle.OnPress = function() return self:ToggleSearchValues() end
else
search_container = XWindow:new({
Id = "idSearchContainer",
Dock = "ignore",
BorderWidth = 1,
Background = RGB(255, 255, 255),
}, self)
search_container:SetVisible(false)
end
local button = XTemplateSpawn("XComboButton", search_container, self.context)
button:SetId("idSearchHistory")
button:SetMargins(empty_box)
button:SetVisible(self.SearchHistory > 0)
button.OnPress = function(button)
if not self.search_popup or self.search_popup.window_state ~= "open" then
self:PopulateSearchValuesCache()
self:OpenSearchHistory()
else
self.search_popup:Close()
end
end
button.FoldWhenHidden = true
XTextButton:new({
Id = "idCancelSearch",
Dock = "right",
VAlign = "center",
Text = "x",
MaxWidth = 20,
MaxHeight = 16,
LayoutHSpacing = 0,
BorderWidth = 0,
Background = RGBA(0, 0, 0, 0),
RolloverBackground = RGB(204, 232, 255),
PressedBackground = RGB(121, 189, 241),
OnPress = function() self:CancelSearch() end,
FoldWhenHidden = true,
}, search_container)
self.idCancelSearch:SetVisible(false)
XEdit:new({
Id = "idSearchEdit",
Dock = "box",
Hint = "Search...",
BorderWidth = 0,
Background = RGBA(0, 0, 0, 0),
AllowEscape = false,
OnTextChanged = function(edit)
XEdit.OnTextChanged(edit)
self:StartUpdateFilterThread()
if edit:GetText() ~= "" then
self:SetSelection(false)
end
self.idCancelSearch:SetVisible(edit:GetText() ~= "")
end,
OnShortcut = function(edit, shortcut, source, ...)
local result = XEdit.OnShortcut(edit, shortcut, source, ...)
if result == "break" then return result end
if shortcut == "Down" or shortcut == "Enter" then
if shortcut == "Down" and self.SearchHistory > 0 and edit:GetText() == "" then
self:OpenSearchHistory("keyboard")
return "break"
else
self:FocusFirstEntry()
return shortcut == "Down" and "break" -- allow Enter to open console, e.g. when typing in Inspector's search
end
end
end,
OnSetFocus = function(edit)
self:PopulateSearchValuesCache()
return XEdit.OnSetFocus(edit)
end,
OnKillFocus = function(edit, new_focus)
self:AddToSearchHistory(edit:GetText())
return XEdit.OnKillFocus(edit, new_focus)
end,
}, search_container)
local search_button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
search_button:SetId("idToggleSearch")
search_button:SetIcon("CommonAssets/UI/Ged/view.tga")
search_button:SetRolloverText("Search (Ctrl-F)")
search_button:SetBackground(self.idTitleContainer:GetBackground())
search_button.OnPress = function() self:ToggleSearch() end
--- End search controls
XText:new({
Id = "idTitle",
Dock = "box",
ZOrder = 1000,
Margins = box(2, 1, 2, 1),
TextStyle = self.Collapsible and "GedDefault" or "GedTitleSmall",
HandleMouse = self.Collapsible,
Translate = true,
OnMouseButtonDown = function(button, pt, button)
if self.Collapsible and button == "L" and
not terminal.IsKeyPressed(const.vkShift) and
not terminal.IsKeyPressed(const.vkControl) then
self:Expand(not self.expanded)
end
end,
}, self.idTitleContainer)
XText:new({
Id = "idWarningText",
Dock = "top",
VAlign = "center",
TextHAlign = "center",
BorderWidth = 1,
BorderColor = RGB(255, 0 , 0),
Background = RGB(255, 196, 196),
FoldWhenHidden = true,
}, self)
self.idWarningText:SetVisible(false)
if self.HorizontalScroll then
XSleekScroll:new({
Id = "idHScroll",
Target = "idContainer",
Dock = "bottom",
Margins = box(0, 2, 7, 0),
Horizontal = true,
AutoHide = true,
FoldWhenHidden = true,
}, self)
end
local vertical_scroll = _G[self.ContainerControlClass]:IsKindOf("XScrollArea")
if vertical_scroll then
XSleekScroll:new({
Id = "idScroll",
Target = "idContainer",
Dock = "right",
Margins = box(2, 0, 0, 0),
Horizontal = false,
AutoHide = true,
FoldWhenHidden = true,
}, self)
end
_G[self.ContainerControlClass]:new({
Id = "idContainer",
HAlign = "stretch",
VAlign = "stretch",
MinHSize = false,
LayoutMethod = "VList",
Padding = box(2, 2, 2, 2),
BorderWidth = 0,
HScroll = self.HorizontalScroll and "idHScroll" or "",
VScroll = vertical_scroll and "idScroll" or "",
FoldWhenHidden = true,
RolloverTemplate = "GedPropRollover",
Translate = self.Translate,
}, self)
self.idContainer.OnSetFocus = function() self:OnSetFocus() end -- fix for edge case with one panel docked in another one
self:UpdateSearchVisiblity()
end
function GedPanel:GetTitleView()
return self.TitleFormatFunc .. "." .. self.Title -- generate unique view id, to make sure different panels won't clash
end
function GedPanel:Open(...)
self:InitControls()
XWindow.Open(self, ...)
if self.context then
self:BindViews()
if self.Title ~= "" then
self:BindView(self:GetTitleView(), self.TitleFormatFunc, self.Title)
end
if self.Predicate ~= "" then
self:BindView("predicate", "GedExecMemberFunc", self.Predicate)
end
self:BindView("read_only", "GedGetReadOnly")
end
if self.PersistentSearch then
if self:IsSearchAvailable() and not self.idSearchContainer:GetVisible() and self:SaveSearchToggled(nil) ~= false then
self:OpenSearch()
end
end
end
function GedPanel:Expand(expand)
self.expanded = expand
self.idContainer:SetVisible(expand)
self.idScroll:SetAutoHide(expand)
self.idScroll:SetVisible(expand)
if self.HorizontalScroll then
self.idHScroll:SetAutoHide(expand)
self.idHScroll:SetVisible(expand)
end
self:UpdateTitle(self.context)
end
function GedPanel:UpdateTitle(context)
local title = self.Title ~= "" and self.Title ~= "<empty>" and self:Obj(context .. "|" .. self:GetTitleView()) or ""
if self.Collapsible then
if title == "" and self.Title ~= "<empty>" then title = "(no description)" end
local match
local obj_id = self:Obj(context)
for _, panel in pairs(self.app.interactive_panels) do
local res = panel.search_value_results
if res and res[obj_id] then
title = GedPanelBase.MatchMark .. title
match = true
break
end
end
local is_empty = self:IsEmpty()
local corner_message = self.EmptyMessage
if not is_empty then
corner_message = (self.expanded and self.ExpandedMessage or "(click to expand)")
end
if not match then
title = (not is_empty and (self.expanded and "- " or "+ ") or "") .. title
if not self.Embedded then
title = title .. "<right><color 128 128 128>" .. corner_message
end
end
end
self.idTitle:SetText(Untranslated(title))
self.idTitleContainer:SetVisible(title ~= "")
self.idTitleContainer:SetDock(title ~= "" and "top" or "ignore")
end
function GedPanel:OnContextUpdate(context, view)
if not view then -- obj changed
if self.Title ~= "" then
self:BindView(self:GetTitleView(), self.TitleFormatFunc, self.Title)
end
if self.Predicate ~= "" then
self:BindView("predicate", "GedExecMemberFunc", self.Predicate)
end
if self.DisplayWarnings and not self.app.actions_toggled["ToggleDisplayWarnings"] then
self:BindView("warning", "GedGetWarning")
end
if not self.Embedded then
self:BindView("read_only", "GedGetReadOnly")
end
self:BindView("documentationLink", "GedGetDocumentationLink")
self:BindView("documentation", "GedGetDocumentation")
if self.Collapsible then
local obj_id = self:Obj(self.context)
for _, panel in pairs(self.app.interactive_panels) do
local results = panel.search_value_results
if results and results[obj_id] then
self.expanded = true
end
end
self:Expand(self.expanded)
end
end
if view == self:GetTitleView() then
self:UpdateTitle(context)
end
if view == "predicate" then
local predicate = self:Obj(context .. "|predicate")
self:SetVisible(predicate)
self:SetDock(not predicate and "ignore")
end
if view == "warning" and self.DisplayWarnings then
local warning = self:Obj(context .. "|warning")
self.idWarningText:SetVisible(warning)
self.idWarningText:SetTextColor(RGB(0, 0, 0))
self.idWarningText:SetRolloverTextColor(RGB(0, 0, 0))
if type(warning) == "table" then
self.idWarningText:SetText(warning[1])
local color = warning[2]
if color == "warning" then color = RGB(255, 140, 0) end
if color == "error" then color = RGB(255, 196, 196) end
if color then
local r, g, b = GetRGB(color)
local max = Max(Max(r, g), b)
self.idWarningText:SetBackground(color)
self.idWarningText:SetBorderColor(RGB(r == max and r or r / 4, g == max and g or g / 4, b == max and b or b / 4))
else
self.idWarningText:SetBackground(RGB(255, 196, 196))
self.idWarningText:SetBorderColor(RGB(255, 0 , 0))
end
else
self.idWarningText:SetText(warning)
self.idWarningText:SetBackground(RGB(255, 196, 196))
self.idWarningText:SetBorderColor(RGB(255, 0 , 0))
end
end
if (view or ""):starts_with("documentation") then
local documentation = self:Obj(context .. "|documentation")
local doc_link = self:Obj(context .. "|documentationLink")
if (documentation or doc_link or "") ~= "" then
if not self.documentation_btn then
self.documentation_btn = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
self.documentation_btn:SetIcon("CommonAssets/UI/Ged/help.tga")
self.documentation_btn:SetZOrder(-1)
end
self.documentation_btn.OnPress = function(button)
button:SetFocus()
if (doc_link or "") ~= "" then
local link = doc_link:starts_with("http") and doc_link or ("file:///" .. doc_link)
OpenUrl(link, "force external browser")
end
button:SetFocus(false)
end
local rollover_text = ""
if doc_link then
rollover_text = string.format("Open %s \n\n", doc_link)
end
rollover_text = rollover_text .. (documentation or "")
self.documentation_btn:SetRolloverText(rollover_text)
self.documentation_btn:SetVisible(true)
if self.Embedded then
if not self.test_btn then
self.test_btn = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
self.test_btn:SetIcon("CommonAssets/UI/Ged/play.tga")
self.test_btn:SetZOrder(-1)
self.test_btn:SetRolloverText("Run now!")
end
self.test_btn.OnPress = function(button)
button:SetFocus()
self:Send("GedTestFunctionObject", self.context)
button:SetFocus(false)
end
end
elseif self.documentation_btn then
self.documentation_btn:SetVisible(false)
end
end
if view == "read_only" then
self.read_only = self:Obj(self.context .. "|read_only")
self.app:ActionsUpdated()
end
GedPanelBase.OnContextUpdate(self, context, view)
end
function GedPanel:SetPanelFocused()
return self.idContainer:SetFocus()
end
function GedPanel:ToggleSearchValues(no_settings_update)
self.search_values = not self.search_values
local button = self.idSearchValuesButton
button:SetToggled(not button:GetToggled())
self.idSearchEdit:SetFocus(false)
self.idSearchEdit:SetFocus()
self:StartUpdateFilterThread()
if not no_settings_update then
local settings = self.app.settings
settings.search_in_props = settings.search_in_props or {}
settings.search_in_props[self.context] = self.search_values
self.app:SaveSettings()
end
end
function GedPanel:StartUpdateFilterThread(not_user_initiated)
if self.search_values and self.idSearchEdit:GetText() ~= "" then
self.app:SetUiStatus("value_search_in_progress", "Searching...")
end
self:DeleteThread("UpdateFilterThread")
self:CreateThread("UpdateFilterThread", function()
Sleep(75)
if self.search_values then
local filter = self.idSearchEdit:GetText()
self.search_value_results = filter ~= "" and self.connection:Call("rfnSearchValues", self.context, self:GetFilterText())
if self.search_value_results == "timeout" then
self.search_value_results = false
end
if self.PersistentSearch and not not_user_initiated then
self:ShowSearchResultsPanel(self:GetFilterText(), self.search_value_results)
end
else
self.search_value_results = nil
end
if self.window_state ~= "destroying" then
self:UpdateFilter()
for _, panel in pairs(self.app.interactive_panels) do
if panel ~= self then
panel:UpdateItemTexts()
end
end
self.app:SetUiStatus("value_search_in_progress", false)
end
end)
end
function GedPanel:PopulateSearchValuesCache()
if self.search_values then
CreateRealTimeThread(function() self.connection:Call("rfnPopulateSearchValuesCache", self.context) end)
end
end
function GedPanel:CreateSearchResultsPanel()
XWindow:new({
Id = "idSearchResultsPanel",
Dock = "bottom",
FoldWhenHidden = true,
Padding = box(0, 1, 0, 1)
}, self.idSearchContainer):SetVisible(false)
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idSearchResultsPanel)
button:SetIcon("CommonAssets/UI/Ged/undo.tga")
button:SetRolloverText("Refresh search results (F5)")
button:SetDock("right")
button.OnPress = function() self:StartUpdateFilterThread() end
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idSearchResultsPanel)
button:SetIcon("CommonAssets/UI/Ged/up.tga")
button:SetRolloverText("Previous match (Shift-F4)")
button:SetDock("right")
button.OnPress = function() self:NextMatch(-1) end
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idSearchResultsPanel)
button:SetIcon("CommonAssets/UI/Ged/down.tga")
button:SetRolloverText("Next match (F4)")
button:SetDock("right")
button.OnPress = function() self:NextMatch(1) end
XText:new({
Id = "idSearchResultsText",
TextStyle = "GedDefault",
}, self.idSearchResultsPanel)
end
function GedPanel:ShowSearchResultsPanel(filter, search_value_results)
local app = self.app
app.search_value_filter_text = filter
app.search_value_results = search_value_results
app.search_value_panel = self
self.idSearchResultsPanel:SetVisible(search_value_results)
if search_value_results then
app.search_result_idx = 1
self:NextMatch(0, "dont_unfocus_search_edit")
end
end
function GedPanel:NextMatch(direction, dont_unfocus_search_edit)
local app = self.app
local count = #app.search_value_results
app.search_result_idx = Clamp(app.search_result_idx + direction, 1, count)
app.display_search_result = true
self.idSearchResultsText:SetText(string.format("Match %d/%d", app.search_result_idx, count))
local focus = self.desktop:GetKeyboardFocus()
if not dont_unfocus_search_edit and focus.Id == "idSearchEdit" then
self.desktop:RemoveKeyboardFocus(focus)
end
self:TryHighlightSearchMatch()
end
function GedPanel:FilterItem(text, item_id, filter_text)
if filter_text == "" then return end
if self.search_values then
return not self.search_value_results or self.search_value_results.hidden[item_id or false]
else
text = IsT(text) and TDevModeGetEnglishText(text) or tostring(text):gsub("<[^>]+>", "")
text = string.lower(text)
return not text:find(filter_text, 1, true)
end
end
function GedPanel:UpdateItemTexts()
self:UpdateTitle(self.context)
end
function GedPanel:IsEmpty()
-- override in child panels
end
----- GedPropPanel
DefineClass.GedPropPanel = {
__parents = { "GedPanel" },
properties = {
-- internal use
{ category = "General", id = "CollapseDefault", editor = "bool", default = false, no_edit = true, },
{ category = "General", id = "ShowInternalNames", editor = "bool", default = false, no_edit = true, },
{ category = "General", id = "EnableUndo", editor = "bool", default = true, },
{ category = "General", id = "EnableCollapseDefault", editor = "bool", default = true, },
{ category = "General", id = "EnableShowInternalNames", editor = "bool", default = true, },
{ category = "General", id = "EnableCollapseCategories", editor = "bool", default = true, },
{ category = "General", id = "HideFirstCategory", editor = "bool", default = false, },
{ category = "General", id = "RootObjectBindName", editor = "text", default = false, },
{ category = "General", id = "SuppressProps", editor = "prop_table", default = false, help = "Set of properties to skip in format { id1 = true, id2 = true, ... }." },
{ category = "Context Menu" , id = "PropActionContext", editor = "text", default = "" },
},
MinWidth = 200,
Interactive = true,
EnableSearch = true,
ShowUnusedPropertyWarnings = false,
update_request = false,
rebuild_props = true,
prop_update_in_progress = false,
parent_obj_id = false,
parent_changed = false,
parent_changed_notified = false,
collapsed_categories = false,
collapse_default_button = false,
active_tab = "All",
-- property selection
selected_properties = false,
last_selected_container_indx = false,
last_selected_property_indx = false,
}
function GedPropPanel:InitControls()
GedPanel.InitControls(self)
self.idContainer:SetPadding(box(0, 3, 0, 0))
self.idContainer:SetLayoutVSpacing(5)
self.collapsed_categories = {}
self.selected_properties = {}
GedPanel.SetSearchActionContexts(self, self.SearchActionContexts)
local host = GetActionsHost(self)
if not host:ActionById("EditCode") then
XAction:new({
ActionId = "EditCode",
ActionContexts = { self.PropActionContext } ,
ActionName = "Edit Code",
ActionTranslate = false,
OnAction = function(action, host)
local panel = host:GetLastFocusedPanel()
if IsKindOf(panel, "GedPropPanel") then
self:Send("GedEditFunction", panel.context, panel:GetSelectedProperties())
end
end,
ActionState = function(action, host)
local panel = host:GetLastFocusedPanel()
if not IsKindOf(panel, "GedPropPanel") then
return "hidden"
end
local selected = panel.selected_properties
if not selected or #selected ~= 1 then return "hidden" end
local prop_meta = selected[1].prop_meta
if prop_meta.editor ~= "func" and prop_meta.editor ~= "expression" and prop_meta.editor ~= "script" then
return "hidden"
end
end,
}, self)
end
local show_collapse_action = self.EnableCollapseDefault and not self.Embedded
if show_collapse_action and not self.collapse_default_button then
self.collapse_default_button = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idTitleContainer)
self.collapse_default_button:SetId("idCollapseDefaultBtn")
self.collapse_default_button:SetIcon("CommonAssets/UI/Ged/collapse.tga")
self.collapse_default_button:SetRolloverText(T(912785185075, "Hide/show all properties with default values"))
self.collapse_default_button:SetBackground(self.idTitleContainer:GetBackground())
self.collapse_default_button:SetToggled(self.CollapseDefault)
self.collapse_default_button.OnPress = function(button)
self:SetFocus()
self:SetCollapseDefault(not self.CollapseDefault)
button:SetToggled(not button:GetToggled())
end
end
local show_internal_names_action = self.EnableShowInternalNames and not self.Embedded
if show_internal_names_action then
local show_internal_names_button = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idTitleContainer)
show_internal_names_button:SetId("idShowInternalNamesBtn")
show_internal_names_button:SetIcon("CommonAssets/UI/Ged/log-focused.tga")
show_internal_names_button:SetRolloverText(T(496361185046, "Hide/show internal names of properties"))
show_internal_names_button:SetBackground(self.idTitleContainer:GetBackground())
show_internal_names_button:SetToggled(self.ShowInternalNames)
show_internal_names_button.OnPress = function(button)
self:ShowInternalPropertyNames(not self.ShowInternalNames)
button:SetToggled(not button:GetToggled())
end
end
if not self.Embedded then
if self.EnableCollapseCategories then
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
button:SetIcon("CommonAssets/UI/Ged/collapse_tree.tga")
button:SetRolloverText("Expand/collapse categories (Shift-C)")
button:SetBackground(self.idTitleContainer:GetBackground())
button.OnPress = function() self:ExpandCollapseCategories() end
end
if self.app.PresetClass and self.DisplayWarnings then
self.ShowUnusedPropertyWarnings = self.app.ShowUnusedPropertyWarnings
local button = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idTitleContainer)
button:SetIcon("CommonAssets/UI/Ged/warning_button.tga")
button:SetRolloverText("Show/hide unused property warnings")
button:SetBackground(self.idTitleContainer:GetBackground())
button:SetToggled(self.ShowUnusedPropertyWarnings)
button.OnPress = function(button)
self.ShowUnusedPropertyWarnings = not self.ShowUnusedPropertyWarnings
button:SetToggled(not button:GetToggled())
self:UpdatePropertyNames(self.ShowInternalNames)
end
end
end
end
function GedPropPanel:Open(...)
GedPanel.Open(self, ...)
self:CreateThread("update", self.UpdateThread, self)
GetActionsHost(self, true):ActionById("EditCode").ActionContexts = { self.PropActionContext }
end
function GedPropPanel:ShowInternalPropertyNames(value)
if self.ShowInternalNames ~= value then
self.ShowInternalNames = value
self:UpdatePropertyNames(value)
end
end
function GedPropPanel:SetSelection(properties)
if not properties then return end
self:ClearSelectedProperties()
for con_indx, win in ipairs(self.idContainer) do
for cat_indx, item in ipairs(win.idCategory) do
for _, id in ipairs(properties) do
if item.prop_meta.id == id then
self:AddToSelected(item, con_indx, cat_indx)
end
end
end
end
end
function GedPropPanel:OnMouseButtonDown(pos, button)
local prop, container_indx, category_indx = self:GetGedPropAt(pos)
local selected_props = self.selected_properties
if button == "L" then
self:SetFocus()
self.app:SetLastFocusedPanel(self)
if prop then
if #selected_props == 0 then
self:AddToSelected(prop, container_indx, category_indx)
else
if terminal.IsKeyPressed(const.vkControl) then -- Ctrl pressed
if prop.selected then
self:RemoveFromSelected(prop, container_indx, category_indx)
else
self:AddToSelected(prop, container_indx, category_indx)
end
elseif terminal.IsKeyPressed(const.vkShift) then -- Shift pressed
self:ShiftSelectMultiple(prop, container_indx, category_indx)
else
local current_prop_selected = prop.selected
self:ClearSelectedProperties()
if not current_prop_selected then
self:AddToSelected(prop, container_indx, category_indx)
end
end
end
else
self:ClearSelectedProperties()
end
return "break"
elseif button == "R" then
self:SetFocus()
local action_context = self:GetActionContext()
if prop then
if #selected_props < 2 then
self:ClearSelectedProperties()
self:AddToSelected(prop, container_indx, category_indx)
end
action_context = self.PropActionContext
end
local host = GetActionsHost(self, true)
if host then
host:OpenContextMenu(action_context, pos)
end
return "break"
end
end
function GedPropPanel:GetSelectedProperties()
local selected_ids = {}
for _, prop in ipairs(self.selected_properties) do
table.insert(selected_ids, prop.prop_meta.id)
end
return selected_ids
end
function GedPropPanel:ShiftSelectMultiple(prop, container_indx, category_indx)
self:ClearSelectedProperties("keep_last_selected_container_indx")
--- default, when selection is in same container ---
local con_indx = container_indx
local max_con_indx = container_indx
local cat_indx = category_indx
local cat_max_indx = self.last_selected_property_indx
if category_indx > self.last_selected_property_indx then
cat_indx = self.last_selected_property_indx
cat_max_indx = category_indx
end
--- when selection is in different container ---
if container_indx ~= self.last_selected_container_indx then
if container_indx > self.last_selected_container_indx then
con_indx = self.last_selected_container_indx
max_con_indx = container_indx
cat_indx = self.last_selected_property_indx
cat_max_indx = category_indx
else
con_indx = container_indx
max_con_indx = self.last_selected_container_indx
cat_indx = category_indx
cat_max_indx = self.last_selected_property_indx
end
end
local container = false
local category_cnt = false
local shift_select = true
while shift_select do
container = self.idContainer[con_indx]
category_cnt = con_indx == max_con_indx and cat_max_indx or #container.idCategory
if container.idCategory.visible then
for i = cat_indx, category_cnt do
self:AddToSelected(container.idCategory[i], self.last_selected_container_indx, self.last_selected_property_indx)
end
end
con_indx = con_indx + 1
cat_indx = 1
if con_indx > max_con_indx then
shift_select = false
end
end
end
function GedPropPanel:OnShortcut(shortcut, source, ...)
local res = GedPanel.OnShortcut(self, shortcut, source, ...)
if res == "break" then return res end
if shortcut == "Escape" or shortcut == "ButtonB" then
self:ClearSelectedProperties()
end
end
function GedPropPanel:ClearSelectedProperties(keep_last_selected)
local selected_props = self.selected_properties
for i = 1, #selected_props do
selected_props[i]:SetSelected(false)
selected_props[i] = nil
end
if not keep_last_selected then
self.last_selected_container_indx = false
self.last_selected_property_indx = false
end
end
function GedPropPanel:AddToSelected(prop, con_indx, prop_indx)
prop:SetSelected(true)
local selected_props = self.selected_properties
assert(not table.find(selected_props, prop))
selected_props[#selected_props + 1] = prop
self.last_selected_container_indx = con_indx
self.last_selected_property_indx = prop_indx
end
function GedPropPanel:RemoveFromSelected(prop, con_indx, prop_indx)
prop:SetSelected(false)
local selected_props = self.selected_properties
local indx = table.find(selected_props, prop)
assert(indx)
selected_props[indx] = nil
self.last_selected_container_indx = con_indx
self.last_selected_property_indx = prop_indx
end
function GedPropPanel:GetGedPropAt(pt)
for container_indx, container in ipairs(self.idContainer) do
if container:HasMember("idCategory") and container.idCategory.visible then -- window which holds the GedPropEditors
for gedprop_index, gedprop in ipairs(container.idCategory) do
if gedprop:MouseInWindow(pt) then
return gedprop, container_indx, gedprop_index
end
end
end
end
end
function GedPropPanel:SetCollapseDefault(value)
if self.CollapseDefault ~= value then
self.CollapseDefault = value
self.rebuild_props = true
self:RequestUpdate()
end
end
function GedPropPanel:BindViews()
if not self.context then return end
-- ensures all views will be resent on rebind
self:UnbindView("props")
self:UnbindView("values")
self:BindView("props", "GedGetProperties", self.SuppressProps)
self:BindView("values", "GedGetValues")
end
function GedPropPanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if view == nil then
self.parent_obj_id = self.connection.bound_objects[context]
self.parent_changed = true
self.parent_changed_notified = true
self.connection.bound_objects[context .. "|values"] = nil
self:RequestUpdate()
end
if view == "values" then
self.rebuild_props = self.rebuild_props or self.CollapseDefault
self:RequestUpdate()
end
if view == "props" then
self.rebuild_props = true
self:RequestUpdate()
self:RebuildTabs()
end
end
function GedPropPanel:GetTabsData()
local data = self:Obj(self.context .. "|props")
local tabs = data and data.tabs
if not tabs then return end
-- list all categories not it any tabs in an Other tab; remove empty tabs
if tabs[#tabs].TabName ~= "Other" then
local categories = {}
for _, prop in ipairs(data) do
categories[prop.category or "Misc"] = true
end
local allcats = table.copy(categories)
for i = #tabs, 1, -1 do
local tab = tabs[i]
local has_content
for category in pairs(tab.Categories) do
has_content = has_content or allcats[category]
categories[category] = nil
end
if not has_content then
table.remove(tabs, i)
end
end
if self.app.PresetClass == "XTemplate" then
categories.Template = nil -- skip Template as a special case for XTemplate Editor (too hard not to make this hack)
end
tabs[#tabs + 1] = { TabName = #table.keys(categories) == 1 and next(categories) or "Other", Categories = categories }
end
return tabs
end
function GedPropPanel:RebuildTabs()
local container = rawget(self, "idTabContainer")
if not container then
container = XWindow:new({
Id = "idTabContainer",
LayoutMethod = "HList",
Dock = "bottom",
ZOrder = -1,
}, self.idTitleContainer)
end
container:DeleteChildren()
local tabs = self:GetTabsData()
self.idContainer:SetLayoutVSpacing(tabs and 0 or 5)
if not tabs then return end
tabs = table.copy(tabs)
table.insert(tabs, 1, { TabName = "All" })
for _, tab in ipairs(tabs) do
if tab.TabName == "All" or next(tab.Categories) then
XToggleButton:new({
Text = tab.TabName,
Toggled = self.active_tab == tab.TabName,
OnChange = function(button)
self.active_tab = tab.TabName
self:RebuildTabs()
self:UpdateVisibility()
end,
Padding = box(2, 1, 2, 1),
BorderWidth = 1,
}, container)
end
end
Msg("XWindowRecreated", container)
end
function GedPropPanel:RequestUpdate()
self.update_request = true
Wakeup(self:GetThread("update"))
end
local function is_slider_dragged()
return IsKindOfClasses(terminal.desktop:GetMouseCapture(), "XSleekScroll", "XCurveEditor", "XCoordAdjuster")
end
function GedPropPanel:UpdateThread()
while true do
if not self.update_request then
WaitWakeup()
if self.app.window_state == "closing" or self.window_state == "destroying" or not self.context then return end
end
self.update_request = false
self:ClearSelectedProperties()
-- don't recreate property editors while the user is dragging a slider
while is_slider_dragged() do
if self.app.window_state == "closing" or self.window_state == "destroying" or not self.context then return end
self:Update(false)
Sleep(50)
end
if self.app.window_state == "closing" or self.window_state == "destroying" or not self.context then return end
self:Update(self.rebuild_props)
end
end
function GedPropPanel:RemoteSetProperty(obj, prop_id, value, parent_obj_id, slider_drag_id)
local rebuild_pending = self.rebuild_props and not slider_drag_id -- see UpdateThread above
if not rebuild_pending and not self.parent_changed and (parent_obj_id or false) == self.parent_obj_id then
return self.app:Op("GedSetProperty", obj, prop_id, value, not self.EnableUndo, slider_drag_id or is_slider_dragged())
end
end
function GedPropPanel:Update(rebuild_props)
local has_values = self:Obj(self.context .. "|values")
if has_values and rebuild_props then -- rebuild controls
local scroll_pos = self.idScroll.Scroll
self:RebuildControls()
self.idScroll:ScrollTo(scroll_pos)
self.rebuild_props = false
else -- only update the values
self.prop_update_in_progress = true
for _, category_window in ipairs(self.idContainer) do
if category_window:HasMember("idCategory") then
for _, win in ipairs(category_window.idCategory) do
if has_values then
win:UpdateValue(self.parent_changed_notified)
end
win.parent_obj_id = self.parent_obj_id
end
end
end
self.prop_update_in_progress = false
end
if has_values then
self.parent_changed_notified = false
end
self.parent_changed = false
self:QueueReassignFocusOrders()
end
function GedPropPanel:ShouldShowButtonForFunc(func_name)
if rawget(self.app, "suppress_property_buttons") then
if table.find(self.app.suppress_property_buttons, func_name) then
return
end
end
return true
end
function GedPropPanel:QueueReassignFocusOrders(x, y)
local obj = self
while obj and obj.Embedded do
obj = GetParentOfKind(obj.parent, "GedPropPanel")
end
if obj and not obj:IsThreadRunning("ReasignFocusOrders") then
obj:CreateThread("ReasignFocusOrders", function() obj:ReassignFocusOrders(x, y) end)
end
end
function GedPropPanel:UpdateFilter()
self:UpdateVisibility()
end
local function find_parent_prop_panel(xcontrol)
return (xcontrol:HasMember("panel") and xcontrol.panel) or
(xcontrol.parent and find_parent_prop_panel(xcontrol.parent))
end
function GedPropPanel:RebuildControls()
-- gather the old prop editors for potential reuse
local editors_by_hash = {}
for _, category_window in ipairs(self.idContainer) do
if category_window:HasMember("idCategory") then
for _, editor in ipairs(category_window.idCategory) do
local hash = xxhash(editor:GetContext(), table.hash(editor.prop_meta))
assert(not editors_by_hash[hash])
editors_by_hash[hash] = editor
editor:DetachForReuse()
end
end
end
self.idContainer:Clear()
-- find the results from a value search to highlight the matching properties
local matching_props = {}
local obj_id = self:Obj(self.context)
for _, panel in pairs(self.app.interactive_panels) do
local res = panel.search_value_results
if res and type(res[obj_id]) == "table" then
for idx, prop in ipairs(res[obj_id]) do
matching_props[prop] = true
end
break
end
end
local categories = {}
local sort_priority = 1
local context = self.context
local values = self:Obj(context .. "|values") or empty_table
local props = self:Obj(context .. "|props") or empty_table
if self.read_only then
for idx, prop_meta in ipairs(props) do
props[idx] = table.copy(prop_meta)
props[idx].read_only = true
end
end
table.stable_sort(props, function(a, b) return (a.sort_order or 0) < (b.sort_order or 0) end)
local filter_text = self:GetFilterText()
local focus = self.desktop.keyboard_focus
local order = focus and focus:IsWithin(self) and focus:GetFocusOrder()
local category_data = self:Obj("root|categories") or empty_table
local parent_panel = find_parent_prop_panel(self)
local collapse_default = filter_text == "" and (self.CollapseDefault or (self.Embedded and parent_panel and parent_panel.CollapseDefault) )
for _, prop_meta in ipairs(props) do
if not collapse_default or prop_meta.editor == "buttons" or
values[prop_meta.id] ~= nil and values[prop_meta.id] ~= prop_meta.default
then
local group
local category = prop_meta.category or "Misc"
local idx = table.find(categories, "category", category)
if idx then
group = categories[idx]
else
group = { prop_metas = {}, category = category, category_name = category, priority = sort_priority}
sort_priority = sort_priority + 1
local property_category = category_data and category_data[category]
if property_category then
group.category_name = _InternalTranslate(property_category.display_name, property_category)
group.priority = group.priority + property_category.SortKey * 1000
end
table.insert(categories, group)
end
table.insert(group.prop_metas, prop_meta)
end
end
if #categories == 0 then
local text = XText:new({
Id = "idNoPropsToShow",
MaxWidth = 300,
TextHAlign = "center",
}, self.idContainer)
text:SetText(collapse_default and "No properties with non-default value were found." or "There are no properties to show.")
text:Open()
goto cleanup
end
table.stable_sort(categories, function(a, b) return a.priority < b.priority end)
for i, group in ipairs(categories) do
local category_window = XWindow:new({
IdNode = true,
FoldWhenHidden = true,
}, self.idContainer)
local container = XWindow:new({
Id = "idCategory",
LayoutMethod = "VList",
LayoutVSpacing = 0,
FoldWhenHidden = true,
}, category_window)
local button = XTextButton:new({
Id = "idCategoryButton",
Dock = "top",
FoldWhenHidden = true,
LayoutMethod = "VList",
Padding = box(2, 2, 2, 2),
Background = RGBA(38, 146, 227, 255),
FocusedBackground = RGBA(24, 123, 197, 255),
DisabledBackground = RGBA(128, 128, 128, 255),
OnPress = function(button)
self:ExpandCategory(container, group.category, not container:GetVisible(), not self.Embedded and "save_settings")
end,
RolloverBackground = RGBA(24, 123, 197, 255),
PressedBackground = RGBA(13, 113, 187, 255),
Image = "CommonAssets/UI/round-frame-20.tga",
ImageScale = point(500, 500),
FrameBox = box(9, 9, 9, 9),
}, category_window)
button:SetTextStyle("GedButton")
button:SetText(group.category_name)
button:SetVisible(not self.HideFirstCategory or i ~= 1)
if self.collapsed_categories[group.category] then
container:SetVisible(false)
end
rawset(category_window, "category", group.category)
category_window:Open()
self.prop_update_in_progress = true
local values_name = context .. "|values"
for p, prop_meta in ipairs(group.prop_metas) do
local editor_name = prop_meta.editor or prop_meta.type
local editor_class = GedPropEditors[editor_name or false]
if not editor_class then
assert(editor_class ~= "objects", "Unknown prop editor name: " .. tostring(editor_name))
elseif not g_Classes[editor_class] then
assert(false, "Unknown prop editor class: " .. tostring(editor_class))
else
local context = self.context .. "." .. prop_meta.id
local editor = editors_by_hash[xxhash(context, table.hash(prop_meta))]
if editor then
editor:SetParent(container)
editor:SetContext(context)
else
editor = g_Classes[editor_class]:new({
panel = self,
obj = values_name,
}, container, context, prop_meta)
editor:Open()
end
editor:SetHighlightSearchMatch(matching_props[prop_meta.id])
editor:UpdatePropertyNames(self.ShowInternalNames)
editor:UpdateValue(true)
editor.parent_obj_id = self.parent_obj_id
end
end
self.prop_update_in_progress = false
end
self:UpdateVisibility()
self:ReassignFocusOrders()
focus = self:GetRelativeFocus(order, "nearest")
if focus then
focus:SetFocus()
end
::cleanup::
self:TryHighlightSearchMatch("skip_visibility_update")
Msg("XWindowRecreated", self)
for _, editor in pairs(editors_by_hash) do
if not editor.parent then
editor:delete()
end
end
end
function GedPropPanel:ReassignFocusOrders(x, y)
x = x or self.focus_column
y = y or 0
if self.window_state == "destroying" then
return y
end
for _, category_window in ipairs(self.idContainer) do
if category_window:GetVisible() and category_window:HasMember("idCategory") then
for _, prop_editor in ipairs(category_window.idCategory) do
y = prop_editor:ReassignFocusOrders(x, y)
assert(y)
end
end
end
return y
end
function GedPropPanel:UpdatePropertyNames(internal)
for _, category_window in ipairs(self.idContainer) do
for _, prop_editor in ipairs(rawget(category_window, "idCategory") or empty_table) do
prop_editor:UpdatePropertyNames(internal)
end
end
end
function GedPropPanel:IsPropEditorVisible(prop_editor, filter_text, highlight_text)
local tab = self.active_tab
local tabs = self:GetTabsData()
if tab ~= "All" and tabs then
local prop = prop_editor.prop_meta
local tab_data = table.find_value(tabs, "TabName", tab)
if tab_data and not tab_data.Categories[prop.category or "Misc"] then
return false
end
end
return prop_editor:FindText(filter_text, highlight_text)
end
function GedPropPanel:UpdateVisibility()
local values = self.context and self:Obj(self.context .. "|values")
if not values then return end
local filter_text = self:GetFilterText()
local highlight_text = self:GetHighlightText()
for _, category_window in ipairs(self.idContainer) do
local hidden = 0
local prop_editors = rawget(category_window, "idCategory") or empty_table
for _, prop_editor in ipairs(prop_editors) do
local visible = self:IsPropEditorVisible(prop_editor, filter_text, highlight_text)
prop_editor:SetVisible(visible)
if not visible then
hidden = hidden + 1
end
end
category_window:SetVisible(hidden ~= #prop_editors)
end
end
function GedPropPanel:LocateEditorById(id)
if self.window_state == "destroying" then return end
for _, category_window in ipairs(self.idContainer) do
if category_window:HasMember("idCategory") then
for _, prop_editor in ipairs(category_window.idCategory) do
if prop_editor.prop_meta.id == id then
return prop_editor
end
end
end
end
end
function GedPropPanel:TryHighlightSearchMatch(skip_visibility_update)
local match_data = self.app:GetDisplayedSearchResultData()
local obj_id = self:Obj(self.context)
if match_data and match_data.path[#match_data.path] == obj_id then
if self.desktop:GetKeyboardFocus().Id ~= "idSearchEdit" then
self:FocusPropEditor(match_data.prop)
end
self.app.display_search_result = false
end
if not skip_visibility_update then
self:UpdateVisibility()
end
end
function GedPropPanel:FocusPropEditor(prop_id)
local old_focused = self.search_value_focused_prop_editor
if old_focused and old_focused.parent and old_focused.window_state ~= "destroying" then
old_focused:HighlightAndSelect(false)
end
local highlight_text = self:GetHighlightText()
local prop_editor = self:LocateEditorById(prop_id)
local focus = highlight_text and prop_editor and prop_editor:HighlightAndSelect(highlight_text)
if focus then
-- scroll the prop editor into the view
local scrollarea = self.idContainer
local parent = GetParentOfKind(scrollarea.parent, "XScrollArea")
while parent do
scrollarea = parent
parent = GetParentOfKind(scrollarea.parent, "XScrollArea")
end
scrollarea:ScrollIntoView(focus)
end
self.search_value_focused_prop_editor = prop_editor
end
function GedPropPanel:ExpandCategory(container, category, expand, save_settings)
self.collapsed_categories[category] = not expand
if save_settings then
self:SaveCollapsedCategories(self.context)
end
container:SetVisible(expand)
end
function GedPropPanel:SaveCollapsedCategories(key)
local app = self.app
local settings = app.settings.collapsed_categories or {}
settings[key] = self.collapsed_categories
app.settings.collapsed_categories = settings
app:SaveSettings()
end
function GedPropPanel:ExpandCollapseCategories()
if self.window_state == "destroying" then return end
local has_expanded
for _, category_window in ipairs(self.idContainer) do
if category_window:HasMember("idCategory") and category_window.idCategory:GetVisible() then
has_expanded = true
break
end
end
for _, category_window in ipairs(self.idContainer) do
if category_window:HasMember("idCategory") then
self:ExpandCategory(category_window.idCategory, category_window.category, not has_expanded, not "save_settings")
end
end
end
----- GedListPanel
DefineClass.GedListPanel = {
__parents = { "GedPanel" },
properties = {
{ category = "General", id = "FormatFunc", editor = "text", default = "GedListObjects", },
{ category = "General", id = "Format", editor = "text", default = "<name>", },
{ category = "General", id = "AllowObjectsOnly", editor = "bool", default = false, },
{ category = "General", id = "FilterName", editor = "text", default = "", },
{ category = "General", id = "FilterClass", editor = "text", default = "", },
{ category = "General", id = "SelectionBind", editor = "text", default = "", },
{ category = "General", id = "OnDoubleClick", editor = "func", params = "self, item_idx", default = function(self, item_idx) end, },
{ category = "General", id = "MultipleSelection", editor = "bool", default = false },
{ category = "General", id = "EmptyText", editor = "text", default = "" },
{ category = "General", id = "DragAndDrop", editor = "bool", default = false },
{ category = "Common Actions", id = "ItemClass", editor = "expression", params = "gedapp", default = function(gedapp) return "" end, },
{ category = "Context Menu", id = "ItemActionContext", editor = "text", default = "" },
},
ContainerControlClass = "XList",
Interactive = true,
EnableSearch = true,
Translate = true,
pending_update = false,
pending_selection = false,
restoring_state = false,
}
function GedListPanel:InitControls()
GedPanel.InitControls(self)
local list = self.idContainer
list.OnSelection = function(list, ...) self:OnSelection(...) end
list.OnDoubleClick = function(list, item_idx) self:OnDoubleClick(item_idx) end
list.CreateTextItem = function(list, ...) return self:CreateTextItem(...) end
list:SetMultipleSelection(true)
list:SetActionContext(self.ActionContext)
list:SetItemActionContext(self.ItemActionContext)
GedPanel.SetSearchActionContexts(self, self.SearchActionContexts)
end
function GedListPanel:Done()
for bind in string.gmatch(self.SelectionBind .. ",", reCommaList) do
self.connection:UnbindObj(bind)
end
end
function GedListPanel:CreateTextItem(text, props, context)
props = props or {}
local item = XListItem:new({ selectable = props.selectable }, self.idContainer)
props.selectable = nil
local label = XText:new(props, item, context)
label:SetText(text)
item.idLabel = label
if self.DragAndDrop then
item.idDragAndDrop = GedListDragAndDrop:new({
Dock = "box",
List = self,
Item = item,
}, item)
end
return item
end
function GedListPanel:BindViews()
if self.FilterName ~= "" and self.FilterClass then
self.connection:BindFilterObj(self.context .. "|list", self.FilterName, self.FilterClass)
end
self:BindView("list", self.FormatFunc, self.Format, self.AllowObjectsOnly)
end
function GedListPanel:GetSelection()
local selection = self.pending_selection or self.idContainer:GetSelection()
if not selection or not next(selection) then return end
return selection[1], selection
end
function GedListPanel:GetMultiSelection()
return self.idContainer:GetSelection()
end
function GedListPanel:SetSelection(selection, multiple_selection, notify, restoring_state)
if restoring_state or self.pending_update then
self.pending_selection = multiple_selection or selection
self.restoring_state = restoring_state
return
end
self.idContainer:SetSelection(multiple_selection or selection, notify)
end
function GedListPanel:OnSelection(selected_item, selected_items)
self:BindSelectedObject(selected_item, selected_items)
end
function GedListPanel:SetSelectionAndFocus(idx)
local list = self.idContainer
if list:GetSelection() == idx then
self.app:TryHighlightSearchMatchInChildPanels(self)
else
-- focusing prevents issues with the focus restored to the leftmost panel (and selecting the wrong object) when cycling through search results
local focus = self.desktop:GetKeyboardFocus()
if not focus or focus.Id ~= "idSearchEdit" then
self:SetFocus()
end
list:SetSelection(idx)
end
end
function GedListPanel:TryHighlightSearchMatch()
local obj_id = self:Obj(self.context)
local match_data = self.app:GetDisplayedSearchResultData()
if match_data then
local match_path = match_data.path
local match_idx = table.find(match_path, obj_id)
if match_idx and match_idx < #match_path then
local match_id = match_path[match_idx + 1]
local list = self:Obj(self.context .. "|list")
local ids = list.ids or empty_table
for idx in ipairs(list) do
if ids[idx] == match_id then
self:SetSelectionAndFocus(idx)
break
end
end
end
end
end
function GedListPanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if view == nil then
self.idContainer:SetSelection(false)
self.pending_update = true
end
if view == "list" then
self:UpdateContent()
if self.search_values then
self:StartUpdateFilterThread("not_user_initiated")
end
self:TryHighlightSearchMatch()
end
if view == "warning" and self.DisplayWarnings then
self:UpdateContent() -- if the displayed items need to be underlined
end
end
function GedListPanel:BindSelectedObject(selected_item, selected_indexes)
if not selected_item then return end
self.app:StoreAppState()
for bind in string.gmatch(self.SelectionBind .. ",", reCommaList) do
if self.MultipleSelection and selected_indexes and #selected_indexes > 1 then
self.app:SelectAndBindMultiObj(bind, self.context, selected_indexes)
else
self.app:SelectAndBindObj(bind, { self.context, selected_item })
end
end
end
function GedListPanel:UpdateContent()
if not self.context then return end
local list = self:Obj(self.context .. "|list")
if not list then
self.idContainer:Clear()
return
end
local sel = self.pending_selection or self.idContainer:GetSelection()
local scroll_pos = self.idScroll.Scroll
local filtered, ids = list.filtered, list.ids
local filter_text = self:GetFilterText()
if filter_text == "" then
filter_text = false
end
local warning_idxs = self.DisplayWarnings and get_warning_nodes(self) or empty_table
if #warning_idxs == 0 then
warning_idxs = false
end
local container = self.idContainer
container:Clear()
local string_format = string.format
for i, item_text in ipairs(list) do
local str = "<literal(text,true)>"
if warning_idxs and table.find(warning_idxs, i) then
str = string_format("<underline>%s</underline>", str)
end
str = string.format("<read_only('%s')>%s</read_only('%s')>", self.context, str, self.context)
local item_id = ids and ids[i]
if item_id then
str = string_format("<ged_marks('%s')>%s", item_id, str)
end
str = T{str, text = item_text, untranslated = true}
local item = container:CreateTextItem(str, { Translate = true })
if filtered and filtered[i] or filter_text and self:FilterItem(item_text, item_id, filter_text) then
item:SetDock("ignore")
item:SetVisible(false)
end
end
if #list == 0 and self.EmptyText then
self.idContainer:CreateTextItem(Untranslated(self.EmptyText), { Translate = true })
end
self.idScroll:ScrollTo(scroll_pos)
self.idContainer:SetSelection(sel, self.pending_update and not self.restoring_state) -- notify only if selection is set externally
self.pending_update = false
self.pending_selection = false
self.restoring_state = false
Msg("XWindowRecreated", self)
end
function GedListPanel:FocusFirstEntry()
for idx, value in ipairs(self.idContainer) do
if value.Dock ~= "ignore" then
self.idContainer:SetSelection(idx)
break
end
end
self:SetPanelFocused()
end
function GedListPanel:UpdateFilter()
self:UpdateContent()
end
function GedListPanel:UpdateItemTexts()
self:UpdateTitle(self.context)
for _, item in ipairs(self.idContainer) do
item[1]:SetText(item[1]:GetText())
end
end
function GedListPanel:IsEmpty()
local list = self:Obj(self.context .. "|list")
return list and #list > 0
end
----- GedTreePanel
DefineClass.GedTreePanel = {
__parents = { "GedPanel" },
properties = {
{ category = "General", id = "FormatFunc", editor = "text", default = "GedObjectTree", },
{ category = "General", id = "Format", editor = "text", default = "<name>", },
{ category = "General", id = "AltFormat", editor = "text", default = "", },
{ category = "General", id = "AllowObjectsOnly", editor = "bool", default = false, },
{ category = "General", id = "FilterName", editor = "text", default = "", },
{ category = "General", id = "FilterClass", editor = "text", default = "", },
{ category = "General", id = "SelectionBind", editor = "text", default = "", },
{ category = "General", id = "OnSelectionChanged", editor = "func", params = "self, selection", default = function(self, selection) end, },
{ category = "General", id = "OnCtrlClick", editor = "func", params = "self, selection", default = function(self, selection) end, },
{ category = "General", id = "OnAltClick", editor = "func", params = "self, selection", default = function(self, selection) end, },
{ category = "General", id = "OnDoubleClick", editor = "func", params = "self, selection", default = function(self, selection) end, },
{ category = "General", id = "MultipleSelection", editor = "bool", default = false },
{ category = "General", id = "DragAndDrop", editor = "bool", default = false },
{ category = "General", id = "EnableRollover", editor = "bool", default = false },
{ category = "General", id = "EmptyText", editor = "text", default = "" },
-- Enables the actions for all nodes of a tree panel. By default actions are disabled for Group nodes but some tree panels don't have groups and can use this option.
{ category = "Common Actions", id = "EnableForRootLevelItems", editor = "bool", default = false, },
{ category = "Common Actions", id = "ItemClass", editor = "expression", params = "gedapp", default = function(gedapp) return "" end, },
{ category = "Context Menu", id = "RootActionContext", editor = "text", default = "" },
{ category = "Context Menu", id = "ChildActionContext", editor = "text", default = "" },
{ category = "Layout", id = "FullWidthText", editor = "bool", default = false, },
{ category = "Layout", id = "ShowToolbarButtons", name = "Show toolbar buttons", editor = "bool", default = true, help = "Show/hide the buttons in the TreePanel toolbar (expand/collapse tree, etc)", },
},
ContainerControlClass = "XTree",
HorizontalScroll = true,
EnableSearch = true,
Interactive = true,
Translate = true,
expanding_node = false,
currently_selected_path = false,
pending_update = false,
pending_selection = false,
alt_format_enabled = false,
filtered_tree = false,
view_warnings_only = false,
view_errors_only = false,
}
function GedTreePanel:InitControls()
GedPanel.InitControls(self)
local tree = self.idContainer
tree:SetSortChildNodes(false)
tree.GetNodeChildren = function(tree, ...) return self:GetNodeChildren(...) end
tree.InitNodeControls = function(tree, ...) self:InitNodeControls(...) end
tree.OnSelection = function(tree, selection, selected_indexes) self:OnSelection(selection, selected_indexes) end
tree.OnCtrlClick = function(tree, ...) self:OnCtrlClick(...) end
tree.OnAltClick = function(tree, ...) self:OnAltClick(...) end
tree.OnUserExpandedNode = function(tree, path) self:OnUserExpandedNode(path) end
tree.OnUserCollapsedNode = function(tree, path) self:OnUserCollapsedNode(path) end
tree.OnDoubleClickedItem = function(tree, path) self:OnDoubleClick(path) end
tree:SetActionContext(self.ActionContext)
tree:SetRootActionContext(self.RootActionContext)
tree:SetChildActionContext(self.ChildActionContext)
tree:SetMultipleSelection(true)
tree:SetFullWidthText(self.FullWidthText)
GedPanel.SetSearchActionContexts(self, self.SearchActionContexts)
local alt_format_button = XTemplateSpawn("GedToolbarToggleButtonSmall", self.idTitleContainer)
alt_format_button:SetId("idAltFormatButton")
alt_format_button:SetIcon("CommonAssets/UI/Ged/log-focused.tga")
alt_format_button:SetRolloverText(T(185486815318, "Hide/show alternative names"))
alt_format_button:SetBackground(self.idTitleContainer:GetBackground())
alt_format_button:SetToggled(false)
alt_format_button:SetFoldWhenHidden(true)
alt_format_button:SetVisible(self.AltFormat and self.AltFormat ~= "")
alt_format_button.OnPress = function(button)
self.alt_format_enabled = not button:GetToggled()
button:SetToggled(self.alt_format_enabled)
self:BindView("tree", self.FormatFunc, self.alt_format_enabled and self.AltFormat or self.Format, self.AllowObjectsOnly)
end
if self.ShowToolbarButtons then
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
button:SetIcon("CommonAssets/UI/Ged/collapse_node.tga")
button:SetRolloverText("Expand/collapse selected node's children (Alt-C)")
button:SetBackground(self.idTitleContainer:GetBackground())
button.OnPress = function()
self.idContainer:ExpandNodeByPath(self.idContainer:GetFocusedNodePath() or empty_table)
self.idContainer:ExpandCollapseChildren(self.idContainer:GetFocusedNodePath() or empty_table, not "recursive", "user_initiated")
end
local button = XTemplateSpawn("GedToolbarButtonSmall", self.idTitleContainer)
button:SetIcon("CommonAssets/UI/Ged/collapse_tree.tga")
button:SetRolloverText("Expand/collapse tree (Shift-C)")
button:SetBackground(self.idTitleContainer:GetBackground())
button.OnPress = function() self.idContainer:ExpandCollapseChildren({}, "recursive", "user_initiated") end
end
end
function GedTreePanel:Done()
for bind in string.gmatch(self.SelectionBind .. ",", reCommaList) do
self.connection:UnbindObj(bind)
end
end
function GedTreePanel:BindViews()
if self.FilterName ~= "" and self.FilterClass then
self.connection:BindFilterObj(self.context .. "|tree", self.FilterName, self.FilterClass)
end
self:BindView("tree", self.FormatFunc, self.alt_format_enabled and self.AltFormat or self.Format, self.AllowObjectsOnly, self.EnableRollover)
if not terminal.desktop:GetKeyboardFocus() then
self:SetPanelFocused()
end
end
local function try_index(root, key, ...)
if key and root then
return try_index(root[key], ...)
end
return root
end
function GedTreePanel:GetNodeChildren(...)
if not self.filtered_tree then return end
local texts, is_leaf, auto_expand, rollovers, user_datas = {}, {}, {}, {}, {}
local node_data = try_index(self.filtered_tree, ...)
local warning_idxs = self.filtered_tree == node_data and get_warning_nodes(self) or empty_table
if type(node_data) == "table" then
for i, subnode in ipairs(node_data) do
if subnode then
local read_only_wrapped_text = string.format("<read_only('%s')><literal(text,true)></read_only('%s')>", self.context, self.context)
local format = subnode.id and ("<ged_marks('%s')>" .. read_only_wrapped_text) or read_only_wrapped_text
if table.find(warning_idxs, i) then
format = string.format("<underline>%s</underline>", format)
end
local str = subnode.id and string.format(format, subnode.id) or format
texts[i] = T{str, text = subnode.name, untranslated = true}
is_leaf[i] = #subnode == 0
auto_expand[i] = not subnode.collapsed
rollovers[i] = subnode.rollover
user_datas[i] = subnode
end
end
end
return texts, is_leaf, auto_expand, rollovers, user_datas
end
GedTreePanel.OnShortcut = GedPanel.OnShortcut
local function new_item(self, child, class, path, button)
local method = child and "GedGetSubItemClassList" or "GedGetSiblingClassList"
local items = self.app:Call(method, self.context, path, self.app.ScriptDomain)
local title = string.format("New %s", class)
GedOpenCreateItemPopup(self, title, items, button, function(class)
if self.window_state == "destroying" then return end
self.app:Op("GedOpTreeNewItem", self.context, path, class, child and "child")
end)
end
function GedTreePanel:InitNodeControls(node, data)
if self.DragAndDrop then
local label = node.idLabel
if label then
local l, u, r, b = label:GetPadding():xyxy()
node.idDragAndDrop = GedTreeDragAndDrop:new({
Dock = "box",
Margins = box(-l, -u, -r, -b),
NodeParent = node,
}, label)
end
end
local mode = data and data.child_button_mode
if not mode then return end
if data.child_class and (mode == "docked" or mode == "docked_if_empty" and #self.idContainer:GetChildNodes(node) == 0) then
local button = XTemplateSpawn("GedPropertyButton", node)
button:SetText("Add new...")
button:SetDock(IsKindOf(node, "XTreeNode") and "bottom")
button:SetZOrder(IsKindOf(node, "XTreeNode") and 0 or 1)
button:SetMargins(IsKindOf(node, "XTreeNode") and box(40, 2, 0, 2) or box(20, 2, 0, 2))
button:SetHAlign("left")
button.OnPress = function() CreateRealTimeThread(new_item, self, "child", data.child_name or data.child_class, node.path, button) end
Msg("XWindowRecreated", button)
return
end
-- create floating button that appears on rollover at the right side of items
if node == self.idContainer then return end
if (mode == "floating" or mode == "children") and not data.child_class then return end
if mode == "floating_combined" and not (data.child_class or data.sibling_class) then return end
local new_parent = XWindow:new({ HandleMouse = true, HAlign = "left" }, node)
node.idLabel:SetParent(new_parent)
local create_button = XTemplateSpawn("GedToolbarButtonSmall", new_parent)
create_button:SetId("idNew")
create_button:SetDock("right")
create_button:SetIcon("CommonAssets/UI/Ged/new.tga")
create_button:SetVisible(not mode:starts_with("floating"))
create_button:SetRolloverText("New " .. (data.child_name or data.child_class))
create_button.OnPress = function(button)
if not self:IsThreadRunning("CreateSubItem") then
self:CreateThread("CreateSubItem", function(self, data)
local mode = data.child_button_mode
if mode == "floating_combined" and data.child_class and data.sibling_class then
local host = XActionsHost:new({}, self)
XAction:new({
ActionId = "AddChild",
ActionMenubar = "menu",
ActionTranslate = false,
ActionName = "Add child item...",
OnAction = function() CreateRealTimeThread(new_item, self, "child", data.child_name or data.child_class, node.path, button) end,
}, host)
XAction:new({
ActionId = "AddSibling",
ActionMenubar = "menu",
ActionTranslate = false,
ActionName = "Add new item below...",
OnAction = function() CreateRealTimeThread(new_item, self, not "child", data.sibling_name or data.sibling_class, node.path, button) end,
}, host)
host:OpenPopupMenu("menu", terminal.GetMousePos())
elseif (mode == "floating_combined" or mode == "floating" or mode == "children") and data.child_class then
new_item(self, "child", data.child_name or data.child_class, node.path, button)
elseif mode == "floating_combined" and data.sibling_class then
new_item(self, not "child", data.sibling_name or data.sibling_class, node.path, button)
end
end, self, data)
end
end
create_button.button_mode = mode
-- A thread to make the hovered button visible; OnSetRollover doesn't work as multiple nested items get rollovered at once
if not self:IsThreadRunning("TreeItemRollover") then
self:CreateThread("TreeItemRollover", function(self)
local last
while true do
local focus = terminal.desktop.keyboard_focus
if not focus or not GetParentOfKind(focus, "XPopup") then
local pt = terminal.GetMousePos()
local rollover = GetParentOfKind(terminal.desktop.modal_window:GetMouseTarget(pt), "XTreeNode")
if rollover ~= last then
if last and last.idNew and last.idNew.button_mode:starts_with("floating") then last.idNew:SetVisible(false) end
last = rollover
if last and last.idNew and last.idNew.button_mode:starts_with("floating") then last.idNew:SetVisible(true) end
end
end
Sleep(50)
end
end, self)
end
end
function GedTreePanel:OnUserExpandedNode(path)
self.connection:Send("rfnTreePanelNodeCollapsed", self.context, path, false)
local entry = try_index(self.filtered_tree, table.unpack(path))
if entry then entry.collapsed = false end
local orig_entry = try_index(self:Obj(self.context.."|tree"), table.unpack(path))
if orig_entry then orig_entry.collapsed = false end
end
function GedTreePanel:OnUserCollapsedNode(path)
self.connection:Send("rfnTreePanelNodeCollapsed", self.context, path, true)
local entry = try_index(self.filtered_tree, table.unpack(path))
if entry then entry.collapsed = true end
local orig_entry = try_index(self:Obj(self.context.."|tree"), table.unpack(path))
if orig_entry then orig_entry.collapsed = true end
end
function GedTreePanel:GetSelection()
return self.idContainer:GetSelection()
end
function GedTreePanel:GetMultiSelection()
return table.pack(self:GetSelection())
end
function GedTreePanel:SetSelection(selection, selected_keys, notify, restoring_state)
if type(selection) == "table" and type(selection[1]) == "table" then
selected_keys = selection[2]
selection = selection[1]
end
if restoring_state or self.pending_update then
self.pending_selection = { selection, selected_keys }
self.restoring_state = restoring_state
return
end
self.idContainer:SetSelection(selection, selected_keys, notify)
end
function GedTreePanel:BindSelectedObject(selection, selected_keys)
if not selection then return end
self.app:StoreAppState()
for bind in string.gmatch(self.SelectionBind .. ",", reCommaList) do
if self.MultipleSelection and selected_keys and #selected_keys > 1 then
self.app:SelectAndBindMultiObj(bind, { self.context, unpack_params(selection, 1, #selection - 1) }, selected_keys)
else
self.app:SelectAndBindObj(bind, { self.context, table.unpack(selection) })
end
end
end
function GedTreePanel:OnSelection(selection, selected_keys)
self:BindSelectedObject(selection, selected_keys)
local old_selection = self.currently_selected_path or empty_table
if not table.iequal(old_selection, selection) then
self:OnSelectionChanged(selection)
self.currently_selected_path = selection
self.app:ActionsUpdated()
end
end
function GedTreePanel:SetSelectionAndFocus(path)
local tree = self.idContainer
if not tree then return end -- window destroying
if table.iequal(tree:GetSelection() or empty_table, path) then
self.app:TryHighlightSearchMatchInChildPanels(self)
else
-- focusing prevents issues with the focus restored to the leftmost panel (and selecting the wrong object) when cycling through search results
local focus = self.desktop:GetKeyboardFocus()
if not focus or focus.Id ~= "idSearchEdit" then
self:SetFocus()
end
tree:SetSelection(path)
end
end
function GedTreePanel:TryHighlightSearchMatch()
local match_data = self.app:GetDisplayedSearchResultData()
if match_data then
local match_path = match_data.path
local max_idx, path = 0
self.idContainer:ForEachNode(function(node)
local text = TDevModeGetEnglishText(node:GetText())
for idx, obj_id in ipairs(match_path) do
if string.find(text, obj_id, 1, true) then
max_idx = Max(max_idx, idx)
path = node.path
end
end
end)
if max_idx > 0 then
self:SetSelectionAndFocus(path)
end
end
end
function GedTreePanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if view == nil then
self.idContainer:ClearSelection(false)
self.pending_update = true
end
if view == "tree" then
self:RebuildTree()
local tree_empty = self:IsEmpty()
if context == "bookmarks" then
if tree_empty then
self:Expand(false) -- collapse bookmarks panel when empty
elseif not self.expanded then
self:Expand(true) -- expand when adding a bookmark and panel is collapsed
end
end
local first_update = self.app.first_update
local sel = self.pending_selection or -- selection set before the tree data arrived
first_update and (tree_empty and {{}, {}} or {{1},{1}}) or -- default selection upon first editor open
table.pack(self.idContainer:GetSelection()) -- previous selection
local scroll_pos = self.idScroll.Scroll
self.idContainer:Clear() -- fills the tree control with content
if tree_empty and self.context ~= "bookmarks" then
self:OnSelection(empty_table) -- will bind "nothing" to child panels to empty them
local text = XText:new({ ZOrder = 1, Margins = box(20, 2, 0, 2) }, self.idContainer)
text:SetText(self.EmptyText)
Msg("XWindowRecreated", text)
else
self.idContainer:SetSelection(sel[1], sel[2], first_update or self.pending_update and not self.restoring_state)
end
self.idScroll:ScrollTo(scroll_pos)
self.pending_update = false
self.pending_selection = false
self.app.first_update = false
self.restoring_state = false
self:InitNodeControls(self.idContainer, self.filtered_tree)
Msg("XWindowRecreated", self)
if self.search_values then
self:StartUpdateFilterThread("not_user_initiated")
end
self:TryHighlightSearchMatch()
end
end
function GedTreePanel:SetViewWarningsOnly(mode)
if self.view_warnings_only ~= mode then
self.view_warnings_only = mode
if self.idSearchEdit:GetText() ~= "" then
self:CancelSearch()
else
self:UpdateFilter()
end
end
end
function GedTreePanel:SetViewErrorsOnly(mode)
if self.view_errors_only ~= mode then
self.view_errors_only = mode
if self.idSearchEdit:GetText() ~= "" then
self:CancelSearch()
else
self:UpdateFilter()
end
end
end
-- If it returns true - hide the item
function GedTreePanel:FilterItem(text, item_id, filter_text, has_child_nodes)
local msg = get_warning_msg(item_id)
local msg_type = msg and msg[#msg]
local filter = GedPanel.FilterItem(self, text, item_id, filter_text)
local children = not has_child_nodes
-- Show only warnings/errors - 4 states
if self.view_warnings_only and not self.view_errors_only then
return filter or children and msg_type ~= "warning" -- only warnings
elseif not self.view_warnings_only and self.view_errors_only then
return filter or children and msg_type ~= "error" -- only errors
elseif self.view_warnings_only and self.view_errors_only then
return filter or children and not msg_type -- warning or error
else
return filter -- everything
end
end
function GedTreePanel:BuildTree(root, filter_text)
if not root or root.filtered then
return false
end
local has_child_nodes = type(root) ~= "string" and #root > 0
if (self.search_value_results or type(root) == "string") and self:FilterItem(root, root.id, filter_text, has_child_nodes) then
return false
end
if type(root) == "string" then
return { id = root.id, name = root }
end
local node = {
id = root.id,
name = root.name,
class = root.class,
child_button_mode = root.child_button_mode,
child_class = root.child_class,
child_name = root.child_name,
sibling_class = root.sibling_class,
sibling_name = root.sibling_name,
rollover = root.rollover,
collapsed = filter_text == "" and not self.view_warnings_only and not self.view_errors_only and root.collapsed
}
local visible = false
for key, subnode in ipairs(root) do
local child = self:BuildTree(subnode, filter_text) or false
table.insert(node, child)
if child then visible = true end
end
if visible or root.name and (self.search_value_results or not self:FilterItem(root.name, root.id, filter_text)) then
return node
end
end
function GedTreePanel:RebuildTree()
local data = self:Obj(self.context .. "|tree")
if not data then return end
self.filtered_tree = self:BuildTree(data, self:GetFilterText()) or empty_table
end
function GedTreePanel:FocusFirstEntry()
if self:GetFilterText() == "" then
if not self:GetSelection() then
self:SetSelection({1})
end
self:SetPanelFocused()
return
end
local path = {}
local root = self.filtered_tree
while type(root) == "table" do
local to_iterate = root
root = nil
for key, node in ipairs(to_iterate) do
if node then
root = node
table.insert(path, key)
break
end
end
end
self:SetSelection(path)
self:SetPanelFocused()
end
function GedTreePanel:UpdateFilter()
self:RebuildTree()
local sel = table.pack(self.idContainer:GetSelection())
local scroll_pos = self.idScroll.Scroll
self.idContainer:Clear() -- fills the tree control with content
self.idContainer:SetSelection(sel[1], sel[2], not "notify") -- restore selection as if nothing has changed (don't notify)
self.idScroll:ScrollTo(scroll_pos)
self:InitNodeControls(self.idContainer, self.filtered_tree)
Msg("XWindowRecreated", self)
end
function GedTreePanel:UpdateItemTexts()
self:UpdateTitle(self.context)
self.idContainer:ForEachNode(function(node) node.idLabel:SetText(node.idLabel:GetText()) end)
end
function GedTreePanel:IsEmpty()
return type(self.filtered_tree) ~= "table"
end
if FirstLoad then
ged_drag_target = false
ged_drag_initatior = false
ged_drop_target = false
ged_drop_type = false
end
DefineClass.GedDragAndDrop = {
__parents = { "XDragAndDropControl", },
ForEachSelectedDragTargetChild = empty_func,
ForEachDragTargetChild = empty_func,
GetGedDragTarget = empty_func,
drop_target_container_class = "XTree",
line_thickness = 2,
}
function GedDragAndDrop:GetDragAndDropError()
if not ged_drag_target or not ged_drop_target then
return true
end
local selected_children = {}
self:ForEachSelectedDragTargetChild(function(selected_child, selected_children)
selected_children[selected_child] = true
end, selected_children)
if not next(selected_children) then
return true
end
local win = ged_drop_target
while win and not IsKindOf(win, self.drop_target_container_class) do
if selected_children[win] then
return true
end
win = win.parent
end
end
function GedDragAndDrop:OnMouseButtonDown(pt, button)
if button ~= "L" then return end
local res = XDragAndDropControl.OnMouseButtonDown(self, pt, button)
if not self.drag_win and self.pt_pressed then
self:SetModal()
end
return res
end
function GedDragAndDrop:OnMouseButtonUp(pt, button)
if self.desktop.modal_window == self then
self:SetModal(false)
end
return XDragAndDropControl.OnMouseButtonUp(self, pt, button)
end
function GedDragAndDrop:GetMouseCursor()
if not self.enabled then return end
if self.drag_win then
if self:GetDragAndDropError() then
return "CommonAssets/UI/ErrorCursor.tga"
else
return "CommonAssets/UI/HandCursor.tga"
end
end
end
function GedDragAndDrop:OnDragStart(pt, button)
local drag_target = self:GetGedDragTarget()
if not drag_target then return end
ged_drag_target = drag_target
ged_drag_initatior = self
local drag_parent_win = XWindow:new({
LayoutMethod = "VList",
ZOrder = 10,
Clip = false,
HideOnEmpty = true,
UseClipBox = false,
}, self.desktop)
self:ForEachSelectedDragTargetChild(function(child, drag_parent_win, self)
return self:AddDragWindowText(child, drag_parent_win)
end, drag_parent_win, self)
if #drag_parent_win == 0 then
drag_parent_win:delete()
return
end
drag_parent_win:UpdateMeasure(drag_parent_win.MaxWidth, drag_parent_win.MaxHeight)
local cursor_width = UIL.MeasureImage(GetMouseCursor())
drag_parent_win:SetBox(pt:x() + cursor_width - 10, pt:y(), drag_parent_win.measure_width, drag_parent_win.measure_height, true)
return drag_parent_win
end
function GedDragAndDrop:GetDragWindowTextControl(control)
return control.idLabel
end
function GedDragAndDrop:AddDragWindowText(control, drag_parent_win)
local label_win = self:GetDragWindowTextControl(control)
if not label_win then return end
XText:new({
HAlign = "left",
VAlign = "top",
ZOrder = 10,
Clip = false,
TextStyle = GetDarkModeSetting() and "GedDefaultDarkMode" or "GedDefault",
Translate = label_win.Translate,
HideOnEmpty = true,
UseClipBox = false,
Background = RGBA(89, 126, 141, 100),
}, drag_parent_win):SetText(label_win:GetText())
end
function GedDragAndDrop:UpdateDrag(drag_win, pt)
XDragAndDropControl.UpdateDrag(self, drag_win, pt)
if not ged_drag_target or ged_drag_target ~= self:GetGedDragTarget() then return end
local drop_target = false
if ged_drag_target.box:minx() < pt:x() and ged_drag_target.box:maxx() > pt:x() then
self:ForEachDragTargetChild(function(child, self, py)
local drop_target_box = self:GetDropTargetBox(child)
if drop_target_box and drop_target_box:miny() <= py and drop_target_box:maxy() > py then
drop_target = child
return "break"
end
end, self, pt:y())
end
if drop_target ~= ged_drop_target then
ged_drop_target = drop_target
UIL.Invalidate()
end
if ged_drop_target then
local drop_target_box = self:GetDropTargetBox()
if not drop_target_box then return end
local miny, maxy = drop_target_box:miny(), drop_target_box:maxy()
local sizey = maxy - miny
local dy = pt:y() - miny
local pct = dy * 100 / sizey
local new_drop_type = self:GetDropType(pct)
if new_drop_type ~= ged_drop_type then
ged_drop_type = new_drop_type
UIL.Invalidate()
end
end
end
function GedDragAndDrop:GetDropTargetBox(drop_target)
drop_target = drop_target or ged_drop_target
return drop_target and drop_target.box
end
function GedDragAndDrop:GetDropType(pct)
return pct < 50 and "Up" or "Down"
end
local function DrawHorizontalLine(width, pt1, pt2, color)
local x1, y1 = pt1:xy()
local x2, y2 = pt2:xy()
for i = -width/2, width - width/2 - 1 do
UIL.DrawLine(point(x1, y1 + i), point(x2, y2 + i), color)
end
end
local highlight_color = RGB(62, 165, 165)
local highlight_color_error = RGB(197, 128, 128)
function GedDragAndDropHighlight()
if not ged_drag_initatior or not ged_drop_target then return end
local color = ged_drag_initatior:GetDragAndDropError() and highlight_color_error or highlight_color
local drop_target_box = ged_drag_initatior:GetDropTargetBox()
local x_padding = ged_drag_initatior.line_thickness/2
if ged_drop_type == "Up" then
DrawHorizontalLine(ged_drag_initatior.line_thickness, point(drop_target_box:minx() - x_padding, drop_target_box:miny() + 1), point(drop_target_box:maxx() + x_padding, drop_target_box:miny() + 1), color)
elseif ged_drop_type == "Down" then
DrawHorizontalLine(ged_drag_initatior.line_thickness, point(drop_target_box:minx() - x_padding, drop_target_box:maxy() + 1), point(drop_target_box:maxx() + x_padding, drop_target_box:maxy() + 1), color)
else
UIL.DrawBorderRect(box(drop_target_box:minx() - x_padding, drop_target_box:miny() - 1 + ged_drag_initatior.line_thickness%2, drop_target_box:maxx() + x_padding, drop_target_box:maxy() + 1), ged_drag_initatior.line_thickness, ged_drag_initatior.line_thickness, color, RGBA(0, 0, 0, 0))
end
end
function OnMsg.Start()
if Platform.desktop then
UIL.Register("GedDragAndDropHighlight", XDesktop.terminal_target_priority + 1)
end
end
function GedDragAndDrop:OnDragEnded(drag_win, last_target, drag_res)
drag_win:delete()
ged_drag_target = false
ged_drag_initatior = false
ged_drop_target = false
ged_drop_type = false
UIL.Invalidate()
end
function GedDragAndDrop:GetDropTarget(...)
if ged_drop_target then
return ged_drop_target.idDragAndDrop
end
return XDragAndDropControl.GetDropTarget(self, ...)
end
DefineClass.GedTreeDragAndDrop = {
__parents = { "GedDragAndDrop", },
NodeParent = false,
}
function GedTreeDragAndDrop:ForEachSelectedDragTargetChild(func, ...)
local panel = GetParentOfKind(ged_drag_target, "GedTreePanel")
if not panel then return end
local first_selected, selected_idxs = panel:GetSelection()
if not first_selected then return end
local parent = ged_drag_target:NodeByPath(first_selected, #first_selected - 1, "allow_root")
local children = ged_drag_target:GetChildNodes(parent)
for _, idx in ipairs(selected_idxs) do
local selected_child = children[idx]
if func(selected_child, ...) == "break" then
return "break"
end
end
end
function GedTreeDragAndDrop:GetGedDragTarget()
return self.NodeParent.tree
end
function GedTreeDragAndDrop:ForEachDragTargetChild(func, ...)
if not IsKindOf(ged_drag_target, self.drop_target_container_class) then return end
return ged_drag_target:ForEachNode(func, ...)
end
function GedTreeDragAndDrop:GetDropTargetBox(drop_target)
drop_target = drop_target or ged_drop_target
return drop_target and drop_target.idLabel and drop_target.idLabel.box
end
function GedTreeDragAndDrop:GetDropType(pct)
if pct < 20 then
return "Up"
elseif pct > 80 and (not ged_drop_target or #ged_drop_target.idSubtree == 0) then
return "Down"
else
return "In"
end
end
gedTreeDragAndDropOps = {
Up = "GedOpTreeDropItemUp",
Down = "GedOpTreeDropItemDown",
In = "GedOpTreeDropItemInwards",
}
function GedTreeDragAndDrop:OnDrop(drag_win, pt, drag_source_win)
if self:GetDragAndDropError() then return end
local panel = GetParentOfKind(ged_drop_target, "GedTreePanel")
local op = gedTreeDragAndDropOps[ged_drop_type]
panel.app:Op(op, panel.context, panel:GetMultiSelection(), table.copy(ged_drop_target.path))
end
DefineClass.GedListDragAndDrop = {
__parents = { "GedDragAndDrop", },
drop_target_container_class = "XList",
List = false,
Item = false,
}
function GedListDragAndDrop:ForEachSelectedDragTargetChild(func, ...)
local list = GetParentOfKind(ged_drag_target, "GedListPanel")
if not 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 GedListDragAndDrop:GetGedDragTarget()
return self.List.idContainer
end
function GedListDragAndDrop: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
gedListDragAndDropOps = {
Up = "GedOpListDropUp",
Down = "GedOpListDropDown",
}
function GedListDragAndDrop:OnDrop(drag_win, pt, drag_source_win)
if self:GetDragAndDropError() then return end
local panel = GetParentOfKind(ged_drop_target, "GedListPanel")
local op = gedListDragAndDropOps[ged_drop_type]
panel.app:Op(op, panel.context, panel:GetMultiSelection(), table.find(panel.idContainer, ged_drop_target))
end
----- GedBreadcrumbPanel
DefineClass.GedBreadcrumbPanel = {
__parents = { "GedPanel" },
properties = {
{ category = "General", id = "FormatFunc", editor = "text", default = "GedFormatObject", },
{ category = "General", id = "TreePanelId", editor = "text", default = "", },
},
ContainerControlClass = "XWindow",
MaxWidth = 1000000,
}
function GedBreadcrumbPanel:InitControls()
GedPanel.InitControls(self)
self.idContainer.LayoutMethod = "HWrap"
end
function GedBreadcrumbPanel:BindViews()
self:BindView("path", self.FormatFunc)
end
function GedBreadcrumbPanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if view == "path" then
local pathdata = self:Obj(self.context .. "|path")
self.idContainer:DeleteChildren()
for k, entry in ipairs(pathdata) do
local button = XButton:new({
OnPress = function(button)
self.app[self.TreePanelId]:SetSelection(entry.path)
end,
Background = RGBA(0, 0, 0, 0),
RolloverBackground = RGBA(72, 72, 72, 255),
}, self.idContainer)
XText:new({}, button):SetText(entry.text)
if k < #pathdata then
XText:new({}, self.idContainer):SetText("<color 32 128 32> >> </color>")
end
end
for _, win in ipairs(self.idContainer) do
win:Open()
end
Msg("XWindowRecreated", self)
end
end
----- GedTextPanel
DefineClass.GedTextPanel = {
__parents = { "GedPanel", "XFontControl" },
properties = {
{ category = "General", id = "FormatFunc", editor = "text", default = "GedFormatObject", },
{ category = "General", id = "Format", editor = "text", default = "", },
{ category = "General", id = "AutoHide", editor = "bool", default = true, },
},
ContainerControlClass = "XText",
MaxWidth = 1000000,
TextStyle = "GedTextPanel",
}
LinkFontPropertiesToChild(GedTextPanel, "idContainer")
function GedTextPanel:InitControls()
GedPanel.InitControls(self)
local text = self.idContainer
text:SetEnabled(false)
text:SetBorderWidth(0)
text:SetFontProps(self)
text:SetWordWrap(false)
end
function GedTextPanel:GetView()
return self.FormatFunc .. "." .. self.Format -- generate unique view id, to make sure different GedTextPanels won't clash
end
function GedTextPanel:BindViews()
self:BindView(self:GetView(), self.FormatFunc, self.Format)
end
function GedTextPanel:GetTextToDisplay()
return self:Obj(self.context .. "|" .. self:GetView()) or ""
end
function GedTextPanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if self.window_state == "open" then
local text = self:GetTextToDisplay()
self.idContainer:SetText(text)
self:SetVisible(not self.AutoHide or text ~= "")
end
end
DefineClass.GedMultiLinePanel = {
__parents = { "GedTextPanel" },
ContainerControlClass = "XMultiLineEdit",
TextStyle = "GedMultiLine",
}
function GedMultiLinePanel:InitControls()
GedTextPanel.InitControls(self)
self.idContainer:SetPlugins({ "XCodeEditorPlugin" })
end
----- GedObjectPanel
DefineClass.GedObjectPanel = {
__parents = { "GedPanel" },
properties = {
{ category = "General", id = "FormatFunc", editor = "text", default = "GedInspectorFormatObject", },
},
ContainerControlClass = "XScrollArea",
MaxWidth = 1000000,
HorizontalScroll = true,
Interactive = true,
}
function GedObjectPanel:BindViews()
self:BindView("objectview", self.FormatFunc)
end
function GedObjectPanel:OnContextUpdate(context, view)
GedPanel.OnContextUpdate(self, context, view)
if view == "objectview" then
self.idContainer:DeleteChildren()
local objectview = self:Obj(self.context .. "|objectview")
if not objectview then return end
local metatable_id = objectview.metatable_id
local text = objectview.name .. (objectview.metatable_id and (" [ <color 128 128 216> <h OpenKey 1>" .. objectview.metatable_name .. "</h></color> ]") or "")
local child = XText:new({
TextStyle = self.app.dark_mode and "GedTitleDarkMode" or "GedTitle",
}, self.idContainer)
child:SetText(text)
child.OnHyperLink = function(this, id, ...)
if id == "OpenKey" then
self.app:Op("GedOpBindObjByRefId", "root", objectview.metatable_id, terminal.IsKeyPressed(const.vkControl))
end
end
local search_text = self.idSearchEdit:GetText()
for _, v in ipairs(objectview.members) do
local key = v.key
local val = v.value
local count = v.count
local value_id = v.value_id
local key_id = v.key_id
local text = (key_id and "<color 128 128 216><h OpenKey q>" .. key .. "</h></color>" or key) .. " = " .. (value_id and "<color 128 128 216><h OpenValue 2>" .. val .. "</h></color>" or val) .. (count and "<color 100 200 220> (#" .. count .. ")</color>" or "")
local child = XText:new({
TextStyle = self.app.dark_mode and "GedDefaultDarkMode" or "GedDefault",
}, self.idContainer)
child:SetText(text)
child.OnHyperLink = function(this, id, ...)
if id == "OpenKey" then
self.app:Op("GedOpBindObjByRefId", "root", key_id, terminal.IsKeyPressed(const.vkControl))
elseif id == "OpenValue" then
self.app:Op("GedOpBindObjByRefId", "root", value_id, terminal.IsKeyPressed(const.vkControl))
end
end
self:UpdateChildVisible(child, search_text)
end
self.idContainer:ScrollTo(0, 0)
end
end
function GedObjectPanel:UpdateChildVisible(child, search_text)
if search_text ~= "" and not string.find_lower(string.strip_tags(child.Text), search_text) then
child:SetDock("ignore")
child:SetVisible(false)
else
child:SetDock(false)
child:SetVisible(true)
end
end
-- Called when there's a change in the search bar
function GedObjectPanel:UpdateFilter()
local search_text = self.idSearchEdit:GetText()
for _, container in ipairs(self) do
if container.Id == "idContainer" then
for idx, child in ipairs(container) do
if idx ~= 1 then
self:UpdateChildVisible(child, search_text) -- hide the items that don't contain the search string and show the rest
end
end
end
end
end
----- GedGraphEditorPanel
DefineClass.GedGraphEditorPanel = {
__parents = { "GedPanel" },
properties = {
{ category = "General", id = "SelectionBind", editor = "text", default = "", },
},
ContainerControlClass = "XGraphEditor",
}
function GedGraphEditorPanel:InitControls()
GedPanel.InitControls(self)
local graph = self.idContainer
graph:SetReadOnly(true)
graph.OnGraphEdited = function()
self:Op("GedSetGraphData", "SelectedPreset", self.idContainer:GetGraphData())
end
graph.OnNodeSelected = function(graph, node)
for bind_name in string.gmatch(self.SelectionBind .. ",", reCommaList) do
self:Send("GedBindGraphNode", "SelectedPreset", node.handle, bind_name)
end
end
graph.NodeClassItems = g_GedApp.ContainerGraphItems
end
function GedGraphEditorPanel:BindViews()
if not self.context then return end
self:BindView("graph", "GedGetGraphData")
end
function GedGraphEditorPanel:OnContextUpdate(context, view)
if view == "graph" then
local data = self:Obj(context .. "|graph") -- will be nil if a non-preset is selected
self.idContainer:SetGraphData(data)
self.idContainer:SetReadOnly(not data)
end
GedPanelBase.OnContextUpdate(self, context, view)
end
----- XPanelSizer
DefineClass.XPanelSizer =
{
__parents = { "XControl" },
properties = {
{ category = "Visual", id = "Cursor", editor = "ui_image", force_extension = ".tga", default = "CommonAssets/UI/Controls/resize03.tga" },
{ category = "Visual", id = "BorderSize", editor = "number", default = 3, },
},
is_horizontal = true,
drag_start_mouse_pos = false,
drag_start_panel1_max_sizes = false,
drag_start_panel2_max_sizes = false,
panel1 = false,
panel2 = false,
valid = true,
}
function XPanelSizer:Open(...)
local layout_method = self.parent.LayoutMethod
assert(layout_method == "HPanel" or layout_method == "VPanel")
self.is_horizontal = layout_method == "HPanel"
if self.is_horizontal then
self:SetMaxWidth(self.BorderSize)
self:SetMinWidth(self.BorderSize)
else
self:SetMaxHeight(self.BorderSize)
self:SetMinHeight(self.BorderSize)
end
if not self.panel1 or not self.panel2 then
local current_index = table.find(self.parent, self)
assert(current_index)
if current_index then
for i = current_index - 1, 1, -1 do
if not self.parent[i].Dock then
assert(not IsKindOf(self.parent[i], "XPanelSizer"))
self.panel1 = self.parent[i]
break
end
end
for i = current_index + 1, #self.parent do
if not self.parent[i].Dock then
assert(not IsKindOf(self.parent[i], "XPanelSizer"))
self.panel2 = self.parent[i]
break
end
end
end
if not self.panel1 or not self.panel2 then
self.valid = false
end
end
end
function XPanelSizer:OnMouseButtonDown(pos, button)
if not self.valid then return "break" end
if button == "L" then
self:SetFocus()
self.desktop:SetMouseCapture(self)
self.drag_start_mouse_pos = pos
self.drag_start_panel1_max_size = point(self.panel1.MaxWidth, self.panel1.MaxHeight)
self.drag_start_panel2_max_size = point(self.panel2.MaxWidth, self.panel2.MaxHeight)
end
return "break"
end
function XPanelSizer:OnMousePos(new_pos)
if self.valid and self.desktop:GetMouseCapture() == self then
self:MovePanel(new_pos)
end
return "break"
end
local MulDivRoundPoint = MulDivRoundPoint
local function ElementwiseMax(min_value, point2)
return point(Max(min_value, point2:x()), Max(min_value, point2:y()))
end
function XPanelSizer:MovePanel(new_pos)
local old_pos = self.drag_start_mouse_pos
local diff = new_pos - old_pos
-- code is analogous to HPanel/VPanel's measure and layout methods
local total_size = point(0, 0)
local min_sizes = point(0, 0)
local total_items = 0
local panel_pixel_sizes = point(0, 0)
for _, win in ipairs(self.parent) do
if not win.Dock and not IsKindOf(win, "XPanelSizer") then
local min_width, min_height, max_width, max_height = ScaleXY(win.scale, win.MinWidth, win.MinHeight, win.MaxWidth, win.MaxHeight)
total_size = total_size + point(max_width, max_height)
min_sizes = min_sizes + point(min_width, min_height)
total_items = total_items + 1
panel_pixel_sizes = panel_pixel_sizes + win.box:size()
end
end
if self.is_horizontal then
assert(total_size:x() >= 100000, "MaxWidths of HPanel's children should be > 1000000")
else
assert(total_size:y() >= 100000, "MaxHeights of VPanel's children should be > 1000000")
end
local pixels_to_distribute = panel_pixel_sizes - min_sizes
if pixels_to_distribute:x() == 0 or pixels_to_distribute:y() == 0 then return end
local pixels_to_max_space_units = MulDivRoundPoint(total_size - min_sizes, 1000, pixels_to_distribute)
local prop_diff = MulDivRoundPoint(diff, pixels_to_max_space_units, 1000)
local min_diff = MulDivRoundPoint(self.panel1.scale,
- ElementwiseMax(0, self.drag_start_panel1_max_size - point(self.panel1.MinWidth, self.panel1.MinHeight)), 1000)
local max_diff = MulDivRoundPoint(self.panel2.scale,
ElementwiseMax(0, self.drag_start_panel2_max_size - point(self.panel2.MinWidth, self.panel2.MinHeight)), 1000)
prop_diff = ClampPoint(prop_diff, box(min_diff, max_diff))
local panel1_new_max_size = self.drag_start_panel1_max_size + MulDivRoundPoint(prop_diff, 1000, self.panel1.scale)
local panel2_new_max_size = self.drag_start_panel2_max_size - MulDivRoundPoint(prop_diff, 1000, self.panel2.scale)
if self.is_horizontal then
self.panel1.MaxWidth = panel1_new_max_size:x()
self.panel2.MaxWidth = panel2_new_max_size:x()
else
self.panel1.MaxHeight = panel1_new_max_size:y()
self.panel2.MaxHeight = panel2_new_max_size:y()
end
self.panel1:InvalidateMeasure()
self.panel2:InvalidateMeasure()
self.parent:InvalidateLayout()
self.parent:UpdateLayout()
end
function XPanelSizer:GetMouseTarget(pos)
return self, self.Cursor
end
function XPanelSizer:OnMouseButtonUp(pos, button)
if self.valid and self.desktop:GetMouseCapture() == self and button == "L" then
self:OnMousePos(pos)
self.desktop:SetMouseCapture()
self.drag_start_mouse_pos = false
end
return "break"
end