Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
--
-- This tool manages external connection points that route to specific
-- UI elements, such as the statusbar icon groups.
--
local cp_handler, gen_cp_menu, open_cp;
local cps = {};
local clients = {};
local last_click = nil;
local log = suppl_add_logfn("tools");
-- external connection point that (by defaults) accepts a primary ICON segment,
-- treates anything spawned from that as a 'popup' and only in
-- response to input on the segment itself, (media | application | tui)
-- all receiving basic fonts, fixed height, restricted width.
menus_register("global", "settings/statusbar/buttons",
{
name = "external",
label = "External",
submenu = true,
kind = "action",
eval = function()
for k,v in pairs(cps) do
return true;
end
end,
handler = function() return gen_cp_menu(); end
}
);
local function register_entry(dir)
menus_register("global", "settings/statusbar/buttons/" .. dir,
{
name = "add_external",
label = "Add External",
kind = "value",
description = "Open an external connection-point for docking icons into the tray",
hint = "(a-Z_0-9)",
validator = strict_fname_valid,
handler = function(ctx, val)
open_cp(val, dir, true);
end
});
end
register_entry("left");
register_entry("right");
register_entry("center");
local function gen_submenu_for_cp(v)
return {
{
name = "close",
label = "Close",
kind = "action",
description = "Close the connection point but keep existing clients around",
handler = function()
table.remove_match(cps, v);
if (valid_vid(v.vid)) then
delete_image(v.vid);
end
end,
},
{
name = "size",
label = "Size",
kind = "value",
description = "Set the maximum size (% of base dimension) allocated",
initial = tostring(v.size_pct) .. "%",
hint = ("5..75"),
validator = gen_valid_num(5, 75),
handler = function(ctx, val)
v.size_pct = tonumber(val);
end
},
{
name = "kill",
label = "Kill",
kind = "action",
description = "Close the connection point and all existing clients",
handler = function()
table.remove_match(cps, v);
delete_image(v.vid);
for _,v in pairs(clients) do
if (v.parent == v) then
v:destroy();
end
end
end,
}
-- other options here: limit connections, specify destination display,
-- allow specific behavior, allow handover to client spawn, font and
-- color configuration, ...
};
end
gen_cp_menu = function()
local res = {};
for k,v in pairs(cps) do
table.insert(res, {
name = k,
label = k,
kind = "action",
submenu = true,
description = "Control connection point behaviour",
handler = gen_submenu_for_cp(v)
});
end
return res;
end
local function setup_default(name, new_vid, dir)
cps[name] = {
vid = new_vid,
group = dir,
size_pct = 10,
allowed_types = {"icon", "tui", "terminal"},
};
end
local function send_fonts(dst, term)
-- send the default UI font config
if term then
main = gconfig_get("term_font");
fallback = gconfig_get("font_fb");
sz = gconfig_get("term_font_sz");
hint = gconfig_get("term_font_hint");
else
main = gconfig_get("font_def");
fallback = gconfig_get("font_fb");
sz = gconfig_get("font_sz");
hint = gconfig_get("font_hint");
end
target_fonthint(dst, main, sz * FONT_PT_SZ, hint, false);
target_fonthint(dst, fallback, sz * FONT_PT_SZ, hint, true);
end
local function size_for_ctx(ctx, bar)
-- Treat it like the message area (no border or any of that jazz),
-- we allow the button on any group.
local base_w = bar.height > bar.width and bar.width or bar.height;
local base_h = base_w;
-- for non-icon types we use a preset percentage
if (ctx.type ~= "icon") then
if bar.height > bar.width then
base_h = ctx.cp.size_pct * 0.01 * bar.height;
else
base_w = ctx.cp.size_pct * 0.01 * bar.width;
end
end
return base_w, base_h;
end
open_cp = function(name, dir, init)
-- did we allocate successfully?
local new_vid = target_alloc(name,
function(source, status)
return cp_handler(cps[name], source, status)
end
);
target_flags(new_vid, TARGET_BLOCKADOPT);
if (not valid_vid(new_vid)) then
-- otherwise, schedule a fire-once timer to try again until a certain time
log("name=traybtn:kind=error:message=could not listen on " .. name);
return
end
image_tracetag(new_vid, "adopt_destroy");
log("name=traybtn:kind=listening:source="
.. tostring(new_vid) .. ":cp=" .. name);
if (init) then
setup_default(name, new_vid, dir);
end
end
local handlers = {
};
handlers["registered"] =
function(ctx, bar, source, status)
if (not table.find_i(ctx.cp.allowed_types, status.segkind)) then
log("name=traybtn:kind=error:source=" .. tostring(source) .. ":message="
.."registered with bad segkind:id=" .. status.segkind);
ctx:destroy();
return;
end
ctx.type = status.segkind;
log("name=traybtn:kind=status:source="
.. tostring(source) .. ":message=registered");
end
handlers["connected"] =
function(ctx, bar, source, status)
ctx.key = status.key;
-- and re-open (do from this context so the backend knows that it can just
-- re-use the same socket immediately, there's tracking for this internally)
log("name=traybtn:kind=connected:key="
.. status.key .. ":source=" .. tostring(source));
open_cp(status.key, ctx.group);
end
handlers["preroll"] =
function(ctx, bar, source, status)
local dtbl = {};
for k,v in pairs(bar.owner.disptbl) do
dtbl[k] = v;
end
-- so this should really be updated if the bar size change, no good
-- hooks for that at the moment, possibly the relayout on the bar
local w, h = size_for_ctx(ctx, bar);
target_flags(source, TARGET_LIMITSIZE, w, h);
target_displayhint(source, w, h, 0, dtbl);
-- and some 'icons' is actually text, like a date / user / ...
send_fonts(source, ctx.type == "terminal" or ctx.type == "tui");
end
handlers["terminated"] =
function(ctx, bar, source, status)
ctx:destroy();
log("name=traybtn:kind=status:message=terminated:id=" .. tostring(source));
end
local function button_mh(ctx, source)
-- want to track this so we don't give the grab to someone else
last_click = ctx;
return {
name = "tray_mouseh",
-- send both press and release
click = function()
log("name=traybtn:kind=clicked:source="..tostring(source));
target_input(source, {
kind = "digital",
mouse = true,
gesture = true,
active = true,
label = "click"
});
target_input(source, {
kind = "digital",
mouse = true,
active = false
});
end,
};
end
handlers["labelhint"] =
function(ctx, bar, source, status)
log("name=traybtn:kind=labelhint:label=" .. status.label);
-- append to the context itself, let the menu expose ways of triggering it
-- if we allow 'global mapping', register that one
end
handlers["viewport"] =
function(ctx, bar, source, status)
if (status.anchor_w > 0) then
target_displayhint(source, status.anchor_w, status.anchor_h);
end
end
handlers["resized"] =
function(ctx, bar, source, status)
-- resize after initial size set, should be permitted or ignored?
-- allow for now and force a relayout
if (ctx.button) then
log(string.format("name=traybtn:kind=resized:source=%d:"
.. "width=%d:height=%d", source, status.width, status.height));
resize_image(source, status.width, status.height);
ctx.button:update(source);
bar:relayout();
return;
end
local base_w, base_h = size_for_ctx(ctx, bar);
resize_image(source, base_w, base_h);
local resolve = function()
local surf = null_surface(base_w, base_h);
if (valid_vid(source) and valid_vid(surf)) then
image_sharestorage(source, surf);
end
return surf;
end
local pad = gconfig_get("sbar_tpad") * active_display().scalef;
ctx.button = bar:add_button(ctx.group, "sbar_msg_bg", "sbar_msg_text",
resolve, pad,
-- set the scale- function resolver to nothing as we are all external
function()
return 1;
end,
-- and the suggested icon size is on the button base
base_w, base_h,
-- lastly forward mouse handler with the proper context
button_mh(ctx, source), {}
);
ctx.button.owner = ctx;
log(string.format("name=traybtn:kind=resized-first:source=%d"
.. ":base_w=%d:base_h=%d:width=%d:height=%d", source,
0, base_h, status.width, status.height)
);
ctx.button:update(source);
bar:relayout();
end
local function setup_grab(ctx, bar, source, status)
-- the anchor here is simply a hidden surface we play at the relative order
-- of the wm components, and it will absorb mouse motion until we release it
local anchor = null_surface(bar.owner.width, bar.owner.height);
show_image(anchor);
if not (valid_vid(anchor)) then
delete_image(source);
return;
end
image_tracetag(anchor, "traybtn_mouse_anchor");
link_image(anchor, source);
image_inherit_order(anchor, true);
order_image(anchor, -1);
image_mask_clear(anchor, MASK_POSITION);
-- save input handler until we release the grab
local old_ioh = _G[APPLID .. "_input"];
-- path the input function so we get the keyboard events and just forward
-- the old so we keep device discovery and translation.
_G[APPLID .. "_input"] = function(iotbl)
if (not iotbl.translated) then
return old_ioh(iotbl);
end
-- and for the translated, we use the SYMTABLE global (if present)
local sym, outsym;
if (SYMTABLE) then
sym, outsym = SYMTABLE:patch(iotbl);
end
-- special meaning for ESCAPE: delete and drop, otherwise: forward!
if (not valid_vid(
source, TYPE_FRAMESERVER) or (sym and sym == "ESCAPE")) then
ctx:destroy();
-- or just send to the client
else
target_input(source, iotbl);
end
end
-- need to block normal input event handling as there are so many
-- interactions that are likely to break things (timers, external
-- IPC, ...)
dispatch_symbol_lock();
-- mouse handler for the grab- surface and for the popup itself,
-- forward motion and buttons to the source, destroy on grab-click
local mouseh = {
own = function(mctx, vid)
return vid == anchor or vid == source;
end,
motion = function(ctx, vid, x, y, rx, ry)
if vid ~= source then
return
end
local aprops = image_surface_resolve(source);
local lx = x - aprops.x;
local ly = y - aprops.y;
target_input(source, {
kind = "analog", mouse = true, devid = 0, subid = 0,
samples = {lx, rx}
});
target_input(source, {
kind = "analog", mouse = true, devid = 0, subid = 1,
samples = {ly, ry}
});
end,
button = function(ctx, vid, ind, pressed, x, y)
if vid == source then
target_input(source, { active = pressed,
devid = 0, subid = ind, kind = "digital", mouse = true});
end
end,
click = function(mctx, vid)
if vid == anchor then
ctx:destroy();
end
end,
name = "extbtn_grab_anchor"
};
mouse_addlistener(mouseh, {"click", "motion", "button"});
-- add our handler
ctx.destroy = function()
_G[APPLID .. "_input"] = old_ioh;
dispatch_symbol_unlock(true);
ctx.destroy = nil;
local time = gconfig_get("animation");
if (valid_vid(source)) then
expire_image(source, time);
blend_image(source, 0.0, time);
end
mouse_droplistener(mouseh);
end
-- use the source properties and the ctx.button vid to figure out
-- position of the popup itself
end
local function reposition(source, bar, btn)
local ap = gconfig_get("sbar_pos");
local sprop = image_surface_resolve(source);
local bprop = image_surface_properties(btn.bg);
bprop.height = bprop.height + bar.owner.scalef * gconfig_get("sbar_popup_pad");
-- align to the lower left unless overflow, then end at lower right
if (ap == "top") then
move_image(source, 0, bprop.height);
else
move_image(source, 0, -sprop.height);
end
log(string.format("%f, %f", sprop.x + sprop.width, bar.owner.width));
if (sprop.x + sprop.width > bar.owner.width) then
nudge_image(source, -sprop.width + bprop.width, 0);
end
end
local function popup_handler(ctx, bar, source, status)
log("name=traybtn:kind=popup_handler:event="
.. status.kind .. ":source=" .. tostring(source));
if (status.kind == "resized") then
if (last_click ~= ctx.parent_ctx) then
-- If the client was slow to wake up and something else was clicked in
-- between, we can / should just kill it off.
log("name=traybtn:kind=error:source="
.. tostring(source) .. ":message=don't own popup slot");
if (ctx.destroy) then
ctx:destroy();
else
delete_image(source);
end
return;
end
-- link to the button anchor itself for positioning
show_image(source);
rendertarget_attach(bar.owner.rtgt_id, source, RENDERTARGET_DETACH);
link_image(source, ctx.parent_ctx.button.bg);
image_inherit_order(source, true);
order_image(source, 1);
resize_image(source, status.width, status.height);
-- set source as anchor attribute so shadow gets attached right
ctx.anchor = source;
suppl_region_shadow(ctx, status.width, status.height);
-- then resolve display- space coordinates and adjust versus screen edge
-- this has quite a few edge cases due to possible 'overflow' and to cover
-- both bar orientations
reposition(source, bar, ctx.parent_ctx.button);
if (not ctx.destroy) then
setup_grab(ctx, bar, source, status);
end
elseif (status.kind == "preroll") then
-- send the current display density
target_displayhint(source, 0, 0, 0, bar.owner.disptbl);
send_fonts(source);
elseif (status.kind == "terminated") then
if (last_click == ctx) then
last_click = nil;
end
if (ctx.destroy) then
ctx:destroy();
else
delete_image(source);
end
end
end
handlers["segment_request"] =
function(ctx, bar, source, status)
log("name=traybtn:kind=segreq:segkind=" .. status.segkind);
local base = bar.height > bar.width and bar.width or bar.height;
-- Hint the dimensions based on the display the bar is attached to, grab
-- at most, a third of each axis. The uiprim_bar does not actually track
-- this, but tiler.lua tags the bar.
local owner = bar.owner;
local outw = VRESW * 0.3;
local outh = VRESH * 0.3;
if (owner) then
outw = owner.width * 0.3;
outh = owner.height * 0.3;
else
end
local popup_ctx = {
parent = source,
parent_ctx = ctx,
outw = outw,
outh = outh
};
local vid = accept_target(source, outw, outh,
function(source, status)
return popup_handler(popup_ctx, bar, source, status);
end
);
-- tag the image in a way that it will be destroyed when trying to recover
-- from a crash, as the parent / producer (the icon) should be able to spawn
-- a new one instead.
if (valid_vid(vid)) then
image_tracetag(vid, "adopt_destroy");
end
end
local function free_tray_button(ctx)
if (ctx.button) then
ctx.button:destroy();
end
if (valid_vid(ctx.vid)) then
delete_image(ctx.vid);
end
clients[ctx.vid] = nil;
end
cp_handler = function(ctx, source, status)
local bar = ctx.statusbar and ctx.statusbar or active_display().statusbar;
if (not bar) then
delete_image(source);
clients[source] = nil;
return;
end
if (not clients[source]) then
clients[source] = {
cp = ctx,
vid = source,
group = ctx.group,
destroy = free_tray_button
};
image_tracetag(source, "adopt_destroy");
end
if (handlers[status.kind]) then
return handlers[status.kind](clients[source], bar, source, status);
else
log("name=traybtn:kind=unhandled:message=" ..
status.kind .. ":source=" .. tostring(source));
end
end