awful.screenshot: Add keyboard support.
This commit is contained in:
parent
fa971ceb7c
commit
b7dcece40c
|
@ -6,7 +6,6 @@
|
||||||
-- @inputmodule awful.screenshot
|
-- @inputmodule awful.screenshot
|
||||||
---------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
-- Grab environment we need
|
-- Grab environment we need
|
||||||
local capi = {
|
local capi = {
|
||||||
root = root,
|
root = root,
|
||||||
|
@ -21,6 +20,7 @@ local wibox = require("wibox")
|
||||||
local cairo = require("lgi").cairo
|
local cairo = require("lgi").cairo
|
||||||
local abutton = require("awful.button")
|
local abutton = require("awful.button")
|
||||||
local akey = require("awful.key")
|
local akey = require("awful.key")
|
||||||
|
local akgrabber = require("awful.keygrabber")
|
||||||
local glib = require("lgi").GLib
|
local glib = require("lgi").GLib
|
||||||
local datetime = glib.DateTime
|
local datetime = glib.DateTime
|
||||||
local timezone = glib.TimeZone
|
local timezone = glib.TimeZone
|
||||||
|
@ -212,14 +212,19 @@ local function show_frame(self, surface, geo)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Emitted when the interactive succeed.
|
--- Emitted when the interactive snipping starts.
|
||||||
|
-- @signal snipping::start
|
||||||
|
-- @tparam awful.screenshot self
|
||||||
|
|
||||||
|
--- Emitted when the interactive snipping succeed.
|
||||||
-- @signal snipping::success
|
-- @signal snipping::success
|
||||||
-- @tparam awful.screenshot self
|
-- @tparam awful.screenshot self
|
||||||
|
|
||||||
--- Emitted when the interactive is cancelled.
|
--- Emitted when the interactive snipping is cancelled.
|
||||||
-- @signal snipping::cancelled
|
-- @signal snipping::cancelled
|
||||||
-- @tparam awful.screenshot self
|
-- @tparam awful.screenshot self
|
||||||
-- @tparam string reason Either `"mouse_button"`, `"key"`, or `"too_small"`.
|
-- @tparam string reason Either `"mouse_button"`, `"key"`, `"no_selection"`
|
||||||
|
-- or `"too_small"`.
|
||||||
|
|
||||||
-- Internal function that generates the callback to be passed to the
|
-- Internal function that generates the callback to be passed to the
|
||||||
-- mousegrabber that implements the interactive mode.
|
-- mousegrabber that implements the interactive mode.
|
||||||
|
@ -244,18 +249,13 @@ local function start_snipping(self, surface, geometry, method)
|
||||||
show_frame(self, surface, geometry)
|
show_frame(self, surface, geometry)
|
||||||
|
|
||||||
local function ret_mg_callback(mouse_data, accept, reject)
|
local function ret_mg_callback(mouse_data, accept, reject)
|
||||||
local frame = self._private.frame
|
|
||||||
|
|
||||||
for btn, status in pairs(mouse_data.buttons) do
|
for btn, status in pairs(mouse_data.buttons) do
|
||||||
accept = accept or (status and accept_buttons[btn])
|
accept = accept or (status and accept_buttons[btn])
|
||||||
reject = reject or (status and reject_buttons[btn])
|
reject = reject or (status and reject_buttons[btn])
|
||||||
end
|
end
|
||||||
|
|
||||||
if reject then
|
if reject then
|
||||||
frame.visible = false
|
self:reject("mouse_button")
|
||||||
self._private.frame, self._private.mg_first_pnt = nil, nil
|
|
||||||
self:emit_signal("snipping::cancelled", "mouse_button")
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
elseif pressed then
|
elseif pressed then
|
||||||
local min_x = math.min(self._private.mg_first_pnt[1], mouse_data.x)
|
local min_x = math.min(self._private.mg_first_pnt[1], mouse_data.x)
|
||||||
|
@ -263,44 +263,25 @@ local function start_snipping(self, surface, geometry, method)
|
||||||
local min_y = math.min(self._private.mg_first_pnt[2], mouse_data.y)
|
local min_y = math.min(self._private.mg_first_pnt[2], mouse_data.y)
|
||||||
local max_y = math.max(self._private.mg_first_pnt[2], mouse_data.y)
|
local max_y = math.max(self._private.mg_first_pnt[2], mouse_data.y)
|
||||||
|
|
||||||
local new_geo = {
|
self._private.selected_geometry = {
|
||||||
x = min_x,
|
x = min_x,
|
||||||
y = min_y,
|
y = min_y,
|
||||||
width = max_x - min_x,
|
width = max_x - min_x,
|
||||||
height = max_y - min_y,
|
height = max_y - min_y,
|
||||||
|
method = method,
|
||||||
|
surface = surface,
|
||||||
}
|
}
|
||||||
|
self:emit_signal("property::selected_geometry", self._private.selected_geometry)
|
||||||
|
|
||||||
if not accept then
|
if not accept then
|
||||||
-- Released
|
-- Released
|
||||||
|
return self:accept()
|
||||||
self._private.frame, self._private.mg_first_pnt = nil, nil
|
|
||||||
|
|
||||||
-- This may fail gracefully anyway but require a minimum 3x3 of pixels
|
|
||||||
if min_x >= max_x-1 or min_y >= max_y-1 then
|
|
||||||
self:emit_signal("snipping::cancelled", "too_small")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
self._private.selection_widget.visible = false
|
|
||||||
|
|
||||||
self._private.surfaces[method] = {
|
|
||||||
surface = crop_shot(surface, new_geo),
|
|
||||||
geometry = new_geo
|
|
||||||
}
|
|
||||||
|
|
||||||
self:emit_signal("snipping::success")
|
|
||||||
self:save()
|
|
||||||
|
|
||||||
frame.visible = false
|
|
||||||
self._private.frame, self._private.mg_first_pnt = nil, nil
|
|
||||||
|
|
||||||
return false
|
|
||||||
else
|
else
|
||||||
-- Update position
|
-- Update position
|
||||||
self._private.selection_widget.point.x = min_x
|
self._private.selection_widget.point.x = min_x
|
||||||
self._private.selection_widget.point.y = min_y
|
self._private.selection_widget.point.y = min_y
|
||||||
self._private.selection_widget.fit = function()
|
self._private.selection_widget.fit = function()
|
||||||
return new_geo.width, new_geo.height
|
return self._private.selected_geometry.width, self._private.selected_geometry.height
|
||||||
end
|
end
|
||||||
self._private.selection_widget:emit_signal("widget::layout_changed")
|
self._private.selection_widget:emit_signal("widget::layout_changed")
|
||||||
self._private.canvas_widget:emit_signal("widget::redraw_needed")
|
self._private.canvas_widget:emit_signal("widget::redraw_needed")
|
||||||
|
@ -317,7 +298,9 @@ local function start_snipping(self, surface, geometry, method)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.keygrabber:start()
|
||||||
capi.mousegrabber.run(ret_mg_callback, self.cursor)
|
capi.mousegrabber.run(ret_mg_callback, self.cursor)
|
||||||
|
self:emit_signal("snipping::start")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Internal function exected when a root window screenshot is taken.
|
-- Internal function exected when a root window screenshot is taken.
|
||||||
|
@ -400,6 +383,7 @@ end
|
||||||
--- The date format used in the default suffix.
|
--- The date format used in the default suffix.
|
||||||
-- @property date_format
|
-- @property date_format
|
||||||
-- @tparam[opt="%Y%m%d%H%M%S"] string date_format
|
-- @tparam[opt="%Y%m%d%H%M%S"] string date_format
|
||||||
|
-- @propemits true false
|
||||||
-- @see wibox.widget.textclock
|
-- @see wibox.widget.textclock
|
||||||
|
|
||||||
--- The cursor used in interactive mode.
|
--- The cursor used in interactive mode.
|
||||||
|
@ -412,11 +396,13 @@ end
|
||||||
--
|
--
|
||||||
-- @property interactive
|
-- @property interactive
|
||||||
-- @tparam[opt=false] boolean interactive
|
-- @tparam[opt=false] boolean interactive
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
--- Get screenshot screen.
|
--- Get screenshot screen.
|
||||||
--
|
--
|
||||||
-- @property screen
|
-- @property screen
|
||||||
-- @tparam[opt=nil] screen|nil screen
|
-- @tparam[opt=nil] screen|nil screen
|
||||||
|
-- @propemits true false
|
||||||
-- @see mouse.screen
|
-- @see mouse.screen
|
||||||
-- @see awful.screen.focused
|
-- @see awful.screen.focused
|
||||||
-- @see screen.primary
|
-- @see screen.primary
|
||||||
|
@ -425,6 +411,7 @@ end
|
||||||
--
|
--
|
||||||
-- @property client
|
-- @property client
|
||||||
-- @tparam[opt=nil] client|nil client
|
-- @tparam[opt=nil] client|nil client
|
||||||
|
-- @propemits true false
|
||||||
-- @see mouse.client
|
-- @see mouse.client
|
||||||
-- @see client.focus
|
-- @see client.focus
|
||||||
|
|
||||||
|
@ -436,6 +423,7 @@ end
|
||||||
-- @tparam table geometry.y
|
-- @tparam table geometry.y
|
||||||
-- @tparam table geometry.width
|
-- @tparam table geometry.width
|
||||||
-- @tparam table geometry.height
|
-- @tparam table geometry.height
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
--- Get screenshot surface.
|
--- Get screenshot surface.
|
||||||
--
|
--
|
||||||
|
@ -472,6 +460,21 @@ end
|
||||||
-- @readonly
|
-- @readonly
|
||||||
-- @see surface
|
-- @see surface
|
||||||
|
|
||||||
|
--- Set a minimum size to save a screenshot.
|
||||||
|
--
|
||||||
|
-- When the screenshot is very small (like 1x1 pixels), it is probably a mistake.
|
||||||
|
-- Rather than save useless files, set this property to auto-reject tiny images.
|
||||||
|
--
|
||||||
|
-- @property minimum_size
|
||||||
|
-- @tparam[opt={width=3, height=3}] nil|integer|table minimum_size
|
||||||
|
-- @tparam integer minimum_size.width
|
||||||
|
-- @tparam integer minimum_size.height
|
||||||
|
-- @propertytype nil No minimum size.
|
||||||
|
-- @propertytype integer Same minimum size for the height and width.
|
||||||
|
-- @propertytype table Different size for the height and width.
|
||||||
|
-- @negativeallowed false
|
||||||
|
-- @propemits true false
|
||||||
|
|
||||||
--- The interactive frame color.
|
--- The interactive frame color.
|
||||||
-- @property frame_color
|
-- @property frame_color
|
||||||
-- @tparam color|nil frame_color
|
-- @tparam color|nil frame_color
|
||||||
|
@ -500,6 +503,30 @@ end
|
||||||
-- @propemits true false
|
-- @propemits true false
|
||||||
-- @see reject_buttons
|
-- @see reject_buttons
|
||||||
|
|
||||||
|
--- The `awful.keygrabber` object used for the accept and reject keys.
|
||||||
|
--
|
||||||
|
-- This can be used to add new keybindings to the interactive snipper mode. For
|
||||||
|
-- examples, this can be used to change the saving path or add some annotations
|
||||||
|
-- to the image.
|
||||||
|
--
|
||||||
|
-- @property keygrabber
|
||||||
|
-- @tparam awful.keygrabber keygrabber
|
||||||
|
-- @propertydefault Autogenerated.
|
||||||
|
-- @readonly
|
||||||
|
|
||||||
|
--- The current interactive snipping mode seletion.
|
||||||
|
-- @property selected_geometry
|
||||||
|
-- @tparam nil|table selected_geometry
|
||||||
|
-- @tparam integer selected_geometry.x
|
||||||
|
-- @tparam integer selected_geometry.y
|
||||||
|
-- @tparam integer selected_geometry.width
|
||||||
|
-- @tparam integer selected_geometry.height
|
||||||
|
-- @tparam image selected_geometry.surface
|
||||||
|
-- @tparam string selected_geometry.method Either `"root"`, `"client"`,
|
||||||
|
-- `"screen"` or `"geometry"`.
|
||||||
|
-- @propemits true false
|
||||||
|
-- @readonly
|
||||||
|
|
||||||
local defaults = {
|
local defaults = {
|
||||||
prefix = "Screenshot-",
|
prefix = "Screenshot-",
|
||||||
directory = screenshot_validation.directory(os.getenv("HOME")),
|
directory = screenshot_validation.directory(os.getenv("HOME")),
|
||||||
|
@ -510,13 +537,14 @@ local defaults = {
|
||||||
accept_buttons = {abutton({}, 1)},
|
accept_buttons = {abutton({}, 1)},
|
||||||
reject_keys = {akey({}, "Escape")},
|
reject_keys = {akey({}, "Escape")},
|
||||||
accept_keys = {akey({}, "Return")},
|
accept_keys = {akey({}, "Return")},
|
||||||
|
minimum_size = {width = 3, height = 3},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Create the standard properties.
|
-- Create the standard properties.
|
||||||
for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_format",
|
for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_format",
|
||||||
"prefix", "directory", "file_path", "file_name", "cursor",
|
"prefix", "directory", "file_path", "file_name", "cursor",
|
||||||
"interactive", "reject_buttons", "accept_buttons",
|
"interactive", "reject_buttons", "accept_buttons",
|
||||||
"reject_keys", "accept_keys", "frame_shape" } do
|
"reject_keys", "accept_keys", "frame_shape", "minimum_size" } do
|
||||||
module["set_"..prop] = function(self, value)
|
module["set_"..prop] = function(self, value)
|
||||||
self._private[prop] = screenshot_validation[prop]
|
self._private[prop] = screenshot_validation[prop]
|
||||||
and screenshot_validation[prop](value) or value
|
and screenshot_validation[prop](value) or value
|
||||||
|
@ -528,6 +556,10 @@ for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_for
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function module:get_selected_geometry()
|
||||||
|
return self._private.selected_geometry
|
||||||
|
end
|
||||||
|
|
||||||
function module:get_file_path()
|
function module:get_file_path()
|
||||||
return self._private.file_path or make_file_path(self)
|
return self._private.file_path or make_file_path(self)
|
||||||
end
|
end
|
||||||
|
@ -560,6 +592,59 @@ function module:get_surfaces()
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function module:get_keygrabber()
|
||||||
|
if self._private.keygrabber then return self._private.keygrabber end
|
||||||
|
|
||||||
|
self._private.keygrabber = akgrabber {
|
||||||
|
stop_key = self.reject_buttons
|
||||||
|
}
|
||||||
|
self._private.keygrabber:connect_signal("keybinding::triggered", function(_, key, event)
|
||||||
|
if event == "press" then return end
|
||||||
|
if self._private.accept_keys_set[key] then
|
||||||
|
self:accept()
|
||||||
|
elseif self._private.reject_keys_set[key] then
|
||||||
|
self:reject()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Reload the keys.
|
||||||
|
self.accept_keys, self.reject_keys = self.accept_keys, self.reject_keys
|
||||||
|
|
||||||
|
return self._private.keygrabber
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put the key in a set rather than a list and add/remove them from the keygrabber.
|
||||||
|
for _, prop in ipairs {"accept_keys", "reject_keys"} do
|
||||||
|
local old = module["set_"..prop]
|
||||||
|
|
||||||
|
module["set_"..prop] = function(self, new_keys)
|
||||||
|
-- Remove old keys.
|
||||||
|
if self._private.keygrabber then
|
||||||
|
for _, key in ipairs(self._private[prop] or {}) do
|
||||||
|
self._private.keygrabber:remove_keybinding(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_set = {}
|
||||||
|
self._private[prop.."_set"] = new_set
|
||||||
|
|
||||||
|
for _, key in ipairs(new_keys) do
|
||||||
|
self.keygrabber:add_keybinding(key)
|
||||||
|
new_set[key] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
old(self, new_keys)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function module:set_minimum_size(size)
|
||||||
|
if size and type(size) ~= "table" then
|
||||||
|
size = {width = math.ceil(size), height = math.ceil(size)}
|
||||||
|
end
|
||||||
|
self._private.minimum_size = size
|
||||||
|
self:emit_signal("property::minimum_size", size)
|
||||||
|
end
|
||||||
|
|
||||||
--- Take new screenshot(s) now.
|
--- Take new screenshot(s) now.
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
|
@ -625,6 +710,70 @@ function module:save(file_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Save and exit the interactive snipping mode.
|
||||||
|
-- @method accept
|
||||||
|
-- @treturn boolean `true` if the screenshot were save, `false` otherwise. It
|
||||||
|
-- can be `false` because the selection is below `minimum_size` or because
|
||||||
|
-- there is nothing to save (so selection).
|
||||||
|
-- @emits snipping::cancelled
|
||||||
|
-- @emits snipping::success
|
||||||
|
|
||||||
|
function module:accept()
|
||||||
|
-- Nothing to do.
|
||||||
|
if not self.interactive then return true end
|
||||||
|
|
||||||
|
local new_geo = self._private.selected_geometry
|
||||||
|
local min_size = self.minimum_size
|
||||||
|
|
||||||
|
if not new_geo then
|
||||||
|
self:reject("no_selection")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This may fail gracefully anyway but require a minimum 3x3 of pixels
|
||||||
|
if min_size and new_geo.width < min_size.width or new_geo.height < min_size.height then
|
||||||
|
self:reject("too_small")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
self._private.selection_widget.visible = false
|
||||||
|
|
||||||
|
self._private.surfaces[new_geo.method] = {
|
||||||
|
surface = crop_shot(new_geo.surface, new_geo),
|
||||||
|
geometry = new_geo
|
||||||
|
}
|
||||||
|
|
||||||
|
self:emit_signal("snipping::success")
|
||||||
|
self:save()
|
||||||
|
|
||||||
|
self._private.frame.visible = false
|
||||||
|
self._private.frame, self._private.mg_first_pnt = nil, nil
|
||||||
|
self.keygrabber:stop()
|
||||||
|
capi.mousegrabber.stop()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Exit the interactive snipping mode without saving.
|
||||||
|
-- @method reject
|
||||||
|
-- @tparam[opt=nil] string||nil reason The reason why it was rejected. This is
|
||||||
|
-- passed to the `"snipping::cancelled"` signal.
|
||||||
|
-- @emits snipping::cancelled
|
||||||
|
|
||||||
|
function module:reject(reason)
|
||||||
|
-- Nothing to do.
|
||||||
|
if not self.interactive then return end
|
||||||
|
|
||||||
|
if self._private.frame then
|
||||||
|
self._private.frame.visible = false
|
||||||
|
self._private.frame = nil
|
||||||
|
end
|
||||||
|
self._private.mg_first_pnt = nil
|
||||||
|
self:emit_signal("snipping::cancelled", reason or "reject_called")
|
||||||
|
capi.mousegrabber.stop()
|
||||||
|
self.keygrabber:stop()
|
||||||
|
end
|
||||||
|
|
||||||
--- Screenshot constructor - it is possible to call this directly, but it is.
|
--- Screenshot constructor - it is possible to call this directly, but it is.
|
||||||
-- recommended to use the helper constructors, such as awful.screenshot.root
|
-- recommended to use the helper constructors, such as awful.screenshot.root
|
||||||
--
|
--
|
||||||
|
|
|
@ -412,4 +412,39 @@ table.insert(steps, function()
|
||||||
return get_pixel(img, 10, 10) == "#00ff00"
|
return get_pixel(img, 10, 10) == "#00ff00"
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local escape_works, enter_works = false, false
|
||||||
|
|
||||||
|
table.insert(steps, function()
|
||||||
|
local ss = awful.screenshot { interactive = true }
|
||||||
|
|
||||||
|
ss:connect_signal("snipping::start", function()
|
||||||
|
ss:connect_signal("snipping::cancelled", function() enter_works = true end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
ss:refresh()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(steps, function()
|
||||||
|
if not enter_works then
|
||||||
|
root.fake_input("key_press","Return")
|
||||||
|
root.fake_input("key_release","Return")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ss = awful.screenshot { interactive = true }
|
||||||
|
ss:connect_signal("snipping::cancelled", function() escape_works = true end)
|
||||||
|
ss:refresh()
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(steps, function()
|
||||||
|
if not escape_works then
|
||||||
|
root.fake_input("key_press","Escape")
|
||||||
|
root.fake_input("key_release","Escape")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
require("_runner").run_steps(steps)
|
require("_runner").run_steps(steps)
|
||||||
|
|
Loading…
Reference in New Issue