awful.screenshot: Add keyboard support.
This commit is contained in:
parent
fa971ceb7c
commit
b7dcece40c
|
@ -6,7 +6,6 @@
|
|||
-- @inputmodule awful.screenshot
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
|
||||
-- Grab environment we need
|
||||
local capi = {
|
||||
root = root,
|
||||
|
@ -21,6 +20,7 @@ local wibox = require("wibox")
|
|||
local cairo = require("lgi").cairo
|
||||
local abutton = require("awful.button")
|
||||
local akey = require("awful.key")
|
||||
local akgrabber = require("awful.keygrabber")
|
||||
local glib = require("lgi").GLib
|
||||
local datetime = glib.DateTime
|
||||
local timezone = glib.TimeZone
|
||||
|
@ -212,14 +212,19 @@ local function show_frame(self, surface, geo)
|
|||
}
|
||||
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
|
||||
-- @tparam awful.screenshot self
|
||||
|
||||
--- Emitted when the interactive is cancelled.
|
||||
--- Emitted when the interactive snipping is cancelled.
|
||||
-- @signal snipping::cancelled
|
||||
-- @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
|
||||
-- mousegrabber that implements the interactive mode.
|
||||
|
@ -244,18 +249,13 @@ local function start_snipping(self, surface, geometry, method)
|
|||
show_frame(self, surface, geometry)
|
||||
|
||||
local function ret_mg_callback(mouse_data, accept, reject)
|
||||
local frame = self._private.frame
|
||||
|
||||
for btn, status in pairs(mouse_data.buttons) do
|
||||
accept = accept or (status and accept_buttons[btn])
|
||||
reject = reject or (status and reject_buttons[btn])
|
||||
end
|
||||
|
||||
if reject then
|
||||
frame.visible = false
|
||||
self._private.frame, self._private.mg_first_pnt = nil, nil
|
||||
self:emit_signal("snipping::cancelled", "mouse_button")
|
||||
|
||||
self:reject("mouse_button")
|
||||
return false
|
||||
elseif pressed then
|
||||
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 max_y = math.max(self._private.mg_first_pnt[2], mouse_data.y)
|
||||
|
||||
local new_geo = {
|
||||
self._private.selected_geometry = {
|
||||
x = min_x,
|
||||
y = min_y,
|
||||
width = max_x - min_x,
|
||||
height = max_y - min_y,
|
||||
method = method,
|
||||
surface = surface,
|
||||
}
|
||||
self:emit_signal("property::selected_geometry", self._private.selected_geometry)
|
||||
|
||||
if not accept then
|
||||
-- Released
|
||||
|
||||
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
|
||||
return self:accept()
|
||||
else
|
||||
-- Update position
|
||||
self._private.selection_widget.point.x = min_x
|
||||
self._private.selection_widget.point.y = min_y
|
||||
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
|
||||
self._private.selection_widget:emit_signal("widget::layout_changed")
|
||||
self._private.canvas_widget:emit_signal("widget::redraw_needed")
|
||||
|
@ -317,7 +298,9 @@ local function start_snipping(self, surface, geometry, method)
|
|||
return true
|
||||
end
|
||||
|
||||
self.keygrabber:start()
|
||||
capi.mousegrabber.run(ret_mg_callback, self.cursor)
|
||||
self:emit_signal("snipping::start")
|
||||
end
|
||||
|
||||
-- Internal function exected when a root window screenshot is taken.
|
||||
|
@ -400,6 +383,7 @@ end
|
|||
--- The date format used in the default suffix.
|
||||
-- @property date_format
|
||||
-- @tparam[opt="%Y%m%d%H%M%S"] string date_format
|
||||
-- @propemits true false
|
||||
-- @see wibox.widget.textclock
|
||||
|
||||
--- The cursor used in interactive mode.
|
||||
|
@ -412,11 +396,13 @@ end
|
|||
--
|
||||
-- @property interactive
|
||||
-- @tparam[opt=false] boolean interactive
|
||||
-- @propemits true false
|
||||
|
||||
--- Get screenshot screen.
|
||||
--
|
||||
-- @property screen
|
||||
-- @tparam[opt=nil] screen|nil screen
|
||||
-- @propemits true false
|
||||
-- @see mouse.screen
|
||||
-- @see awful.screen.focused
|
||||
-- @see screen.primary
|
||||
|
@ -425,6 +411,7 @@ end
|
|||
--
|
||||
-- @property client
|
||||
-- @tparam[opt=nil] client|nil client
|
||||
-- @propemits true false
|
||||
-- @see mouse.client
|
||||
-- @see client.focus
|
||||
|
||||
|
@ -436,6 +423,7 @@ end
|
|||
-- @tparam table geometry.y
|
||||
-- @tparam table geometry.width
|
||||
-- @tparam table geometry.height
|
||||
-- @propemits true false
|
||||
|
||||
--- Get screenshot surface.
|
||||
--
|
||||
|
@ -472,6 +460,21 @@ end
|
|||
-- @readonly
|
||||
-- @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.
|
||||
-- @property frame_color
|
||||
-- @tparam color|nil frame_color
|
||||
|
@ -500,6 +503,30 @@ end
|
|||
-- @propemits true false
|
||||
-- @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 = {
|
||||
prefix = "Screenshot-",
|
||||
directory = screenshot_validation.directory(os.getenv("HOME")),
|
||||
|
@ -510,13 +537,14 @@ local defaults = {
|
|||
accept_buttons = {abutton({}, 1)},
|
||||
reject_keys = {akey({}, "Escape")},
|
||||
accept_keys = {akey({}, "Return")},
|
||||
minimum_size = {width = 3, height = 3},
|
||||
}
|
||||
|
||||
-- Create the standard properties.
|
||||
for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_format",
|
||||
"prefix", "directory", "file_path", "file_name", "cursor",
|
||||
"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)
|
||||
self._private[prop] = screenshot_validation[prop]
|
||||
and screenshot_validation[prop](value) or value
|
||||
|
@ -528,6 +556,10 @@ for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_for
|
|||
end
|
||||
end
|
||||
|
||||
function module:get_selected_geometry()
|
||||
return self._private.selected_geometry
|
||||
end
|
||||
|
||||
function module:get_file_path()
|
||||
return self._private.file_path or make_file_path(self)
|
||||
end
|
||||
|
@ -560,6 +592,59 @@ function module:get_surfaces()
|
|||
return ret
|
||||
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.
|
||||
--
|
||||
--
|
||||
|
@ -625,6 +710,70 @@ function module:save(file_path)
|
|||
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.
|
||||
-- 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"
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue