1--
2-- Keyboard dispatch
3--
4local tbl = {};
5
6-- state tracking table for locking/unlocking, double-tap tracking, and sticky
7local mtrack = {
8	m1 = nil,
9	m2 = nil,
10	last_m1 = 0,
11	last_m2 = 0,
12	unstick_ctr = 0,
13	dblrate = 10,
14	mstick = 0,
15	mlock = "none"
16};
17
18local dispatch_debug = suppl_add_logfn("dispatch");
19
20local function update_meta(m1, m2)
21	mtrack.m1 = m1;
22	mtrack.m2 = m2;
23
24-- forward the state change to the led manager
25	m1, m2 = dispatch_meta();
26	if (m1 or m2) then
27		local pref = (m1 and "m1_" or "") .. (m2 and "m2_" or "");
28		local global = {};
29
30-- filter out set of valid bindings
31		for k,v in pairs(tbl) do
32			if (string.sub(k, 1, string.len(pref)) == pref) then
33				if (string.sub(k, string.len(pref)+1, 2) == "m2") then
34-- special case, m1_ on m1_m2__ binding
35				else
36					table.insert(global, {string.sub(k, string.len(pref)+1), v});
37				end
38			end
39		end
40
41		ledm_kbd_state(m1, m2, dispatch_locked(), global);
42	else
43	-- get the default and bound locals
44		ledm_kbd_state(m1, m2, dispatch_locked()
45			-- locked
46			-- globals
47			-- locals
48		);
49	end
50end
51
52-- the following line can be removed if meta state protection is not needed
53system_load("meta_guard.lua")();
54
55function dispatch_system(key, val)
56	if (SYSTEM_KEYS[key] ~= nil) then
57		SYSTEM_KEYS[key] = val;
58		store_key("sysk_" .. key, val);
59	else
60		warning("tried to assign " .. key .. " / " .. val .. " as system key");
61	end
62end
63
64function dispatch_tick()
65	if (mtrack.unstick_ctr > 0) then
66		mtrack.unstick_ctr = mtrack.unstick_ctr - 1;
67		if (mtrack.unstick_ctr == 0) then
68			update_meta(nil, nil);
69		end
70	end
71end
72
73function dispatch_locked()
74	return mtrack.ignore ~= false and mtrack.ignore ~= nil;
75end
76
77local function load_keys()
78	for k,v in pairs(SYSTEM_KEYS) do
79		local km = get_key("sysk_" .. k);
80		if (km ~= nil) then
81			SYSTEM_KEYS[k] = tostring(km);
82		end
83	end
84
85	for _, v in ipairs(match_keys("custom_%")) do
86		local pos, stop = string.find(v, "=", 1);
87		if (pos and stop) then
88			local key = string.sub(v, 8, pos - 1);
89			local val = string.sub(v, stop + 1);
90			if (val and string.len(val) > 0) then
91				tbl[key] = val;
92			end
93		end
94	end
95end
96
97-- allow an external call to ignore all defaults and define new tables
98-- primarily intended for swittching ui schemas
99function dispatch_binding_table(newtbl)
100	if newtbl and type(newtbl) == "table" and #newtbl > 0 then
101		tbl = {};
102		for k,v in pairs(newtbl) do
103			tbl[k] = v;
104		end
105	else
106		tbl = system_load("keybindings.lua")();
107	end
108
109-- still apply any custom overrides
110	load_keys();
111end
112
113function dispatch_load(locktog)
114	dispatch_binding_table()
115
116	gconfig_listen("meta_stick_time", "dispatch.lua",
117	function(key, val)
118		mtrack.mstick = val;
119	end);
120	gconfig_listen("meta_dbltime", "dispatch.lua",
121	function(key, val)
122		mtrack.dblrate = val;
123	end
124	);
125	gconfig_listen("meta_lock", "dispatch.lua",
126	function(key, val)
127		mtrack.mlock = val;
128	end
129	);
130
131	mtrack.dblrate = gconfig_get("meta_dbltime");
132	mtrack.mstick = gconfig_get("meta_stick_time");
133	mtrack.mlock = gconfig_get("meta_lock");
134	mtrack.locktog = locktog;
135end
136
137function dispatch_list()
138	local res = {};
139	for k,v in pairs(tbl) do
140		table.insert(res, k .. "=" .. v);
141	end
142	table.sort(res);
143	return res;
144end
145
146function dispatch_meta()
147	return mtrack.m1 ~= nil, mtrack.m2 ~= nil;
148end
149
150function dispatch_set(key, path)
151	store_key("custom_" ..key, path);
152	tbl[key] = path;
153end
154
155function dispatch_meta_reset(m1, m2)
156	update_meta(m1 and CLOCK or nil, m2 and CLOCK or nil);
157end
158
159function dispatch_toggle(forcev, state)
160	local oldign = mtrack.ignore;
161
162	if (mtrack.mlock == "none") then
163		mtrack.ignore = false;
164		return;
165	end
166
167	if (forcev ~= nil) then
168		mtrack.ignore = forcev;
169	else
170		mtrack.ignore = not mtrack.ignore;
171	end
172
173-- run cleanup hook
174	if (type(oldign) == "function" and mtrack.ignore ~= oldign) then
175		oldign();
176	end
177
178	if (mtrack.locktog) then
179		mtrack.locktog(mtrack.ignore, state);
180	end
181	local m1, m2 = dispatch_meta();
182	ledm_kbd_state(m1, m2, mtrack.ignore);
183end
184
185local function track_label(iotbl, keysym, hook_handler)
186	local metadrop = false;
187	local metam = false;
188
189-- notable state considerations here, we need to construct
190-- a string label prefix that correspond to the active meta keys
191-- but also take 'sticky' (release- take artificially longer) and
192-- figure out 'gesture' (double-press)
193	local function metatrack(s1)
194		local rv1, rv2;
195		if (iotbl.active) then
196			if (mtrack.mstick > 0) then
197				mtrack.unstick_ctr = mtrack.mstick;
198			end
199			rv1 = CLOCK;
200		else
201			if (mtrack.mstick > 0) then
202				rv1 = s1;
203			else
204-- rv already nil
205			end
206			rv2 = CLOCK;
207		end
208		metam = true;
209		return rv1, rv2;
210	end
211
212	if (keysym == SYSTEM_KEYS["meta_1"]) then
213		local m1, m1d = metatrack(mtrack.m1, mtrack.last_m1);
214		update_meta(m1, mtrack.m2);
215		if (m1d and mtrack.mlock == "m1") then
216			if (m1d - mtrack.last_m1 <= mtrack.dblrate) then
217				dispatch_toggle();
218			end
219			mtrack.last_m1 = m1d;
220		end
221	elseif (keysym == SYSTEM_KEYS["meta_2"]) then
222		local m2, m2d = metatrack(mtrack.m2, mtrack.last_m2);
223		update_meta(mtrack.m1, m2);
224		if (m2d and mtrack.mlock == "m2") then
225			if (m2d - mtrack.last_m2 <= mtrack.dblrate) then
226				dispatch_toggle();
227			end
228			mtrack.last_m2 = m2d;
229		end
230	end
231
232	local lutsym = "" ..
233		(mtrack.m1 and "m1_" or "") ..
234		(mtrack.m2 and "m2_" or "") .. keysym;
235
236	if (hook_handler) then
237		hook_handler(active_display(), keysym, iotbl, lutsym, metam, tbl[lutsym]);
238		return true, lutsym;
239	end
240
241	if (metam or not meta_guard(mtrack.m1 ~= nil, mtrack.m2 ~= nil)) then
242		return true, lutsym;
243	end
244
245	return false, lutsym;
246end
247
248--
249-- Central input management / routing / translation outside of
250-- mouse handlers and iostatem_ specific translation and patching.
251--
252-- definitions:
253-- SYM = internal SYMTABLE level symble
254-- LUTSYM = prefix with META1 or META2 (m1, m2) state (or device data)
255-- OUTSYM = prefix with normal modifiers (ALT+x, etc.)
256-- LABEL = more abstract and target specific identifier
257--
258local last_deferred = nil;
259local deferred_id = 0;
260
261function dispatch_repeatblock(iotbl)
262	if (iotbl.translated) then
263		sym, outsym = SYMTABLE:patch(iotbl);
264		return (sym == SYSTEM_KEYS["meta_1"] or sym == SYSTEM_KEYS["meta_2"]);
265	end
266	return false;
267end
268
269-- sym contains multiple symbols embedded, with linefeed as a separator
270local function dispatch_multi(sym, arg, ext)
271	local last_i = 2;
272	local len = string.len(sym, arg, ext);
273	for i=2,len do
274		if ((string.sub(sym, i, i) == '\n' or i == len) and i ~= last_i) then
275			dispatch_symbol(string.sub(sym, last_i, i), arg, ext);
276			last_i = i;
277		end
278	end
279end
280
281local dispatch_locked = nil;
282local dispatch_queue = {};
283local last_unlock = "";
284
285-- take the list of accumulated symbols to dispatch and push them out now,
286-- note that this can trigger another dispatch_symbol_lock and so on..
287function dispatch_symbol_unlock(flush)
288	if (not dispatch_locked) then
289		dispatch_debug(
290			"kind=api_error:message=unlock_not_locked:trace=" .. last_unlock);
291		return;
292	end
293	dispatch_locked = nil;
294	last_unlock = debug.traceback();
295
296	local old_queue = dispatch_queue;
297	dispatch_queue = {};
298	if (flush) then
299		for i,v in ipairs(old_queue) do
300			dispatch_symbol(v);
301		end
302	end
303end
304
305function dispatch_symbol_lock()
306	assert(dispatch_locked == nil);
307	dispatch_locked = true;
308	dispatch_queue = {};
309end
310
311local bindpath;
312function dispatch_bindtarget(path)
313	bindpath = path;
314end
315
316-- Setup menu navigation (interactively unless bindtarget is set) in a way that
317-- we can hook rather than activated a selected path or even path/key=value.
318-- There is a special case for a tiler where the lbar is currently active
319-- (timers) as we want to wait after the current one has been destroyed or the
320-- hook will fire erroneously.
321function dispatch_symbol_bind(callback, path, opts)
322	if (bindpath) then
323		callback(bindpath);
324		bindpath = nil;
325		return;
326	end
327
328	local menu = menu_resolve(path and path or "/");
329	dispatch_debug("bind:path=" .. tostring(path));
330
331	menu_hook_launch(callback);
332	opts = opts and opts or {};
333
334-- old default behavior before we started reusing this thing
335	if (opts.show_invisible == nil) then
336		opts.show_invisible = true;
337	end
338	opts.list = menu;
339
340	menu_launch(active_display(), opts, {}, "/", menu_default_lookup(menu));
341end
342
343-- Due to the (current) ugly of lots of active_display() calls being used,
344-- we need to do some rather unorthodox things for this to work until all
345-- those calls have been factored out.
346function dispatch_symbol_wnd(wnd, sym)
347	if (not wnd or not wnd.wm) then
348		dispatch_debug("dispatch_wnd:status=error:message=bad window");
349		return;
350	end
351
352	dispatch_debug(string.format("dispatch_wnd:set_dst=%s", wnd.name));
353
354-- fake "selecting" the window
355	local old_sel = wnd.wm.selected;
356	local wm = wnd.wm;
357
358	wm.selected = wnd;
359
360-- need to run in the context of the display as any object creation gets
361-- tied to the output rendertarget
362	display_action(wnd.wm, function()
363		dispatch_symbol(sym);
364	end)
365
366-- the symbol might have actually destroyed the window or caused a change
367-- in selection, so not always save to revert, but might also wanted to
368-- run a command that changes selection relative to the target window.
369	if old_sel then
370		if wm.selected == wnd and old_sel.select then
371			wm.selected = old_sel;
372		elseif old_sel.select then
373			old_sel:select();
374		end
375	end
376end
377
378local last_symbol = "/";
379function dispatch_symbol(sym, menu_opts)
380-- note, it's up to us to forward the argument for validator before exec
381	local menu, msg, val, enttbl = menu_resolve(sym);
382	last_symbol = sym;
383	dispatch_debug("run=" .. sym);
384
385-- catch all the 'value path returned', submenu returned, ...
386	if (not menu) then
387		dispatch_debug("status=error:kind=einval:message=could not resolve " .. sym);
388		return false;
389	elseif (menu.validator and not menu.validator(val)) then
390		dispatch_debug("status=error:kind=efault:message=validator rejected " .. sym);
391		return false;
392	end
393
394-- just queue if locked
395	if (dispatch_locked) then
396		dispatch_debug("status=queued");
397		table.insert(dispatch_queue, sym);
398		return true;
399	end
400
401-- shortpath the common case
402	if (menu.handler and not menu.submenu) then
403		dispatch_debug("status=trigger");
404		menu:handler(val);
405		return true;
406	end
407
408-- actual menu returned, need to spawn
409	if (type(menu[1]) == "table") then
410		dispatch_debug("status=menu");
411		menu_launch(active_display(),
412			{list = menu}, menu_opts, sym, menu_default_lookup(enttbl));
413	else
414-- actually broken result
415		return false;
416	end
417
418	return true;
419end
420
421function dispatch_last_symbol()
422	return last_symbol;
423end
424
425function dispatch_translate(iotbl, nodispatch)
426	local ok, sym, outsym, lutsym;
427	local sel = active_display().selected;
428
429-- apply keymap (or possibly local keymap), note that at this stage,
430-- iostatem_ has converted any digital inputs that are active to act
431-- like translated
432	if (iotbl.translated or iotbl.dsym) then
433		if (iotbl.dsym) then
434			sym = iotbl.dsym;
435			outsym = sym;
436		elseif (sel and sel.symtable) then
437			sym, outsym = sel.symtable:patch(iotbl);
438		else
439			sym, outsym = SYMTABLE:patch(iotbl);
440		end
441-- generate durden specific meta- tracking or apply binding hooks
442		ok, lutsym = track_label(iotbl, sym, active_display().input_lock);
443	end
444
445	if (not lutsym or mtrack.ignore) then
446		if (type(mtrack.ignore) == "function") then
447			return mtrack.ignore(lutsym, iotbl, tbl[lutsym]);
448		end
449
450		return false, nil, iotbl;
451	end
452
453-- just perform the translation?
454	if (ok or nodispatch) then
455		return true, lutsym, iotbl, tbl[lutsym];
456	end
457
458-- active display always receives cancellation / accept input,
459-- typically needed for a keyboard way out of the cursor tagging
460	if (iotbl.active and
461		(sym == SYSTEM_KEYS["cancel"] or sym == SYSTEM_KEYS["accept"])) then
462		active_display():cancellation(sym == SYSTEM_KEYS["accept"]);
463	end
464
465-- we can have special bindings on a per window basis
466	if (sel and sel.bindings and sel.bindings[lutsym]) then
467		if (iotbl.active) then
468			if (type(sel.bindings[lutsym]) == "function") then
469				sel.bindings[lutsym](sel);
470			else
471				dispatch_symbol(sel.bindings[lutsym]);
472			end
473		end
474
475-- don't want to run repeat for valid bindings
476		iostatem_reset_repeat();
477		return true, lutsym, iotbl;
478	end
479
480	local rlut = "f_" ..lutsym;
481	if (tbl[lutsym] or (not iotbl.active and tbl[rlut])) then
482		if (iotbl.active and tbl[lutsym]) then
483			dispatch_symbol(tbl[lutsym]);
484			if (tbl[rlut]) then
485				last_deferred = tbl[rlut];
486				deferred_id = iotbl.devid;
487			end
488
489		elseif (tbl[rlut]) then
490			dispatch_symbol(tbl[rlut]);
491			last_deferred = nil;
492		end
493
494-- don't want to run repeat for valid bindings
495		iostatem_reset_repeat();
496		return true, lutsym, iotbl;
497	elseif (last_deferred and iotbl.devid == deferred_id) then
498		dispatch_symbol(last_deferred);
499		last_deferred = nil;
500		return true, lutsym, iotbl;
501	elseif (not sel) then
502		return false, lutsym, iotbl;
503	end
504
505-- or an input handler unique for the window
506	if (not iotbl.analog and sel.key_input) then
507		sel:key_input(outsym, iotbl);
508		ok = true;
509	else
510
511-- for label bindings, we go with the prefixed view of modifiers
512		if (sel.labels and sel.labels[outsym]) then
513			iotbl.label = sel.labels[outsym];
514		end
515	end
516
517	return ok, outsym, iotbl;
518end
519