File size: 15,899 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
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

----- keyboard

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
				-- print(tostring(target), event, ...)
				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

----- gamepad

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

----- mouse

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
				-- print(event, button, pt, _GetUIPath(target))
				--[[
				if event == "OnMouseButtonDown" then
					local info = debug.getinfo(target[event])
					print("\t", info.short_src, info.linedefined)
				end
				--]]
				return "break"
			end
		end
		target = target.parent
	end
end

function XDesktop:ResetMousePosTarget()
	self.last_mouse_pos = false
	self.last_mouse_target = false
end

----- activation

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

-- touch

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

-- draw

local UIL = UIL
function XDesktop:Invalidate()
	if self.invalidated then return end
	self.invalidated = true
	UIL.Invalidate()
end

if false then -- debug invalidation
	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 -- if there is a redraw pending let it do the MeasureAndLayout
					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

-- start

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

-- margin update

function OnMsg.EngineOptionsSaved()
	local desktop = terminal.desktop
	if desktop then
		desktop:InvalidateMeasure()
		desktop:InvalidateLayout()
	end
end

-- debug

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