File size: 27,526 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
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
--[[@@@
@class XAction
Defines a UI action, which can be bound to buttons or used on its own.
Always initialize it with a parent or if that is not possible, call RegisterInHost afterwards with the correct XActionsHost
]]
DefineClass.XAction = {
	__parents = { "XRollover" },
	properties = {
		{ category = "Action", id = "ActionId", editor = "text", default = "" },
		{ category = "Action", id = "ActionMode", editor = "text", default = "" },
		{ category = "Action", id = "InheritedActionModes", editor = "text", default = "", read_only = true, help = "ActionModes inherited from parent if ActionMode is empty", dont_save = true },
		{ category = "Action", id = "ActionSortKey", editor = "text", default = "",
		  buttons = { { name = "Rebuild", func = "RebuildSortKeys" } },
		},
		{ category = "Action", id = "ActionTranslate", editor = "bool", default = true, },
		{ category = "Action", id = "ActionName", editor = "text", default = "", translate = function(obj) return obj:GetProperty("ActionTranslate") end, },
		{ category = "Action", id = "ActionDescription", editor = "text", default = "", translate = function(obj) return obj:GetProperty("ActionTranslate") end, },
		{ category = "Action", id = "ActionIcon", editor = "ui_image", default = "", },
		{ category = "Action", id = "ActionMenubar", editor = "text", default = "", },
		{ category = "Action", id = "ActionToolbar", editor = "text", default = "" },
		{ category = "Action", id = "ActionToolbarSplit", editor = "bool", default = false },
		{ category = "Action", id = "ActionToolbarSection", editor = "text", default = "" },
		{ category = "Action", id = "ActionUIStyle", editor = "choice", default = "auto", items = {"auto", "gamepad", "keyboard"}, }, 
		{ category = "Action", id = "ActionShortcut", editor = "shortcut", default = "" },
		{ category = "Action", id = "ActionShortcut2", editor = "shortcut", default = "" },
		{ category = "Action", id = "ActionGamepad", editor = "shortcut", shortcut_type = "gamepad", default = "" },
		{ category = "Action", id = "ActionGamepadHold", editor = "bool", default = false },
		{ category = "Action", id = "ActionBindable", editor = "bool", default = false },
		{ category = "Action", id = "ActionMouseBindable", editor = "bool", default = true },
		{ category = "Action", id = "ActionBindSingleKey", editor = "bool", default = false },
		{ category = "Action", id = "BindingsMenuCategory", editor = "text", default = "Default" },
		{ category = "Action", id = "ActionButtonTemplate", editor = "choice", default = false, items = function() return XTemplateCombo("XButton") end, },
		{ category = "Action", id = "ActionToggle", editor = "bool", default = false },
		{ category = "Action", id = "ActionToggled", editor = "func", params = "self, host", read_only = function(self) return not self:GetProperty("ActionToggle") end, },
		{ category = "Action", id = "ActionToggledIcon", editor = "ui_image", default = "", read_only = function(self) return not self:GetProperty("ActionToggle") end, },
		{ category = "Action", id = "ActionState", editor = "func", params = "self, host" },
		{ category = "Action", id = "OnActionEffect", editor = "choice", default = "", items = {"", "popup", "back", "close", "mode"}, },
		{ category = "Action", id = "OnActionParam", editor = "text", default = "" },
		{ category = "Action", id = "OnAction", editor = "func", params = "self, host, source, ...", },
		{ category = "Action", id = "OnShortcutUp", editor = "func", params = "self, host, source, ...", default = false },
		{ category = "Action", id = "OnAltAction", editor = "func", params = "self, host, source, ...", default = false},
		{ category = "Action", id = "IgnoreRepeated", editor = "bool", default = false },
		{ category = "Action", id = "ActionContexts", editor = "string_list", default = false }, 
		
		{ category = "FX", id = "FXMouseIn", editor = "text", default = "", },
		{ category = "FX", id = "FXPress", editor = "text", default = "", },
		{ category = "FX", id = "FXPressDisabled", editor = "text", default = "", },
		
		-- properties of XRollover should regard ActionTranslate
		{ category = "Rollover", id = "RolloverTranslate", editor = false },
		{ category = "Rollover", id = "RolloverAnchor", editor = false },
		{ category = "Rollover", id = "RolloverText", editor = "text", default = "", translate = function(obj) return obj:GetProperty("ActionTranslate") end, lines = 3, },
		{ category = "Rollover", id = "RolloverDisabledText", editor = "text", default = "", translate = function(obj) return obj:GetProperty("ActionTranslate") end, lines = 3, },
	},
	
	-- the default values are needed when saving and loading from AccountStorage
	default_ActionShortcut = false,
	default_ActionShortcut2 = false,
	default_ActionGamepad = false,
	
	shortcut_up_thread = false,
	host = false,
	
	multi_mode_cache = false
}

function XAction:RegisterInHost(host, replace_matching_id)
	self.host = host
	if host then host:_InternalAddAction(self, replace_matching_id) end
	self:BindShortcuts()
	
	if self.OnShortcutUp and host and self.OnAction ~= XAction.OnAction then
		local oldAction = self.OnAction
		self.OnAction = function(self, ...)
			oldAction(self, ...)
			
			local keyOne = self.ActionShortcut and VKStrNamesInverse[self.ActionShortcut]
			local keyTwo = self.ActionShortcut2 and VKStrNamesInverse[self.ActionShortcut2]
			local downKey = (keyOne and terminal.IsKeyPressed(keyOne) and keyOne) or (keyTwo and terminal.IsKeyPressed(keyTwo) and keyTwo)
			
			if IsValidThread(self.shortcut_up_thread) then
				DeleteThread(self.shortcut_up_thread)
			end
			self.shortcut_up_thread = CreateRealTimeThread(function(self, ...)
				while downKey and terminal.IsKeyPressed(downKey) and not terminal.desktop.inactive do
					Sleep(16)
				end
				self.OnShortcutUp(self, ...)
			end, self, ...)
		end
	end
end

function XAction:Init(parent, context, replace_matching_id)
	self:RegisterInHost(GetActionsHost(parent), replace_matching_id)
end

function XAction:BindShortcuts()
	self.default_ActionShortcut = self.ActionShortcut
	self.default_ActionShortcut2 = self.ActionShortcut2
	self.default_ActionGamepad = self.ActionGamepad
	if self.ActionBindable then
		local bindings = AccountStorage and AccountStorage.Shortcuts[self.ActionId]
		if bindings then
			self:SetActionShortcuts(bindings[1] or self.ActionShortcut, bindings[2] or self.ActionShortcut2, bindings[3] or self.ActionGamepad)
		end
	end
end

local function RemoveShortcut(action, name, shortcut)
	shortcut = shortcut or ""
	local old_shortcut = action[name]
	if shortcut == old_shortcut then
		-- no change
		return
	end
	
	action[name] = nil
	local host = action.host
	if not host then return end
	
	-- unregister old shortcut
	host:CallHostParents("RemoveShortcutToAction", action, old_shortcut)
end

local function AddShortcut(action, name, shortcut)
	shortcut = shortcut or ""
	local old_shortcut = action[name]
	if shortcut == old_shortcut then
		-- no change
		return
	end
	
	action[name] = shortcut
	local host = action.host
	if not host then return end
	
	-- register new one
	host:CallHostParents("AddShortcutToAction", action, shortcut)
end

function XAction:SetActionShortcuts(shortcut, shortcut2, shortcut_gamepad)
	-- remove old shortcuts before adding any of the new ones
	RemoveShortcut(self, "ActionShortcut", shortcut)
	RemoveShortcut(self, "ActionShortcut2", shortcut2)
	RemoveShortcut(self, "ActionGamepad", shortcut_gamepad)
	-- add new shortcuts
	AddShortcut(self, "ActionShortcut", shortcut)
	AddShortcut(self, "ActionShortcut2", shortcut2)
	AddShortcut(self, "ActionGamepad", shortcut_gamepad)
end

function XAction:SetActionMenubar(menubar)
	local host = self.host
	if host and self.ActionMenubar ~= menubar then
		host:CallHostParents("RemoveMenubarAction", self)
	end
	self.ActionMenubar = menubar
	if not host then return end
	host:CallHostParents("AddMenubarAction", self)
end

function XAction:SetActionToolbar(toolbar)
	local host = self.host
	if host and self.ActionToolbar ~= toolbar then
		host:CallHostParents("RemoveToolbarAction", self)
	end
	self.ActionToolbar = toolbar
	if not host then return end
	host:CallHostParents("AddToolbarAction", self)
end

function XAction:SetActionSortKey(sort_key)
	self.ActionSortKey = sort_key
	if not self.host then return end
	self.host:InvalidateActionSortKey(self)
end

function XAction:ActionState(host)
end

function XAction:ActionToggled(host)
end

function XAction:OnAction(host, source, ...)
	local effect = self.OnActionEffect
	local param = self.OnActionParam
	if effect == "close" and host and host.window_state ~= "destroying" then
		host:Close(param ~= "" and param or nil, source, ...)
	elseif effect == "mode" and host then
		assert(IsKindOf(host, "XDialog"))
		host:SetMode(param)
	elseif effect == "back" and host then
		assert(IsKindOf(host, "XDialog"))
		SetBackDialogMode(host)
	elseif effect == "popup" then
		local actions_view = GetParentOfKind(source, "XActionsView")
		if actions_view then
			actions_view:PopupAction(self.ActionId, host, source)
		else
			XShortcutsTarget:OpenPopupMenu(self.ActionId, terminal.GetMousePos())
		end
	else
		--print(self.ActionId, "activated")
	end
end

function XAction:OnXTemplateSetProperty(prop_id, old_value)
	-- toggle text properties between Ts and strings when ActionTranslate is edited
	if prop_id == "ActionTranslate" then
		self:UpdateLocalizedProperty("ActionName", self.ActionTranslate)
		self:UpdateLocalizedProperty("RolloverText", self.ActionTranslate)
		self:UpdateLocalizedProperty("RolloverDisabledText", self.ActionTranslate)
		ObjModified(self)
	end
	if prop_id == "ActionSortKey" and self.ActionSortKey ~= "" then
		local preset = GetParentTableOfKind(self, "XTemplate")
		preset.RequireActionSortKeys = true
	end
end

function XAction:EnabledInMode(mode)
	local myMode = self.ActionMode
	if myMode == "" then return true end
	if mode == myMode then return true end

	if not self.multi_mode_cache or self.multi_mode_cache.strsrc ~= myMode then
		local modeCache = { strsrc = myMode }
		for str in string.gmatch(myMode, "([%w%-_]+)") do
			modeCache[str] = true
		end
		self.multi_mode_cache = modeCache
	end
	return self.multi_mode_cache[mode]
end

local function assign_sortkeys(node, sortkey)
	if node:IsKindOf("XTemplateAction") and node.ActionId ~= "" then
		if node.ActionSortKey ~= "" then
			print("Overwriting SortKey of", node.ActionId, "to", node.ActionSortKey)
		end
		node:SetActionSortKey(tostring(sortkey * 10))
		sortkey = sortkey + 1
	end
	for _, item in ipairs(node) do
		sortkey = assign_sortkeys(item, sortkey)
	end
	return sortkey
end

function XAction:RebuildSortKeys(root, prop_id, ged, btn_param)
	if ged:WaitQuestion("Rebuild SortKeys", "This will assign SortKeys to each Action in the current file, in the order\nthey appear, overwriting existing ones.\n\nContinue?", "OK", "Cancel") ~= "ok" then
		return
	end
	assign_sortkeys(root, 100) -- start from 100 so that alphabetical order and numerical order coincide
	root.RequireActionSortKeys = true
	ObjModified(self)
	ObjModified(root)
end

----- XActionsHost

DefineClass.XActionsHost = {
	__parents = { "XContextWindow", "XHoldButton" },
	properties = {
		{ category = "Actions", id = "ActionsMode", editor = "text", default = "", },
		{ category = "Actions", id = "Translate", editor = "bool", default = false, },
		{ category = "Actions", id = "HostInParent", editor = "bool", default = false, },
	},
	actions = false,
	shortcut_to_actions = false,
	menubar_actions = false,
	toolbar_actions = false,
	action_hold_buttons = false,
	
	dirty_actions_order = false,
	dirty_menubars = false,
	dirty_toolbars = false,
	dirty_shortcuts = false,
}

function XActionsHost:Init()
	self.actions = self.actions or {}
	self.shortcut_to_actions = {}
	self.menubar_actions = {}
	self.toolbar_actions = {}
	self.dirty_menubars = {}
	self.dirty_toolbars = {}
	self.dirty_shortcuts = {}
	-- register already created actions in the host
	for _, action in ipairs(self.actions) do
		action:RegisterInHost(self)
	end
end

function XActionsHost:ClearActions()
	if self.HostInParent then
		local host = GetActionsHost(self.parent)
		if host then
			for _, action in ipairs(host and self.actions) do
				host:RemoveAction(action)
			end
		end
	end
	table.clear(self.actions)
	table.clear(self.shortcut_to_actions)
	table.clear(self.menubar_actions)
	table.clear(self.toolbar_actions)
end

function XActionsHost:Done()
	self:ClearActions()
end

function XActionsHost:ActionsUpdated()
	if not self:GetThread("UpdateActionViews") then
		self:CreateThread("UpdateActionViews", self.UpdateActionViews, self, self)
	end
end

function XActionsHost:SetActionsMode(mode)
	if self.ActionsMode ~= mode then
		self.ActionsMode = mode
		self:ActionsUpdated()
	end
end

function XActionsHost:InvalidateActionSortKey(action)
	self.dirty_actions_order = true
	if action.ActionMenubar ~= "" then
		self.dirty_menubars[action.ActionMenubar] = true
	end
	if action.ActionToolbar ~= "" then
		self.dirty_toolbars[action.ActionToolbar] = true
	end
	if action.ActionShortcut ~= "" then
		self.dirty_shortcuts[action.ActionShortcut] = true
	end
	if action.ActionShortcut2 ~= "" then
		self.dirty_shortcuts[action.ActionShortcut2] = true
	end
	if action.ActionGamepad ~= "" then
		self.dirty_shortcuts[action.ActionGamepad] = true
	end
end

local function sort_actions(actions)
	table.stable_sort(actions, function(a,b)
		return a.ActionSortKey < b.ActionSortKey
	end)
end

function XActionsHost:GetActions()
	if self.dirty_actions_order then
		sort_actions(self.actions)
		self.dirty_actions_order = nil
	end
	return self.actions
end

function XActionsHost:GetMenubarActions(menubar)
	if self.dirty_menubars[menubar] then
		sort_actions(self.menubar_actions[menubar])
		self.dirty_menubars[menubar] = nil
	end
	return self.menubar_actions[menubar]
end

function XActionsHost:GetToolbarActions(toolbar)
	if self.dirty_toolbars[toolbar] then
		sort_actions(self.toolbar_actions[toolbar])
		self.dirty_toolbars[toolbar] = nil
	end
	return self.toolbar_actions[toolbar]
end

function XActionsHost:GetShortcutActions(shortcut)
	if self.dirty_shortcuts[shortcut] then
		sort_actions(self.shortcut_to_actions[shortcut])
		self.dirty_shortcuts[shortcut] = nil
	end
	return self.shortcut_to_actions[shortcut]
end

function XActionsHost:CallHostParents(func, ...)
	self[func](self, ...)
	if self.HostInParent then
		local host = GetActionsHost(self.parent)
		if host then
			host:CallHostParents(func, ...)
			return
		end
	end
end

local function add_sorted(actions, action)
	actions = actions or {}
	local i = 1
	local key = action.ActionSortKey
	local skip_add
	while i <= #actions and actions[i].ActionSortKey <= key do
		if actions[i] == action then
			-- don't duplicate actions
			skip_add = true
			break
		end
		i = i + 1
	end
	if not skip_add then
		table.insert(actions, i, action)
	end
	return actions
end

function XActionsHost:AddShortcutToAction(action, shortcut)
	if (shortcut or "") == "" then return end
	-- insert in list based on ActionSortKey
	self.shortcut_to_actions[shortcut] = add_sorted(self.shortcut_to_actions[shortcut], action)
end

function XActionsHost:RemoveShortcutToAction(action, shortcut)
	if (shortcut or "") == "" then return end
	local actions = self.shortcut_to_actions[shortcut]
	if not actions then return end
	table.remove_value(actions, action)
end

function XActionsHost:AddMenubarAction(action)
	local menubar = action.ActionMenubar
	if (menubar or "") == "" then return end
	self.menubar_actions[menubar] = add_sorted(self.menubar_actions[menubar], action)
end

function XActionsHost:RemoveMenubarAction(action)
	table.remove_entry(self.menubar_actions[action.ActionMenubar], action)
end

function XActionsHost:AddToolbarAction(action)
	local toolbar = action.ActionToolbar
	if (toolbar or "") == "" then return end
	self.toolbar_actions[toolbar] = add_sorted(self.toolbar_actions[toolbar], action)
end

function XActionsHost:RemoveToolbarAction(action)
	table.remove_entry(self.toolbar_actions[action.ActionToolbar], action)
end

function XActionsHost:_InternalAddAction(action, replace_matching_id)
	local actions = self.actions
	local key = action.ActionSortKey
	local old_idx = replace_matching_id and self:RemoveAction(self:ActionById(action.ActionId))
	-- is the new action still on a correct place at the old index?
	if old_idx and (old_idx == 1 or actions[old_idx - 1].ActionSortKey <= key) and
	  (old_idx > #actions or actions[old_idx].ActionSortKey >= key) then
		table.insert(actions, old_idx, action)
	else
		-- insert in list based on ActionSortKey
		add_sorted(actions, action)
	end
	
	-- add action to menubar and toolbar mappings
	self:AddMenubarAction(action)
	self:AddToolbarAction(action)
	
	-- add action to shortcuts mapping
	self:AddShortcutToAction(action, action.ActionShortcut)
	self:AddShortcutToAction(action, action.ActionShortcut2)
	self:AddShortcutToAction(action, action.ActionGamepad)
	
	if self.HostInParent then
		local host = GetActionsHost(self.parent)
		if host then
			host:_InternalAddAction(action, replace_matching_id)
			return
		end
	end
	self:ActionsUpdated()
	--assert(action.ActionName == "" or (IsT(action.ActionName) or false) == self.Translate)
end

function XActionsHost:RemoveAction(action)
	if not action then return end
	local actions = self.actions
	local idx = table.remove_entry(self.actions, action)
	
	-- remove action from menubar and toolbar mappings
	self:RemoveMenubarAction(action)
	self:RemoveToolbarAction(action)
	
	-- remove action from shortcuts mapping
	self:RemoveShortcutToAction(action, action.ActionShortcut)
	self:RemoveShortcutToAction(action, action.ActionShortcut2)
	self:RemoveShortcutToAction(action, action.ActionGamepad)
	
	if self.HostInParent then
		local host = GetActionsHost(self.parent)
		if host then
			host:RemoveAction(action)
			return
		end
	end
	self:ActionsUpdated()
	return idx
end

function XActionsHost:SetHostInParent(host_in_parent)
	if self.HostInParent == host_in_parent then return end
	self.HostInParent = host_in_parent
	local host = GetActionsHost(self.parent)
	if host then
		for _, action in ipairs(self.actions) do
			if host_in_parent then
				host:_InternalAddAction(action)
			else
				host:RemoveAction(action)
			end
		end
	end
end

function XActionsHost:ActionsSanityCheck()
	--[[local ids, shortcuts = {}, {}
	for _, action in ipairs(self.actions) do
		if action.ActionId ~= "" then
			assert(not ids[action.ActionId], string.format("Conflicting action Id %s", action.ActionId))
			ids[action.ActionId] = action
		end
		
		local mode = action.ActionMode
		if not shortcuts[mode] then
			shortcuts[mode] = {}
		end
		local mode_actions = shortcuts[mode]
		if action.ActionShortcut ~= "" then
			local other = mode_actions[action.ActionShortcut]
			assert(not other, string.format("Conflicting shortcut %s (between actions %s & %s)", action.ActionShortcut, other and other.ActionId or "", action.ActionId))
			mode_actions[action.ActionShortcut] = action
		end
		if action.ActionShortcut2 ~= "" then
			local other = mode_actions[action.ActionShortcut2]
			assert(not other, string.format("Conflicting shortcut %s (between actions %s & %s)", action.ActionShortcut2, other and other.ActionId or "", action.ActionId))
			mode_actions[action.ActionShortcut2] = action
		end
	end]]
end

function XActionsHost:UpdateActionViews(win)
	if Platform.developer then
		self:ActionsSanityCheck()
	end
	for _, win in ipairs(win) do
		if IsKindOf(win, "XActionsView") then
			win:OnUpdateActions()
		end
		if not IsKindOf(win, "XActionsHost") or win.HostInParent then
			self:UpdateActionViews(win)
		end
	end
end

function XActionsHost:ShowActionBar(bShow)
	local action_bar = self:HasMember("idActionBar") and self.idActionBar
	if action_bar then
		action_bar:SetVisible(bShow)
	end
end

function XActionsHost:FilterAction(action, action_context)
	if not action_context then
		return action:EnabledInMode(self.ActionsMode) and self:ActionState(action) ~= "hidden"
	end
	
	for _, context in ipairs(action.ActionContexts) do
		if context == action_context and self:ActionState(action) ~= "hidden" then
			return true
		end
	end
	return false
end

function XActionsHost:ActionState(action)
	local action_id = action.ActionId
	if action.OnActionEffect == "popup" and action.OnAction == XAction.OnAction and
	   not self:HasMenubarActions(action_id) and not self:HasToolbarActions(action_id) then
	   return "hidden" -- hide menu entries with no children actions, that would result in an empty popup
	end
	return action:ActionState(self)
end

function XActionsHost:HasMenubarActions(action_id)
	return next(self.menubar_actions[action_id])
end

function XActionsHost:HasToolbarActions(action_id)
	return next(self.toolbar_actions[action_id])
end

function XActionsHost:OnAction(action, ctrl, ...)
	local hasFx = ctrl and ctrl.FXPress
	local ret = action:OnAction(self, ctrl, ...)
	if #(action.FXPress or "") ~= 0 and not hasFx then PlayFX(action.FXPress, "start", action) end
	if action.ActionToggle then
		self:ActionsUpdated()
	end
	Msg("XActionActivated", self, action, ctrl, ...)
	return ret
end

function XActionsHost:ActionById(id)
	return table.find_value(self.actions, "ActionId", id)
end

function XActionsHost:IsActionShortcut(id, shortcut)
	local action = self:ActionById(id)
	if not action then return end
	return action.ActionShortcut == shortcut or action.ActionShortcut2 == shortcut or action.ActionGamepad == shortcut
end

function XActionsHost:ActionByShortcut(shortcut, input, controller_id, repeated, ...)
	local found
	for _, action in ipairs(self:GetShortcutActions(shortcut)) do
		if (not action.IgnoreRepeated or not repeated) then
			if self:FilterAction(action) then
				local state = action:ActionState(self)
				if state ~= "disabled" and state ~= "hidden" then
					found = action
					break
				end
			end
		end
	end
	
	return found
end

function XActionsHost:GamepadHoldActionByShortcut(shortcut)
	local found
	for _, action in ipairs(self:GetShortcutActions(shortcut)) do
		if action.ActionGamepadHold and self:FilterAction(action) then
			local state = action:ActionState(self)
			if state ~= "disabled" and state ~= "hidden" then
				found = action
				break
			end
		end
	end
	return found
end

if FirstLoad then
	KbdShortcutToRelation = {
		["Tab"] = "next",
		["Shift-Tab"] = "prev",
		["Up"] = "up",
		["Down"] = "down",
		["Left"] = "left",
		["Right"] = "right",
	}

	XShortcutToRelation = {
		["LeftThumbLeft"] = "left", ["LeftThumbDownLeft"] = "left", ["LeftThumbUpLeft"] = "left",
		["LeftThumbRight"] = "right", ["LeftThumbDownRight"] = "right", ["LeftThumbUpRight"] = "right",
		["LeftThumbUp"] = "up",
		["LeftThumbDown"] = "down",
		["DPadLeft"] = "left",
		["DPadRight"] = "right",
		["DPadUp"] = "up",
		["DPadDown"] = "down",
	}
end

function XActionsHost:OnHoldDown(pt, button)
	local action = self:GamepadHoldActionByShortcut(button)
	action:OnAction(self,button)
end

function XActionsHost:OnHoldButtonTick(i, shortcut)	
	local action = self:GamepadHoldActionByShortcut(shortcut)
	if not action then
		return 
	end	
	local ctrl = self.action_hold_buttons and self.action_hold_buttons[action.ActionId]
	if ctrl and ctrl:HasMember("OnHoldButtonTick") then
		ctrl:OnHoldButtonTick(i)
	else
		 XHoldButton.OnHoldButtonTick(self, i, shortcut)	
	end
end

function XActionsHost:OnXButtonRepeat(shortcut, controller_id,...)
	if self.HostInParent then return end
	if not RepeatableXButtons[shortcut] then
		local found = self:GamepadHoldActionByShortcut(shortcut)
		if found then
			XHoldButton.OnHoldButtonRepeat(self,shortcut, controller_id)
			return "break"
		end	
	end
end

function XActionsHost:OnShortcut(shortcut, source, controller_id, ...)
	if self.HostInParent then return end
	local found
	if source=="gamepad" then
		if shortcut:starts_with("-") then
			local org_shortcut = shortcut:gsub("-", "")
			found = self:GamepadHoldActionByShortcut(org_shortcut)
			if found then
				found =  XHoldButton.OnHoldButtonUp(self, org_shortcut, controller_id) 
				if found then
					return "break"
				end
			end	
		elseif shortcut:starts_with("+") then
			local org_shortcut = shortcut:gsub("+", "")
			found = self:GamepadHoldActionByShortcut(org_shortcut)
			if found then
				XHoldButton.OnHoldButtonDown(self,org_shortcut, controller_id)
			end	
		else
			found = self:GamepadHoldActionByShortcut(shortcut)
			if found then
				XHoldButton.OnHoldButtonRepeat(self,shortcut, controller_id)
			end
		end	
	end	
	local action = not found and self:ActionByShortcut(shortcut, source, controller_id, ...)
	if action then
		self:OnAction(action, source, controller_id, ...)
		return "break"
	end
	if source ~= "mouse" then
		local relation = (source == "keyboard") and KbdShortcutToRelation[shortcut] or XShortcutToRelation[shortcut]
		if relation then
			local focus = self.desktop and self.desktop.keyboard_focus
			local order = focus and focus:IsWithin(self) and focus:GetFocusOrder() or point(0, 0)
			focus = self:GetRelativeFocus(order, relation)
			if focus then
				-- the thread prevents Tab keys to be processed in OnKbdChar of the new focus
				CreateRealTimeThread(function()
					if focus.window_state ~= "destroying" then
						focus:SetFocus()
						if source == "gamepad" and RolloverControl ~= focus then
							XCreateRolloverWindow(focus, true)
						end
					end
				end)
				return "break"
			end
		end
	end
end

function XActionsHost:OpenContextMenu(action_context, anchor_pt)
	if not action_context or not anchor_pt or action_context == "" then return end
	local menu = XPopupMenu:new({
		ActionContextEntries = action_context,
		Anchor = anchor_pt,
		AnchorType = "mouse",
		MaxItems = 12,
		GetActionsHost = function() return self end,
		popup_parent = self,
	}, terminal.desktop)
	menu:Open()
	return menu
end

function XActionsHost:OpenPopupMenu(menubar_id, anchor_pt)
	local menu = XPopupMenu:new({
		MenuEntries = menubar_id,
		Anchor = anchor_pt,
		AnchorType = "mouse",
		MaxItems = 12,
		GetActionsHost = function() return self end,
		popup_parent = self,
	}, terminal.desktop)
	menu:Open()
	return menu
end


----- XActionsView

DefineClass.XActionsView = {
	__parents = { "XContextWindow" },
	properties = {
		{ category = "General", id = "HideWithoutActions", name = "Hide without actions", editor = "bool", default = false },
	}
}

function XActionsView:GetActionsHost()
	return GetActionsHost(self, true)
end

function XActionsView:Open(...)
	XContextWindow.Open(self, ...)
	self:OnUpdateActions()
end

function XActionsView:PopupAction(action_id, host, source)
	assert(false)
end

function XActionsView:OnUpdateActions()
	local host = self:GetActionsHost()
	if not host or self.window_state == "new" then return end
	self:RebuildActions(host)
	if self.HideWithoutActions then
		self:SetVisible(#self > 0)
	end
	Msg("XWindowRecreated", self)
end

function XActionsView:RebuildActions(host)
end


----- globals

function GetActionsHost(win, final)
	while win and (not win:IsKindOf("XActionsHost") or win.HostInParent and final) do
		win = win.parent
		if final and win and win:IsKindOf("XActionsView") then
			return win:GetActionsHost()
		end
	end
	return win
end

function EnabledInModes(givenModes, modes)
	if givenModes == "" or modes == "" or modes == givenModes then return true end

	for givenMode in string.gmatch(givenModes, "([%w%-_]+)") do
		for mode in string.gmatch(modes, "([%w%-_]+)") do
			if givenMode == mode or givenMode == "ForwardToC" or mode == "ForwardToC" then
				return true
			end
		end
	end
	
	return false
end