1-- Copyright: None claimed, Public Domain 2-- 3-- Description: Cookbook- style functions for the normal tedium 4-- (string and table manipulation, mostly plucked from the AWB project) 5-- These should either expand the various basic tables (string, math) 6-- or namespace prefix with suppl_... 7 8function string.split(instr, delim) 9 if (not instr) then 10 return {}; 11 end 12 13 local res = {}; 14 local strt = 1; 15 local delim_pos, delim_stp = string.find(instr, delim, strt); 16 17 while delim_pos do 18 table.insert(res, string.sub(instr, strt, delim_pos-1)); 19 strt = delim_stp + 1; 20 delim_pos, delim_stp = string.find(instr, delim, strt); 21 end 22 23 table.insert(res, string.sub(instr, strt)); 24 return res; 25end 26 27function string.starts_with(instr, prefix) 28 return string.sub(instr, 1, #prefix) == prefix; 29end 30 31-- 32-- Similar to split but only returns 'first' and 'rest'. 33-- 34-- The edge cases of the delim being at first or last part of the 35-- string, empty strings will be returned instead of nil. 36-- 37function string.split_first(instr, delim) 38 if (not instr) then 39 return; 40 end 41 local delim_pos, delim_stp = string.find(instr, delim, 1); 42 if (delim_pos) then 43 local first = string.sub(instr, 1, delim_pos - 1); 44 local rest = string.sub(instr, delim_stp + 1); 45 first = first and first or ""; 46 rest = rest and rest or ""; 47 return first, rest; 48 else 49 return "", instr; 50 end 51end 52 53-- can shorten further by dropping vowels and characters 54-- in beginning and end as we match more on those 55function string.shorten(s, len) 56 if (s == nil or string.len(s) == 0) then 57 return ""; 58 end 59 60 local r = string.gsub( 61 string.gsub(s, " ", ""), "\n", "" 62 ); 63 return string.sub(r and r or "", 1, len); 64end 65 66function string.utf8back(src, ofs) 67 if (ofs > 1 and string.len(src)+1 >= ofs) then 68 ofs = ofs - 1; 69 while (ofs > 1 and utf8kind(string.byte(src,ofs) ) == 2) do 70 ofs = ofs - 1; 71 end 72 end 73 74 return ofs; 75end 76 77function math.sign(val) 78 return (val < 0 and -1) or 1; 79end 80 81function math.clamp(val, low, high) 82 if (low and val < low) then 83 return low; 84 end 85 if (high and val > high) then 86 return high; 87 end 88 return val; 89end 90 91function string.to_u8(instr) 92-- drop spaces and make sure we have %2 93 instr = string.gsub(instr, " ", ""); 94 local len = string.len(instr); 95 if (len % 2 ~= 0 or len > 8) then 96 return; 97 end 98 99 local s = ""; 100 for i=1,len,2 do 101 local num = tonumber(string.sub(instr, i, i+1), 16); 102 if (not num) then 103 return nil; 104 end 105 s = s .. string.char(num); 106 end 107 108 return s; 109end 110 111function string.utf8forward(src, ofs) 112 if (ofs <= string.len(src)) then 113 repeat 114 ofs = ofs + 1; 115 until (ofs > string.len(src) or 116 utf8kind( string.byte(src, ofs) ) < 2); 117 end 118 119 return ofs; 120end 121 122function string.utf8lalign(src, ofs) 123 while (ofs > 1 and utf8kind(string.byte(src, ofs)) == 2) do 124 ofs = ofs - 1; 125 end 126 return ofs; 127end 128 129function string.utf8ralign(src, ofs) 130 while (ofs <= string.len(src) and string.byte(src, ofs) 131 and utf8kind(string.byte(src, ofs)) == 2) do 132 ofs = ofs + 1; 133 end 134 return ofs; 135end 136 137function string.translateofs(src, ofs, beg) 138 local i = beg; 139 local eos = string.len(src); 140 141 -- scan for corresponding UTF-8 position 142 while ofs > 1 and i <= eos do 143 local kind = utf8kind( string.byte(src, i) ); 144 if (kind < 2) then 145 ofs = ofs - 1; 146 end 147 148 i = i + 1; 149 end 150 151 return i; 152end 153 154function string.utf8len(src, ofs) 155 local i = 0; 156 local rawlen = string.len(src); 157 ofs = ofs < 1 and 1 or ofs; 158 159 while (ofs <= rawlen) do 160 local kind = utf8kind( string.byte(src, ofs) ); 161 if (kind < 2) then 162 i = i + 1; 163 end 164 165 ofs = ofs + 1; 166 end 167 168 return i; 169end 170 171function string.insert(src, msg, ofs, limit) 172 if (limit == nil) then 173 limit = string.len(msg) + ofs; 174 end 175 176 if ofs + string.len(msg) > limit then 177 msg = string.sub(msg, 1, limit - ofs); 178 179-- align to the last possible UTF8 char.. 180 181 while (string.len(msg) > 0 and 182 utf8kind( string.byte(msg, string.len(msg))) == 2) do 183 msg = string.sub(msg, 1, string.len(msg) - 1); 184 end 185 end 186 187 return string.sub(src, 1, ofs - 1) .. msg .. 188 string.sub(src, ofs, string.len(src)), string.len(msg); 189end 190 191function string.delete_at(src, ofs) 192 local fwd = string.utf8forward(src, ofs); 193 if (fwd ~= ofs) then 194 return string.sub(src, 1, ofs - 1) .. string.sub(src, fwd, string.len(src)); 195 end 196 197 return src; 198end 199 200local function hb(ch) 201 local th = {"0", "1", "2", "3", "4", "5", 202 "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; 203 204 local fd = math.floor(ch/16); 205 local sd = ch - fd * 16; 206 return th[fd+1] .. th[sd+1]; 207end 208 209function string.hexenc(instr) 210 return string.gsub(instr, "(.)", function(ch) 211 return hb(ch:byte(1)); 212 end); 213end 214 215function string.trim(s) 216 return (s:gsub("^%s*(.-)%s*$", "%1")); 217end 218 219-- to ensure "special" names e.g. connection paths for target alloc, 220-- or for connection pipes where we want to make sure the user can input 221-- no matter keylayout etc. 222strict_fname_valid = function(val) 223 for i in string.gmatch(val, "%W") do 224 if (i ~= '_') then 225 return false; 226 end 227 end 228 return true; 229end 230 231function string.utf8back(src, ofs) 232 if (ofs > 1 and string.len(src)+1 >= ofs) then 233 ofs = ofs - 1; 234 while (ofs > 1 and utf8kind(string.byte(src,ofs) ) == 2) do 235 ofs = ofs - 1; 236 end 237 end 238 239 return ofs; 240end 241 242function table.set_unless_exists(tbl, key, val) 243 tbl[key] = tbl[key] and tbl[key] or val; 244end 245 246function table.intersect(tbl, tbl2) 247 local res = {} 248 for _, v in ipairs(tbl) do 249 if table.find_i(tbl2) then 250 table.insert(res, v) 251 end 252 end 253 return res 254end 255 256-- take the entries in ref and apply to src if they match type 257-- with ref, otherwise use the value in ref 258function table.merge(dst, src, ref, on_error) 259 for k,v in pairs(ref) do 260 if src[k] and type(src[k]) == type(v) then 261 v = src[k]; 262 elseif src[k] then 263-- type mismatch is recoverable error 264 on_error(k); 265 end 266 267 if type(k) == "table" then 268 dst[k] = table.copy(v); 269 else 270 dst[k] = v; 271 end 272 end 273end 274 275function table.copy(tbl) 276 if not tbl or not type(tbl) == "table" then 277 return {}; 278 end 279 280 local res = {}; 281 for k,v in pairs(tbl) do 282 if type(v) == "table" then 283 res[k] = table.copy(v); 284 else 285 res[k] = v; 286 end 287 end 288 289 return res; 290end 291 292function table.remove_match(tbl, match) 293 if (tbl == nil) then 294 return; 295 end 296 297 for k,v in ipairs(tbl) do 298 if (v == match) then 299 table.remove(tbl, k); 300 return v, k; 301 end 302 end 303 304 return nil; 305end 306 307function string.dump(msg) 308 local bt ={}; 309 for i=1,string.len(msg) do 310 local ch = string.byte(msg, i); 311 bt[i] = ch; 312 end 313end 314 315function table.remove_vmatch(tbl, match) 316 if (tbl == nil) then 317 return; 318 end 319 320 for k,v in pairs(tbl) do 321 if (v == match) then 322 tbl[k] = nil; 323 return v; 324 end 325 end 326 327 return nil; 328end 329 330function suppl_delete_image_if(vid) 331 if valid_vid(vid) then 332 delete_image(vid); 333 end 334end 335 336function table.find_i(table, r) 337 for k,v in ipairs(table) do 338 if (v == r) then return k; end 339 end 340end 341 342function table.find_key_i(table, field, r) 343 for k,v in ipairs(table) do 344 if (v[field] == r) then 345 return k; 346 end 347 end 348end 349 350function table.insert_unique_i(tbl, i, v) 351 local ind = table.find_i(tbl, v); 352 if (not ind) then 353 table.insert(tbl, i, v); 354 else 355 local cpy = tbl[i]; 356 tbl[i] = tbl[ind]; 357 tbl[ind] = cpy; 358 end 359end 360 361-- 362-- Extract subset of array-like table using 363-- given filter function 364-- 365-- Accepts table and filter function. 366-- Input table is not modified in process. 367-- Rest of arguments are passed to filter 368-- function. 369-- 370function table.filter(tbl, filter_fn, ...) 371 local res = {}; 372 373 for _,v in ipairs(tbl) do 374 if (filter_fn(v, ...) == true) then 375 table.insert(res, v); 376 end 377 end 378 379 return res; 380end 381 382function suppl_strcol_fmt(str, sel) 383 local hv = util.hash(str); 384 return HC_PALETTE[(hv % #HC_PALETTE) + 1]; 385end 386 387function suppl_region_stop(trig) 388-- restore repeat- rate state etc. 389 iostatem_restore(); 390 391-- then return the input processing pipeline 392 durden_input_sethandler() 393 394-- and allow external input triggers to re-appear 395 dispatch_symbol_unlock(true); 396 397-- and trigger the on-end callback 398 mouse_select_end(trig); 399end 400 401-- Attach a shadow to ctx. 402-- 403-- This is a simple/naive version that repeats the fragment shader for every 404-- updated friend. A decent optimization (depending on memory) is to RTT it 405-- and maintain a cache for non/dynamic updates (animations and drag-resize). 406-- 407-- Shadows can use a single color or a weighted mix between a base color and 408-- a reference texture map. For this case, the opts.reference is set to the 409-- textured source and the global 'shadow_style' config is set to textured. 410-- 411function suppl_region_shadow(ctx, w, h, opts) 412 opts = opts and opts or {}; 413 opts.method = opts.method and opts.method or gconfig_get("shadow_style"); 414 if (opts.method == "none") then 415 if (valid_vid(ctx.shadow)) then 416 delete_image(ctx.shadow); 417 ctx.shadow = nil; 418 end 419 return; 420 end 421 422-- assume 'soft' for now 423 local shname = opts.shader and opts.shader or "dropshadow"; 424 425 local time = opts.time and opts.time or 0; 426 local t = opts.t and opts.t or gconfig_get("shadow_t"); 427 local l = opts.l and opts.l or gconfig_get("shadow_l"); 428 local d = opts.d and opts.d or gconfig_get("shadow_d"); 429 local r = opts.r and opts.r or gconfig_get("shadow_r"); 430 local interp = opts.interp and opts.interp or INTERP_SMOOTHSTEP; 431 local cr, cg, cb; 432 433 if (opts.color) then 434 cr, cg, cb = unpack(opts.color); 435 else 436 cr, cg, cb = unpack(gconfig_get("shadow_color")); 437 end 438 439-- allocate on first call 440 if not valid_vid(ctx.shadow) then 441 ctx.shadow = color_surface(w + l + r, h + t + d, cr, cg, cb); 442 443-- and handle OOM 444 if (not valid_vid(ctx.shadow)) then 445 return; 446 end 447 448 if opts.reference and opts.method == "textured" then 449 image_sharestorage(opts.reference, ctx.shadow); 450 end 451 452-- assume we can patch ctx and that it has an anchor 453 blend_image(ctx.shadow, 1.0, time); 454 link_image(ctx.shadow, ctx.anchor); 455 image_inherit_order(ctx.shadow, true); 456 order_image(ctx.shadow, -1); 457 458-- This is slightly problematic as the uniforms are shared, thus 459-- the option of colour vs texture source etc. will be shared. 460-- 461-- Though this does not apply here, multi-pass effect composition 462-- etc. that requires indirect blits would not work this way either. 463 local shid = shader_ui_lookup(ctx.shadow, "ui", shname, "active"); 464 if shid then 465 shader_uniform(shid, "color", "fff", cr, cg, cb); 466 end 467 else 468 reset_image_transform(ctx.shadow); 469 show_image(ctx.shadow, time, interp); 470 end 471 472 image_color(ctx.shadow, cr, cg, cb); 473 resize_image(ctx.shadow, w + l + r, h + t + d, time, interp); 474 move_image(ctx.shadow, -l, -t); 475end 476 477function suppl_region_select(r, g, b, handler) 478 local col = fill_surface(1, 1, r, g, b); 479 blend_image(col, 0.2); 480 iostatem_save(); 481 mouse_select_begin(col); 482 dispatch_meta_reset(); 483 shader_setup(col, "ui", "regsel", "active"); 484 dispatch_symbol_lock(); 485 durden_input_sethandler(durden_regionsel_input, "region-select"); 486 DURDEN_REGIONSEL_TRIGGER = handler; 487end 488 489local ffmts = 490{ 491 jpg = "image", jpeg = "image", png = "image", bmp = "image", 492 ogg = "audio", m4a = "audio", flac = "audio", mp3 = "audio", 493 mp4 = "video", wmv = "video", mkv = "video", avi = "video", 494 flv = "video", mpg = "video", mpeg = "video", mov = "video", 495 webm = "video", ["*"] = "file", 496}; 497 498local function match_ext(v, tbl) 499 if (tbl == nil) then 500 return true; 501 end 502 503 local ext = string.match(v, "^.+(%..+)$"); 504 ext = ext ~= nil and string.sub(ext, 2) or ext; 505 if (ext == nil or string.len(ext) == 0) then 506 return false; 507 end 508 509 local ent = tbl[string.lower(ext)]; 510 if ent then 511 return ent; 512 else 513 return tbl["*"]; 514 end 515end 516 517-- filename to classifier [media, image, audio] 518function suppl_ext_type(fn) 519 return match_ext(fn, ffmts); 520end 521 522local function defer_spawn(wnd, new, t, l, d, r, w, h, closure) 523-- window died before timer? 524 if (not wnd.add_handler) then 525 delete_image(new); 526 return; 527 end 528 529-- don't make the source visible until we can spawn new 530 show_image(new); 531 local cwin = active_display():add_window(new, {scalemode = "stretch"}); 532 if (not cwin) then 533 delete_image(new); 534 return; 535 end 536 537-- closure to update the crop if the source changes (shaders etc.) 538 local function recrop() 539 local sprops = image_storage_properties(wnd.canvas); 540 cwin.origo_ll = wnd.origo_ll; 541 cwin:set_crop( 542 t * sprops.height, l * sprops.width, 543 d * sprops.height, r * sprops.width, false, true 544 ); 545 end 546 547-- deregister UNLESS the source window is already dead 548 cwin:add_handler("destroy", 549 function() 550 if (wnd.drop_handler) then 551 wnd:drop_handler("resize", recrop); 552 end 553 end 554 ); 555 556-- add event handlers so that we update the scaling every time the source changes 557 recrop(); 558 cwin:set_title("Slice"); 559 cwin.source_name = wnd.name; 560 cwin.name = cwin.name .. "_crop"; 561 562-- finally send to a possible source that wants to do additional modifications 563 if closure then 564 closure(cwin, t, l, d, r, w, h); 565 end 566end 567 568local function slice_handler(wnd, x1, y1, x2, y2, closure) 569-- grab the current values 570 local props = image_surface_resolve(wnd.canvas); 571 local px2 = props.x + props.width; 572 local py2 = props.y + props.height; 573 574-- and actually clamp 575 x1 = x1 < props.x and props.x or x1; 576 y1 = y1 < props.y and props.y or y1; 577 x2 = x2 > px2 and px2 or x2; 578 y2 = y2 > py2 and py2 or y2; 579 580-- safeguard against range problems 581 if (x2 - x1 <= 0 or y2 - y1 <= 0) then 582 return; 583 end 584 585-- create clone with proper texture coordinates, this has problems with 586-- source windows that do other coordinate transforms as well and switch 587-- back and forth. 588 local new = null_surface(x2-x1, y2-y1); 589 image_sharestorage(wnd.canvas, new); 590 591-- calculate crop in source surface relative coordinates 592 local t = (y1 - props.y) / props.height; 593 local l = (x1 - props.x) / props.width; 594 local d = (py2 - y2) / props.height; 595 local r = (px2 - x2) / props.width; 596 local w = (x2 - x1); 597 local h = (y2 - y1); 598 599-- work-around the chaining-region-select problem with a timer 600 timer_add_periodic("wndspawn", 1, true, function() 601 defer_spawn(wnd, new, t, l, d, r, w, h, closure); 602 end); 603end 604 605function suppl_wnd_slice(wnd, closure) 606-- like with all suppl_region_select calls, this is race:y as the 607-- selection state can go on indefinitely and things might've changed 608-- due to some event (thing wnd being destroyed while select state is 609-- active) 610 local wnd = active_display().selected; 611 local props = image_surface_resolve(wnd.canvas); 612 613 suppl_region_select(255, 0, 255, 614 function(x1, y1, x2, y2) 615 if (valid_vid(wnd.canvas)) then 616 slice_handler(wnd, x1, y1, x2, y2, closure); 617 end 618 end 619 ); 620end 621 622local function build_rt_reg(drt, x1, y1, w, h, srate) 623 if (w <= 0 or h <= 0) then 624 return; 625 end 626 627-- grab in worldspace, translate 628 local props = image_surface_resolve_properties(drt); 629 x1 = x1 - props.x; 630 y1 = y1 - props.y; 631 632 local dst = alloc_surface(w, h); 633 if (not valid_vid(dst)) then 634 warning("build_rt: failed to create intermediate"); 635 return; 636 end 637 local cont = null_surface(w, h); 638 if (not valid_vid(cont)) then 639 delete_image(dst); 640 return; 641 end 642 643 image_sharestorage(drt, cont); 644 645-- convert to surface coordinates 646 local s1 = x1 / props.width; 647 local t1 = y1 / props.height; 648 local s2 = (x1+w) / props.width; 649 local t2 = (y1+h) / props.height; 650 651 local txcos = {s1, t1, s2, t1, s2, t2, s1, t2}; 652 image_set_txcos(cont, txcos); 653 show_image({cont, dst}); 654 655 local shid = image_shader(drt); 656 if (shid) then 657 image_shader(cont, shid); 658 end 659 return dst, {cont}; 660end 661 662-- lifted from the label definitions and order in shmif, the values are 663-- offset by 2 as shown in arcan_tuisym.h 664local color_labels = 665{ 666 {"primary", "Dominant foreground"}, 667 {"secondary", "Dominant alternative foreground"}, 668 {"background", "Default background"}, 669 {"text", "Default text"}, 670 {"cursor", "Default caret or mouse cursor"}, 671 {"altcursor", "Default alternative-state caret or mouse cursor"}, 672 {"highlight", "Default marked / selection state"}, 673 {"label", "Text labels and content annotations"}, 674 {"warning", "Labels and text that require additional consideration"}, 675 {"error", "Indicate wrong input or other erroneous state"}, 676 {"alert", "Areas that require immediate attention"}, 677 {"inactive", "Labels where the related content is currently inaccessible"}, 678 {"reference", "Actions that reference external contents or trigger navigation"} 679}; 680 681-- Generate menu entries for defining colors, where the output will be 682-- sent to cb. This is here in order to reuse the same tables and code 683-- path for both per-window overrides and some global option 684function suppl_color_menu(cb, lookup) 685 local res = {}; 686 for k,v in ipairs(color_labels) do 687 table.insert(res, { 688 name = v[1], 689 label = 690 string.upper(string.sub(v[1], 1, 1)) .. string.upper(string.sub(v[1], 2)), 691 kind = "value", 692 hint = "(r g b)(0..255)", 693 widget = "special:colorpick_r8g8b8", 694 validator = suppl_valid_typestr("fff", 0, 255, 0), 695 initial = function() 696 local r, g, b = lookup(v[1]); 697 return string.format("%.0f %.0f %.0f", r, g, b); 698 end, 699 handler = function(ctx, val) 700 local col = suppl_unpack_typestr("fff", val, 0, 255); 701 cb(val, col[1], col[2], col[3]); 702 end 703 }); 704 end 705 return res; 706end 707 708-- all the boiler plate needed to figure out the types a uniform has, 709-- generate the corresponding menu entry and with validators for type 710-- and range, taking locale and separators into accoutn. 711local bdelim = (tonumber("1,01") == nil) and "." or ","; 712local rdelim = (bdelim == ".") and "," or "."; 713 714function suppl_unpack_typestr(typestr, val, lowv, highv) 715 string.gsub(val, rdelim, bdelim); 716 local rtbl = string.split(val, ' '); 717 for i=1,#rtbl do 718 rtbl[i] = tonumber(rtbl[i]); 719 if (not rtbl[i]) then 720 return; 721 end 722 if (lowv and rtbl[i] < lowv) then 723 return; 724 end 725 if (highv and rtbl[i] > highv) then 726 return; 727 end 728 end 729 return rtbl; 730end 731 732-- allows empty string in order to 'unset' 733function suppl_valid_name(val) 734 if not string or #val == 0 or string.match(val, "%W") then 735 return false; 736 end 737 738 return true; 739end 740 741-- icon symbol reference or valid utf-8 codepoint 742function suppl_valid_vsymbol(val, base) 743 if (not val) then 744 return false; 745 end 746 747 if (string.len(val) == 0) then 748 return false; 749 end 750 751 if (string.sub(val, 1, 3) == "0x_") then 752 if (not val or not string.to_u8(string.sub(val, 4))) then 753 return false; 754 end 755 val = string.to_u8(string.sub(val, 4)); 756 end 757 758-- do note that the icon_ setup actually returns a factory function, 759-- this may be called repeatedly to generate different sizes of the 760-- same icon reference 761 if (string.sub(val, 1, 5) == "icon_") then 762 val = string.sub(val, 6); 763 if icon_known(val) then 764 return true, function(w) 765 local vid = icon_lookup(val, w); 766 local props = image_surface_properties(vid); 767 local new = null_surface(props.width, props.height); 768 image_sharestorage(vid, new); 769 return new; 770 end 771 end 772 return false; 773 end 774 775 if (string.find(val, ":")) then 776 return false; 777 end 778 779 return true, val; 780end 781 782local function append_color_menu(r, g, b, tbl, update_fun) 783 tbl.kind = "value"; 784 tbl.widget = "special:colorpick_r8g8b8"; 785 tbl.hint = "(r g b)(0..255)"; 786 tbl.initial = string.format("%.0f %.0f %.0f", r, g, b); 787 tbl.validator = suppl_valid_typestr("fff", 0, 255, 0); 788 tbl.handler = function(ctx, val) 789 local tbl = suppl_unpack_typestr("fff", val, 0, 255); 790 if (not tbl) then 791 return; 792 end 793 update_fun( 794 string.format("\\#%02x%02x%02x", tbl[1], tbl[2], tbl[3]), 795 tbl[1], tbl[2], tbl[3]); 796 end 797end 798 799function suppl_hexstr_to_rgb(str) 800 local base; 801 802-- safeguard 1. 803 if not type(str) == "string" then 804 str = "" 805 end 806 807-- check for the normal # and \\# 808 if (string.sub(str, 1,1) == "#") then 809 base = 2; 810 elseif (string.sub(str, 2,2) == "#") then 811 base = 3; 812 else 813 base = 1; 814 end 815 816-- convert based on our assumed starting pos 817 local r = tonumber(string.sub(str, base+0, base+1), 16); 818 local g = tonumber(string.sub(str, base+2, base+3), 16); 819 local b = tonumber(string.sub(str, base+4, base+5), 16); 820 821-- safe so we always return a value 822 r = r and r or 255; 823 g = g and g or 255; 824 b = b and b or 255; 825 826 return r, g, b; 827end 828 829function suppl_append_color_menu(v, tbl, update_fun) 830 if (type(v) == "table") then 831 append_color_menu(v[1], v[2], v[3], tbl, update_fun); 832 else 833 local r, g, b = suppl_hexstr_to_rgb(v); 834 append_color_menu(r, g, b, tbl, update_fun); 835 end 836end 837 838function suppl_button_default_mh(wnd, cmd, altcmd) 839 local res = 840{ 841 click = function(btn) 842 dispatch_symbol_wnd(wnd, cmd); 843 end, 844 over = function(btn) 845 btn:switch_state("alert"); 846 end, 847 out = function(btn) 848 btn:switch_state(wnd.wm.selected == wnd and "active" or "inactive"); 849 end 850}; 851 if (altcmd) then 852 res.rclick = function() 853 dispatch_symbol_wnd(altcmd); 854 end 855 end 856 return res; 857end 858 859function suppl_valid_typestr(utype, lowv, highv, defaultv) 860 return function(val) 861 local tbl = suppl_unpack_typestr(utype, val, lowv, highv); 862 return tbl ~= nil and #tbl == string.len(utype); 863 end 864end 865 866function suppl_region_setup(x1, y1, x2, y2, nodef, static, title) 867 local w = x2 - x1; 868 local h = y2 - y1; 869 870-- check sample points if we match a single vid or we need to 871-- use the aggregate surface and restrict to the behaviors of rt 872 local drt = active_display(true); 873 local tiler = active_display(); 874 875 local i1 = pick_items(x1, y1, 1, true, drt); 876 local i2 = pick_items(x2, y1, 1, true, drt); 877 local i3 = pick_items(x1, y2, 1, true, drt); 878 local i4 = pick_items(x2, y2, 1, true, drt); 879 local img = drt; 880 local in_float = (tiler.spaces[tiler.space_ind].mode == "float"); 881 882-- a possibly better option would be to generate subslices of each 883-- window in the set and dynamically manage the rendertarget, but that 884-- is for later 885 if ( 886 in_float or 887 #i1 == 0 or #i2 == 0 or #i3 == 0 or #i4 == 0 or 888 i1[1] ~= i2[1] or i1[1] ~= i3[1] or i1[1] ~= i4[1]) then 889 rendertarget_forceupdate(drt); 890 else 891 img = i1[1]; 892 end 893 894 local dvid, grp = build_rt_reg(img, x1, y1, w, h); 895 if (not valid_vid(dvid)) then 896 return; 897 end 898 899 if (nodef) then 900 return dvid, grp; 901 end 902 903 define_rendertarget(dvid, grp, 904 RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, static and 0 or -1); 905 906-- just render once, store and drop the rendertarget as they are costly 907 if (static) then 908 rendertarget_forceupdate(dvid); 909 local dsrf = null_surface(w, h); 910 image_sharestorage(dvid, dsrf); 911 delete_image(dvid); 912 show_image(dsrf); 913 dvid = dsrf; 914 end 915 916 return dvid, grp, {}; 917end 918 919local ptn_lut = { 920 p = "prefix", 921 t = "title", 922 i = "ident", 923 a = "atype" 924}; 925 926local function get_ptn_str(cb, wnd) 927 if (string.len(cb) == 0) then 928 return; 929 end 930 931 local field = ptn_lut[string.sub(cb, 1, 1)]; 932 if (not field or not wnd[field] or not (string.len(wnd[field]) > 0)) then 933 return; 934 end 935 936 local len = tonumber(string.sub(cb, 2)); 937 return string.sub(wnd[field], 1, tonumber(string.sub(cb, 2))); 938end 939 940function suppl_ptn_expand(tbl, ptn, wnd) 941 local i = 1; 942 local cb = ""; 943 local inch = false; 944 945 local flush_cb = function() 946 local msg = cb; 947 948 if (inch) then 949 msg = get_ptn_str(cb, wnd); 950 msg = msg and msg or ""; 951 msg = string.trim(msg); 952 end 953 if (string.len(msg) > 0) then 954 table.insert(tbl, msg); 955 table.insert(tbl, ""); -- need to maintain %2 956 end 957 cb = ""; 958 end 959 960 while (i <= string.len(ptn)) do 961 local ch = string.sub(ptn, i, i); 962 if (ch == " " and inch) then 963 flush_cb(); 964 inch = false; 965 elseif (ch == "%") then 966 flush_cb(); 967 inch = true; 968 else 969 cb = cb .. ch; 970 end 971 i = i + 1; 972 end 973 flush_cb(); 974end 975 976function suppl_setup_rec(wnd, val, noaudio) 977 local svid = wnd; 978 local aarr = {}; 979 980 if (type(wnd) == "table") then 981 svid = wnd.external; 982 if (not noaudio and wnd.source_audio) then 983 table.insert(aarr, wnd.source_audio); 984 end 985 end 986 987 if (not valid_vid(svid)) then 988 return; 989 end 990 991-- work around link_image constraint 992 local props = image_storage_properties(svid); 993 local pw = props.width + props.width % 2; 994 local ph = props.height + props.height % 2; 995 996 local nsurf = null_surface(pw, ph); 997 image_sharestorage(svid, nsurf); 998 show_image(nsurf); 999 local varr = {nsurf}; 1000 1001 local db = alloc_surface(pw, ph); 1002 if (not valid_vid(db)) then 1003 delete_image(nsurf); 1004 warning("setup_rec, couldn't allocate output buffer"); 1005 return; 1006 end 1007 1008 local argstr, srate, fn = suppl_build_recargs(varr, aarr, false, val); 1009 define_recordtarget(db, fn, argstr, varr, aarr, 1010 RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, srate, 1011 function(source, stat) 1012 if (stat.kind == "terminated") then 1013 delete_image(source); 1014 end 1015 end 1016 ); 1017 1018 if (not valid_vid(db)) then 1019 delete_image(db); 1020 delete_image(nsurf); 1021 warning("setup_rec, failed to spawn recordtarget"); 1022 return; 1023 end 1024 1025-- useful for debugging, spawn a new window that shares 1026-- the contents of the allocated surface 1027-- local ns = null_surface(pw, ph); 1028-- image_sharestorage(db, ns); 1029-- show_image(ms); 1030-- local wnd = active_display():add_window(ns); 1031-- wnd:set_tile("record-test"); 1032-- 1033-- link the recordtarget with the source for automatic deletion 1034 link_image(db, svid); 1035 return db; 1036end 1037 1038function drop_keys(matchstr) 1039 local rst = {}; 1040 for i,v in ipairs(match_keys(matchstr)) do 1041 local pos, stop = string.find(v, "=", 1); 1042 local key = string.sub(v, 1, pos-1); 1043 rst[key] = ""; 1044 end 1045 store_key(rst); 1046end 1047 1048-- reformated PD snippet 1049function string.utf8valid(str) 1050 local i, len = 1, #str 1051 local find = string.find; 1052 while i <= len do 1053 if (i == find(str, "[%z\1-\127]", i)) then 1054 i = i + 1; 1055 elseif (i == find(str, "[\194-\223][\123-\191]", i)) then 1056 i = i + 2; 1057 elseif (i == find(str, "\224[\160-\191][\128-\191]", i) 1058 or (i == find(str, "[\225-\236][\128-\191][\128-\191]", i)) 1059 or (i == find(str, "\237[\128-\159][\128-\191]", i)) 1060 or (i == find(str, "[\238-\239][\128-\191][\128-\191]", i))) then 1061 i = i + 3; 1062 elseif (i == find(str, "\240[\144-\191][\128-\191][\128-\191]", i) 1063 or (i == find(str, "[\241-\243][\128-\191][\128-\191][\128-\191]", i)) 1064 or (i == find(str, "\244[\128-\143][\128-\191][\128-\191]", i))) then 1065 i = i + 4; 1066 else 1067 return false, i; 1068 end 1069 end 1070 1071 return true; 1072end 1073 1074function suppl_bind_u8(hook) 1075 local bwt = gconfig_get("bind_waittime"); 1076 local tbhook = function(sym, done, sym2, iotbl) 1077 if (not done) then 1078 return; 1079 end 1080 1081 local bar = active_display():lbar( 1082 function(ctx, instr, done, lastv) 1083 if (not done) then 1084 return instr and string.len(instr) > 0 and string.to_u8(instr) ~= nil; 1085 end 1086 1087 instr = string.to_u8(instr); 1088 if (instr and string.utf8valid(instr)) then 1089 hook(sym, instr, sym2, iotbl); 1090 else 1091 active_display():message("invalid utf-8 sequence specified"); 1092 end 1093 end, ctx, {label = "specify byte-sequence (like f0 9f 92 a9):"}); 1094 suppl_widget_path(bar, bar.text_anchor, "special:u8"); 1095 end; 1096 1097 tiler_bbar(active_display(), 1098 string.format(LBL_BIND_COMBINATION, SYSTEM_KEYS["cancel"]), 1099 "keyorcombo", bwt, nil, SYSTEM_KEYS["cancel"], tbhook); 1100end 1101 1102function suppl_binding_helper(prefix, suffix, bind_fn) 1103 local bwt = gconfig_get("bind_waittime"); 1104 1105 local on_input = function(sym, done) 1106 if (not done) then 1107 return; 1108 end 1109 1110 dispatch_symbol_bind(function(path) 1111 if (not path) then 1112 return; 1113 end 1114 bind_fn(prefix .. sym .. suffix, path); 1115 end); 1116 end 1117 1118 local bind_msg = string.format( 1119 LBL_BIND_COMBINATION_REP, SYSTEM_KEYS["cancel"]); 1120 1121 local ctx = tiler_bbar(active_display(), bind_msg, 1122 false, gconfig_get("bind_waittime"), nil, 1123 SYSTEM_KEYS["cancel"], 1124 on_input, gconfig_get("bind_repeat") 1125 ); 1126 1127 local lbsz = 2 * active_display().scalef * gconfig_get("lbar_sz"); 1128 1129-- tell the widget system that we are in a special context 1130 suppl_widget_path(ctx, ctx.bar, "special:custom", lbsz); 1131 return ctx; 1132end 1133 1134-- 1135-- used for the ugly case with the meta-guard where we want to chain multiple 1136-- binding query paths if one binding in the chain succeeds 1137-- 1138local binding_queue = {}; 1139function suppl_binding_queue(arg) 1140 if (type(arg) == "function") then 1141 table.insert(binding_queue, arg); 1142 elseif (arg) then 1143 binding_queue = {}; 1144 else 1145 local ent = table.remove(binding_queue, 1); 1146 if (ent) then 1147 ent(); 1148 end 1149 end 1150end 1151 1152local function text_input_sym(ctx, sym) 1153 1154end 1155 1156local function text_input_table(ctx, io, sym) 1157-- first check if modifier is held, and apply normal 'readline' translation 1158 if not io.active then 1159 return; 1160 end 1161 1162-- then check if the symbol matches our default overrides 1163 if sym and ctx.bindings[sym] then 1164 ctx.bindings[sym](ctx); 1165 return; 1166 end 1167 1168-- last normal text input 1169 local keych = io.utf8; 1170 if (keych == nil or keych == '') then 1171 return ctx; 1172 end 1173 1174 ctx.oldmsg = ctx.msg; 1175 ctx.oldpos = ctx.caretpos; 1176 ctx.msg, nch = string.insert(ctx.msg, keych, ctx.caretpos, ctx.nchars); 1177 1178 ctx.caretpos = ctx.caretpos + nch; 1179 ctx:update_caret(); 1180end 1181 1182local function text_input_view(ctx) 1183 local rofs = string.utf8ralign(ctx.msg, ctx.chofs + ctx.ulim); 1184 local str = string.sub(ctx.msg, string.utf8ralign(ctx.msg, ctx.chofs), rofs-1); 1185 return str; 1186end 1187 1188local function text_input_caret_str(ctx) 1189 return string.sub(ctx.msg, ctx.chofs, ctx.caretpos - 1); 1190end 1191 1192-- should really be more sophisticated, i.e. a push- function that deletes 1193-- everything after the current undo index, a back function that moves the 1194-- index upwards, a forward function that moves it down, and possible hist 1195-- get / set. 1196local function text_input_undo(ctx) 1197 if (ctx.oldmsg) then 1198 ctx.msg = ctx.oldmsg; 1199 ctx.caretpos = ctx.oldpos; 1200-- redraw(ctx); 1201 end 1202end 1203 1204local function text_input_set(ctx, str) 1205 ctx.msg = (str and #str > 0) and str or ""; 1206 ctx.caretpos = string.len( ctx.msg ) + 1; 1207 ctx.chofs = ctx.caretpos - ctx.ulim; 1208 ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs; 1209 ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs); 1210 ctx:update_caret(); 1211end 1212 1213-- caret index has changed to some arbitrary position, 1214-- make sure the visible window etc. is updated to match 1215local function text_input_caretalign(ctx) 1216 if (ctx.caretpos - ctx.chofs + 1 > ctx.ulim) then 1217 ctx.chofs = string.utf8lalign(ctx.msg, ctx.caretpos - ctx.ulim); 1218 end 1219 ctx:draw(); 1220end 1221 1222local function text_input_chome(ctx) 1223 ctx.caretpos = 1; 1224 ctx.chofs = 1; 1225 ctx:update_caret(); 1226end 1227 1228local function text_input_cend(ctx) 1229 ctx.caretpos = string.len( ctx.msg ) + 1; 1230 ctx.chofs = ctx.caretpos - ctx.ulim; 1231 ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs; 1232 ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs); 1233 ctx:update_caret(); 1234end 1235 1236local function text_input_cleft(ctx) 1237 ctx.caretpos = string.utf8back(ctx.msg, ctx.caretpos); 1238 1239 if (ctx.caretpos < ctx.chofs) then 1240 ctx.chofs = ctx.chofs - ctx.ulim; 1241 ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs; 1242 ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs); 1243 end 1244 1245 ctx:update_caret(); 1246end 1247 1248local function text_input_cright(ctx) 1249 ctx.caretpos = string.utf8forward(ctx.msg, ctx.caretpos); 1250 1251 if (ctx.chofs + ctx.ulim <= ctx.caretpos) then 1252 ctx.chofs = ctx.chofs + 1; 1253 end 1254 1255 ctx:update_caret(); 1256end 1257 1258local function text_input_cdel(ctx) 1259 ctx.msg = string.delete_at(ctx.msg, ctx.caretpos); 1260 ctx:update_caret(); 1261end 1262 1263local function text_input_cerase(ctx) 1264 if (ctx.caretpos < 1) then 1265 return; 1266 end 1267 1268 ctx.caretpos = string.utf8back(ctx.msg, ctx.caretpos); 1269 if (ctx.caretpos <= ctx.chofs) then 1270 ctx.chofs = ctx.caretpos - ctx.ulim; 1271 ctx.chofs = ctx.chofs < 0 and 1 or ctx.chofs; 1272 end 1273 1274 ctx.msg = string.delete_at(ctx.msg, ctx.caretpos); 1275 ctx:update_caret(); 1276end 1277 1278local function text_input_clear(ctx) 1279 ctx.caretpos = 1; 1280 ctx.msg = ""; 1281 ctx:update_caret(); 1282end 1283 1284-- 1285-- Setup an input field for readline- like text input. 1286-- This function can be used both to feed input and to initialize the context 1287-- for the first time. It does not perform any rendering or allocation itself, 1288-- that is deferred to the caller via the draw(ctx) callback. 1289-- 1290-- All relevant offsets [chofs : start drawing @] [caret : current cursor @] 1291-- are aligned positions and the string being built is represented in utf8. 1292-- 1293-- example use: 1294-- ctx = suppl_text_input(NULL, input_table, resolved_symbol, draw_function, opts) 1295-- ctx:input(tbl, sym) OR suppl_text_input(ctx, ...) 1296-- 1297function suppl_text_input(ctx, iotbl, sym, redraw, opts) 1298 ctx = ctx == nil and { 1299 caretpos = 1, 1300 limit = -1, 1301 chofs = 1, 1302 ulim = VRESW / gconfig_get("font_sz"), 1303 msg = "", 1304 1305-- mainly internal use or for complementing render hooks via the redraw 1306 draw = redraw and redraw or function() end, 1307 view_str = text_input_view, 1308 caret_str = text_input_caret_str, 1309 set_str = text_input_set, 1310 update_caret = text_input_caretalign, 1311 caret_home = text_input_chome, 1312 caret_end = text_input_cend, 1313 caret_left = text_input_cleft, 1314 caret_right = text_input_cright, 1315 erase = text_input_cerase, 1316 delete = text_input_cdel, 1317 clear = text_input_clear, 1318 1319 undo = text_input_undo, 1320 input = text_input_table, 1321 } or ctx; 1322 1323 local bindings = { 1324 k_left = "LEFT", 1325 k_right = "RIGHT", 1326 k_home = "HOME", 1327 k_end = "END", 1328 k_delete = "DELETE", 1329 k_erase = "ERASE" 1330 }; 1331 1332 local flut = { 1333 k_left = text_input_cleft, 1334 k_right = text_input_cright, 1335 k_home = text_input_chome, 1336 k_end = text_input_cend, 1337 k_delete = text_input_cdelete, 1338 k_erase = text_input_cerase 1339 }; 1340 1341-- overlay any provided keybindings 1342 if (opts.bindings) then 1343 for k,v in pairs(opts.bindings) do 1344 if bindings[k] then 1345 bindings[k] = v; 1346 end 1347 end 1348 end 1349 1350-- and build the real lut 1351 ctx.bindings = {}; 1352 for k,v in pairs(bindings) do 1353 ctx.bindings[v] = flut[k]; 1354 end 1355 1356 ctx:input(iotbl, sym); 1357 return ctx; 1358end 1359 1360function gen_valid_float(lb, ub) 1361 return gen_valid_num(lb, ub); 1362end 1363 1364function merge_dispatch(m1, m2) 1365 local kt = {}; 1366 local res = {}; 1367 if (m1 == nil) then 1368 return m2; 1369 end 1370 if (m2 == nil) then 1371 return m1; 1372 end 1373 for k,v in pairs(m1) do 1374 res[k] = v; 1375 end 1376 for k,v in pairs(m2) do 1377 res[k] = v; 1378 end 1379 return res; 1380end 1381 1382function shared_valid_str(inv) 1383 return type(inv) == "string" and #inv > 0; 1384end 1385 1386function shared_valid01_float(inv) 1387 if (string.len(inv) == 0) then 1388 return true; 1389 end 1390 1391 local val = tonumber(inv); 1392 return val and (val >= 0.0 and val <= 1.0) or false; 1393end 1394 1395-- validator returns a function that checks if [val] is tolerated or not, 1396-- but also ranging values to allow other components to provide a selection UI 1397function gen_valid_num(lb, ub, step) 1398 local range = ub - lb; 1399 local step_sz = step ~= nil and step or range * 0.01; 1400 1401 return function(val) 1402 if (not val) then 1403 warning("validator activated with missing val"); 1404 return false, lb, ub, step_sz; 1405 end 1406 1407 if (string.len(val) == 0) then 1408 return false, lb, ub, step_sz; 1409 end 1410 local num = tonumber(val); 1411 if (num == nil) then 1412 return false, lb, ub, step_sz; 1413 end 1414 return not(num < lb or num > ub), lb, ub, step_sz; 1415 end 1416end 1417 1418local widgets = {}; 1419 1420function suppl_flip_handler(key) 1421 return function(ctx, val) 1422 if (val == LBL_FLIP) then 1423 gconfig_set(key, not gconfig_get(key)); 1424 else 1425 gconfig_set(key, val == LBL_YES); 1426 end 1427 end 1428end 1429 1430function suppl_scan_tools() 1431 local list = glob_resource("tools/*.lua", APPL_RESOURCE); 1432 for k,v in ipairs(list) do 1433 local res, msg = system_load("tools/" .. v, false); 1434 if (not res) then 1435 warning(string.format("couldn't parse tool: %s", v)); 1436 else 1437 local okstate, msg = pcall(res); 1438 if (not okstate) then 1439 warning(string.format("runtime error loading tool: %s - %s", v, msg)); 1440 end 1441 end 1442 end 1443end 1444 1445function suppl_chain_callback(tbl, field, new) 1446 local old = tbl[field]; 1447 tbl[field] = function(...) 1448 if (new) then 1449 new(...); 1450 end 1451 if (old) then 1452 tbl[field] = old; 1453 old(...); 1454 end 1455 end 1456end 1457 1458function suppl_scan_widgets() 1459 local res = glob_resource("widgets/*.lua", APPL_RESOURCE); 1460 for k,v in ipairs(res) do 1461 local res = system_load("widgets/" .. v, false); 1462 if (res) then 1463 local ok, wtbl = pcall(res); 1464-- would be a much needed feature to have a compact and elegant 1465-- way of specifying a stronger contract on fields and types in 1466-- place like this. 1467 if (ok and wtbl and wtbl.name and type(wtbl.name) == "string" and 1468 string.len(wtbl.name) > 0 and wtbl.paths and 1469 type(wtbl.paths) == "table") then 1470 widgets[wtbl.name] = wtbl; 1471 else 1472 warning("widget " .. v .. " failed to load"); 1473 end 1474 else 1475 warning("widget " .. v .. "f failed to parse"); 1476 end 1477 end 1478end 1479 1480-- 1481-- used to find and activate support widgets and tie to the set [ctx]:tbl, 1482-- [anchor]:vid will be used for deletion (and should be 0,0 world-space) 1483-- [ident]:str matches path/special:function to match against widget 1484-- paths and [reserved]:num is the number of center- used pixels to avoid. 1485-- 1486local widget_destr = {}; 1487function suppl_widget_path(ctx, anchor, ident, barh) 1488 local match = {}; 1489 local fi = 0; 1490 1491 for k,v in pairs(widget_destr) do 1492 k:destroy(); 1493 end 1494 widget_destr = {}; 1495 1496 local props = image_surface_resolve_properties(anchor); 1497 local y1 = props.y; 1498 local y2 = props.y + props.height; 1499 local ad = active_display(); 1500 local th = math.ceil(gconfig_get("lbar_sz") * active_display().scalef); 1501 local rh = y1 - th; 1502 1503-- sweep all widgets and check their 'paths' table for a path 1504-- or dynamic eval function and compare to the supplied ident 1505 for k,v in pairs(widgets) do 1506 for i,j in ipairs(v.paths) do 1507 local ident_tag; 1508 if (type(j) == "function") then 1509 ident_tag = j(v, ident); 1510 end 1511 1512-- if we actually find a match, probe the widget for how many 1513-- groups of the maximum slot- height that is needed to present 1514 if ((type(j) == "string" and j == ident) or ident_tag) then 1515 local nc = v.probe and v:probe(rh, ident_tag) or 1; 1516 1517-- and if there is a number of groups returned, mark those in the 1518-- tracking table (for later deallocation) and add to the set of 1519-- groups to layout 1520 if (nc > 0) then 1521 widget_destr[v] = true; 1522 for n=1,nc do 1523 table.insert(match, {v, n}); 1524 end 1525 end 1526 end 1527 end 1528 end 1529 1530-- abort if there were no widgets that wanted to present a group, 1531-- otherwise start allocating visual resources for the groups and 1532-- proceed to layout 1533 local nm = #match; 1534 if (nm == 0) then 1535 return; 1536 end 1537 1538 local pad = 00; 1539 1540-- create anchors linked to background for automatic deletion, as they 1541-- are used for clipping, distribute in a fair way between top and bottom 1542-- but with special treatment for floating widgets 1543 local start = fi+1; 1544 local ctr = 0; 1545 1546-- the layouting algorithm here is a bit clunky. The algorithms evolved 1547-- from the advfloat autolayouter should really be generalized into a 1548-- helper script and simply be used here as well. 1549 if (nm - fi > 0) then 1550 local ndiv = (#match - fi) / 2; 1551 local cellw = ndiv > 1 and (ad.width - pad - pad) / ndiv or ad.width; 1552 local cx = pad; 1553 while start <= nm do 1554 ctr = ctr + 1; 1555 local anch = null_surface(cellw, rh); 1556 link_image(anch, anchor); 1557 local dy = 0; 1558 1559-- only account for the helper unless the caller explicitly set a height 1560 if (gconfig_get("menu_helper") and not barh and ctr % 2 == 1) then 1561 dy = th; 1562 end 1563 1564 blend_image(anch, 1.0, gconfig_get("animation") * 0.5, INTERP_SINE); 1565 image_inherit_order(anch, true); 1566 image_mask_set(anch, MASK_UNPICKABLE); 1567 local w, h = match[start][1]:show(anch, match[start][2], rh); 1568 start = start + 1; 1569 1570-- position and slide only if we get a hint on dimensions consumed 1571 if (w and h) then 1572 if (ctr % 2 == 1) then 1573 move_image(anch, cx, -h - dy); 1574 else 1575 move_image(anch, cx, props.height + dy + th); 1576 cx = cx + cellw; 1577 end 1578 else 1579 delete_image(anch); 1580 end 1581 1582 end 1583 end 1584end 1585 1586-- register a prefix_debug_listener function to attach/define a 1587-- new debug listener, and return a local queue function to append 1588-- to the log without exposing the table in the global namespace 1589local prefixes = { 1590}; 1591function suppl_add_logfn(prefix) 1592 if (prefixes[prefix]) then 1593 return prefixes[prefix][1], prefixes[prefix][2]; 1594 end 1595 1596-- nest one level so we can pull the scope down with us 1597 local logscope = 1598 function() 1599 local queue = {}; 1600 local handler = nil; 1601 1602 prefixes[prefix] = 1603 { 1604 function(msg) 1605 local exp_msg = CLOCK .. ":" .. msg .. "\n"; 1606 if (handler) then 1607 handler(exp_msg); 1608 else 1609 table.insert(queue, exp_msg); 1610 if (#queue > 200) then 1611 table.remove(queue, 1); 1612 end 1613 end 1614 end, 1615-- return a formatter as well so we can nop-out logging when not needed 1616 string.format, 1617 }; 1618 1619-- and register a global function that can be used to set the singleton 1620-- that the queue flush to or messages gets immediately forwarded to 1621 _G[prefix .. "_debug_listener"] = 1622 function(newh) 1623 if (newh and type(newh) == "function") then 1624 handler = newh; 1625 for i,v in ipairs(queue) do 1626 newh(v); 1627 end 1628 else 1629 handler = nil; 1630 end 1631 queue = {}; 1632 end 1633 end 1634 1635 logscope(); 1636 return prefixes[prefix][1], prefixes[prefix][2]; 1637end 1638