|
DefineClass.XDesktop = { |
|
__parents = { "XActionsHost" }, |
|
LayoutMethod = "Box", |
|
|
|
keyboard_focus = false, |
|
modal_window = false, |
|
modal_log = false, |
|
mouse_capture = false, |
|
touch = false, |
|
last_mouse_pos = false, |
|
last_mouse_target = false, |
|
inactive = false, |
|
mouse_target_update = false, |
|
last_event_at = false, |
|
terminal_target_priority = -1, |
|
layout_thread = false, |
|
mouse_target_thread = false, |
|
focus_logging_enabled = false, |
|
rollover_logging_enabled = false, |
|
|
|
HandleMouse = true, |
|
IdNode = true, |
|
} |
|
|
|
function XDesktop:Init() |
|
self.desktop = self |
|
self.focus_log = {} |
|
self.modal_log = {} |
|
self.touch = {} |
|
self.modal_window = self |
|
end |
|
|
|
function XDesktop:WindowLeaving(win) |
|
local last_target = self.last_mouse_target |
|
if last_target and last_target:IsWithin(win) then |
|
local current = last_target |
|
while current do |
|
current:OnMouseLeft(self.last_mouse_pos, last_target) |
|
if current == win then break end |
|
current = current.parent |
|
end |
|
repeat |
|
win = win.parent |
|
until not win or win.HandleMouse |
|
self.last_mouse_target = win |
|
end |
|
end |
|
|
|
function XDesktop:WindowLeft(win) |
|
if self.mouse_capture == win then |
|
self:SetMouseCapture(false) |
|
end |
|
self:RemoveModalWindow(win) |
|
self:RemoveKeyboardFocus(win) |
|
if RolloverControl == win then |
|
XDestroyRolloverWindow("immediate") |
|
end |
|
for id, touch in pairs(self.touch) do |
|
if touch.target == win then |
|
touch.target = nil |
|
end |
|
if touch.capture == win then |
|
touch.capture = nil |
|
end |
|
end |
|
end |
|
|
|
|
|
|
|
function XDesktop:NextFocusCandidate() |
|
for i = #self.focus_log, 1, -1 do |
|
local win = self.focus_log[i] |
|
if win:IsWithin(self.modal_window) and win:IsVisible() and win:GetEnabled() then |
|
return win |
|
end |
|
end |
|
return false |
|
end |
|
|
|
function XDesktop:RestoreFocus() |
|
self:SetKeyboardFocus(self:NextFocusCandidate()) |
|
end |
|
|
|
function XDesktop:SetKeyboardFocus(focus) |
|
assert(not focus or focus:IsWithin(self)) |
|
local last_focus = self.keyboard_focus |
|
if last_focus == focus or focus and not focus:IsWithin(self) then |
|
return |
|
end |
|
|
|
if self.focus_logging_enabled then |
|
print("New Focus:", FormatWindowPath(focus)) |
|
print(GetStack(2)) |
|
end |
|
|
|
if focus then |
|
table.remove_entry(self.focus_log, focus) |
|
table.insert(self.focus_log, focus or nil) |
|
if not focus:IsWithin(self.modal_window) or not focus:IsVisible() then |
|
return |
|
end |
|
end |
|
|
|
if self.inactive then |
|
self.keyboard_focus = focus |
|
return |
|
end |
|
|
|
self.keyboard_focus = focus |
|
local common_parent = XFindCommonParent(last_focus, focus) |
|
local win = last_focus |
|
while win and win ~= common_parent do |
|
win:OnKillFocus(focus) |
|
win = win.parent |
|
end |
|
win = focus |
|
while win and win ~= common_parent do |
|
win:OnSetFocus(focus) |
|
win = win.parent |
|
end |
|
end |
|
|
|
function XDesktop:RemoveKeyboardFocus(win, children) |
|
local is_focused = win:IsFocused(children) |
|
local log = self.focus_log |
|
if children then |
|
for i = #log, 1, -1 do |
|
if log[i]:IsWithin(win) then |
|
table.remove(log, i) |
|
end |
|
end |
|
else |
|
table.remove_entry(log, win) |
|
end |
|
if is_focused then |
|
self:RestoreFocus() |
|
end |
|
end |
|
|
|
function XDesktop:GetKeyboardFocus() |
|
return self.keyboard_focus |
|
end |
|
|
|
function GetKeyboardFocus() |
|
local desktop = terminal and terminal.desktop |
|
return desktop and desktop:GetKeyboardFocus() |
|
end |
|
|
|
function WaitSwitchControls(state, title, text, ok_text, cancel_text) |
|
ok_text = ok_text or T(1138, "Yes") |
|
cancel_text = cancel_text or T(1139, "No") |
|
if WaitQuestion(terminal.desktop, title, text, ok_text, cancel_text, {forced_ui_style = state}) == "ok" then |
|
SwitchControls(state == "gamepad") |
|
end |
|
end |
|
|
|
function XDesktop:KeyboardEvent(event, button, ...) |
|
if config.AutoControllerHandling and event == "OnXButtonDown" and AccountStorage and not GetUIStyleGamepad() then |
|
if config.AutoControllerHandlingType == "popup" then |
|
if not IsValidThread(SwitchControlQuestionThread) then |
|
SwitchControlQuestionThread = CreateRealTimeThread(function() |
|
WaitSwitchControls("gamepad", T(383758760550, "Switch to controller?"), T(207085726945, "Are you sure you want to use a controller?")) |
|
end) |
|
return "break" |
|
end |
|
elseif config.AutoControllerHandlingType == "auto" then |
|
SwitchControls(true) |
|
end |
|
end |
|
|
|
self.last_event_at = RealTime() |
|
local target = self.keyboard_focus |
|
while target do |
|
if target.HandleKeyboard then |
|
if target[event](target, button, ...) == "break" then |
|
|
|
return "break" |
|
end |
|
end |
|
target = target.parent |
|
end |
|
end |
|
|
|
function XDesktop:OnShortcut(shortcut, source, ...) |
|
if source == "mouse" then |
|
local target = self.last_mouse_target or self.mouse_capture |
|
while target and target ~= self do |
|
if target.window_state ~= "destroying" then |
|
if target:OnShortcut(shortcut, source, ...) == "break" then |
|
return "break" |
|
end |
|
end |
|
target = target.parent |
|
end |
|
else |
|
local focus = self.keyboard_focus |
|
while focus and focus ~= self do |
|
if focus.HandleKeyboard then |
|
if focus:OnShortcut(shortcut, source, ...) == "break" then |
|
return "break" |
|
end |
|
end |
|
focus = focus.parent |
|
end |
|
end |
|
end |
|
|
|
function XDesktop:OnSystemVirtualKeyboard() |
|
ConsoleLogResize() |
|
ConsoleResize() |
|
end |
|
|
|
|
|
|
|
function XDesktop:XEvent(event, ...) |
|
if not self.inactive then |
|
if not IsMouseViaGamepadActive() then |
|
self:SetLastMouseTarget(false) |
|
self:ResetMousePosTarget() |
|
end |
|
return self:KeyboardEvent(event, ...) |
|
end |
|
end |
|
|
|
|
|
|
|
function XDesktop:SetMouseCapture(win) |
|
if not win or not win:IsWithin(self.modal_window) then |
|
win = false |
|
end |
|
local old_capture = self.mouse_capture |
|
if old_capture == win then return end |
|
self.mouse_capture = win |
|
if old_capture then |
|
old_capture:OnCaptureLost(self.last_mouse_pos) |
|
end |
|
end |
|
|
|
function XDesktop:GetMouseCapture() |
|
return self.mouse_capture |
|
end |
|
|
|
function XDesktop:RestoreModalWindow() |
|
local win |
|
local log = self.modal_log |
|
for i = #log,1,-1 do |
|
if log[i]:IsVisible() and (not win or log[i]:IsOnTop(win)) then |
|
win = log[i] |
|
end |
|
end |
|
self.desktop:SetModalWindow(win or self) |
|
end |
|
|
|
function XDesktop:SetModalWindow(win) |
|
if not win or not win:IsWithin(self) or win == self.modal_window then |
|
return |
|
end |
|
|
|
table.remove_entry(self.modal_log, win) |
|
if win ~= self then |
|
self.modal_log[#self.modal_log + 1] = win |
|
end |
|
|
|
if not win:IsVisible() or not win:IsOnTop(self.modal_window) then |
|
return |
|
end |
|
|
|
if self.focus_logging_enabled then |
|
print("Modal window:", FormatWindowPath(win)) |
|
end |
|
self.modal_window = win |
|
|
|
for id, touch in pairs(self.touch) do |
|
if not touch.capture and not touch.target:IsWithin(win) then |
|
touch.target:OnTouchCancelled(id, touch.pos, touch) |
|
touch.target = nil |
|
end |
|
end |
|
|
|
if RolloverControl and not RolloverControl:IsWithin(win) then |
|
XDestroyRolloverWindow("immediate") |
|
end |
|
|
|
if self.keyboard_focus and not self.keyboard_focus:IsWithin(win) then |
|
self:SetKeyboardFocus(false) |
|
end |
|
self:RestoreFocus() |
|
|
|
if self.mouse_capture and not self.mouse_capture:IsWithin(win) then |
|
self:SetMouseCapture(false) |
|
end |
|
self:UpdateMouseTarget() |
|
end |
|
|
|
function XDesktop:RemoveModalWindow(win) |
|
table.remove_entry(self.modal_log, win) |
|
if self.modal_window == win then |
|
self.modal_window = false |
|
self:RestoreModalWindow() |
|
assert(self.modal_window) |
|
end |
|
end |
|
|
|
function XDesktop:GetModalWindow() |
|
return self.modal_window |
|
end |
|
|
|
if FirstLoad then |
|
prev_cursor = false |
|
end |
|
|
|
function XDesktop:UpdateCursor(pt) |
|
pt = pt or self.last_mouse_pos |
|
if not pt then return end |
|
local target, cursor = self.modal_window:GetMouseTarget(pt) |
|
target = target or self.modal_window |
|
if self.mouse_capture and target ~= self.mouse_capture then |
|
cursor = self.mouse_capture:GetMouseCursor() |
|
target = false |
|
end |
|
|
|
local curr_cursor = cursor or const.DefaultMouseCursor |
|
if prev_cursor ~= curr_cursor then |
|
SetUIMouseCursor(curr_cursor) |
|
Msg("MouseCursor", curr_cursor) |
|
prev_cursor = curr_cursor |
|
end |
|
return target |
|
end |
|
|
|
function XDesktop:SetLastMouseTarget(target, pt) |
|
local last_target = self.last_mouse_target |
|
if last_target == target then return end |
|
|
|
pt = pt or self.last_mouse_pos |
|
assert(not self.mouse_target_update, "Recursive mouse target update!") |
|
|
|
if self.rollover_logging_enabled then |
|
print("MouseTarget:", FormatWindowPath(target)) |
|
if last_target then last_target:Invalidate() end |
|
if target then target:Invalidate() end |
|
end |
|
|
|
self.mouse_target_update = true |
|
self.last_mouse_target = target |
|
local common_parent = XFindCommonParent(last_target, target) |
|
local win = last_target |
|
while win and win ~= common_parent do |
|
win:OnMouseLeft(pt, last_target) |
|
win = win.parent |
|
end |
|
win = target |
|
while win and win ~= common_parent do |
|
win:OnMouseEnter(pt, target) |
|
win = win.parent |
|
end |
|
self.mouse_target_update = false |
|
end |
|
|
|
function XDesktop:UpdateMouseTarget(pt) |
|
pt = pt or self.last_mouse_pos |
|
local target = self:UpdateCursor(pt) or false |
|
self:SetLastMouseTarget(target, pt) |
|
|
|
return target or self.mouse_capture |
|
end |
|
|
|
function XDesktop:MouseEvent(event, pt, button, time) |
|
local target = self:UpdateMouseTarget(pt) |
|
if config.AutoControllerHandling and event == "OnMouseButtonDown" and GetUIStyleGamepad() and (button == "L" or button == "R") |
|
and not GetParentOfKind(target, "DeveloperInterface") |
|
and not GetParentOfKind(target, "XPopupMenu") |
|
and not GetParentOfKind(target, "XBugReportDlg") |
|
and time ~= "gamepad" |
|
then |
|
if config.AutoControllerHandlingType == "popup" then |
|
if not IsValidThread(SwitchControlQuestionThread) then |
|
SwitchControlQuestionThread = CreateRealTimeThread(function() |
|
ForceShowMouseCursor("control scheme change") |
|
WaitSwitchControls("keyboard", T(477820487236, "Switch to mouse?"), T(184341668469, "Are you sure you want to switch to keyboard/mouse controls?")) |
|
UnforceShowMouseCursor("control scheme change") |
|
end) |
|
return "break" |
|
end |
|
elseif config.AutoControllerHandlingType == "auto" then |
|
SwitchControls(false) |
|
end |
|
end |
|
|
|
self.last_mouse_pos = pt |
|
self.last_event_at = RealTime() |
|
|
|
while target do |
|
if target.window_state ~= "destroying" then |
|
if target[event](target, pt, button) == "break" then |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return "break" |
|
end |
|
end |
|
target = target.parent |
|
end |
|
end |
|
|
|
function XDesktop:ResetMousePosTarget() |
|
self.last_mouse_pos = false |
|
self.last_mouse_target = false |
|
end |
|
|
|
|
|
|
|
function XDesktop:OnSystemActivate() |
|
if self.inactive then |
|
self:KeyboardEvent("OnSetFocus") |
|
self.inactive = false |
|
Msg("SystemActivate") |
|
end |
|
end |
|
|
|
function XDesktop:OnSystemInactivate() |
|
if not self.inactive then |
|
self.inactive = true |
|
self:KeyboardEvent("OnKillFocus") |
|
self:SetMouseCapture(false) |
|
Msg("SystemInactivate") |
|
end |
|
end |
|
|
|
function XDesktop:OnSystemMinimize() |
|
Msg("SystemMinimize") |
|
end |
|
|
|
function XDesktop:OnSystemSize(pt) |
|
local x, y = pt:xy() |
|
if x == 0 or y == 0 then |
|
return |
|
end |
|
local scale = GetUIScale(pt) |
|
self:SetOutsideScale(point(scale, scale)) |
|
self:SetBox(0, 0, x, y) |
|
self:InvalidateMeasure() |
|
self:InvalidateLayout() |
|
Msg("SystemSize", pt) |
|
end |
|
|
|
function XDesktop:OnMouseInside() |
|
Msg("MouseInside") |
|
end |
|
|
|
function XDesktop:OnMouseOutside() |
|
Msg("MouseOutside") |
|
end |
|
|
|
function XDesktop:OnFileDrop(filename) |
|
if Platform.developer or Platform.asserts then |
|
if string.ends_with(filename, ".sav", true) then |
|
CreateRealTimeThread(function() |
|
WaitDataLoaded() |
|
local err = LoadGame(filename, { save_as_last = true }) |
|
if err then |
|
OpenPreGameMainMenu() |
|
end |
|
end) |
|
end |
|
if string.ends_with(filename, ".fbx", true) then |
|
OpenSceneImportEditor(filename) |
|
end |
|
end |
|
end |
|
|
|
|
|
|
|
function XDesktop:TouchEvent(event, id, pos) |
|
local touch = self.touch[id] |
|
if touch then |
|
touch.event = event |
|
touch.pos = pos |
|
else |
|
touch = { id = id, event = event, pos = pos } |
|
self.touch[id] = touch |
|
end |
|
if event == "OnTouchEnded" or event == "OnTouchCancelled" then |
|
self.touch[id] = nil |
|
end |
|
|
|
local result |
|
if touch.capture then |
|
result = touch.capture[event](touch.capture, id, pos, touch) |
|
end |
|
|
|
if not result then |
|
local target = self:UpdateMouseTarget(pos) |
|
while target do |
|
if target.window_state ~= "destroying" then |
|
touch.target = target |
|
result = target[event](target, id, pos, touch) |
|
if result then |
|
break |
|
end |
|
end |
|
target = target.parent |
|
end |
|
end |
|
if result == "capture" then |
|
touch.capture = touch.target |
|
result = "break" |
|
end |
|
return result |
|
end |
|
|
|
|
|
|
|
local UIL = UIL |
|
function XDesktop:Invalidate() |
|
if self.invalidated then return end |
|
self.invalidated = true |
|
UIL.Invalidate() |
|
end |
|
|
|
if false then |
|
IgnoreInvalidateSources = { |
|
"DeveloperInterface.lua", |
|
"uiConsoleLog.lua", |
|
"XControl.lua.* SetText", |
|
"KeyboardEventDispatch", |
|
"method ChangeHappiness", |
|
} |
|
function XDesktop:Invalidate() |
|
if not self.invalidated then |
|
local stack = GetStack() |
|
local show = true |
|
for _, text in ipairs(IgnoreInvalidateSources) do |
|
if stack:find(text) then |
|
show = false |
|
break |
|
end |
|
end |
|
if show then |
|
self.invalidated = true |
|
print(stack) |
|
end |
|
end |
|
UIL.Invalidate() |
|
end |
|
function XTranslateText:OnTextChanged(text) |
|
if not GetParentOfKind(self, "DeveloperInterface") then |
|
print(string.concat(" ", "TEXT CHANGE", self.text, "-->", text)) |
|
end |
|
end |
|
end |
|
|
|
function XDesktop:InvalidateMeasure(child) |
|
if self.measure_update then return end |
|
XActionsHost.InvalidateMeasure(self, child) |
|
if self.invalidated then return end |
|
self:RequestLayout() |
|
end |
|
|
|
function XDesktop:InvalidateLayout() |
|
if self.layout_update then return end |
|
XActionsHost.InvalidateLayout(self) |
|
if self.invalidated then return end |
|
self:RequestLayout() |
|
end |
|
|
|
function XDesktop:MeasureAndLayout() |
|
local w, h = self.box:sizexyz() |
|
self:UpdateMeasure(w, h) |
|
self:UpdateLayout() |
|
self:UpdateMouseTarget() |
|
if self.measure_update or self.layout_update then |
|
self:UpdateMeasure(w, h) |
|
self:UpdateLayout() |
|
self:UpdateMouseTarget() |
|
end |
|
end |
|
|
|
function XDesktop:RequestLayout() |
|
if IsValidThread(self.layout_thread) then |
|
Wakeup(self.layout_thread) |
|
else |
|
self.layout_thread = CreateRealTimeThread(function(self) |
|
while true do |
|
if next(TextStyles) and not self.invalidated then |
|
PauseInfiniteLoopDetection("XDesktop.MeasureAndLayout") |
|
procall(self.MeasureAndLayout, self) |
|
ResumeInfiniteLoopDetection("XDesktop.MeasureAndLayout") |
|
end |
|
WaitWakeup() |
|
end |
|
end, self) |
|
if Platform.developer then |
|
ThreadsSetThreadSource(self.layout_thread, "LayoutThread") |
|
end |
|
end |
|
end |
|
|
|
function XDesktop:RequestUpdateMouseTarget() |
|
if IsValidThread(self.mouse_target_thread) then |
|
Wakeup(self.mouse_target_thread) |
|
else |
|
self.mouse_target_thread = CreateRealTimeThread(function(self) |
|
while true do |
|
procall(self.UpdateMouseTarget, self) |
|
WaitWakeup() |
|
end |
|
end, self) |
|
end |
|
end |
|
|
|
function XRender() |
|
if not next(TextStyles) then return end |
|
|
|
PauseInfiniteLoopDetection("XRender") |
|
local desktop = terminal.desktop |
|
desktop:MeasureAndLayout() |
|
desktop:DrawWindow(desktop.box) |
|
ResumeInfiniteLoopDetection("XRender") |
|
end |
|
|
|
|
|
|
|
function OnMsg.Start() |
|
terminal.desktop = XDesktop:new() |
|
terminal.AddTarget(terminal.desktop) |
|
UIL.Register("XRender", terminal.desktop.terminal_target_priority) |
|
terminal.desktop:OnSystemSize(UIL.GetScreenSize()) |
|
terminal.desktop:Open() |
|
Msg("DesktopCreated") |
|
end |
|
|
|
|
|
|
|
function OnMsg.EngineOptionsSaved() |
|
local desktop = terminal.desktop |
|
if desktop then |
|
desktop:InvalidateMeasure() |
|
desktop:InvalidateLayout() |
|
end |
|
end |
|
|
|
|
|
|
|
if Platform.developer then |
|
function FormatWindowPath(win) |
|
if not win then return "" end |
|
local path = {} |
|
repeat |
|
table.insert(path, 1, _InternalTranslate(T(357840043382, "<class> <Id>"), win, false)) |
|
win = win.parent |
|
until not win |
|
return table.concat(path, " / ") |
|
end |
|
end |
|
|