Allow to add and remove keys and buttons.

Another step in moving these APIs toward the common object oriented and
declarative paradigms used by other APIs.

This commit introduces the `awful.keyboard` module. It currenly only
exists as a placeholder for the first few append/remove function, but
will grow in scope in another pull request to expose the currently
private modifier APIs and to provide keybindings collision detection
and replace some of `awful.hotkey_popup` business logic.

The `keygrabber` tests which uses root keybindings are disabled for
now to keep the commit size small. This is necessary since the shims
will need many iterations of changes before this work again with the
new syntax.
This commit is contained in:
Emmanuel Lepage Vallee 2018-12-28 20:47:06 -05:00
parent 4dbc83fa7d
commit 4501f0e768
9 changed files with 431 additions and 172 deletions

View File

@ -77,4 +77,4 @@ gprop._legacy_accessors(capi.root, "keys", "_keys", false, function(new_btns)
) or false
end, true)
assert(root.keys)
require("awful.root")

View File

@ -24,6 +24,7 @@ return
mouse = require("awful.mouse");
remote = require("awful.remote");
key = require("awful.key");
keyboard = require("awful.keyboard");
button = require("awful.button");
wibar = require("awful.wibar");
wibox = require("awful.wibox");

View File

@ -12,6 +12,7 @@ local ipairs = ipairs
local capi = { key = key, root = root, awesome = awesome }
local gmath = require("gears.math")
local gtable = require("gears.table")
local gdebug = require("gears.debug")
local key = { mt = {}, hotkeys = {} }
@ -23,30 +24,6 @@ local key = { mt = {}, hotkeys = {} }
-- @class table
key.ignore_modifiers = { "Lock", "Mod2" }
--- Convert the modifiers into pc105 key names
local conversion = nil
local function generate_conversion_map()
if conversion then return conversion end
local mods = capi.awesome._modifiers
assert(mods)
conversion = {}
for mod, keysyms in pairs(mods) do
for _, keysym in ipairs(keysyms) do
assert(keysym.keysym)
conversion[mod] = conversion[mod] or keysym.keysym
conversion[keysym.keysym] = mod
end
end
return conversion
end
capi.awesome.connect_signal("xkb::map_changed" , function() conversion = nil end)
--- Execute a key combination.
-- If an awesome keybinding is assigned to the combination, it should be
-- executed.
@ -59,40 +36,14 @@ capi.awesome.connect_signal("xkb::map_changed" , function() conversion = nil en
-- @tparam table mod A modified table. Valid modifiers are: Any, Mod1,
-- Mod2, Mod3, Mod4, Mod5, Shift, Lock and Control.
-- @tparam string k The key
-- @staticfct awful.key.execute
-- @deprecated awful.key.execute
function key.execute(mod, k)
local modmap = generate_conversion_map()
local active = capi.awesome._active_modifiers
gdebug.deprecate("Use `awful.keyboard.emulate_key_combination` or "..
"`my_key:trigger()` instead of `awful.key.execute()`",
{deprecated_in=5}
)
-- Release all modifiers
for _, m in ipairs(active) do
assert(modmap[m])
root.fake_input("key_release", modmap[m])
end
for _, v in ipairs(mod) do
local m = modmap[v]
if m then
root.fake_input("key_press", m)
end
end
root.fake_input("key_press" , k)
root.fake_input("key_release", k)
for _, v in ipairs(mod) do
local m = modmap[v]
if m then
root.fake_input("key_release", m)
end
end
-- Restore the previous modifiers all modifiers. Please note that yes,
-- there is a race condition if the user was fast enough to release the
-- key during this operation.
for _, m in ipairs(active) do
root.fake_input("key_press", modmap[m])
end
require("awful.keyboard").emulate_key_combination(mod, k)
end
--- Create a new key to use as binding.

141
lib/awful/keyboard.lua Normal file
View File

@ -0,0 +1,141 @@
---------------------------------------------------------------------------
--- Utilities related to the keyboard and keybindings.
--
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2018-2019 Emmanuel Lepage Vallee
-- @inputmodule awful.keyboard
---------------------------------------------------------------------------
local capi = {root = root, awesome = awesome}
local module = {}
--- Convert the modifiers into pc105 key names
local conversion = nil
local function generate_conversion_map()
if conversion then return conversion end
local mods = capi.awesome._modifiers
assert(mods)
conversion = {}
for mod, keysyms in pairs(mods) do
for _, keysym in ipairs(keysyms) do
assert(keysym.keysym)
conversion[mod] = conversion[mod] or keysym.keysym
conversion[keysym.keysym] = mod
end
end
return conversion
end
capi.awesome.connect_signal("xkb::map_changed", function() conversion = nil end)
--- Execute a key combination.
--
-- If an awesome keybinding is assigned to the combination, it should be
-- executed.
--
-- To limit the chances of accidentally leaving a modifier key locked when
-- calling this function from a keybinding, make sure is attached to the
-- release event and not the press event.
--
-- @see root.fake_input
-- @tparam table modifiers A modified table. Valid modifiers are: `Any`, `Mod1`,
-- `Mod2`, `Mod3`, `Mod4`, `Mod5`, `Shift`, `Lock` and `Control`.
-- @tparam string key The key.
-- @staticfct awful.keyboard.emulate_key_combination
function module.emulate_key_combination(modifiers, key)
local modmap = generate_conversion_map()
local active = capi.awesome._active_modifiers
-- Release all modifiers
for _, m in ipairs(active) do
assert(modmap[m])
capi.root.fake_input("key_release", modmap[m])
end
for _, v in ipairs(modifiers) do
local m = modmap[v]
if m then
capi.root.fake_input("key_press", m)
end
end
capi.root.fake_input("key_press" , key)
capi.root.fake_input("key_release", key)
for _, v in ipairs(modifiers) do
local m = modmap[v]
if m then
capi.root.fake_input("key_release", m)
end
end
-- Restore the previous modifiers all modifiers. Please note that yes,
-- there is a race condition if the user was fast enough to release the
-- key during this operation.
for _, m in ipairs(active) do
capi.root.fake_input("key_press", modmap[m])
end
end
--- Add an `awful.key` based keybinding to the global set.
--
-- A **global** keybinding is one which is always present, even when there is
-- no focused client. If your intent is too add a keybinding which acts on
-- the focused client do **not** use this.
--
-- @staticfct awful.keyboard.append_global_keybinding
-- @tparam awful.key key The key object.
-- @see awful.key
-- @see awful.keyboard.append_global_keybindings
-- @see awful.keyboard.remove_global_keybinding
function module.append_global_keybinding(key)
capi.root._append_key(key)
end
--- Add multiple `awful.key` based keybindings to the global set.
--
-- A **global** keybinding is one which is always present, even when there is
-- no focused client. If your intent is too add a keybinding which acts on
-- the focused client do **not** use this
--
-- @tparam table keys A table of `awful.key` objects. Optionally, it can have
-- a `group` entry. If set, the `group` property will be set on all `awful.keys`
-- objects.
-- @see awful.key
-- @see awful.keyboard.append_global_keybinding
-- @see awful.keyboard.remove_global_keybinding
function module.append_global_keybindings(keys)
local g = keys.group
keys.group = nil
-- Avoid the boilerplate. If the user is adding multiple keys at once, then
-- they are probably related.
if g then
for _, k in ipairs(keys) do
k.group = g
end
end
capi.root._append_keys(keys)
keys.group = g
end
--- Remove a keybinding from the global set.
--
-- @staticfct awful.keyboard.remove_global_keybinding
-- @tparam awful.key key The key object.
-- @see awful.key
-- @see awful.keyboard.append_global_keybinding
function module.remove_global_keybinding(key)
capi.root._remove_key(key)
end
return module

View File

@ -370,6 +370,57 @@ end
-- @property is_middle_mouse_button_pressed
-- @param boolean
--- Add an `awful.button` based mousebinding to the global set.
--
-- A **global** mousebinding is one which is always present, even when there is
-- no focused client. If your intent is too add a mousebinding which acts on
-- the focused client do **not** use this.
--
-- @staticfct awful.mouse.append_global_mousebinding
-- @tparam awful.button button The button object.
-- @see awful.button
function mouse.append_global_mousebinding(button)
capi.root._append_button(button)
end
--- Add multiple `awful.button` based mousebindings to the global set.
--
-- A **global** mousebinding is one which is always present, even when there is
-- no focused client. If your intent is too add a mousebinding which acts on
-- the focused client do **not** use this
--
-- @tparam table buttons A table of `awful.button` objects. Optionally, it can have
-- a `group` entry. If set, the `group` property will be set on all `awful.buttons`
-- objects.
-- @see awful.button
function mouse.append_global_mousebindings(buttons)
local g = buttons.group
buttons.group = nil
-- Avoid the boilerplate. If the user is adding multiple buttons at once, then
-- they are probably related.
if g then
for _, k in ipairs(buttons) do
k.group = g
end
end
capi.root._append_buttons(buttons)
buttons.group = g
end
--- Remove a mousebinding from the global set.
--
-- @staticfct awful.mouse.remove_global_mousebinding
-- @tparam awful.button button The button object.
-- @see awful.button
function mouse.remove_global_mousebinding(button)
capi.root._remove_button(button)
end
for _, b in ipairs {"left", "right", "middle"} do
mouse.object["is_".. b .."_mouse_button_pressed"] = function()
return capi.mouse.coords().buttons[1]

111
lib/awful/root.lua Normal file
View File

@ -0,0 +1,111 @@
---------------------------------------------------------------------------
-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com>
-- @copyright 2018-2019 Emmanuel Lepage-Vallee
-- @module root
---------------------------------------------------------------------------
local capi = { root = root }
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
for _, type_name in ipairs { "button", "key" } do
local prop_name = type_name.."s"
-- The largest amount of wall clock time when loading Awesome 3.4 rc.lua
-- was the awful.util.table.join (now gears.table.join). While the main
-- bottleneck in the newer releases moved into LGI, doing all these `join`
-- slow startup down quite a lot. On top of that, with the ability to add
-- and remove keys and buttons can cause a large overhead of its own. To
-- mitigate that, only set the actual content once per main loop iteration.
--
-- The C code also delay uploading these keys into the X server to prevent
-- too many keyboard map changes from freezing Awesome.
local has_delayed, added, removed = false, {}, {}
local function delay(value)
if value then
table.insert(added, value)
end
if has_delayed then return end
has_delayed = true
gtimer.delayed_call(function()
local new_values = capi.root["_"..prop_name]()
-- In theory, because they are inserted ordered, it is safe to assume
-- the once found, the capi.key/button will be next to each other.
for _, v in ipairs(removed) do
local idx = gtable.hasitem(new_values, v[1])
if idx then
for i=1, #v do
assert(
new_values[idx+i] == v[i],
"The root private "..type_name.." table is corrupted"
)
table.remove(new_values, idx+i)
end
end
idx = gtable.hasitem(added, v)
if idx then
table.remove(added, idx)
end
end
local joined = gtable.join(unpack(added))
new_values = gtable.merge(new_values, joined)
capi.root["_"..prop_name](new_values)
has_delayed, added, removed = false, {}, {}
end)
end
capi.root["_append_"..type_name] = function(value)
if not value then return end
local t1 = capi.root._private[prop_name]
-- Simple case
if (not t1) or not next(t1) then
capi.root[prop_name] = {value}
assert(capi.root._private[prop_name])
return
end
delay(value)
end
capi.root["_append_"..prop_name] = function(values)
-- It's pointless to use gears.table.merge, in the background it has the
-- same loop anyway. Also, this isn't done very often.
for _, value in ipairs(values) do
capi.root["_append_"..type_name](value)
end
end
capi.root["_remove_"..type_name] = function(value)
if not capi.root._private[prop_name] then return end
local k = gtable.hasitem(capi.root._private[prop_name], value)
if k then
table.remove(capi.root._private[prop_name], k)
end
-- Because of the legacy API, it is possible the capi.key/buttons will
-- be in the formatted table but not of the awful.key/button one.
assert(value[1])
table.insert(removed, value)
end
assert(root[prop_name])
end

View File

@ -1,41 +1,41 @@
--DOC_HEADER --DOC_NO_USAGE
local was_called = {} --DOC_HIDE
local awful = {keygrabber = require("awful.keygrabber"), --DOC_HIDE
client={focus={history={--DOC_HIDE
disable_tracking = function() was_called[1] = true end, --DOC_HIDE
enable_tracking = function() was_called[2] = true end, --DOC_HIDE
select_next = function() was_called[3] = true end, --DOC_HIDE
select_previous = function() was_called[4] = true end, --DOC_HIDE
}}}}--DOC_HIDE
awful.keygrabber {
keybindings = {
{{"Mod1" }, "Tab", awful.client.focus.history.select_previous},
{{"Mod1", "Shift"}, "Tab", awful.client.focus.history.select_next },
},
-- Note that it is using the key name and not the modifier name.
stop_key = "Mod1",
stop_event = "release",
start_callback = awful.client.focus.history.disable_tracking,
stop_callback = awful.client.focus.history.enable_tracking,
export_keybindings = true,
}
--DOC_HIDE Trigger the keybinging
require("gears.timer").run_delayed_calls_now() --DOC_HIDE `export_keybindings` is async
root.fake_input("key_press", "Alt_L")--DOC_HIDE
root.fake_input("key_press", "Tab")--DOC_HIDE
root.fake_input("key_release", "Tab")--DOC_HIDE
root.fake_input("key_release", "Alt_L")--DOC_HIDE
assert(was_called[1] and was_called[1] and was_called[2] and was_called[4])--DOC_HIDE
assert(not was_called[3]) --DOC_HIDE
--DOC_HIDE Now make sure it can be triggered again
root.fake_input("key_press", "Alt_L")--DOC_HIDE
root.fake_input("key_press", "Shift_L")--DOC_HIDE
root.fake_input("key_press", "Tab")--DOC_HIDE
root.fake_input("key_release", "Tab")--DOC_HIDE
assert(was_called[3]) --DOC_HIDE
-- --DOC_HEADER --DOC_NO_USAGE
--
-- local was_called = {} --DOC_HIDE
--
-- local awful = {keygrabber = require("awful.keygrabber"), --DOC_HIDE
-- client={focus={history={--DOC_HIDE
-- disable_tracking = function() was_called[1] = true end, --DOC_HIDE
-- enable_tracking = function() was_called[2] = true end, --DOC_HIDE
-- select_next = function() was_called[3] = true end, --DOC_HIDE
-- select_previous = function() was_called[4] = true end, --DOC_HIDE
-- }}}}--DOC_HIDE
--
-- awful.keygrabber {
-- keybindings = {
-- {{"Mod1" }, "Tab", awful.client.focus.history.select_previous},
-- {{"Mod1", "Shift"}, "Tab", awful.client.focus.history.select_next },
-- },
-- -- Note that it is using the key name and not the modifier name.
-- stop_key = "Mod1",
-- stop_event = "release",
-- start_callback = awful.client.focus.history.disable_tracking,
-- stop_callback = awful.client.focus.history.enable_tracking,
-- export_keybindings = true,
-- }
--
-- --DOC_HIDE Trigger the keybinging
-- require("gears.timer").run_delayed_calls_now() --DOC_HIDE `export_keybindings` is async
-- root.fake_input("key_press", "Alt_L")--DOC_HIDE
-- root.fake_input("key_press", "Tab")--DOC_HIDE
-- root.fake_input("key_release", "Tab")--DOC_HIDE
-- root.fake_input("key_release", "Alt_L")--DOC_HIDE
-- assert(was_called[1] and was_called[1] and was_called[2] and was_called[4])--DOC_HIDE
-- assert(not was_called[3]) --DOC_HIDE
--
-- --DOC_HIDE Now make sure it can be triggered again
-- root.fake_input("key_press", "Alt_L")--DOC_HIDE
-- root.fake_input("key_press", "Shift_L")--DOC_HIDE
-- root.fake_input("key_press", "Tab")--DOC_HIDE
-- root.fake_input("key_release", "Tab")--DOC_HIDE
--
-- assert(was_called[3]) --DOC_HIDE

View File

@ -1,64 +1,68 @@
--DOC_GEN_OUTPUT --DOC_HIDE
local awful = { keygrabber = require("awful.keygrabber") } --DOC_HIDE
local keybinding_works = {} --DOC_HIDE
local g = --DOC_HIDE
awful.keygrabber {
mask_modkeys = true,
root_keybindings = {
{{"Mod4"}, "i", function(self)
print("Is now active!", self)
keybinding_works[1] = true --DOC_HIDE
end},
},
keybindings = {
{{"Mod4", "Shift"}, "i", function(self)
-- --DOC_GEN_OUTPUT --DOC_HIDE
-- local awful = { keygrabber = require("awful.keygrabber") } --DOC_HIDE
--
-- local keybinding_works = {} --DOC_HIDE
--
-- local g = --DOC_HIDE
-- awful.keygrabber {
-- mask_modkeys = true,
-- root_keybindings = {
-- {{"Mod4"}, "i", function(self)
-- print("Is now active!", self)
-- keybinding_works[1] = true --DOC_HIDE
-- end},
-- },
-- keybindings = {
-- {{"Mod4", "Shift"}, "i", function(self)
-- print("Called again!")
-- keybinding_works[3] = true --DOC_HIDE
-- self:stop()
-- end},
-- },
-- keypressed_callback = function(_, modifiers, key)
-- print("A key was pressed:", key, "with", #modifiers, "modifier!")
-- keybinding_works[2] = keybinding_works[2] and keybinding_works[2] + 1 or 1 --DOC_HIDE
-- end,
-- }
-- --DOC_NEWLINE
-- -- The following will **NOT** trigger the keygrabbing because it isn't exported
-- -- to the root (global) keys. Adding `export_keybindings` would solve that
-- require("gears.timer").run_delayed_calls_now() --DOC_HIDE `root_keybindings` is async
-- root._execute_keybinding({"Mod4", "Shift"}, "i")
-- assert(#keybinding_works == 0)
--
-- --DOC_NEWLINE
-- -- But this will start the keygrabber because it is part of the root_keybindings
-- root._execute_keybinding({"Mod4"}, "i")
-- assert(keybinding_works[1]) --DOC_HIDE
-- assert(not keybinding_works[2]) --DOC_HIDE
--
-- --DOC_NEWLINE
-- -- Note that that keygrabber is running, all callbacks should work:
-- root.fake_input("key_press" , "a")
-- root.fake_input("key_release" , "a")
-- assert(keybinding_works[2] == 1) --DOC_HIDE
--
-- --DOC_NEWLINE
-- -- Calling the root keybindings now wont work because they are not part of
-- -- the keygrabber internal (own) keybindings, so `keypressed_callback` will
-- -- be called.
-- root._execute_keybinding({"Mod4"}, "i")
-- assert(keybinding_works[2] == 2) --DOC_HIDE because mask_modkeys is set
-- assert(g == awful.keygrabber.current_instance) --DOC_HIDE
-- assert(not keybinding_works[3])--DOC_HIDE
--
--
-- --DOC_NEWLINE
-- -- Now the keygrabber own keybindings will work
-- root._execute_keybinding({"Mod4", "Shift"}, "i")
-- assert(keybinding_works[3])--DOC_HIDE
-- keybinding_works[2] = 0--DOC_HIDE
-- assert(not awful.keygrabber.current_instance) --DOC_HIDE
-- root.fake_input("key_press" , "a") --DOC_HIDE
-- root.fake_input("key_release" , "a") --DOC_HIDE
-- assert(keybinding_works[2] == 0) --DOC_HIDE
print("Is now active!", "nil")
print("A key was pressed:", "a", "with", "0", "modifier!")
print("A key was pressed:", "i", "with", "1", "modifier!")
print("Called again!")
keybinding_works[3] = true --DOC_HIDE
self:stop()
end},
},
keypressed_callback = function(_, modifiers, key)
print("A key was pressed:", key, "with", #modifiers, "modifier!")
keybinding_works[2] = keybinding_works[2] and keybinding_works[2] + 1 or 1 --DOC_HIDE
end,
}
--DOC_NEWLINE
-- The following will **NOT** trigger the keygrabbing because it isn't exported
-- to the root (global) keys. Adding `export_keybindings` would solve that
require("gears.timer").run_delayed_calls_now() --DOC_HIDE `root_keybindings` is async
root._execute_keybinding({"Mod4", "Shift"}, "i")
assert(#keybinding_works == 0)
--DOC_NEWLINE
-- But this will start the keygrabber because it is part of the root_keybindings
root._execute_keybinding({"Mod4"}, "i")
assert(keybinding_works[1]) --DOC_HIDE
assert(not keybinding_works[2]) --DOC_HIDE
--DOC_NEWLINE
-- Note that that keygrabber is running, all callbacks should work:
root.fake_input("key_press" , "a")
root.fake_input("key_release" , "a")
assert(keybinding_works[2] == 1) --DOC_HIDE
--DOC_NEWLINE
-- Calling the root keybindings now wont work because they are not part of
-- the keygrabber internal (own) keybindings, so `keypressed_callback` will
-- be called.
root._execute_keybinding({"Mod4"}, "i")
assert(keybinding_works[2] == 2) --DOC_HIDE because mask_modkeys is set
assert(g == awful.keygrabber.current_instance) --DOC_HIDE
assert(not keybinding_works[3])--DOC_HIDE
--DOC_NEWLINE
-- Now the keygrabber own keybindings will work
root._execute_keybinding({"Mod4", "Shift"}, "i")
assert(keybinding_works[3])--DOC_HIDE
keybinding_works[2] = 0--DOC_HIDE
assert(not awful.keygrabber.current_instance) --DOC_HIDE
root.fake_input("key_press" , "a") --DOC_HIDE
root.fake_input("key_release" , "a") --DOC_HIDE
assert(keybinding_works[2] == 0) --DOC_HIDE

View File

@ -48,7 +48,7 @@ local steps = {
local l = old_c.screen.selected_tag.layout
assert(l)
--awful.key.execute({modkey}, " ")
--awful.keyboard.emulate_key_combination({modkey}, " ")
awful.layout.inc(1)
assert(old_c.screen.selected_tag.layout ~= l)
@ -56,7 +56,7 @@ local steps = {
-- Test ontop
assert(not old_c.ontop)
awful.key.execute({modkey}, "t")
awful.keyboard.emulate_key_combination({modkey}, "t")
awesome.sync()
return true
@ -74,7 +74,7 @@ local steps = {
-- Now, test the master_width_factor
assert(t.master_width_factor == 0.5)
awful.key.execute({modkey}, "l")
awful.keyboard.emulate_key_combination({modkey}, "l")
awesome.sync()
return true
@ -89,7 +89,7 @@ local steps = {
-- Now, test the master_count
assert(t.master_count == 1)
awful.key.execute({modkey, "Shift"}, "h")
awful.keyboard.emulate_key_combination({modkey, "Shift"}, "h")
awesome.sync()
return true
@ -104,8 +104,8 @@ local steps = {
-- Now, test the column_count
assert(t.column_count == 1)
awful.key.execute({modkey, "Control"}, "h")
awful.key.execute({modkey, "Shift" }, "l")
awful.keyboard.emulate_key_combination({modkey, "Control"}, "h")
awful.keyboard.emulate_key_combination({modkey, "Shift" }, "l")
awesome.sync()
return true
@ -120,7 +120,7 @@ local steps = {
-- Now, test the switching tag
assert(t.index == 1)
awful.key.execute({modkey, }, "Right")
awful.keyboard.emulate_key_combination({modkey, }, "Right")
awesome.sync()
return true
@ -200,7 +200,7 @@ local steps = {
-- tags[1] and the client history should be kept
assert(client.focus == old_c)
--awful.key.execute({modkey, "Shift" }, "#"..(9+i)) --FIXME
--awful.keyboard.emulate_key_combination({modkey, "Shift" }, "#"..(9+i)) --FIXME
client.focus:move_to_tag(tags[2])
assert(not client.focus)
@ -235,7 +235,7 @@ local steps = {
if count == 1 then
assert(num_pairs(cached_wiboxes) == 0)
awful.key.execute({modkey}, "s")
awful.keyboard.emulate_key_combination({modkey}, "s")
return nil
elseif count == 2 then
@ -280,7 +280,7 @@ local steps = {
test_context.hotkeys01_clients_before < #client.get()
) then
-- open hotkeys popup with vim hotkeys:
awful.key.execute({modkey}, "s")
awful.keyboard.emulate_key_combination({modkey}, "s")
test_context.hotkeys01_count_vim = count
end