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