Merge pull request #824 from Elv13/rules_refactor

Rules refactor, part 1

This fix the following awful.rules bugs:

 * **x**: Broken when "border_width" is set or left titlebars are used
 * **y**: Borken when"border_width" is set or top titlebars are used
 * **width**: See above + right litlebar
 * **height**: Same as above
 * **switchtotag**: Have a race with the "manage" -> "tag.withcurrent" code in `awful.tag`
 * **tag**: Had dead code
 * **screen**: Had a race condition with switchtotag
 * **urgent**: Had a race with screen and another with switchtotag+focus
 * **focusable**: Was broken yet again when request::activate was introduced (and also because of FS1098, that I also fixed)
 * **no_overlap**: The "no_overlap" call in rc.lua "manage" section conflict with the geometry rules as the hints are not set (idk why).
 * **size_hints_honor**: If set to false, it would be applied too late, causing height and width offsets due to placement or various geometry related properties
This commit is contained in:
Emmanuel Lepage Vallée 2016-04-20 00:39:45 -04:00
commit cdc6909bc7
32 changed files with 1221 additions and 225 deletions

View File

@ -29,7 +29,7 @@ install:
# Install build dependencies.
# See also `apt-cache showsrc awesome | grep -E '^(Version|Build-Depends)'`.
- sudo apt-get install -y libcairo2-dev xmlto asciidoc libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev
- sudo apt-get install -y libcairo2-dev gtk+3.0 xmlto asciidoc libpango1.0-dev libxcb-xtest0-dev libxcb-icccm4-dev libxcb-randr0-dev libxcb-keysyms1-dev libxcb-xinerama0-dev libdbus-1-dev libxdg-basedir-dev libstartup-notification0-dev imagemagick libxcb1-dev libxcb-shape0-dev libxcb-util0-dev libx11-xcb-dev libxcb-cursor-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev
# Deps for functional tests.
- sudo apt-get install -y dbus-x11 xterm xdotool xterm xvfb rxvt-unicode

View File

@ -1,7 +1,6 @@
-- Standard awesome library
local gears = require("gears")
local awful = require("awful")
awful.rules = require("awful.rules")
require("awful.autofocus")
-- Widget and layout library
local wibox = require("wibox")
@ -431,7 +430,10 @@ awful.rules.rules = {
focus = awful.client.focus.filter,
raise = true,
keys = clientkeys,
buttons = clientbuttons } },
buttons = clientbuttons,
placement = awful.placement.no_overlap+awful.placement.no_offscreen
}
},
-- Floating clients.
{ rule_any = {
@ -459,6 +461,11 @@ awful.rules.rules = {
}
}, properties = { floating = true }},
-- Add titlebars to normal clients and dialogs
{ rule_any = {type = { "normal", "dialog" }
}, properties = { titlebars_enabled = true }
},
-- Set Firefox to always map on the tag named "2" on screen 1.
-- { rule = { class = "Firefox" },
-- properties = { screen = 1, tag = "2" } },
@ -468,62 +475,58 @@ awful.rules.rules = {
-- {{{ Signals
-- Signal function to execute when a new client appears.
client.connect_signal("manage", function (c)
if not awesome.startup then
-- Set the windows at the slave,
-- i.e. put it at the end of others instead of setting it master.
-- awful.client.setslave(c)
-- Set the windows at the slave,
-- i.e. put it at the end of others instead of setting it master.
-- if not awesome.startup then awful.client.setslave(c) end
-- Put windows in a smart way, only if they do not set an initial position.
if not c.size_hints.user_position and not c.size_hints.program_position then
awful.placement.no_overlap(c)
awful.placement.no_offscreen(c)
end
elseif not c.size_hints.user_position and not c.size_hints.program_position then
if awesome.startup and
not c.size_hints.user_position
and not c.size_hints.program_position then
-- Prevent clients from being unreachable after screen count changes.
awful.placement.no_offscreen(c)
end
end)
local titlebars_enabled = true
if titlebars_enabled and (c.type == "normal" or c.type == "dialog") then
-- buttons for the titlebar
local buttons = awful.util.table.join(
awful.button({ }, 1, function()
client.focus = c
c:raise()
awful.mouse.client.move(c)
end),
awful.button({ }, 3, function()
client.focus = c
c:raise()
awful.mouse.client.resize(c)
end)
)
-- Add a titlebar if titlebars_enabled is set to true in the rules.
client.connect_signal("request::titlebars", function(c)
-- buttons for the titlebar
local buttons = awful.util.table.join(
awful.button({ }, 1, function()
client.focus = c
c:raise()
awful.mouse.client.move(c)
end),
awful.button({ }, 3, function()
client.focus = c
c:raise()
awful.mouse.client.resize(c)
end)
)
awful.titlebar(c) : setup {
{ -- Left
awful.titlebar.widget.iconwidget(c),
buttons = buttons,
layout = wibox.layout.fixed.horizontal
awful.titlebar(c) : setup {
{ -- Left
awful.titlebar.widget.iconwidget(c),
buttons = buttons,
layout = wibox.layout.fixed.horizontal
},
{ -- Middle
{ -- Title
align = "center",
widget = awful.titlebar.widget.titlewidget(c)
},
{ -- Middle
{ -- Title
align = "center",
widget = awful.titlebar.widget.titlewidget(c)
},
buttons = buttons,
layout = wibox.layout.flex.horizontal
},
{ -- Right
awful.titlebar.widget.floatingbutton (c),
awful.titlebar.widget.maximizedbutton(c),
awful.titlebar.widget.stickybutton (c),
awful.titlebar.widget.ontopbutton (c),
awful.titlebar.widget.closebutton (c),
layout = wibox.layout.fixed.horizontal()
},
layout = wibox.layout.align.horizontal
}
end
buttons = buttons,
layout = wibox.layout.flex.horizontal
},
{ -- Right
awful.titlebar.widget.floatingbutton (c),
awful.titlebar.widget.maximizedbutton(c),
awful.titlebar.widget.stickybutton (c),
awful.titlebar.widget.ontopbutton (c),
awful.titlebar.widget.closebutton (c),
layout = wibox.layout.fixed.horizontal()
},
layout = wibox.layout.align.horizontal
}
end)
-- Enable sloppy focus

2
ewmh.c
View File

@ -399,7 +399,7 @@ ewmh_process_desktop(client_t *c, uint32_t desktop)
if(desktop == 0xffffffff)
{
luaA_object_push(L, c);
lua_pushnil(L);
lua_pushboolean(L, true);
luaA_object_emit_signal(L, -2, "request::tag", 1);
/* Pop the client, arguments are already popped */
lua_pop(L, 1);

View File

@ -1114,6 +1114,12 @@ capi.client.add_signal("property::floating")
capi.client.add_signal("property::dockable")
--- Emited when a client need to get a titlebar.
-- @signal request::titlebars
-- @tparam[opt=nil] string content The context (like "rules")
-- @tparam[opt=nil] table hints Some hints.
capi.client.add_signal("request::titlebars")
--- The client marked signal (deprecated).
-- @signal .marked
capi.client.add_signal("marked")

View File

@ -12,71 +12,14 @@ local client = client
local screen = screen
local ipairs = ipairs
local math = math
local util = require("awful.util")
local aclient = require("awful.client")
local aplace = require("awful.placement")
local ewmh = {}
local data = setmetatable({}, { __mode = 'k' })
local function store_geometry(window, reqtype)
if not data[window] then data[window] = {} end
if not data[window][reqtype] then data[window][reqtype] = {} end
data[window][reqtype] = window:geometry()
data[window][reqtype].screen = window.screen
end
--- Maximize a window horizontally.
--
-- @param window The window.
-- @param set Set or unset the maximized values.
local function maximized_horizontal(window, set)
if set then
store_geometry(window, "maximized_horizontal")
local g = screen[window.screen].workarea
local bw = window.border_width or 0
window:geometry { width = g.width - 2*bw, x = g.x }
elseif data[window] and data[window].maximized_horizontal
and data[window].maximized_horizontal.x
and data[window].maximized_horizontal.width then
local g = data[window].maximized_horizontal
window:geometry { width = g.width, x = g.x }
end
end
--- Maximize a window vertically.
--
-- @param window The window.
-- @param set Set or unset the maximized values.
local function maximized_vertical(window, set)
if set then
store_geometry(window, "maximized_vertical")
local g = screen[window.screen].workarea
local bw = window.border_width or 0
window:geometry { height = g.height - 2*bw, y = g.y }
elseif data[window] and data[window].maximized_vertical
and data[window].maximized_vertical.y
and data[window].maximized_vertical.height then
local g = data[window].maximized_vertical
window:geometry { height = g.height, y = g.y }
end
end
--- Fullscreen a window.
--
-- @param window The window.
-- @param set Set or unset the fullscreen values.
local function fullscreen(window, set)
if set then
store_geometry(window, "fullscreen")
data[window].fullscreen.border_width = window.border_width
window.border_width = 0
window:geometry(screen[window.screen].geometry)
elseif data[window] and data[window].fullscreen then
window.border_width = data[window].fullscreen.border_width
window:geometry(data[window].fullscreen)
end
end
local function screen_change(window)
if data[window] then
for _, reqtype in ipairs({ "maximized_vertical", "maximized_horizontal", "fullscreen" }) do
@ -162,6 +105,10 @@ end
-- @tparam[opt] table hints A table with additional hints:
-- @tparam[opt=false] boolean hints.raise should the client be raised?
function ewmh.activate(c, context, hints) -- luacheck: no unused args
hints = hints or {}
if c.focusable == false and not hints.force then return end
if c:isvisible() then
client.focus = c
end
@ -177,8 +124,14 @@ end
--
-- @client c A client to tag
-- @tag[opt] t A tag to use. If omitted, then the client is made sticky.
function ewmh.tag(c, t)
-- @tparam[opt={}] table hints Extra information
function ewmh.tag(c, t, hints) --luacheck: no unused
-- There is nothing to do
if not t and #c:tags() > 0 then return end
if not t then
c:to_selected_tags()
elseif type(t) == "boolean" and t then
c.sticky = true
else
c.screen = t.screen
@ -192,12 +145,60 @@ function ewmh.urgent(c, urgent)
end
end
-- Map the state to the action name
local context_mapper = {
maximized_vertical = "maximize_vertically",
maximized_horizontal = "maximize_horizontally",
fullscreen = "maximize"
}
--- Move and resize the client.
--
-- This is the default geometry request handler.
--
-- @tparam client c The client
-- @tparam string context The context
-- @tparam[opt={}] table hints The hints to pass to the handler
function ewmh.geometry(c, context, hints)
context = context or ""
local original_context = context
-- Now, map it to something useful
context = context_mapper[context] or context
local props = util.table.clone(hints or {}, false)
props.store_geometry = props.store_geometry==nil and true or props.store_geometry
-- If it is a known placement function, then apply it, otherwise let
-- other potential handler resize the client (like in-layout resize or
-- floating client resize)
if aplace[context] then
-- Check if it correspond to a boolean property
local state = c[original_context]
-- If the property is boolean and it correspond to the undo operation,
-- restore the stored geometry.
if state == false then
aplace.restore(c,{context=context})
return
end
local honor_default = original_context ~= "fullscreen"
if props.honor_workarea == nil then
props.honor_workarea = honor_default
end
aplace[context](c, props)
end
end
client.connect_signal("request::activate", ewmh.activate)
client.connect_signal("request::tag", ewmh.tag)
client.connect_signal("request::urgent", ewmh.urgent)
client.connect_signal("request::maximized_horizontal", maximized_horizontal)
client.connect_signal("request::maximized_vertical", maximized_vertical)
client.connect_signal("request::fullscreen", fullscreen)
client.connect_signal("request::geometry", ewmh.geometry)
client.connect_signal("property::screen", screen_change)
client.connect_signal("property::border_width", geometry_change)
client.connect_signal("property::geometry", geometry_change)

View File

@ -56,6 +56,7 @@ return
tooltip = require("awful.tooltip");
ewmh = require("awful.ewmh");
titlebar = require("awful.titlebar");
rules = require("awful.rules");
spawn = spawn;
}

View File

@ -10,7 +10,12 @@
-- * Re-use the same functions for the `mouse`, `client`s, `screen`s and `wibox`es
--
--
--
--
-- It is possible to compose placement function using the `+` or `*` operator:
--
-- local f = (awful.placement.right + awful.placement.left)
-- f(client.focus)
--
-- ### Common arguments
--
-- **honor_workarea** (*boolean*):
@ -39,6 +44,11 @@
--
-- **attach** (*boolean*):
--
-- **store_geometry** (*boolean*):
--
-- Keep a single history of each type of placement. It can be restored using
-- `awful.placement.restore` by setting the right `context` argument.
--
-- When either the parent or the screen geometry change, call the placement
-- function again.
--
@ -73,7 +83,37 @@ local function get_screen(s)
return s and capi.screen[s]
end
local placement = {}
local wrap_client = nil
local function compose(w1, w2)
return wrap_client(function(...)
w1(...)
w2(...)
return --It make no sense to keep a return value
end)
end
wrap_client = function(f)
return setmetatable({is_placement=true}, {
__call = function(_,...) return f(...) end,
__add = compose, -- Composition is usually defined as +
__mul = compose -- Make sense if you think of the functions as matrices
})
end
local placement_private = {}
-- The module is a proxy in front of the "real" functions.
-- This allow syntax like:
--
-- (awful.placement.no_overlap + awful.placement.no_offscreen)(c)
--
local placement = setmetatable({}, {
__index = placement_private,
__newindex = function(_, k, f)
placement_private[k] = wrap_client(f)
end
})
-- 3x3 matrix of the valid sides and corners
local corners3x3 = {{"top_left" , "top" , "top_right" },
@ -103,6 +143,26 @@ local align_map = {
-- Store function -> keys
local reverse_align_map = {}
--- Add a context to the arguments.
-- This function extend the argument table. The context is used by some
-- internal helper methods. If there already is a context, it has priority and
-- is kept.
local function add_context(args, context)
return setmetatable({context = (args or {}).context or context }, {__index=args})
end
local data = setmetatable({}, { __mode = 'k' })
--- Store a drawable geometry (per context) in a weak table.
-- @param d The drawin
-- @tparam string reqtype The context.
local function store_geometry(d, reqtype)
if not data[d] then data[d] = {} end
if not data[d][reqtype] then data[d][reqtype] = {} end
data[d][reqtype] = d:geometry()
data[d][reqtype].screen = d.screen
end
--- Get the area covered by a drawin.
-- @param d The drawin
-- @tparam[opt=nil] table new_geo A new geometry
@ -112,8 +172,8 @@ local function area_common(d, new_geo, ignore_border_width)
-- The C side expect no arguments, nil isn't valid
local geometry = new_geo and d:geometry(new_geo) or d:geometry()
local border = ignore_border_width and 0 or d.border_width or 0
geometry.x = geometry.x - border
geometry.y = geometry.y - border
geometry.x = geometry.x
geometry.y = geometry.y
geometry.width = geometry.width + 2 * border
geometry.height = geometry.height + 2 * border
return geometry
@ -128,6 +188,12 @@ end
-- @tparam[opt=false] boolean ignore_border_width Ignore the border
-- @treturn table A table with *x*, *y*, *width* and *height*.
local function geometry_common(obj, args, new_geo, ignore_border_width)
-- Store the current geometry in a singleton-memento
if args.store_geometry and new_geo and args.context then
store_geometry(obj, args.context)
end
-- It's a mouse
if obj.coords then
local coords = new_geo and obj.coords(new_geo) or obj.coords()
@ -381,6 +447,7 @@ end
-- @tparam[opt={}] table args The arguments
-- @treturn string The corner name
function placement.closest_corner(d, args)
args = add_context(args, "closest_corner")
d = d or capi.client.focus
local sgeo = get_parent_geometry(d, args)
@ -409,7 +476,7 @@ function placement.closest_corner(d, args)
-- Transpose the corner back to the original size
local new_args = setmetatable({position = corner}, {__index=args})
placement.align(d, new_args)
placement_private.align(d, new_args)
return corner
end
@ -420,6 +487,11 @@ end
-- @tparam[opt=client's screen] integer screen The screen.
-- @treturn table The new client geometry.
function placement.no_offscreen(c, screen)
--HACK necessary for composition to work. The API will be changed soon
if type(screen) == "table" then
screen = nil
end
c = c or capi.client.focus
local geometry = area_common(c)
screen = get_screen(screen or c.screen or a_screen.getbycoord(geometry.x, geometry.y))
@ -439,7 +511,10 @@ function placement.no_offscreen(c, screen)
geometry.y = screen_geometry.y
end
return c:geometry({ x = geometry.x, y = geometry.y })
return c:geometry {
x = geometry.x,
y = geometry.y
}
end
--- Place the client where there's place available with minimum overlap.
@ -567,7 +642,7 @@ end
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
function placement.align(d, args)
args = args or {}
args = add_context(args, "align")
d = d or capi.client.focus
if not d or not args.position then return end
@ -584,8 +659,8 @@ function placement.align(d, args)
)
geometry_common(d, args, {
x = (pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x) + bw ,
y = (pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y) + bw ,
x = (pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x) ,
y = (pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y) ,
width = math.ceil(dgeo.width ) - 2*bw,
height = math.ceil(dgeo.height ) - 2*bw,
})
@ -596,8 +671,9 @@ end
-- Add the alias functions
for k in pairs(align_map) do
placement[k] = function(d, args)
local new_args = setmetatable({position = k}, {__index=args})
placement.align(d, new_args)
args = add_context(args, k)
args.position = k
placement_private.align(d, args)
end
reverse_align_map[placement[k]] = k
end
@ -636,7 +712,7 @@ end
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
function placement.stretch(d, args)
args = args or {}
args = add_context(args, "stretch")
d = d or capi.client.focus
if not d or not args.direction then return end
@ -644,8 +720,8 @@ function placement.stretch(d, args)
-- In case there is multiple directions, call `stretch` for each of them
if type(args.direction) == "table" then
for _, dir in ipairs(args.direction) do
local new_args = setmetatable({direction = dir}, {__index=args})
placement.stretch(dir, new_args)
args.direction = dir
placement_private.stretch(dir, args)
end
return
end
@ -656,15 +732,15 @@ function placement.stretch(d, args)
local bw = d.border_width or 0
if args.direction == "left" then
ngeo.x = sgeo.x + bw
ngeo.x = sgeo.x
ngeo.width = dgeo.width + (dgeo.x - ngeo.x)
elseif args.direction == "right" then
ngeo.width = sgeo.width - ngeo.x - bw
ngeo.width = sgeo.width - ngeo.x - 2*bw
elseif args.direction == "up" then
ngeo.y = sgeo.y + bw
ngeo.y = sgeo.y
ngeo.height = dgeo.height + (dgeo.y - ngeo.y)
elseif args.direction == "down" then
ngeo.height = sgeo.height - dgeo.y - bw
ngeo.height = sgeo.height - dgeo.y - 2*bw
else
assert(false)
end
@ -681,8 +757,9 @@ end
-- Add the alias functions
for _,v in ipairs {"left", "right", "up", "down"} do
placement["stretch_"..v] = function(d, args)
local new_args = setmetatable({direction = v}, {__index=args})
placement.stretch(d, new_args)
args = add_context(args, "stretch_"..v)
args.direction = v
placement_private.stretch(d, args)
end
end
@ -704,7 +781,7 @@ end
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
function placement.maximize(d, args)
args = args or {}
args = add_context(args, "maximize")
d = d or capi.client.focus
if not d then return end
@ -714,12 +791,12 @@ function placement.maximize(d, args)
local bw = d.border_width or 0
if (not args.axis) or args.axis :match "vertical" then
ngeo.y = sgeo.y + bw
ngeo.y = sgeo.y
ngeo.height = sgeo.height - 2*bw
end
if (not args.axis) or args.axis :match "horizontal" then
ngeo.x = sgeo.x + bw
ngeo.x = sgeo.x
ngeo.width = sgeo.width - 2*bw
end
@ -731,9 +808,9 @@ end
-- Add the alias functions
for _, v in ipairs {"vertically", "horizontally"} do
placement["maximize_"..v] = function(d2, args)
args = args or {}
local new_args = setmetatable({axis = v}, {__index=args})
placement.maximize(d2, new_args)
args = add_context(args, "maximize_"..v)
args.axis = v
placement_private.maximize(d2, args)
end
end
@ -741,6 +818,25 @@ end
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
--- Restore the geometry.
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn boolean If the geometry was restored
function placement.restore(d, args)
if not args or not args.context then return false end
d = d or capi.client.focus
if not data[d] then return false end
local memento = data[d][args.context]
if not memento then return false end
memento.screen = nil --TODO use it
d:geometry(memento)
return true
end
return placement
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -9,12 +9,15 @@
-- Grab environment we need
local client = client
local awesome = awesome
local screen = screen
local table = table
local type = type
local ipairs = ipairs
local pairs = pairs
local atag = require("awful.tag")
local util = require("awful.util")
local a_place = require("awful.placement")
local rules = {}
@ -188,6 +191,7 @@ end
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
local props = {}
local callbacks = {}
@ -205,36 +209,253 @@ function rules.apply(c)
rules.execute(c, props, callbacks)
end
local function add_to_tag(c, t)
if not t then return end
local tags = c:tags()
table.insert(tags, t)
c:tags(tags)
end
--- Extra rules properties.
--
-- These properties are used in the rules only and are not sent to the client
-- afterward.
--
-- To add a new properties, just do:
--
-- function awful.rules.extra_properties.my_new_property(c, value, props)
-- -- do something
-- end
--
-- By default, the table has the following functions:
--
-- * geometry
-- * switchtotag
--
-- @tfield table awful.rules.extra_properties
rules.extra_properties = {}
--- Extra high priority properties.
--
-- Some properties, such as anything related to tags, geometry or focus, will
-- cause a race condition if set in the main property section. This is why
-- they have a section for them.
--
-- To add a new properties, just do:
--
-- function awful.rules.high_priority_properties.my_new_property(c, value, props)
-- -- do something
-- end
--
-- By default, the table has the following functions:
--
-- * tag
-- * new_tag
--
-- @tfield table awful.rules.high_priority_properties
rules.high_priority_properties = {}
rules.delayed_properties = {}
local force_ignore = {
titlebars_enabled=true, focus=true, screen=true, x=true,
y=true, width=true, height=true, geometry=true,placement=true,
border_width=true,floating=true,size_hints_honor=true
}
function rules.high_priority_properties.tag(c, value)
if value then
if type(value) == "string" then
value = atag.find_by_name(nil, value)
end
c:tags{ value }
end
end
function rules.delayed_properties.switchtotag(c, value)
if not value then return end
local selected_tags = {}
for _,v in ipairs(c.screen.selected_tags) do
selected_tags[v] = true
end
local tags = c:tags()
for _, t in ipairs(tags) do
t.selected = true
selected_tags[t] = nil
end
for t in pairs(selected_tags) do
t.selected = false
end
end
function rules.extra_properties.geometry(c, _, props)
local cur_geo = c:geometry()
local new_geo = type(props.geometry) == "function"
and props.geometry(c, props) or props.geometry or {}
for _, v in ipairs {"x", "y", "width", "height"} do
new_geo[v] = type(props[v]) == "function" and props[v](c, props)
or props[v] or new_geo[v] or cur_geo[v]
end
c:geometry(new_geo) --TODO use request::geometry
end
--- Create a new tag based on a rule.
-- @tparam client c The client
-- @tparam boolean|function|string value The value.
-- @treturn tag The new tag
function rules.high_priority_properties.new_tag(c, value)
local ty = type(value)
local t = nil
if ty == "boolean" then
-- Create a new tag named after the client class
t = atag.add(c.class or "N/A", {screen=c.screen, volatile=true})
elseif ty == "string" then
-- Create a tag named after "value"
t = atag.add(value, {screen=c.screen, volatile=true})
elseif ty == "table" then
-- Assume a table of tags properties
t = atag.add(value.name or c.class or "N/A", value)
else
assert(false)
end
add_to_tag(c, t)
return t
end
function rules.extra_properties.placement(c, value)
-- Avoid problems
if awesome.startup and
(c.size_hints.user_position or c.size_hints.program_position) then
return
end
local ty = type(value)
local args = {
honor_workarea = true,
honor_padding = true
}
if ty == "function" or (ty == "table" and
getmetatable(value) and getmetatable(value).__call
) then
value(c, args)
elseif ty == "string" and a_place[value] then
a_place[value](c, args)
end
end
function rules.extra_properties.tags(c, value)
local current = c:tags()
c:tags(util.table.merge(current, value))
end
--- Apply properties and callbacks to a client.
-- @client c The client.
-- @tab props Properties to apply.
-- @tab[opt] callbacks Callbacks to apply.
function rules.execute(c, props, callbacks)
local handle_later = { focus = true, switchtotag = true }
local switchtotag = props.switchtotag
-- This has to be done first, as it will impact geometry related props.
if props.titlebars_enabled then
c:emit_signal("request::titlebars", "rules", {properties=props})
end
-- Border width will also cause geometry related properties to fail
if props.border_width then
c.border_width = type(props.border_width) == "function" and
props.border_width(c, props) or props.border_width
end
-- Size hints will be re-applied when setting width/height unless it is
-- disabled first
if props.size_hints_honor ~= nil then
c.size_hints_honor = type(props.size_hints_honor) == "function" and props.size_hints_honor(c,props)
or props.size_hints_honor
end
-- Geometry will only work if floating is true, otherwise the "saved"
-- geometry will be restored.
if props.floating then
c.floating = type(props.floating) == "function" and props.floating(c,props)
or props.floating
end
-- Before requesting a tag, make sure the screen is right
if props.screen then
c.screen = type(props.screen) == "function" and screen[props.screen(c,props)]
or screen[props.screen]
end
-- Some properties need to be handled first. For example, many properties
-- depend that the client is tagged, this isn't yet the case.
for prop, handler in pairs(rules.high_priority_properties) do
local value = props[prop]
if value ~= nil then
if type(value) == "function" then
value = value(c, props)
end
handler(c, value)
end
end
-- By default, rc.lua use no_overlap+no_offscreen placement. This has to
-- be executed before x/y/width/height/geometry as it would otherwise
-- always override the user specified position with the default rule.
if props.placement then
-- It may be a function, so this one doesn't execute it like others
rules.extra_properties.placement(c, props.placement, props)
end
-- Make sure the tag is selected before the main rules are called.
-- Otherwise properties like "urgent" or "focus" may fail because they
-- will be overiden by various callbacks.
-- Previously, this was done in a second client.manage callback, but caused
-- a race condition where the order the require() would change the output.
c:emit_signal("request::tag", nil, {reason="rules"})
-- By default, rc.lua use no_overlap+no_offscreen placement. This has to
-- be executed before x/y/width/height/geometry as it would otherwise
-- always override the user specified position with the default rule.
if props.placement then
-- It may be a function, so this one doesn't execute it like others
rules.extra_properties.placement(c, props.placement, props)
end
-- Now that the tags and screen are set, handle the geometry
if props.height or props.width or props.x or props.y or props.geometry then
rules.extra_properties.geometry(c, nil, props)
end
-- As most race conditions should now have been avoided, apply the remaining
-- properties.
for property, value in pairs(props) do
if property ~= "focus" and type(value) == "function" then
value = value(c)
value = value(c, props)
end
if property == "screen" then
-- Support specifying screens by name ("VGA1")
c.screen = screen[value]
elseif property == "tag" then
local t = value
if type(t) == "string" then
t = atag.find_by_name(props.screen, t)
end
c.screen = t.screen
c:tags({ t })
elseif property == "height" or property == "width" or
property == "x" or property == "y" then
local geo = c:geometry();
geo[property] = value
c:geometry(geo);
elseif not handle_later[property] then
if type(c[property]) == "function" then
local ignore = rules.high_priority_properties[property] or
rules.delayed_properties[property] or force_ignore[property]
if not ignore then
if rules.extra_properties[property] then
rules.extra_properties[property](c, value)
elseif type(c[property]) == "function" then
c[property](c, value)
else
c[property] = value
@ -242,11 +463,6 @@ function rules.execute(c, props, callbacks)
end
end
-- Only do this after the tag has been (possibly) set
if switchtotag and c.first_tag then
c.first_tag:view_only()
end
-- Apply all callbacks.
if callbacks then
for _, callback in pairs(callbacks) do
@ -254,6 +470,21 @@ function rules.execute(c, props, callbacks)
end
end
-- Apply the delayed properties
for prop, handler in pairs(rules.delayed_properties) do
if not force_ignore[prop] then
local value = props[prop]
if value ~= nil then
if type(value) == "function" then
value = value(c, props)
end
handler(c, value, props)
end
end
end
-- Do this at last so we do not erase things done by the focus signal.
if props.focus and (type(props.focus) ~= "function" or props.focus(c)) then
c:emit_signal('request::activate', "rules", {raise=true})

View File

@ -29,10 +29,6 @@ local function get_screen(s)
return s and capi.screen[s]
end
-- awful.client is required() at the end of this file so the miss_handler is set
-- before it is being required.
local client
local tag = {object = {}, mt = {} }
-- Private data
@ -1370,11 +1366,6 @@ object.properties(capi.tag, {
setter_fallback = tag.setproperty,
})
-- fix a load loop
client = require("awful.client")
capi.client.connect_signal("manage", function(c) client.object.to_selected_tags(c) end)
return setmetatable(tag, tag.mt)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -1697,9 +1697,9 @@ client_set_fullscreen(lua_State *L, int cidx, bool s)
client_set_ontop(L, cidx, false);
}
int abs_cidx = luaA_absindex(L, cidx); \
lua_pushboolean(L, s);
lua_pushstring(L, "fullscreen");
c->fullscreen = s;
luaA_object_emit_signal(L, abs_cidx, "request::fullscreen", 1);
luaA_object_emit_signal(L, abs_cidx, "request::geometry", 1);
luaA_object_emit_signal(L, abs_cidx, "property::fullscreen", 0);
/* Force a client resize, so that titlebars get shown/hidden */
client_resize_do(c, c->geometry, true);
@ -1730,10 +1730,10 @@ client_get_maximized(client_t *c)
if(c->maximized_##type != s) \
{ \
int abs_cidx = luaA_absindex(L, cidx); \
lua_pushboolean(L, s); \
int max_before = client_get_maximized(c); \
c->maximized_##type = s; \
luaA_object_emit_signal(L, abs_cidx, "request::maximized_" #type, 1); \
lua_pushstring(L, "maximized_"#type);\
luaA_object_emit_signal(L, abs_cidx, "request::geometry", 1); \
luaA_object_emit_signal(L, abs_cidx, "property::maximized_" #type, 0); \
if(max_before != client_get_maximized(c)) \
luaA_object_emit_signal(L, abs_cidx, "property::maximized", 0); \
@ -3446,17 +3446,14 @@ client_class_setup(lua_State *L)
*/
signal_add(&client_class.signals, "request::activate");
/**
* @signal request::fullscreen
* @signal request::geometry
* @tparam client c The client
* @tparam string context Why and what to resize. This is used for the
* handlers to know if they are capable of applying the new geometry.
* @tparam[opt={}] table Additional arguments. Each context handler may
* interpret this differently.
*/
signal_add(&client_class.signals, "request::fullscreen");
/**
* @signal request::maximized_horizontal
*/
signal_add(&client_class.signals, "request::maximized_horizontal");
/**
* @signal request::maximized_vertical
*/
signal_add(&client_class.signals, "request::maximized_vertical");
signal_add(&client_class.signals, "request::geometry");
/**
* @signal request::tag
*/

32
tests/_client.lua Normal file
View File

@ -0,0 +1,32 @@
local spawn = require("awful.spawn")
-- This file provide a simple, yet flexible, test client.
-- It is used to test the `awful.rules`
return function(class, title)
title = title or 'Awesome test client'
local cmd = {"lua" , "-e", table.concat {
"local lgi = require 'lgi';",
"local Gtk = lgi.require('Gtk');",
"Gtk.init();",
"local class = '",
class or 'test_app',"';",
"local window = Gtk.Window {",
" default_width = 100,",
" default_height = 100,",
" title = '",title,
"'};",
"window:set_wmclass(class, class);",
"local app = Gtk.Application {",
" application_id = 'org.awesomewm.tests.",class,
"'};",
"function app:on_activate()",
" window.application = self;",
" window:show_all();",
"end;",
"app:run {''}"
}}
spawn(cmd)
end

View File

@ -10,6 +10,6 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.bottom(client.focus)
assert(c.x == screen[1].geometry.width/2-40/2--DOC_HIDE
and c.y==screen[1].geometry.height-30-c.border_width--DOC_HIDE
assert(c.x == screen[1].geometry.width/2-40/2-c.border_width--DOC_HIDE
and c.y==screen[1].geometry.height-30-2*c.border_width--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE

View File

@ -11,8 +11,8 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.bottom_left(client.focus)
assert( --DOC_HIDE
c.x == c.border_width --DOC_HIDE
and c.y == screen[1].geometry.height-30-c.border_width --DOC_HIDE
c.x == 0 --DOC_HIDE
and c.y+2*c.border_width == screen[1].geometry.height-30 --DOC_HIDE
and c.width == 40--DOC_HIDE
and c.height == 30--DOC_HIDE
) --DOC_HIDE

View File

@ -10,6 +10,6 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.bottom_right(client.focus)
assert(c.x == screen[1].geometry.width-40-c.border_width) --DOC_HIDE
assert(c.y==screen[1].geometry.height-30-c.border_width) --DOC_HIDE
assert(c.x == screen[1].geometry.width-40-2*c.border_width) --DOC_HIDE
assert(c.y==screen[1].geometry.height-30-2*c.border_width) --DOC_HIDE
assert(c.width==40 and c.height==30)--DOC_HIDE

View File

@ -9,6 +9,6 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.center_horizontal(client.focus)
assert(c.x == screen[1].geometry.width/2-40/2)--DOC_HIDE
assert(c.x == screen[1].geometry.width/2-40/2-c.border_width)--DOC_HIDE
assert(c.y==35)--DOC_HIDE
assert(c.width==40 and c.height==30)--DOC_HIDE

View File

@ -10,5 +10,6 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.centered(client.focus)
assert(c.x == screen[1].geometry.width/2-40/2 and c.y==screen[1].geometry.height/2-30/2--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE
assert(c.x == screen[1].geometry.width/2-40/2-c.border_width) --DOC_HIDE
assert(c.y==screen[1].geometry.height/2-30/2-c.border_width) --DOC_HIDE
assert(c.width==40 and c.height==30)--DOC_HIDE

View File

@ -8,49 +8,50 @@ mouse.coords {x=100,y=100} --DOC_HIDE
-- Move the mouse to the closest corner of the focused client
awful.placement.closest_corner(mouse, {include_sides=true, parent=c})
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x-bw and mouse.coords().y == (c.y) + (c.height)/2) --DOC_HIDE
assert(mouse.coords().x == c.x) --DOC_HIDE
assert(mouse.coords().y == (c.y) + (c.height)/2+bw) --DOC_HIDE
-- Top left --DOC_HIDE
mouse.coords {x=60,y=40} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x-bw and mouse.coords().y == c.y-bw) --DOC_HIDE
assert(mouse.coords().x == c.x and mouse.coords().y == c.y) --DOC_HIDE
-- Top right --DOC_HIDE
mouse.coords {x=230,y=50} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+bw and mouse.coords().y == c.y-bw) --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+2*bw and mouse.coords().y == c.y) --DOC_HIDE
-- Right --DOC_HIDE
mouse.coords {x=240,y=140} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+bw and mouse.coords().y == c.y+c.height/2) --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+2*bw and mouse.coords().y == c.y+c.height/2+bw) --DOC_HIDE
-- Bottom right --DOC_HIDE
mouse.coords {x=210,y=190} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+bw and mouse.coords().y == c.y+c.height+bw) --DOC_HIDE
assert(mouse.coords().x == c.x+c.width+2*bw and mouse.coords().y == c.y+c.height+2*bw) --DOC_HIDE
-- Bottom --DOC_HIDE
mouse.coords {x=130,y=190} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x+c.width/2 and mouse.coords().y == c.y + c.height + bw) --DOC_HIDE
assert(mouse.coords().x == c.x+c.width/2+bw and mouse.coords().y == c.y + c.height + 2*bw) --DOC_HIDE
-- Top --DOC_HIDE
mouse.coords {x=130,y=30} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x + c.width/2 and mouse.coords().y == c.y-bw) --DOC_HIDE
assert(mouse.coords().x == c.x + c.width/2+bw and mouse.coords().y == c.y) --DOC_HIDE
-- Bottom left + outside of "c" --DOC_HIDE
mouse.coords {x=0,y=230} --DOC_HIDE
awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) --DOC_HIDE
mouse.push_history() --DOC_HIDE
assert(mouse.coords().x == c.x-bw and mouse.coords().y == c.y+c.height+bw) --DOC_HIDE
assert(mouse.coords().x == c.x and mouse.coords().y == c.y+c.height+2*bw) --DOC_HIDE
-- It is possible to emulate the mouse API to get the closest corner of
-- random area

View File

@ -0,0 +1,9 @@
screen[1]._resize {width = 128, height = 96} --DOC_HIDE
local awful = {placement = require("awful.placement")} --DOC_HIDE
local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
local f = (awful.placement.right + awful.placement.left)
f(client.focus)
assert(c.x == 0 and c.y==screen[1].geometry.height/2-30/2-c.border_width--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE

View File

@ -10,5 +10,5 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.left(client.focus)
assert(c.x == c.border_width and c.y==screen[1].geometry.height/2-30/2--DOC_HIDE
assert(c.x == 0 and c.y==screen[1].geometry.height/2-30/2-c.border_width--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE

View File

@ -10,6 +10,6 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.right(client.focus)
assert(c.x == screen[1].geometry.width-40-c.border_width --DOC_HIDE
and c.y==screen[1].geometry.height/2-30/2--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE
assert(c.x == screen[1].geometry.width-40-2*c.border_width) --DOC_HIDE
assert( c.y==screen[1].geometry.height/2-30/2-c.border_width)--DOC_HIDE
assert( c.width==40 and c.height==30)--DOC_HIDE

View File

@ -13,5 +13,5 @@ placement.stretch_down(client.focus)
assert(c.x==45) --DOC_HIDE
assert(c.y==35) --DOC_HIDE
assert(c.width == 40) --DOC_HIDE
assert(c.y+c.height == --DOC_HIDE
assert(c.y+c.height+2*c.border_width == --DOC_HIDE
screen[1].geometry.y + screen[1].geometry.height) --DOC_HIDE

View File

@ -10,5 +10,5 @@ local placement = require("awful.placement") --DOC_HIDE
local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
placement.stretch_left(client.focus)
assert(c.x == c.border_width and c.y == 35 and c.height == 30 --DOC_HIDE
and c.width == 45+40) --DOC_HIDE
assert(c.x == 0 and c.y == 35 and c.height == 30) --DOC_HIDE
print(c.width-2*c.border_width == 45+40) --DOC_HIDE

View File

@ -11,4 +11,4 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
placement.stretch_right(client.focus)
local right = screen[1].geometry.x + screen[1].geometry.width --DOC_HIDE
assert(c.height == 30 and c.x == 45 and c.x+c.width+c.border_width == right) --DOC_HIDE
assert(c.height == 30 and c.x == 45 and c.x+c.width+2*c.border_width == right) --DOC_HIDE

View File

@ -10,8 +10,8 @@ local placement = require("awful.placement") --DOC_HIDE
local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
placement.stretch_up(client.focus)
assert(c.y==c.border_width) --DOC_HIDE
assert(c.y==0) --DOC_HIDE
assert(c.x==45) --DOC_HIDE
assert(c.width == 40) --DOC_HIDE
print(c.height)
assert(c.height == 35+30) --DOC_HIDE
print(c.height-2*c.border_width,35+30)
assert(c.height-2*c.border_width == 35+30) --DOC_HIDE

View File

@ -10,5 +10,7 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.top(client.focus)
assert(c.x == screen[1].geometry.width/2-40/2 and c.y==c.border_width--DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE
assert(c.x == screen[1].geometry.width/2-40/2-c.border_width)
assert(c.y==0) --DOC_HIDE
assert( c.width==40) --DOC_HIDE
assert(c.height==30)--DOC_HIDE

View File

@ -10,4 +10,4 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.top_left(client.focus)
assert(c.x == c.border_width and c.y==c.border_width and c.width==40 and c.height==30)--DOC_HIDE
assert(c.x == 0 and c.y==0 and c.width==40 and c.height==30)--DOC_HIDE

View File

@ -10,5 +10,5 @@ local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE
awful.placement.top_right(client.focus)
assert(c.x == screen[1].geometry.width-40-c.border_width and c.y==c.border_width --DOC_HIDE
assert(c.x == screen[1].geometry.width-40-2*c.border_width and c.y==0 --DOC_HIDE
and c.width==40 and c.height==30)--DOC_HIDE

View File

@ -12,6 +12,7 @@ local function add_signals(c)
c:add_signal("property::screen")
c:add_signal("property::geometry")
c:add_signal("request::geometry")
c:add_signal("request::tag")
c:add_signal("swapped")
c:add_signal("raised")
c:add_signal("property::_label") --Used internally

291
tests/test-awful-rules.lua Normal file
View File

@ -0,0 +1,291 @@
local awful = require("awful")
local beautiful = require("beautiful")
local test_client = require("_client")
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local callback_called = false
-- Magic table to store tests
local tests = {}
local tb_height = awful.util.round(beautiful.get_font_height() * 1.5)
-- local border_width = beautiful.border_width
local function test_rule(rule)
rule.rule = rule.rule or {}
-- Create a random class. The number need to be large as "rule1" and "rule10" would collide
-- The math.ceil is necessary to remove the floating point, this create an invalid dbus name
local class = string.format("rule%010d", 42+#tests)
rule.rule.class = rule.rule.class or class
test_client(rule.rule.class, rule.properties.name or "Foo")
if rule.test then
local t = rule.test
table.insert(tests, function() return t(rule.rule.class) end)
rule.test = nil
end
table.insert(awful.rules.rules, rule)
end
-- Helper function to search clients
local function get_client_by_class(class)
for _, c in ipairs(client.get()) do
if class == c.class then
return c
end
end
end
-- Test callback and floating
test_rule {
properties = { floating = true },
callback = function(c)
assert(type(c) == "client")
callback_called = true
end,
test = function(class)
-- Test if callbacks works
assert(callback_called)
-- Make sure "smart" dynamic properties are applied
assert(get_client_by_class(class).floating)
-- The size should not have changed
local geo = get_client_by_class(class):geometry()
assert(geo.width == 100 and geo.height == 100+tb_height)
return true
end
}
-- Test ontop
test_rule {
properties = { ontop = true },
test = function(class)
-- Make sure C-API properties are applied
assert(get_client_by_class(class).ontop)
return true
end
}
-- Test placement
test_rule {
properties = {
floating = true,
placement = "bottom_right"
},
test = function(class)
-- Test placement
local sgeo = mouse.screen.workarea
local c = get_client_by_class(class)
local geo = c:geometry()
assert(geo.y+geo.height+2*c.border_width == sgeo.y+sgeo.height)
assert(geo.x+geo.width+2*c.border_width == sgeo.x+sgeo.width)
return true
end
}
-- Create a tag named after the class name
test_rule {
properties = { new_tag=true },
test = function(class)
local c_new_tag1 = get_client_by_class(class)
assert(#c_new_tag1:tags()[1]:clients() == 1)
assert(c_new_tag1:tags()[1].name == class)
return true
end
}
-- Create a tag named Foo Tag with a magnifier layout
test_rule {
properties = {
new_tag = {
name = "Foo Tag",
layout = awful.layout.suit.magnifier
}
},
test = function(class)
local t_new_tag2 = get_client_by_class(class):tags()[1]
assert( #t_new_tag2:clients() == 1 )
assert( t_new_tag2.name == "Foo Tag" )
assert( t_new_tag2.layout.name == "magnifier" )
return true
end
}
-- Create a tag named "Bar Tag"
test_rule {
properties = { new_tag= "Bar Tag" },
test = function(class)
local c_new_tag3 = get_client_by_class(class)
assert(#c_new_tag3:tags()[1]:clients() == 1)
assert(c_new_tag3:tags()[1].name == "Bar Tag")
return true
end
}
-- Test if setting the geometry work
test_rule {
properties = {
floating = true,
x = 200,
y = 200,
width = 200,
height = 200,
},
test = function(class)
local c = get_client_by_class(class)
local geo = c:geometry()
-- Give it some time
if geo.y < 180 then return end
assert(geo.x == 200)
assert(geo.y == 200)
assert(geo.width == 200)
assert(geo.height == 200)
return true
end
}
-- Test if setting a partial geometry preserve the other attributes
test_rule {
properties = {
floating = true,
x = 200,
geometry = {
height = 220
}
},
test = function(class)
local c = get_client_by_class(class)
local geo = c:geometry()
-- Give it some time
if geo.height < 200 then return end
assert(geo.x == 200)
assert(geo.width == 100)
assert(geo.height == 220)
return true
end
}
-- Test maximized_horizontal
test_rule {
properties = { maximized_horizontal = true },
test = function(class)
local c = get_client_by_class(class)
-- Make sure C-API properties are applied
assert(c.maximized_horizontal)
local geo = c:geometry()
local sgeo = c.screen.workarea
assert(geo.x==sgeo.x)
assert(geo.width+2*c.border_width==sgeo.width)
return true
end
}
-- Test maximized_vertical
test_rule {
properties = { maximized_vertical = true },
test = function(class)
local c = get_client_by_class(class)
-- Make sure C-API properties are applied
assert(c.maximized_vertical)
local geo = c:geometry()
local sgeo = c.screen.workarea
assert(geo.y==sgeo.y)
assert(geo.height+2*c.border_width==sgeo.height)
return true
end
}
-- Test fullscreen
test_rule {
properties = { fullscreen = true },
test = function(class)
local c = get_client_by_class(class)
-- Make sure C-API properties are applied
assert(c.fullscreen)
local geo = c:geometry()
local sgeo = c.screen.geometry
assert(geo.x==sgeo.x)
assert(geo.y==sgeo.y)
assert(geo.height+2*c.border_width==sgeo.height)
assert(geo.width+2*c.border_width==sgeo.width)
return true
end
}
-- Test tag and switchtotag
test_rule {
properties = {
tag = "9",
switchtotag = true
},
test = function(class)
local c = get_client_by_class(class)
-- Make sure C-API properties are applied
assert(#c:tags() == 1)
assert(c:tags()[1].name == "9")
assert(c.screen.selected_tag.name == "9")
return true
end
}
test_rule {
properties = {
tag = "8",
switchtotag = false
},
test = function(class)
local c = get_client_by_class(class)
-- Make sure C-API properties are applied
assert(#c:tags() == 1)
assert(c:tags()[1].name == "8")
assert(c.screen.selected_tag.name ~= "8")
return true
end
}
-- Wait until all the auto-generated clients are ready
local function spawn_clients()
if #client.get() >= #tests then
-- Set tiled
awful.layout.inc(1)
return true
end
end
require("_runner").run_steps{spawn_clients, unpack(tests)}

243
tests/test-geometry.lua Normal file
View File

@ -0,0 +1,243 @@
--- Tests for drawing geometry
local runner = require( "_runner" )
local awful = require( "awful" )
local wibox = require( "wibox" )
local beautiful = require( "beautiful" )
local w = nil
local w1_draw, w2_draw
-- Disable automatic placement
awful.rules.rules = {
{ rule = { }, properties = {
border_width = 0,
size_hints_honor = false,
x = 0,
y = 0,
width = 100,
height = 100,
border_color = beautiful.border_normal
}
}
}
local steps = {
function()
if #client.get() == 0 then
return true
end
for _,c in ipairs(client.get()) do
c:kill()
end
end,
-- border_color should get applied via focus signal for first client on tag.
function(count)
if count == 1 then
awful.spawn("xterm")
else
local c = client.get()[1]
if c then
assert(c.size_hints_honor == false )
assert(c.border_width == 0 )
assert(c:geometry().x == 0 )
assert(c:geometry().y == 0 )
assert(c:geometry().height == 100 )
assert(c:geometry().width == 100 )
c:kill()
return true
end
end
end,
function()
awful.rules.rules = {
-- All clients will match this rule.
{ rule = { },properties = {
titlebars_enabled = true,
border_width = 10,
border_color = "#00ff00",
size_hints_honor = false,
x = 0,
y = 0,
width = 100,
height = 100
}
}
}
return true
end,
function(count)
if count == 1 then
awful.spawn("xterm")
else
local c = client.get()[1]
if c then
assert(c.border_width == 10 )
assert(c:geometry().x == 0 )
assert(c:geometry().y == 0 )
assert(c:geometry().height == 100 )
assert(c:geometry().width == 100 )
c.border_width = 20
return true
end
end
end,
function()
local c = client.get()[1]
assert(c.border_width == 20 )
assert(c:geometry().x == 0 )
assert(c:geometry().y == 0 )
assert(c:geometry().height == 100 )
assert(c:geometry().width == 100 )
c.border_width = 0
return true
end,
function()
local c = client.get()[1]
assert(not pcall(function() c.border_width = -2000 end))
assert(c.border_width==0)
c.border_width = 125
return true
end,
function()
local c = client.get()[1]
assert(c.border_width == 125 )
assert(c:geometry().x == 0 )
assert(c:geometry().y == 0 )
assert(c:geometry().height == 100 )
assert(c:geometry().width == 100 )
-- So it doesn't hide the other tests
c:kill()
return true
end,
function()
w = wibox {
ontop = true,
border_width = 20,
x = 100,
y = 100,
width = 100,
height = 100,
visible = true,
}
assert(w)
assert(w.border_width == 20 )
assert(w.width == 100 )
assert(w.height == 100 )
assert(w.x == 100 )
assert(w.y == 100 )
w:setup {
fit = function(_, _, _, height)
return height, height -- A square taking the full height
end,
draw = function(_, _, cr, width, height)
assert(width == 100)
assert(height == 100)
w1_draw = true
cr:set_source_rgb(1, 0, 0) -- Red
cr:arc(height/2, height/2, height/2, 0, math.pi*2)
cr:fill()
end,
layout = wibox.widget.base.make_widget,
}
return true
end,
function()
w.border_width = 0
assert(w1_draw)
return true
end,
function()
assert(w.border_width == 0 )
assert(w.width == 100 )
assert(w.height == 100 )
assert(w.x == 100 )
assert(w.y == 100 )
w:setup {
fit = function(_, _, _, height)
return height, height -- A square taking the full height
end,
draw = function(_, _, cr, width, height)
assert(width == 100)
assert(height == 100)
w2_draw = true
cr:set_source_rgb(1, 0, 0) -- Red
cr:arc(height/2, height/2, height/2, 0, math.pi*2)
cr:fill()
end,
layout = wibox.widget.base.make_widget,
}
w.visible = false
return true
end,
function()
-- Remove the widget before the size change to avoid the asserts
w:setup {
fit = function(_, _, _, height)
return height, height -- A square taking the full height
end,
draw = function(_, _, cr, _, height)
cr:set_source_rgb(1, 0, 1) -- Purple
cr:arc(height/2, height/2, height/2, 0, math.pi*2)
cr:fill()
end,
layout = wibox.widget.base.make_widget,
}
w.visible = true
assert(w2_draw)
assert(w.border_width == 0 )
assert(w.width == 100 )
assert(w.height == 100 )
assert(w.x == 100 )
assert(w.y == 100 )
w.border_width = 5
w:geometry {
x = 200,
y = 200,
width = 200,
height = 200,
}
return true
end,
function()
assert(w.border_width == 5 )
assert(w.width == 200 )
assert(w.height == 200 )
assert(w.x == 200 )
assert(w.y == 200 )
return true
end
}
runner.run_steps(steps)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

90
tests/test-maximize.lua Normal file
View File

@ -0,0 +1,90 @@
--- Tests maximize and fullscreen
local runner = require("_runner")
local awful = require("awful")
local original_geo = nil
local steps = {
function(count)
if count == 1 then
awful.spawn("xterm")
else
local c = client.get()[1]
if c then
original_geo = c:geometry()
return true
end
end
end,
-- maximize horizontally
function()
local c = client.get()[1]
assert(not c.maximized_horizontal)
assert(not c.maximized_vertical )
assert(not c.fullscreen )
c.maximized_horizontal = true
return true
end,
function()
local c = client.get()[1]
--local new_geo = c:geometry()
--local sgeo = c.screen.workarea
--assert(new_geo.x-c.border_width==sgeo.x) --FIXME c:geometry({x=1}).x ~= 1
--assert(new_geo.width+2*c.border_width==sgeo.width) --FIXME off by 4px
c.maximized_horizontal = false
return true
end,
-- Test restoring a geometry
function()
local c = client.get()[1]
local new_geo = c:geometry()
for k,v in pairs(original_geo) do
assert(new_geo[k] == v)
end
c.fullscreen = true
return true
end,
function()
local c = client.get()[1]
local new_geo = c:geometry()
local sgeo = c.screen.geometry
local bw = c.border_width
assert(c.fullscreen)
assert(new_geo.x-bw==sgeo.x)
assert(new_geo.y-bw==sgeo.y)
assert(new_geo.x+new_geo.width+bw==sgeo.x+sgeo.width)
assert(new_geo.y+new_geo.height+bw==sgeo.y+sgeo.height)
c.fullscreen = false
return true
end,
function()
local c = client.get()[1]
local new_geo = c:geometry()
for k,v in pairs(original_geo) do
assert(new_geo[k] == v)
end
return true
end
}
runner.run_steps(steps)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -55,7 +55,7 @@ local steps = {
root.fake_input("key_release", "2")
root.fake_input("key_release", "Super_L")
elseif awful.tag.selectedlist()[1] == tags[awful.screen.focused()][2] then
elseif awful.screen.focused().selected_tags[1] == tags[awful.screen.focused()][2] then
assert(#client.get() == 1)
local c = client.get()[1]
assert(not c.urgent, "Client is not urgent anymore.")
@ -79,11 +79,11 @@ local steps = {
awful.spawn("xterm")
elseif awful.tag.selectedlist()[1] == tags[awful.screen.focused()][2] then
elseif awful.screen.focused().selected_tags[1] == tags[awful.screen.focused()][2] then
assert(not urgent_cb_done)
assert(awful.tag.getproperty(tags[awful.screen.focused()][2], "urgent") == false)
assert(awful.tag.getproperty(tags[awful.screen.focused()][2], "urgent_count") == 0)
assert(awful.tag.selectedlist()[2] == nil)
assert(awful.screen.focused().selected_tags[2] == nil)
return true
end
end,