diff --git a/lib/awful/ewmh.lua b/lib/awful/ewmh.lua index ced26772..5b557c12 100644 --- a/lib/awful/ewmh.lua +++ b/lib/awful/ewmh.lua @@ -15,7 +15,26 @@ local aclient = require("awful.client") local aplace = require("awful.placement") local asuit = require("awful.layout.suit") -local ewmh = {} +local ewmh = { + generic_activate_filters = {}, + contextual_activate_filters = {}, +} + +--- The list of all registered generic request::activate (focus stealing) +-- filters. If a filter is added to only one context, it will be in +-- `ewmh.contextual_activate_filters`["context_name"]. +-- @table[opt={}] generic_activate_filters +-- @see ewmh.activate +-- @see ewmh.add_activate_filter +-- @see ewmh.remove_activate_filter + +--- The list of all registered contextual request::activate (focus stealing) +-- filters. If a filter is added to only one context, it will be in +-- `ewmh.generic_activate_filters`. +-- @table[opt={}] contextual_activate_filters +-- @see ewmh.activate +-- @see ewmh.add_activate_filter +-- @see ewmh.remove_activate_filter --- Update a client's settings when its geometry changes, skipping signals -- resulting from calls within. @@ -57,9 +76,27 @@ function ewmh.activate(c, context, hints) -- luacheck: no unused args if c.focusable == false and not hints.force then return end - if c:isvisible() then - client.focus = c + local found, ret = false + + -- Execute the filters until something handle the request + for _, tab in ipairs { + ewmh.contextual_activate_filters[context] or {}, + ewmh.generic_activate_filters + } do + for i=#tab, 1, -1 do + ret = tab[i](c, context, hints) + if ret ~= nil then found=true; break end + end + + if found then break end end + + if ret ~= false and c:isvisible() then + client.focus = c + elseif ret == false and not hints.force then + return + end + if hints and hints.raise then c:raise() if not awesome.startup and not c:isvisible() then @@ -68,6 +105,69 @@ function ewmh.activate(c, context, hints) -- luacheck: no unused args end end +--- Add an activate (focus stealing) filter function. +-- +-- The callback takes the following parameters: +-- +-- * **c** (*client*) The client requesting the activation +-- * **context** (*string*) The activation context. +-- * **hints** (*table*) Some additional hints (depending on the context) +-- +-- If the callback returns `true`, the client will be activated unless the `force` +-- hint is set. If the callback returns `false`, the activation request is +-- cancelled. If the callback returns `nil`, the previous callback will be +-- executed. This will continue until either a callback handles the request or +-- when it runs out of callbacks. In that case, the request will be granted if +-- the client is visible. +-- +-- For example, to block Firefox from stealing the focus, use: +-- +-- awful.ewmh.add_activate_filter(function(c, "ewmh") +-- if c.class == "Firefox" then return false end +-- end) +-- +-- @tparam function f The callback +-- @tparam[opt] string context The `request::activate` context +-- @see generic_activate_filters +-- @see contextual_activate_filters +-- @see remove_activate_filter +function ewmh.add_activate_filter(f, context) + if not context then + table.insert(ewmh.generic_activate_filters, f) + else + ewmh.contextual_activate_filters[context] = + ewmh.contextual_activate_filters[context] or {} + + table.insert(ewmh.contextual_activate_filters[context], f) + end +end + +--- Remove an activate (focus stealing) filter function. +-- This is an helper to avoid dealing with `ewmh.add_activate_filter` directly. +-- @tparam function f The callback +-- @tparam[opt] string context The `request::activate` context +-- @treturn boolean If the callback existed +-- @see generic_activate_filters +-- @see contextual_activate_filters +-- @see add_activate_filter +function ewmh.remove_activate_filter(f, context) + local tab = context and (ewmh.contextual_activate_filters[context] or {}) + or ewmh.generic_activate_filters + + for k, v in ipairs(tab) do + if v == f then + table.remove(tab, k) + + -- In case the callback is there multiple time. + ewmh.remove_activate_filter(f, context) + + return true + end + end + + return false +end + -- Get tags that are on the same screen as the client. This should _almost_ -- always return the same content as c:tags(). local function get_valid_tags(c, s) diff --git a/objects/client.c b/objects/client.c index 6d28b5aa..0b81bdc1 100644 --- a/objects/client.c +++ b/objects/client.c @@ -152,8 +152,34 @@ */ /** When a client should get activated (focused and/or raised). + * + * **Contexts are:** + * + * * *ewmh*: When a client ask for focus (from `X11` events) + * * *autofocus.check_focus*: When autofocus is enabled(from `awful.autofocus`) + * * *autofocus.check_focus_tag*: When autofocus is enabled + * (from `awful.autofocus`) + * * *client.jumpto*: When a custom lua extension ask a client to be focused + * (from `client.jump_to`) + * * *client.swap.global_bydirection*: When client swapping require a focus + * change (from `awful.client.swap.bydirection`) + * * *client.movetotag*: When a client is moved to a new tag + * (from `client.move_to_tag`) + * * *client.movetoscreen*: When the client is moved to a new screen + * (from `client.move_to_screen`) + * * *client.focus.byidx*: When selecting a client using its index + * (from `awful.client.focus.byidx`) + * * *client.focus.history.previous*: When cycling through history + * (from `awful.client.focus.history.previous`) + * * *menu.clients*: When using the build in client menu + * (from `awful.menu.clients`) + * * *rules*: When a new client is focused from a rule (from `awful.rules`) + * * *screen.focus*: When a screen is focused (from `awful.screen.focus`) * * Default implementation: `awful.ewmh.activate`. + * + * To implement focus stealing filters see `awful.ewmh.add_activate_filter`. + * * @signal request::activate * @tparam string context The context where this signal was used. * @tparam[opt] table hints A table with additional hints: diff --git a/tests/test-awful-client.lua b/tests/test-awful-client.lua index 25d9bfa2..26c11968 100644 --- a/tests/test-awful-client.lua +++ b/tests/test-awful-client.lua @@ -77,6 +77,54 @@ end end } +local original_count, c1, c2 = 0 + +-- Check request::activate +table.insert(steps, function() + c1, c2 = client.get()[1], client.get()[2] + + -- This should still be the case + assert(client.focus == c1) + + c2:emit_signal("request::activate", "i_said_so") + + return true +end) + +-- Check if writing a focus stealing filter works. +table.insert(steps, function() + -- This should still be the case + assert(client.focus == c2) + + original_count = #awful.ewmh.generic_activate_filters + + awful.ewmh.add_activate_filter(function(c) + if c == c1 then return false end + end) + + c1:emit_signal("request::activate", "i_said_so") + + return true +end) + +table.insert(steps, function() + -- The request should have been denied + assert(client.focus == c2) + + -- Test the remove function + awful.ewmh.remove_activate_filter(function() end) + + awful.ewmh.add_activate_filter(awful.ewmh.generic_activate_filters[1]) + + awful.ewmh.remove_activate_filter(awful.ewmh.generic_activate_filters[1]) + + assert(original_count == #awful.ewmh.generic_activate_filters) + + c1:emit_signal("request::activate", "i_said_so") + + return client.focus == c1 +end) + local has_error -- Disable awful.screen.preferred(c)