tests: Add a sequence template.

This template allows to display a sequence of events for the clients,
tags and screens. Currently, it is hard to display images where the
state of an object is more complex than "here how it was before" and
"here how it is now". With this template, it is possible to have a
timeline of events from the initial states to the final states.

Now, as the line count shows, this isn't small. It is in fact an
enormous template. Worst still, this commit is the first *half* of
it. The second half adds the ability to `print()`, display
inline code and support mouse and keyboard events. The code also isn't
world class. Maintaining this template might be non-trivial in the
long run. I am fully aware of those issues. On the other hand, there
is ~100 places where this will be used once the entire
"new rule library" project is completed. This will bring the ~1.2k
line of code to ~12 lines per consumer. From that point of view,
it makes a lot more sense to merge this given how useful it is
at explaining changes within the "core objects".

It is also important to keep in mind that there is currently very
little or no documentation (beside the mandatory one-liner summary)
for these concepts. Those are the most important aspects of AwesomeWM
API and they are the least documented. This is just wrong.
This commit is contained in:
Emmanuel Lepage Vallee 2019-09-29 18:42:32 -04:00
parent 75c281e3af
commit 12f28305a0
1 changed files with 789 additions and 0 deletions

View File

@ -0,0 +1,789 @@
-- This template provides a timeline like display for tag changes.
-- Basically a taglist with some pretty dots on the side. Note that the way it
-- copy tags is fragile and probably not really supportable.
local file_path, image_path = ...
require("_common_template")(...)
local capi = {client = client, screen = screen}
local Pango = require("lgi").Pango
local PangoCairo = require("lgi").PangoCairo
require("awful.screen")
require("awful.tag")
local floating_l = require("awful.layout.suit.floating")
local taglist = require("awful.widget.taglist")
local gtable = require("gears.table")
local shape = require("gears.shape")
local color = require("gears.color")
local wibox = require("wibox")
local beautiful = require("beautiful")
local bar_size, radius = 18, 2
local screen_scale_factor = 5
local history = {}
local module = {}
-- Draw a mouse cursor at [x,y]
local function draw_mouse(cr, x, y)
cr:set_source_rgb(1, 0, 0)
cr:move_to(x, y)
cr:rel_line_to( 0, 10)
cr:rel_line_to( 3, -2)
cr:rel_line_to( 3, 4)
cr:rel_line_to( 2, 0)
cr:rel_line_to(-3, -4)
cr:rel_line_to( 4, 0)
cr:close_path()
cr:fill()
end
-- Instead of returning the maximum size, return the preferred one.
local function fixed_fit2(self, context, orig_width, orig_height)
local width, height = orig_width, orig_height
local used_in_dir, used_max = 0, 0
for _, v in pairs(self._private.widgets) do
local w, h = wibox.widget.base.fit_widget(self, context, v, width, height)
-- Catch bugs before they crash Chrome (Firefox handles this better)
assert(w < 5000)
assert(h < 5000)
local in_dir, max
if self._private.dir == "y" then
max, in_dir = w, h
height = height - in_dir
else
in_dir, max = w, h
width = width - in_dir
end
if max > used_max then
used_max = max
end
used_in_dir = used_in_dir + in_dir
end
local spacing = self._private.spacing * (#self._private.widgets-1)
-- Catch bugs before they crash Chrome (Firefox handles this better)
assert(used_max < 9000)
assert(used_in_dir < 9000)
if self._private.dir == "y" then
return used_max, used_in_dir + spacing
end
return used_in_dir + spacing, used_max
end
-- Imported from the Collision module.
local function draw_lines()
local ret = wibox.widget.base.make_widget()
function ret:fit()
local pager_w = math.max(root.size()/screen_scale_factor, 60)
--FIXME support multiple screens.
local w = (#screen[1].tags * pager_w)
+ ((#screen[1].tags - 1)*5)
return w, self.widget_pos and #self.widget_pos*6 or 30
end
function ret:draw(_, cr, _, h)
if (not self.widget_pos) or (not self.pager_pos) then return end
cr:set_line_width(1)
cr:set_source_rgba(0,0,0,0.3)
local count = #self.widget_pos
for k, t in ipairs(self.widget_pos) do
local point1 = {x = t.widget_x, y = 0, width = t.widget_width, height = 1}
local point2 = {x = t.pager_x, y = 0, width = t.pager_width, height = h}
assert(point1.x and point1.width)
assert(point2.x and point2.width)
local dx = (point1.x == 0 and radius or 0) + (point1.width and point1.width/2 or 0)
cr:move_to(bar_size+dx+point1.x, point1.y+2*radius)
cr:line_to(bar_size+dx+point1.x, point2.y+(count-k)*((h-2*radius)/count)+2*radius)
cr:line_to(point2.x+point2.width/2, point2.y+(count-k)*((h-2*radius)/count)+2*radius)
cr:line_to(point2.x+point2.width/2, point2.y+point2.height)
cr:stroke()
cr:arc(bar_size+dx+point1.x, point1.y+2*radius, radius, 0, 2*math.pi)
cr:fill()
cr:arc(point2.x+point2.width/2, point2.y+point2.height-radius, radius, 0, 2*math.pi)
cr:fill()
end
end
return ret
end
local function gen_vertical_line(args)
args = args or {}
local w = wibox.widget.base.make_widget()
function w:draw(_, cr, w2, h)
cr:set_source_rgba(0,0,0,0.5)
if args.begin then
cr:rectangle(w2/2-0.5, h/2, 1, h/2)
elseif args.finish then
cr:rectangle(w2/2-0.5, 0, 1, h/2)
else
cr:rectangle(w2/2-0.5, 0, 1, h)
end
cr:fill()
if args.dot then
cr:arc(w2/2, args.center and h/2 or w2/2 ,bar_size/4, 0, 2*math.pi)
cr:set_source_rgb(1,1,1)
cr:fill_preserve()
cr:set_source_rgba(0,0,0,0.5)
cr:stroke()
end
end
function w:fit()
return bar_size, bar_size
end
return w
end
local function gen_taglist_layout_proxy(tags, w2, name)
local l = wibox.layout.fixed.horizontal()
l.fit = fixed_fit2
local layout = l.layout
l.layout = function(self,context, width, height)
local ret = layout(self,context, width, height)
for k, v in ipairs(ret) do
tags[k][name.."_x" ] = v._matrix.x0
tags[k][name.."_width"] = v._width
end
if w2 then
w2[name.."_pos"] = tags
if not w2[name.."_configured"] then
rawset(w2, name.."_configured", true)
w2:emit_signal("widget::redraw_needed")
w2:emit_signal("widget::layout_changed")
end
end
return ret
end
return l
end
local function gen_fake_taglist_wibar(tags, w2)
local layout = gen_taglist_layout_proxy(tags, w2, "widget")
local w = wibox.widget {
{
{
forced_height = bar_size,
forced_width = bar_size,
image = beautiful.awesome_icon,
widget = wibox.widget.imagebox,
},
taglist {
forced_height = 14,
forced_width = 300,
layout = layout,
screen = screen[1],
filter = taglist.filter.all,
source = function() return tags end,
},
fit = fixed_fit2,
layout = wibox.layout.fixed.horizontal,
},
bg = beautiful.bg_normal,
widget = wibox.container.background
}
-- Make sure it nevers goes unbounded by accident.
local w3, h3 = w:fit({dpi=96}, 9999, 9999)
assert(w3 < 5000 and h3 < 5000)
return w
end
local function gen_cls(c,results)
local ret = setmetatable({},{__index = function(_, i)
local ret2 = c[i]
if type(ret2) == "function" then
if i == "geometry" then
return function(_, val)
if val then
c:geometry(gtable.crush(c:geometry(), val))
-- Make a copy as the original will be changed
results[c] = gtable.clone(c:geometry())
end
return c:geometry()
end
else
return function(_,...) return ret2(c,...) end
end
end
return ret2
end})
return ret
end
local function fake_arrange(tag)
local cls,results,flt = {},setmetatable({},{__mode="k"}),{}
local _, l = tag.screen, tag.layout
local focus, focus_wrap = capi.client.focus, nil
for _ ,c in ipairs (tag:clients()) do
-- Handle floating client separately
if not c.minimized then
local floating = c.floating
if (not floating) and (l ~= floating_l) then
cls[#cls+1] = gen_cls(c,results)
if c == focus then
focus_wrap = cls[#cls]
end
else
flt[#flt+1] = gtable.clone(c:geometry())
flt[#flt].c = c
end
end
end
-- The magnifier layout require a focussed client
-- there wont be any as that layout is not selected
-- take one at random or (TODO) use stack data
if not focus_wrap then
focus_wrap = cls[1]
end
local param = {
tag = tag,
screen = 1,
clients = cls,
focus = focus_wrap,
geometries = setmetatable({}, {__mode = "k"}),
workarea = tag.screen.workarea,
useless_gap = tag.gaps or 4,
apply_size_hints = false,
}
l.arrange(param)
local ret = {}
for _, geo_src in ipairs {param.geometries, flt } do
for c, geo in pairs(geo_src) do
geo.c = geo.c or c
table.insert(ret, geo)
end
end
return ret
end
local function gen_fake_clients(tag, args)
local pager = wibox.widget.base.make_widget()
function pager:fit()
return 60, 48
end
if not tag then return end
local sgeo = tag.screen.geometry
local show_name = args.display_client_name or args.display_label
function pager:draw(_, cr, w, h)
if not tag.client_geo then return end
for _, geom in ipairs(tag.client_geo) do
local x = (geom.x*w)/sgeo.width
local y = (geom.y*h)/sgeo.height
local width = (geom.width*w)/sgeo.width
local height = (geom.height*h)/sgeo.height
cr:set_source(color(geom.c.color or beautiful.bg_normal))
cr:rectangle(x,y,width,height)
cr:fill_preserve()
cr:set_source(color(geom.c.border_color or beautiful.border_color))
cr:stroke()
if show_name and type(geom.c) == "table" and geom.c.name then
cr:set_source_rgb(0, 0, 0)
cr:move_to(x + 2, y + height - 2)
cr:show_text(geom.c.name)
end
end
-- Draw the screen outline.
cr:set_source(color("#00000044"))
cr:set_line_width(1.5)
cr:set_dash({10,4},1)
cr:rectangle(0, 0, w, h)
cr:stroke()
end
return pager
end
local function gen_fake_pager_widget(tags, w2, args)
local layout = gen_taglist_layout_proxy(tags, w2, "pager")
layout.spacing = 10
for _, t in ipairs(tags) do
layout:add(wibox.widget {
gen_fake_clients(t, args),
widget = wibox.container.background
})
end
return layout
end
local function wrap_timeline(w, dot)
return wibox.widget {
gen_vertical_line { dot = dot or false},
{
w,
top = dot and 5 or 0,
bottom = dot and 5 or 0,
left = 0,
widget = wibox.container.margin
},
fit = fixed_fit2,
layout = wibox.layout.fixed.horizontal
}
end
local function gen_label(text)
return wibox.widget {
gen_vertical_line {
dot = true,
begin = text == "Begin",
finish = text == "End",
center = true,
},
{
{
{
{
markup = "<span size='smaller'><b>"..text.."</b> </span>",
forced_width = 50,
widget = wibox.widget.textbox
},
top = 2,
bottom = 2,
right = 5,
left = 10,
widget = wibox.container.margin
},
shape = shape.rectangular_tag,
border_width = 2,
border_color = beautiful.border_color,
bg = beautiful.bg_normal,
widget = wibox.container.background
},
top = 10,
bottom = 10,
widget = wibox.container.margin
},
fit = fixed_fit2,
layout = wibox.layout.fixed.horizontal
}
end
local function draw_info(s, cr, factor)
cr:set_source_rgba(0, 0, 0, 0.4)
local pctx = PangoCairo.font_map_get_default():create_context()
local playout = Pango.Layout.new(pctx)
local pdesc = Pango.FontDescription()
pdesc:set_absolute_size(11 * Pango.SCALE)
playout:set_font_description(pdesc)
local rows = {
"primary", "index", "geometry", "dpi", "dpi range", "outputs:"
}
local dpi_range = s.minimum_dpi and s.preferred_dpi and s.maximum_dpi
and (s.minimum_dpi.."-"..s.preferred_dpi.."-"..s.maximum_dpi)
or s.dpi.."-"..s.dpi
local values = {
s.primary and "true" or "false",
s.index,
s.x..":"..s.y.." "..s.width.."x"..s.height,
s.dpi,
dpi_range,
"",
}
for n, o in pairs(s.outputs) do
table.insert(rows, " "..n)
table.insert(values,
math.ceil(o.mm_width).."mm x "..math.ceil(o.mm_height).."mm"
)
end
local col1_width, col2_width, height = 0, 0, 0
-- Get the extents of the longest label.
for k, label in ipairs(rows) do
local attr, parsed = Pango.parse_markup(label..":", -1, 0)
playout.attributes, playout.text = attr, parsed
local _, logical = playout:get_pixel_extents()
col1_width = math.max(col1_width, logical.width+10)
attr, parsed = Pango.parse_markup(values[k], -1, 0)
playout.attributes, playout.text = attr, parsed
_, logical = playout:get_pixel_extents()
col2_width = math.max(col2_width, logical.width+10)
height = math.max(height, logical.height)
end
local dx = (s.width*factor - col1_width - col2_width - 5)/2
local dy = (s.height*factor - #values*height)/2 - height
-- Draw everything.
for k, label in ipairs(rows) do
local attr, parsed = Pango.parse_markup(label..":", -1, 0)
playout.attributes, playout.text = attr, parsed
playout:get_pixel_extents()
cr:move_to(dx, dy)
cr:show_layout(playout)
attr, parsed = Pango.parse_markup(values[k], -1, 0)
playout.attributes, playout.text = attr, parsed
local _, logical = playout:get_pixel_extents()
cr:move_to( dx+col1_width+5, dy)
cr:show_layout(playout)
dy = dy + 5 + logical.height
end
end
local function gen_ruler(h_or_v, factor, margins)
local ret = wibox.widget.base.make_widget()
function ret:fit()
local w, h
local rw, rh = root.size()
rw, rh = rw*factor, rh*factor
if h_or_v == "vertical" then
w = 1
h = rh + margins.top/2 + margins.bottom/2
else
w = rw + margins.left/2 + margins.right/2
h = 1
end
return w, h
end
function ret:draw(_, cr, w, h)
cr:set_source(color("#77000033"))
cr:set_line_width(2)
cr:set_dash({1,1},1)
cr:move_to(0, 0)
cr:line_to(w == 1 and 0 or w, h == 1 and 0 or h)
cr:stroke()
end
return ret
end
-- When multiple tags are present, only show the selected tag(s) for each screen.
local function gen_screens(l, screens, args)
local margins = {left=50, right=50, top=30, bottom=30}
local ret = wibox.layout.manual()
local sreen_copies = {}
-- Keep a copy because it can change.
local rw, rh = root.size()
-- Find the current origin.
local x0, y0 = math.huge, math.huge
for s in screen do
x0, y0 = math.min(x0, s.geometry.x), math.min(y0, s.geometry.y)
local scr_cpy = gtable.clone(s.geometry, false)
scr_cpy.outputs = gtable.clone(s.outputs, false)
scr_cpy.primary = screen.primary == s
for _, prop in ipairs {
"dpi", "index", "maximum_dpi", "minimum_dpi", "preferred_dpi" } do
scr_cpy[prop] = s[prop]
end
table.insert(sreen_copies, scr_cpy)
end
function ret:fit()
local w = margins.left+(x0+rw)/screen_scale_factor + 5 + margins.right
local h = margins.top +(y0+rh)/screen_scale_factor + 5 + margins.bottom
return w, h
end
-- Add the rulers.
for _, s in ipairs(sreen_copies) do
ret:add_at(
gen_ruler("vertical" , 1/screen_scale_factor, margins),
{x=margins.left+s.x/screen_scale_factor, y =margins.top/2}
)
ret:add_at(
gen_ruler("vertical" , 1/screen_scale_factor, margins),
{x=margins.left+s.x/screen_scale_factor+s.width/screen_scale_factor, y =margins.top/2}
)
ret:add_at(
gen_ruler("horizontal", 1/screen_scale_factor, margins),
{y=margins.top+s.y/screen_scale_factor, x =margins.left/2}
)
ret:add_at(
gen_ruler("horizontal", 1/screen_scale_factor, margins),
{y=margins.top+s.y/screen_scale_factor+s.height/screen_scale_factor, x =margins.left/2}
)
end
-- Print an outline for the screens
for k, s in ipairs(sreen_copies) do
s.widget = wibox.widget.base.make_widget()
local wb = gen_fake_taglist_wibar(screens[k].tags)
wb.forced_width = s.width/screen_scale_factor
-- The clients have an absolute geometry, transform to relative.
if screens[k].tags[1] then
for _, geo in ipairs(screens[k].tags[1].client_geo) do
geo.x = geo.x - s.x
geo.y = geo.y - s.y
end
end
local clients_w = gen_fake_clients(screens[k].tags[1], args)
local content = wibox.widget {
wb,
clients_w,
nil,
layout = wibox.layout.align.vertical,
forced_width = s.width/screen_scale_factor,
}
function s.widget:fit()
return s.width/screen_scale_factor, s.height/screen_scale_factor
end
function s.widget:draw(_, cr, w, h)
cr:set_source(color("#00000044"))
cr:set_line_width(1.5)
cr:set_dash({10,4},1)
cr:rectangle(1,1,w-2,h-2)
cr:stroke()
if args.display_label ~= false then
draw_info(s, cr, 1/screen_scale_factor)
end
end
function s.widget:after_draw_children(_, cr)
if args.display_mouse and mouse.screen.index == s.index then
local rel_x = mouse.coords().x - s.x
local rel_y = mouse.coords().y - s.y
draw_mouse(cr, rel_x/screen_scale_factor+5, rel_y/screen_scale_factor+5)
end
end
function s.widget:layout(_, width, height)
return { wibox.widget.base.place_widget_at(
content, 0, 0, width, height
) }
end
ret:add_at(s.widget, {x=margins.left+s.x/screen_scale_factor, y=margins.top+s.y/screen_scale_factor})
end
l:add(wrap_timeline(wibox.widget {
markup = "<i>Current tags:</i>",
opacity = 0.5,
widget = wibox.widget.textbox
}, true))
l:add(wrap_timeline(ret,false))
end
-- When a single screen is present, show all tags.
local function gen_noscreen(l, tags, args)
local w2 = draw_lines()
l:add(wrap_timeline(wibox.widget {
markup = "<i>Current screens:</i>",
opacity = 0.5,
widget = wibox.widget.textbox
}, true))
local wrapped_wibar = wibox.widget {
gen_fake_taglist_wibar(tags, w2),
fill_space = false,
fit = fixed_fit2,
layout = wibox.layout.fixed.horizontal
}
l:add(wrap_timeline(wrapped_wibar, false))
if #capi.client.get() > 0 or args.show_empty then
local w3, h3 = w2:fit({dpi=96}, 9999, 9999)
assert(w3 < 5000 and h3 < 5000)
l:add(wrap_timeline(w2, false))
l:add(wrap_timeline(gen_fake_pager_widget(tags, w2, args), false))
end
end
local function gen_timeline(args)
local l = wibox.layout.fixed.vertical()
l.fit = fixed_fit2
l:add(gen_label("Begin"))
for _, event in ipairs(history) do
local ret = event.callback()
if event.event == "event" then
l:add(wrap_timeline(wibox.widget {
markup = "<u><b>"..event.description.."</b></u>",
widget = wibox.widget.textbox
}, true))
elseif event.event == "tags" and #ret == 1 and not args.display_screen then
gen_noscreen(l, ret[1].tags, args)
elseif event.event == "tags" and (#ret > 1 or args.display_screen) then
gen_screens(l, ret, args)
end
end
-- Spacing.
l:add(wrap_timeline( wibox.widget {
draw = function() end,
fit = function() return 1, 10 end,
widget = wibox.widget.base.make_widget()
}))
l:add(gen_label("End"))
return l
end
local function wrap_with_arrows(widget)
local w = widget:fit({dpi=96}, 9999,9999)
local arrows = wibox.widget.base.make_widget()
function arrows:fit()
return w, 10
end
function arrows:draw(_, cr, w2, h)
cr:set_line_width(1)
cr:set_source_rgba(1, 0, 0, 0.3)
w2 = math.min(640, w2)
local x = (w2 % 24) / 2
while x + 15 < w2 do
cr:move_to(x+2 , 0 )
cr:line_to(x+10 , h-1)
cr:line_to(x+20 , 0 )
cr:stroke()
x = x + 24
end
end
assert(w < 5000)
return wibox.widget {
widget,
{
draw = function() end,
fit = function() return 1, 10 end,
widget = wibox.widget.base.make_widget()
},
{
markup = "<span color='#ff000055'>Code for this sequence</span>",
align = "center",
foced_width = w,
widget = wibox.widget.textbox,
},
arrows,
forced_width = w,
fit = fixed_fit2,
layout = wibox.layout.fixed.vertical
}
end
function module.display_tags()
local function do_it()
local ret = {}
for s in screen do
local st = {}
for _, t in ipairs(s.tags) do
-- Copy just enough for the taglist to work.
table.insert(st, {
name = t.name,
selected = t.selected,
icon = t.icon,
screen = t.screen,
data = t.data,
clients = t.clients,
layout = t.layout,
master_width_factor = t.master_width_factor,
client_geo = fake_arrange(t),
})
assert(#st[#st].client_geo == #t:clients())
end
table.insert(ret, {tags=st})
end
return ret
end
table.insert(history, {event="tags", callback = do_it})
end
function module.add_event(description, callback)
assert(description and callback)
table.insert(history, {
event = "event",
description = description,
callback = callback
})
end
function module.execute(args)
local widget = gen_timeline(args or {})
require("gears.timer").run_delayed_calls_now()
require("gears.timer").run_delayed_calls_now()
require("gears.timer").run_delayed_calls_now()
if (not args) or args.show_code_pointer ~= false then
widget = wrap_with_arrows(widget)
end
local w, h = widget:fit({dpi=96}, 9999,9999)
wibox.widget.draw_to_svg_file(widget, image_path..".svg", w, h)
end
loadfile(file_path)(module)