Merge pull request #3461 from poisson-aerohead/screenshot-lib
User Facing Screenshot API
This commit is contained in:
commit
17eee18d8b
|
@ -39,6 +39,7 @@ local ret = {
|
|||
rules = require("awful.rules");
|
||||
popup = require("awful.popup");
|
||||
spawn = require("awful.spawn");
|
||||
screenshot = require("awful.screenshot");
|
||||
}
|
||||
|
||||
-- Lazy load deprecated modules to reduce the numbers of loop dependencies.
|
||||
|
|
|
@ -321,7 +321,7 @@ local function new_common(mod, keys, press, release, data)
|
|||
|
||||
-- append custom userdata (like description) to a hotkey
|
||||
data = data and gtable.clone(data) or {}
|
||||
data.mod = mod
|
||||
data.mod, data.modifiers = mod, mod
|
||||
data.keys = keys
|
||||
data.on_press = press
|
||||
data.on_release = release
|
||||
|
|
|
@ -222,7 +222,7 @@ local function runner(self, modifiers, key, event)
|
|||
local filtered_modifiers = {}
|
||||
|
||||
-- User defined cases
|
||||
if self._private.keybindings[key] and event == "press" then
|
||||
if self._private.keybindings[key] then
|
||||
-- Remove caps and num lock
|
||||
for _, m in ipairs(modifiers) do
|
||||
if not gtable.hasitem(akey.ignore_modifiers, m) then
|
||||
|
@ -236,11 +236,18 @@ local function runner(self, modifiers, key, event)
|
|||
for _,v2 in ipairs(v.modifiers) do
|
||||
match = match and mod[v2]
|
||||
end
|
||||
if match and v.on_press then
|
||||
v.on_press(self)
|
||||
|
||||
if self.mask_event_callback ~= false then
|
||||
return
|
||||
if match then
|
||||
self:emit_signal("keybinding::triggered", v, event)
|
||||
|
||||
if event == "press" and v.on_press then
|
||||
v.on_press(self)
|
||||
|
||||
if self.mask_event_callback ~= false then
|
||||
return
|
||||
end
|
||||
elseif event == "release" and v.on_release then
|
||||
v.on_release(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -542,6 +549,7 @@ end
|
|||
-- @tparam awful.key key The key.
|
||||
-- @tparam string description.group The keybinding group
|
||||
-- @noreturn
|
||||
-- @see remove_keybinding
|
||||
|
||||
function keygrabber:add_keybinding(key, keycode, callback, description)
|
||||
local mods = not key._is_awful_key and key or nil
|
||||
|
@ -574,6 +582,27 @@ function keygrabber:add_keybinding(key, keycode, callback, description)
|
|||
end
|
||||
end
|
||||
|
||||
--- Remove a keybinding from the keygrabber.
|
||||
--
|
||||
-- @method remove_keybinding
|
||||
-- @treturn boolean `true` if removed, `false` if not found.
|
||||
-- @see add_keybinding
|
||||
|
||||
function keygrabber:remove_keybinding(key)
|
||||
for idx, obj in ipairs(self._private.keybindings[key.key]) do
|
||||
if obj == key then
|
||||
table.remove(self._private.keybindings[key.key], idx)
|
||||
|
||||
if #self._private.keybindings[key.key] == 0 then
|
||||
self._private.keybindings[key.key] = nil
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function keygrabber:set_root_keybindings(keys)
|
||||
local real_keys = {}
|
||||
|
||||
|
@ -630,6 +659,12 @@ end
|
|||
--- When the keygrabber stops.
|
||||
-- @signal stopped
|
||||
|
||||
--- When an `awful.key` is pressed.
|
||||
-- @signal keybinding::triggered
|
||||
-- @tparam awful.keybinding self
|
||||
-- @tparam awful.key key The keybinding.
|
||||
-- @tparam string event Either `"press"` or `"release"`.
|
||||
|
||||
--- A function called when a keygrabber starts.
|
||||
-- @callback start_callback
|
||||
-- @tparam keygrabber keygrabber The keygrabber.
|
||||
|
|
|
@ -0,0 +1,943 @@
|
|||
---------------------------------------------------------------------------
|
||||
--- Take screenshots of clients, screens, geometry and export to files or widgets.
|
||||
--
|
||||
-- Common keybindings
|
||||
-- ==================
|
||||
--
|
||||
-- This example setups keybinding for the "Print Screen" key. Shift means
|
||||
-- interactive, Control means delayed and Mod4 (Super) means current client only.
|
||||
-- This example also creates convinient notifications.
|
||||
--
|
||||
-- @DOC_awful_screenshot_keybindings1_EXAMPLE@
|
||||
--
|
||||
-- Convert to widgets
|
||||
-- ==================
|
||||
--
|
||||
-- This example creates and `Alt+Tab` like popup with client images. Note that
|
||||
-- it might display black rectangles if you are not using a compositing manager
|
||||
-- such as `picom`.
|
||||
--
|
||||
-- @DOC_awful_screenshot_popup_EXAMPLE@
|
||||
--
|
||||
-- @author Brian Sobulefsky <brian.sobulefsky@protonmail.com>
|
||||
-- @copyright 2021 Brian Sobulefsky
|
||||
-- @classmod awful.screenshot
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- Grab environment we need
|
||||
local capi = {
|
||||
root = root,
|
||||
screen = screen,
|
||||
client = client,
|
||||
mousegrabber = mousegrabber
|
||||
}
|
||||
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
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 gtimer = require("gears.timer")
|
||||
local glib = require("lgi").GLib
|
||||
local datetime = glib.DateTime
|
||||
local timezone = glib.TimeZone
|
||||
|
||||
-- The module to be returned
|
||||
local module = { mt = {}, _screenshot_methods = {} }
|
||||
local screenshot_validation = {}
|
||||
|
||||
local datetime_obj = datetime.new_now
|
||||
|
||||
-- When $SOURCE_DATE_EPOCH and $SOURCE_DIRECTORY are both set, then this code is
|
||||
-- most likely being run by the test runner. Ensure reproducible dates.
|
||||
local source_date_epoch = tonumber(os.getenv("SOURCE_DATE_EPOCH"))
|
||||
if source_date_epoch and os.getenv("SOURCE_DIRECTORY") then
|
||||
datetime_obj = function()
|
||||
return datetime.new_from_unix_utc(source_date_epoch)
|
||||
end
|
||||
end
|
||||
|
||||
-- Generate a date string with the same format as the `textclock` and also with
|
||||
-- support Debian reproducible builds.
|
||||
local function get_date(format)
|
||||
return datetime_obj(timezone.new_local()):format(format)
|
||||
end
|
||||
|
||||
-- Convert to a real image surface so it can be added to an imagebox.
|
||||
local function to_surface(raw_surface, width, height)
|
||||
local img = cairo.ImageSurface(cairo.Format.RGB24, width, height)
|
||||
local cr = cairo.Context(img)
|
||||
cr:set_source_surface(gears.surface(raw_surface))
|
||||
cr:paint()
|
||||
|
||||
return img
|
||||
end
|
||||
|
||||
--- The screenshot interactive frame color.
|
||||
-- @beautiful beautiful.screenshot_frame_color
|
||||
-- @tparam[opt="#ff0000"] color screenshot_frame_color
|
||||
|
||||
--- The screenshot interactive frame shape.
|
||||
-- @beautiful beautiful.screenshot_frame_shape
|
||||
-- @tparam[opt=gears.shape.rectangle] shape screenshot_frame_shape
|
||||
|
||||
function screenshot_validation.directory(directory)
|
||||
-- Fully qualify a "~/" path or a relative path to $HOME. One might argue
|
||||
-- that the relative path should fully qualify to $PWD, but for a window
|
||||
-- manager this should be the same thing to the extent that anything else
|
||||
-- is arguably unexpected behavior.
|
||||
if string.find(directory, "^~/") then
|
||||
directory = string.gsub(directory, "^~/",
|
||||
string.gsub(os.getenv("HOME"), "/*$", "/", 1))
|
||||
elseif string.find(directory, "^[^/]") then
|
||||
directory = string.gsub(os.getenv("HOME"), "/*$", "/", 1) .. directory
|
||||
end
|
||||
|
||||
-- Assure that we return exactly one trailing slash
|
||||
directory = string.gsub(directory, '/*$', '/', 1)
|
||||
|
||||
-- Add a trailing "/" if none is present.
|
||||
if directory:sub(-1) ~= "/" then
|
||||
directory = directory .. "/"
|
||||
end
|
||||
|
||||
-- If the directory eixsts, but cannot be used, print and error.
|
||||
if gears.filesystem.is_dir(directory) and not gears.filesystem.dir_writable(directory) then
|
||||
gears.debug.print_error("`"..directory.. "` is not writable.")
|
||||
end
|
||||
|
||||
return directory
|
||||
end
|
||||
|
||||
-- Internal function to sanitize a prefix string
|
||||
--
|
||||
-- Currently only strips all path separators ('/'). Allows for empty prefix.
|
||||
function screenshot_validation.prefix(prefix)
|
||||
if prefix:match("[/.]") then
|
||||
gears.debug.print_error("`"..prefix..
|
||||
"` is not a valid prefix because it contains `/` or `.`")
|
||||
end
|
||||
|
||||
return prefix
|
||||
end
|
||||
|
||||
-- Internal routine to verify that a file_path is valid.
|
||||
function screenshot_validation.file_path(file_path)
|
||||
if gears.filesystem.file_readable(file_path) then
|
||||
gears.debug.print_error("`"..file_path.."` already exist.")
|
||||
end
|
||||
|
||||
return file_path
|
||||
end
|
||||
|
||||
-- Warn about invalid geometries.
|
||||
function screenshot_validation.geometry(geo)
|
||||
for _, part in ipairs {"x", "y", "width", "height" } do
|
||||
if not geo[part] then
|
||||
gears.debug.print_error("The screenshot geometry must be a table with "..
|
||||
"`x`, `y`, `width` and `height`"
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return geo
|
||||
end
|
||||
|
||||
function screenshot_validation.screen(scr)
|
||||
return capi.screen[scr]
|
||||
end
|
||||
|
||||
local function make_file_name(self, method)
|
||||
local date_time = get_date(self.date_format)
|
||||
method = method and method.."_" or ""
|
||||
return self.prefix .. "_" .. method .. date_time .. ".png"
|
||||
end
|
||||
|
||||
local function make_file_path(self, method)
|
||||
return self.directory .. (self._private.file_name or make_file_name(self, method))
|
||||
end
|
||||
|
||||
-- Internal function to do the actual work of taking a cropped screenshot
|
||||
--
|
||||
-- This function does not do any data checking. Public functions should do
|
||||
-- all data checking. This function was made to combine the interactive sipper
|
||||
-- run by the mousegrabber and the programmatically defined snip function,
|
||||
-- though there may be other uses.
|
||||
local function crop_shot(source, geo)
|
||||
local target = source:create_similar(cairo.Content.COLOR, geo.width, geo.height)
|
||||
|
||||
local cr = cairo.Context(target)
|
||||
cr:set_source_surface(source, -geo.x, -geo.y)
|
||||
cr:rectangle(0, 0, geo.width, geo.height)
|
||||
cr:fill()
|
||||
|
||||
return target
|
||||
end
|
||||
|
||||
-- Internal function used by the interactive mode mousegrabber to update the
|
||||
-- frame outline of the current state of the cropped screenshot
|
||||
--
|
||||
-- This function is largely a copy of awful.mouse.snap.show_placeholder(geo),
|
||||
-- so it is probably a good idea to create a utility routine to handle both use
|
||||
-- cases. It did not seem appropriate to make that change as a part of the pull
|
||||
-- request that was creating the screenshot API, as it would clutter and
|
||||
-- confuse the change.
|
||||
local function show_frame(self, surface, geo)
|
||||
local col = self._private.frame_color
|
||||
or beautiful.screenshot_frame_color
|
||||
or "#ff0000"
|
||||
|
||||
local shape = self.frame_shape
|
||||
or beautiful.screenshot_frame_shape
|
||||
or gears.shape.rectangle
|
||||
|
||||
local w, h = root.size()
|
||||
|
||||
self._private.selection_widget = wibox.widget {
|
||||
border_width = 3,
|
||||
border_color = col,
|
||||
shape = shape,
|
||||
color = "transparent",
|
||||
visible = false,
|
||||
widget = wibox.widget.separator
|
||||
}
|
||||
self._private.selection_widget.point = {x=0, y=0}
|
||||
self._private.selection_widget.fit = function() return 0,0 end
|
||||
|
||||
self._private.canvas_widget = wibox.widget {
|
||||
widget = wibox.layout.manual
|
||||
}
|
||||
|
||||
self._private.imagebox = wibox.widget {
|
||||
image = surface,
|
||||
widget = wibox.widget.imagebox
|
||||
}
|
||||
|
||||
self._private.imagebox.point = geo
|
||||
self._private.canvas_widget:add(self._private.imagebox)
|
||||
self._private.canvas_widget:add(self._private.selection_widget)
|
||||
|
||||
self._private.frame = wibox {
|
||||
ontop = true,
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = w,
|
||||
height = h,
|
||||
widget = self._private.canvas_widget,
|
||||
visible = true,
|
||||
}
|
||||
end
|
||||
|
||||
--- 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 snipping is cancelled.
|
||||
-- @signal snipping::cancelled
|
||||
-- @tparam awful.screenshot self
|
||||
-- @tparam string reason Either `"mouse_button"`, `"key"`, `"no_selection"`
|
||||
-- or `"too_small"`.
|
||||
|
||||
--- Emitted when the `auto_save_delay` timer starts.
|
||||
-- @signal timer::started
|
||||
-- @tparam awful.screenshot self
|
||||
|
||||
--- Emitted after each elapsed second when `auto_save_delay` is set.
|
||||
-- @signal timer::tick
|
||||
-- @tparam awful.screenshot self
|
||||
-- @tparam integer remaining Number of seconds remaining.
|
||||
|
||||
--- Emitted when the `auto_save_delay` timer stops.
|
||||
--
|
||||
-- This is before the screenshot is taken. If you need to hide notifications
|
||||
-- or other element, it has to be done using this signal.
|
||||
--
|
||||
-- @signal timer::timeout
|
||||
-- @tparam awful.screenshot self
|
||||
|
||||
-- Internal function that generates the callback to be passed to the
|
||||
-- mousegrabber that implements the interactive mode.
|
||||
--
|
||||
-- The interactive tool is basically a mousegrabber, which takes a single function
|
||||
-- of one argument, representing the mouse state data.
|
||||
local function start_snipping(self, surface, geometry, method)
|
||||
self._private.mg_first_pnt = {}
|
||||
|
||||
local accept_buttons, reject_buttons = {}, {}
|
||||
|
||||
--TODO support modifiers.
|
||||
for _, btn in ipairs(self.accept_buttons) do
|
||||
accept_buttons[btn.button] = true
|
||||
end
|
||||
for _, btn in ipairs(self.reject_buttons) do
|
||||
reject_buttons[btn.button] = true
|
||||
end
|
||||
|
||||
local pressed = false
|
||||
|
||||
show_frame(self, surface, geometry)
|
||||
|
||||
local function ret_mg_callback(mouse_data, accept, reject)
|
||||
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
|
||||
self:reject("mouse_button")
|
||||
return false
|
||||
elseif pressed then
|
||||
local min_x = math.min(self._private.mg_first_pnt[1], mouse_data.x)
|
||||
local max_x = math.max(self._private.mg_first_pnt[1], mouse_data.x)
|
||||
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)
|
||||
|
||||
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
|
||||
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 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")
|
||||
end
|
||||
elseif accept then
|
||||
pressed = true
|
||||
self._private.selection_widget.visible = true
|
||||
self._private.selection_widget.point.x = mouse_data.x
|
||||
self._private.selection_widget.point.y = mouse_data.y
|
||||
self._private.mg_first_pnt[1] = mouse_data.x
|
||||
self._private.mg_first_pnt[2] = mouse_data.y
|
||||
end
|
||||
|
||||
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.
|
||||
function module._screenshot_methods.root()
|
||||
local w, h = root.size()
|
||||
return to_surface(capi.root.content(), w, h), {x = 0, y = 0, width = w, height = h}
|
||||
end
|
||||
|
||||
-- Internal function executed when a physical screen screenshot is taken.
|
||||
function module._screenshot_methods.screen(self)
|
||||
local geo = self.screen.geometry
|
||||
return to_surface(self.screen.content, geo.width, geo.height), geo
|
||||
end
|
||||
|
||||
-- Internal function executed when a client window screenshot is taken.
|
||||
function module._screenshot_methods.client(self)
|
||||
local c = self.client
|
||||
local bw = c.border_width
|
||||
local _, top_size = c:titlebar_top()
|
||||
local _, right_size = c:titlebar_right()
|
||||
local _, bottom_size = c:titlebar_bottom()
|
||||
local _, left_size = c:titlebar_left()
|
||||
|
||||
local c_geo = c:geometry()
|
||||
|
||||
local actual_geo = {
|
||||
x = c_geo.x + left_size + bw,
|
||||
y = c_geo.y + top_size + bw,
|
||||
width = c_geo.width - right_size - left_size,
|
||||
height = c_geo.height - bottom_size - top_size,
|
||||
}
|
||||
|
||||
return to_surface(c.content, actual_geo.width, actual_geo.height), actual_geo
|
||||
end
|
||||
|
||||
-- Internal function executed when a snip screenshow (a defined geometry) is
|
||||
-- taken.
|
||||
function module._screenshot_methods.geometry(self)
|
||||
local root_w, root_h = root.size()
|
||||
|
||||
local root_intrsct = gears.geometry.rectangle.get_intersection(self.geometry, {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = root_w,
|
||||
height = root_h
|
||||
})
|
||||
|
||||
return crop_shot(module._screenshot_methods.root(self), root_intrsct), root_intrsct
|
||||
end
|
||||
|
||||
-- Various accessors for the screenshot object returned by any public
|
||||
-- module method.
|
||||
|
||||
--- Get screenshot directory property.
|
||||
--
|
||||
-- @property directory
|
||||
-- @tparam[opt=os.getenv("HOME")] string directory
|
||||
-- @propemits true false
|
||||
|
||||
--- Get screenshot prefix property.
|
||||
--
|
||||
-- @property prefix
|
||||
-- @tparam[opt="Screenshot-"] string prefix
|
||||
-- @propemits true false
|
||||
|
||||
--- Get screenshot file path.
|
||||
--
|
||||
-- @property file_path
|
||||
-- @tparam[opt=self.directory..self.prefix..os.date()..".png"] string file_path
|
||||
-- @propemits true false
|
||||
-- @see file_name
|
||||
|
||||
--- Get screenshot file name.
|
||||
--
|
||||
-- @property file_name
|
||||
-- @tparam[opt=self.prefix..os.date()..".png"] string file_name
|
||||
-- @propemits true false
|
||||
-- @see file_path
|
||||
|
||||
--- 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.
|
||||
--
|
||||
-- @property cursor
|
||||
-- @tparam[opt="crosshair"] string cursor
|
||||
-- @propemits true false
|
||||
|
||||
--- Use the mouse to select an area (snipping tool).
|
||||
--
|
||||
-- @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
|
||||
|
||||
--- Get screenshot client.
|
||||
--
|
||||
-- @property client
|
||||
-- @tparam[opt=nil] client|nil client
|
||||
-- @propemits true false
|
||||
-- @see mouse.client
|
||||
-- @see client.focus
|
||||
|
||||
--- Get screenshot geometry.
|
||||
--
|
||||
-- @property geometry
|
||||
-- @tparam table geometry
|
||||
-- @tparam number geometry.x
|
||||
-- @tparam number geometry.y
|
||||
-- @tparam number geometry.width
|
||||
-- @tparam number geometry.height
|
||||
-- @propemits true false
|
||||
|
||||
--- Get screenshot surface.
|
||||
--
|
||||
-- If none, or only one of: `screen`, `client` or `geometry` is provided, then
|
||||
-- this screenshot object will have a single target image. While specifying
|
||||
-- multiple target isn't recommended, you can use the `surfaces` properties
|
||||
-- to access them.
|
||||
--
|
||||
-- Note that this is empty until either `save` or `refresh` is called.
|
||||
--
|
||||
-- @property surface
|
||||
-- @tparam nil|image surface
|
||||
-- @propertydefault `nil` if the screenshot hasn't been taken yet, a `gears.surface`
|
||||
-- compatible image otherwise.
|
||||
-- @readonly
|
||||
-- @see surfaces
|
||||
|
||||
--- Get screenshot surfaces.
|
||||
--
|
||||
-- When multiple methods are enabled for the same screenshot, then it will
|
||||
-- have multiple images. This property exposes each image individually.
|
||||
--
|
||||
-- Note that this is empty until either `save` or `refresh` is called. Also
|
||||
-- note that you should make multiple `awful.screenshot` objects rather than
|
||||
-- put multiple targets in the same object.
|
||||
--
|
||||
-- @property surfaces
|
||||
-- @tparam[opt={}] table surfaces
|
||||
-- @tparam[opt=nil] image surfaces.root The screenshot of all screens. This is
|
||||
-- the default if none of `client`, screen` or `geometry` have been set.
|
||||
-- @tparam[opt=nil] image surfaces.client The surface for the `client` property.
|
||||
-- @tparam[opt=nil] image surfaces.screen The surface for the `screen` property.
|
||||
-- @tparam[opt=nil] image surfaces.geometry The surface for the `geometry` property.
|
||||
-- @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
|
||||
-- @propbeautiful
|
||||
-- @propemits true false
|
||||
|
||||
--- The interactive frame shape.
|
||||
-- @property frame_shape
|
||||
-- @tparam shape|nil frame_shape
|
||||
-- @propbeautiful
|
||||
-- @propemits true false
|
||||
|
||||
--- Define which mouse button exit the interactive snipping mode.
|
||||
--
|
||||
-- @property reject_buttons
|
||||
-- @tparam[opt={awful.button({}, 3)}}] table reject_buttons
|
||||
-- @tablerowtype A list of `awful.button` objects.
|
||||
-- @propemits true false
|
||||
-- @see accept_buttons
|
||||
|
||||
--- Mouse buttons used to save the screenshot when using the interactive snipping mode.
|
||||
--
|
||||
-- @property accept_buttons
|
||||
-- @tparam[opt={awful.button({}, 1)}}] table accept_buttons
|
||||
-- @tablerowtype A list of `awful.button` objects.
|
||||
-- @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
|
||||
|
||||
--- Number of seconds before auto-saving (or entering the interactive snipper).
|
||||
--
|
||||
-- You can use `0` to auto-save immediatly.
|
||||
--
|
||||
-- @property auto_save_delay
|
||||
-- @tparam[opt=nil] integer|nil auto_save_delay
|
||||
-- @propertytype nil Do not auto-save.
|
||||
-- @propertyunit second
|
||||
-- @negativeallowed false
|
||||
-- @propemits true false
|
||||
|
||||
--- Duration between the `"timer::tick"` signals when using `auto_save_delay`.
|
||||
-- @property auto_save_tick_duration
|
||||
-- @tparam[opt=1.0] number auto_save_tick_duration
|
||||
-- @propertyunit second
|
||||
-- @negativeallowed false
|
||||
-- @propemits true false
|
||||
|
||||
--- Export this screenshot as an `wibox.widget.imagebox` instead of a file.
|
||||
--
|
||||
-- This can be used to place the screenshot in a `wibox`, `awful.popup`
|
||||
-- or `awful.wibar`. Note that this isn't a live view of the object, you have
|
||||
-- to call `:refresh()` to update the content.
|
||||
--
|
||||
-- Note that it only makes sense when only 1 surface is exported by the
|
||||
-- screenhot. If that doesn't work for your use case, consider making multiple
|
||||
-- `awful.screenshot` objects.
|
||||
--
|
||||
-- @property content_widget
|
||||
-- @tparam wibox.widget.imagebox content_widget
|
||||
-- @readonly
|
||||
-- @propertydefault Autogenerated on first access.
|
||||
|
||||
local defaults = {
|
||||
prefix = "Screenshot-",
|
||||
directory = screenshot_validation.directory(os.getenv("HOME")),
|
||||
cursor = "crosshair",
|
||||
date_format = "%Y%m%d%H%M%S",
|
||||
interactive = false,
|
||||
reject_buttons = {abutton({}, 3)},
|
||||
accept_buttons = {abutton({}, 1)},
|
||||
reject_keys = {akey({}, "Escape")},
|
||||
accept_keys = {akey({}, "Return")},
|
||||
minimum_size = {width = 3, height = 3},
|
||||
auto_save_tick_duration = 1,
|
||||
}
|
||||
|
||||
-- Create the standard properties.
|
||||
for _, prop in ipairs { "frame_color", "geometry", "screen", "client", "date_format",
|
||||
"prefix", "directory", "file_path", "file_name", "auto_save_delay",
|
||||
"interactive", "reject_buttons", "accept_buttons", "cursor",
|
||||
"reject_keys", "accept_keys", "frame_shape", "minimum_size",
|
||||
"auto_save_tick_duration" } do
|
||||
module["set_"..prop] = function(self, value)
|
||||
self._private[prop] = screenshot_validation[prop]
|
||||
and screenshot_validation[prop](value) or value
|
||||
self:emit_signal("property::"..prop, value)
|
||||
end
|
||||
|
||||
module["get_"..prop] = function(self)
|
||||
return self._private[prop] or defaults[prop]
|
||||
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
|
||||
|
||||
function module:get_file_name()
|
||||
return self._private.file_name or make_file_name(self)
|
||||
end
|
||||
|
||||
function module:get_surface()
|
||||
return self.surfaces[1]
|
||||
end
|
||||
|
||||
function module:get_surfaces()
|
||||
local ret = {}
|
||||
|
||||
for method, surface in pairs(self._private.surfaces or {}) do
|
||||
ret[method] = surface.surface
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function module:get_surface()
|
||||
local pair = select(2, next(self._private.surfaces or {}))
|
||||
return pair and pair.surface
|
||||
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
|
||||
|
||||
function module:set_auto_save_delay(value)
|
||||
self._private.auto_save_delay = math.ceil(value)
|
||||
self:emit_signal("property::auto_save_delay", value)
|
||||
|
||||
if not value then
|
||||
if self._private.timer then
|
||||
self._private.timer:stop()
|
||||
self._private.timer = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if value == 0 then
|
||||
self:refresh()
|
||||
if not self.interactive then
|
||||
self:save()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
self._private.current_delay = self._private.auto_save_delay
|
||||
|
||||
if not self._private.timer then
|
||||
local dur = self.auto_save_tick_duration
|
||||
self._private.timer = gtimer {timeout = dur}
|
||||
local fct
|
||||
fct = function()
|
||||
self._private.current_delay = self._private.current_delay - dur
|
||||
self:emit_signal("timer::tick", self._private.current_delay)
|
||||
|
||||
if self._private.current_delay <= 0 then
|
||||
self:emit_signal("timer::timeout")
|
||||
awesome.sync()
|
||||
self:refresh()
|
||||
if not self.interactive then
|
||||
self:save()
|
||||
end
|
||||
|
||||
-- For garbage collection of the `awful.screenshot` object.
|
||||
self._private.timer:stop()
|
||||
self._private.timer:disconnect_signal("timeout", fct)
|
||||
self._private.timer = nil
|
||||
end
|
||||
end
|
||||
self._private.timer:connect_signal("timeout", fct)
|
||||
end
|
||||
|
||||
self._private.timer:again()
|
||||
self:emit_signal("timer::started")
|
||||
end
|
||||
|
||||
function module:get_content_widget()
|
||||
if not self._private.output_imagebox then
|
||||
self._private.output_imagebox = wibox.widget.imagebox()
|
||||
end
|
||||
|
||||
local s = self.surface
|
||||
|
||||
if s then
|
||||
self._private.output_imagebox.image = s
|
||||
end
|
||||
|
||||
return self._private.output_imagebox
|
||||
end
|
||||
|
||||
--- Take new screenshot(s) now.
|
||||
--
|
||||
-- @method refresh
|
||||
-- @treturn table A table with the method name as key and the images as value.
|
||||
-- @see save
|
||||
|
||||
function module:refresh()
|
||||
local methods = {}
|
||||
self._private.surfaces = {}
|
||||
|
||||
for method in pairs(module._screenshot_methods) do
|
||||
if self._private[method] then
|
||||
table.insert(methods, method)
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback to a screenshot of everything.
|
||||
methods = #methods > 0 and methods or {"root"}
|
||||
|
||||
for _, method in ipairs(methods) do
|
||||
local surface, geo = module._screenshot_methods[method](self)
|
||||
|
||||
if self.interactive then
|
||||
start_snipping(self, surface, geo, method)
|
||||
else
|
||||
self._private.surfaces[method] = {surface = surface, geometry = geo}
|
||||
end
|
||||
end
|
||||
|
||||
if self._private.output_imagebox then
|
||||
self._private.output_imagebox.image = self.surface
|
||||
end
|
||||
|
||||
return self.surfaces
|
||||
end
|
||||
|
||||
--- Emitted when a the screenshot is saved.
|
||||
--
|
||||
-- This can be due to `:save()` being called, the `interactive` mode finishing
|
||||
-- or `auto_save_delay` timing out.
|
||||
--
|
||||
-- @signal file::saved
|
||||
-- @tparam awful.screenshot self The screenshot.
|
||||
-- @tparam string file_path The path.
|
||||
-- @tparam[opt] string|nil method The method associated with the file_path. This
|
||||
-- can be `root`, `geometry`, `client` or `screen`.
|
||||
|
||||
--- Save screenshot.
|
||||
--
|
||||
-- @method save
|
||||
-- @tparam[opt=self.file_path] string file_path Optionally override the file path.
|
||||
-- @noreturn
|
||||
-- @emits saved
|
||||
-- @see refresh
|
||||
function module:save(file_path)
|
||||
if not self._private.surfaces then self:refresh() end
|
||||
|
||||
for method, surface in pairs(self._private.surfaces) do
|
||||
file_path = file_path
|
||||
or self._private.file_path
|
||||
or make_file_path(self, #self._private.surfaces > 1 and method or nil)
|
||||
|
||||
surface.surface:write_to_png(file_path)
|
||||
self:emit_signal("file::saved", file_path, method)
|
||||
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
|
||||
--
|
||||
-- @constructorfct awful.screenshot
|
||||
-- @tparam[opt={}] table args
|
||||
-- @tparam[opt] string args.directory Get screenshot directory property.
|
||||
-- @tparam[opt] string args.prefix Get screenshot prefix property.
|
||||
-- @tparam[opt] string args.file_path Get screenshot file path.
|
||||
-- @tparam[opt] string args.file_name Get screenshot file name.
|
||||
-- @tparam[opt] screen args.screen Get screenshot screen.
|
||||
-- @tparam[opt] string args.date_format The date format used in the default suffix.
|
||||
-- @tparam[opt] string args.cursor The cursor used in interactive mode.
|
||||
-- @tparam[opt] boolean args.interactive Rather than take a screenshot of an
|
||||
-- object, use the mouse to select an area.
|
||||
-- @tparam[opt] client args.client Get screenshot client.
|
||||
-- @tparam[opt] table args.geometry Get screenshot geometry.
|
||||
-- @tparam[opt] color args.frame_color The frame color.
|
||||
|
||||
local function new(_, args)
|
||||
args = (type(args) == "table" and args) or {}
|
||||
local self = gears.object({
|
||||
enable_auto_signals = true,
|
||||
enable_properties = true
|
||||
})
|
||||
|
||||
self._private = {}
|
||||
gears.table.crush(self, module, true)
|
||||
gears.table.crush(self, args)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return setmetatable(module, {__call = new})
|
||||
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|
|
@ -83,6 +83,18 @@ function filesystem.dir_readable(path)
|
|||
gfileinfo:get_attribute_boolean("access::can-read")
|
||||
end
|
||||
|
||||
--- Check if a path exists, is writable and a directory.
|
||||
-- @tparam string path The directory path.
|
||||
-- @treturn boolean True if path exists and is writable.
|
||||
-- @staticfct gears.filesystem.dir_writable
|
||||
function filesystem.dir_writable(path)
|
||||
local gfile = Gio.File.new_for_path(path)
|
||||
local gfileinfo = gfile:query_info("standard::type,access::can-write",
|
||||
Gio.FileQueryInfoFlags.NONE)
|
||||
return gfileinfo and gfileinfo:get_file_type() == "DIRECTORY" and
|
||||
gfileinfo:get_attribute_boolean("access::can-write")
|
||||
end
|
||||
|
||||
--- Check if a path is a directory.
|
||||
-- @tparam string path The directory path
|
||||
-- @treturn boolean True if path exists and is a directory.
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
--DOC_NO_USAGE --DOC_GEN_IMAGE --DOC_HIDE_START
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local beautiful = require("beautiful")
|
||||
local naughty = require("naughty")
|
||||
local s = screen[1]
|
||||
screen[1]._resize {width = 640, height = 320}
|
||||
|
||||
local wb = awful.wibar { position = "top" }
|
||||
local modkey = "Mod4"
|
||||
|
||||
-- Create the same number of tags as the default config
|
||||
awful.tag({ "1", "2", "3", "4", "5", "6", "7", "8", "9" }, screen[1], awful.layout.layouts[1])
|
||||
|
||||
-- Only bother with widgets that are visible by default
|
||||
local mykeyboardlayout = awful.widget.keyboardlayout()
|
||||
local mytextclock = wibox.widget.textclock()
|
||||
local mytaglist = awful.widget.taglist(screen[1], awful.widget.taglist.filter.all, {})
|
||||
local mytasklist = awful.widget.tasklist(screen[1], awful.widget.tasklist.filter.currenttags, {})
|
||||
|
||||
client.connect_signal("request::titlebars", function(c)
|
||||
local top_titlebar = awful.titlebar(c, {
|
||||
height = 20,
|
||||
bg_normal = beautiful.bg_normal,
|
||||
})
|
||||
|
||||
top_titlebar : setup {
|
||||
{ -- Left
|
||||
awful.titlebar.widget.iconwidget(c),
|
||||
layout = wibox.layout.fixed.horizontal
|
||||
},
|
||||
{ -- Middle
|
||||
{ -- Title
|
||||
align = "center",
|
||||
widget = awful.titlebar.widget.titlewidget(c)
|
||||
},
|
||||
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)
|
||||
|
||||
wb:setup {
|
||||
layout = wibox.layout.align.horizontal,
|
||||
{
|
||||
mytaglist,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
mytasklist,
|
||||
{
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
mykeyboardlayout,
|
||||
mytextclock,
|
||||
},
|
||||
}
|
||||
|
||||
require("gears.timer").run_delayed_calls_now()
|
||||
local counter = 0
|
||||
|
||||
local function gen_client(label)
|
||||
local c = client.gen_fake {hide_first=true}
|
||||
|
||||
c:geometry {
|
||||
x = 45 + counter*1.75,
|
||||
y = 30 + counter,
|
||||
height = 60,
|
||||
width = 230,
|
||||
}
|
||||
c._old_geo = {c:geometry()}
|
||||
c:set_label(label)
|
||||
c:emit_signal("request::titlebars")
|
||||
c.border_color = beautiful.bg_highlight
|
||||
counter = counter + 40
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
gen_client("C1")
|
||||
gen_client("C2")
|
||||
|
||||
--DOC_HIDE_END
|
||||
|
||||
|
||||
local function saved_screenshot(args)
|
||||
local ss = awful.screenshot(args)
|
||||
|
||||
--DOC_NEWLINE
|
||||
local function notify(self)
|
||||
naughty.notification {
|
||||
title = self.file_name,
|
||||
message = "Screenshot saved",
|
||||
icon = self.surface,
|
||||
icon_size = 128,
|
||||
}
|
||||
end
|
||||
--DOC_NEWLINE
|
||||
|
||||
if args.auto_save_delay > 0 then
|
||||
ss:connect_signal("file::saved", notify)
|
||||
else
|
||||
notify(ss)
|
||||
end
|
||||
--DOC_NEWLINE
|
||||
|
||||
return ss
|
||||
end
|
||||
--DOC_NEWLINE
|
||||
local function delayed_screenshot(args)
|
||||
local ss = saved_screenshot(args)
|
||||
local notif = naughty.notification {
|
||||
title = "Screenshot in:",
|
||||
message = tostring(args.auto_save_delay) .. " seconds"
|
||||
}
|
||||
--DOC_NEWLINE
|
||||
|
||||
ss:connect_signal("timer::tick", function(_, remain)
|
||||
notif.message = tostring(remain) .. " seconds"
|
||||
end)
|
||||
--DOC_NEWLINE
|
||||
|
||||
ss:connect_signal("timer::timeout", function()
|
||||
if notif then notif:destroy() end
|
||||
end)
|
||||
--DOC_NEWLINE
|
||||
|
||||
return ss
|
||||
end
|
||||
--DOC_NEWLINE
|
||||
client.connect_signal("request::default_keybindings", function()
|
||||
awful.keyboard.append_client_keybindings({
|
||||
awful.key({modkey}, "Print",
|
||||
function (c) saved_screenshot { auto_save_delay = 0, client = c } end ,
|
||||
{description = "take client screenshot", group = "client"}),
|
||||
awful.key({modkey, "Shift"}, "Print",
|
||||
function (c) saved_screenshot { auto_save_delay = 0, interactive = true, client = c } end ,
|
||||
{description = "take interactive client screenshot", group = "client"}),
|
||||
awful.key({modkey, "Control"}, "Print",
|
||||
function (c) delayed_screenshot { auto_save_delay = 5, client = c } end ,
|
||||
{description = "take screenshot in 5 seconds", group = "client"}),
|
||||
awful.key({modkey, "Shift", "Control"}, "Print",
|
||||
function (c) delayed_screenshot { auto_save_delay = 5, interactive = true, client = c } end ,
|
||||
{description = "take interactive screenshot in 5 seconds", group = "client"}),
|
||||
})
|
||||
end)
|
||||
--DOC_NEWLINE
|
||||
|
||||
awful.keyboard.append_global_keybindings({
|
||||
awful.key({}, "Print",
|
||||
function () saved_screenshot { auto_save_delay = 0 } end ,
|
||||
{description = "take screenshot", group = "client"}),
|
||||
awful.key({"Shift"}, "Print",
|
||||
function () saved_screenshot { auto_save_delay = 0, interactive = true } end ,
|
||||
{description = "take interactive screenshot", group = "client"}),
|
||||
awful.key({"Control"}, "Print",
|
||||
function () delayed_screenshot { auto_save_delay = 5 } end ,
|
||||
{description = "take screenshot in 5 seconds", group = "client"}),
|
||||
awful.key({"Shift", "Control"}, "Print",
|
||||
function () delayed_screenshot { auto_save_delay = 5, interactive = true } end ,
|
||||
{description = "take interactive screenshot in 5 seconds", group = "client"}),
|
||||
})
|
||||
|
||||
--DOC_HIDE_START
|
||||
|
||||
client.emit_signal("request::default_keybindings")
|
||||
|
||||
-- A notification popup using the default widget_template.
|
||||
naughty.connect_signal("request::display", function(n)
|
||||
naughty.layout.box {notification = n}
|
||||
end)
|
||||
|
||||
awful.wallpaper {
|
||||
screen = s,
|
||||
widget = {
|
||||
image = beautiful.wallpaper,
|
||||
resize = true,
|
||||
widget = wibox.widget.imagebox,
|
||||
horizontal_fit_policy = "fit",
|
||||
vertical_fit_policy = "fit",
|
||||
}
|
||||
}
|
||||
|
||||
saved_screenshot { auto_save_delay = 0 }
|
||||
delayed_screenshot { auto_save_delay = 5 }
|
||||
|
||||
require("gears.timer").run_delayed_calls_now() --DOC_HIDE
|
||||
--DOC_HIDE_END
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
--DOC_NO_USAGE --DOC_GEN_IMAGE --DOC_HIDE_START
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local gears = require("gears")
|
||||
local beautiful = require("beautiful")
|
||||
local s = screen[1]
|
||||
screen[1]._resize {width = 640, height = 320}
|
||||
|
||||
local wb = awful.wibar { position = "top" }
|
||||
|
||||
-- Create the same number of tags as the default config
|
||||
awful.tag({ "1", "2", "3", "4", "5", "6", "7", "8", "9" }, screen[1], awful.layout.layouts[1])
|
||||
|
||||
-- Only bother with widgets that are visible by default
|
||||
local mykeyboardlayout = awful.widget.keyboardlayout()
|
||||
local mytextclock = wibox.widget.textclock()
|
||||
local mytaglist = awful.widget.taglist(screen[1], awful.widget.taglist.filter.all, {})
|
||||
local mytasklist = awful.widget.tasklist(screen[1], awful.widget.tasklist.filter.currenttags, {})
|
||||
|
||||
client.connect_signal("request::titlebars", function(c)
|
||||
local top_titlebar = awful.titlebar(c, {
|
||||
height = 20,
|
||||
bg_normal = beautiful.bg_normal,
|
||||
})
|
||||
|
||||
top_titlebar : setup {
|
||||
{ -- Left
|
||||
awful.titlebar.widget.iconwidget(c),
|
||||
layout = wibox.layout.fixed.horizontal
|
||||
},
|
||||
{ -- Middle
|
||||
{ -- Title
|
||||
align = "center",
|
||||
widget = awful.titlebar.widget.titlewidget(c)
|
||||
},
|
||||
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)
|
||||
|
||||
wb:setup {
|
||||
layout = wibox.layout.align.horizontal,
|
||||
{
|
||||
mytaglist,
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
},
|
||||
mytasklist,
|
||||
{
|
||||
layout = wibox.layout.fixed.horizontal,
|
||||
mykeyboardlayout,
|
||||
mytextclock,
|
||||
},
|
||||
}
|
||||
|
||||
require("gears.timer").run_delayed_calls_now()
|
||||
local counter = 0
|
||||
|
||||
local function gen_client(label)
|
||||
local c = client.gen_fake {hide_first=true}
|
||||
|
||||
c:geometry {
|
||||
x = 45 + counter*1.75,
|
||||
y = 30 + counter,
|
||||
height = 60,
|
||||
width = 230,
|
||||
}
|
||||
c._old_geo = {c:geometry()}
|
||||
c:set_label(label)
|
||||
c:emit_signal("request::titlebars")
|
||||
c.border_color = beautiful.bg_highlight
|
||||
counter = counter + 40
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
local tasklist_buttons = {}
|
||||
gen_client("C1")
|
||||
gen_client("C2")
|
||||
|
||||
--DOC_HIDE_END
|
||||
|
||||
awful.popup {
|
||||
widget = awful.widget.tasklist {
|
||||
screen = screen[1],
|
||||
filter = awful.widget.tasklist.filter.allscreen,
|
||||
buttons = tasklist_buttons,
|
||||
style = {
|
||||
shape = gears.shape.rounded_rect,
|
||||
align = "center"
|
||||
},
|
||||
layout = {
|
||||
spacing = 5,
|
||||
forced_num_rows = 1,
|
||||
layout = wibox.layout.grid.horizontal
|
||||
},
|
||||
widget_template = {
|
||||
{
|
||||
{
|
||||
id = "screenshot",
|
||||
margins = 4,
|
||||
forced_height = 128,
|
||||
forced_width = 240,
|
||||
widget = wibox.container.margin,
|
||||
},
|
||||
{
|
||||
id = "text_role",
|
||||
forced_height = 20,
|
||||
forced_width = 240,
|
||||
widget = wibox.widget.textbox,
|
||||
},
|
||||
widget = wibox.layout.fixed.vertical
|
||||
},
|
||||
id = "background_role",
|
||||
widget = wibox.container.background,
|
||||
create_callback = function(self, c) --luacheck: no unused
|
||||
assert(c) --DOC_HIDE
|
||||
local ss = awful.screenshot {
|
||||
client = c,
|
||||
}
|
||||
ss:refresh()
|
||||
local ib = ss.content_widget
|
||||
ib.valign = "center"
|
||||
ib.halign = "center"
|
||||
self:get_children_by_id("screenshot")[1].widget = ib
|
||||
assert(ss.surface) --DOC_HIDE
|
||||
end,
|
||||
},
|
||||
},
|
||||
border_color = "#777777",
|
||||
border_width = 2,
|
||||
ontop = true,
|
||||
placement = awful.placement.centered,
|
||||
shape = gears.shape.rounded_rect
|
||||
}
|
||||
|
||||
--DOC_HIDE_START
|
||||
awful.wallpaper {
|
||||
screen = s,
|
||||
widget = {
|
||||
image = beautiful.wallpaper,
|
||||
resize = true,
|
||||
widget = wibox.widget.imagebox,
|
||||
horizontal_fit_policy = "fit",
|
||||
vertical_fit_policy = "fit",
|
||||
}
|
||||
}
|
||||
|
||||
require("gears.timer").run_delayed_calls_now() --DOC_HIDE
|
||||
--DOC_HIDE_END
|
|
@ -2,11 +2,123 @@ local file_path, image_path = ...
|
|||
require("_common_template")(...)
|
||||
|
||||
local cairo = require("lgi").cairo
|
||||
local color = require( "gears.color" )
|
||||
local shape = require( "gears.shape" )
|
||||
local beautiful = require( "beautiful" )
|
||||
local wibox = require( "wibox" )
|
||||
local screenshot = require( "awful.screenshot")
|
||||
|
||||
local color = require( "gears.color" )
|
||||
local shape = require( "gears.shape" )
|
||||
local beautiful = require( "beautiful" )
|
||||
local wibox = require( "wibox" )
|
||||
local function wrap_titlebar(tb, width, height, args)
|
||||
local bg, fg
|
||||
|
||||
if args.honor_titlebar_colors then
|
||||
bg = tb.drawable.background_color or tb.args.bg_normal
|
||||
fg = tb.drawable.foreground_color or tb.args.fg_normal
|
||||
else
|
||||
bg, fg = tb.args.bg_normal, tb.args.fg_normal
|
||||
end
|
||||
|
||||
return wibox.widget {
|
||||
tb.drawable.widget,
|
||||
bg = bg,
|
||||
fg = fg,
|
||||
forced_width = width,
|
||||
forced_height = height,
|
||||
widget = wibox.container.background
|
||||
}
|
||||
end
|
||||
|
||||
local function client_widget(c, col, label, args)
|
||||
local geo = c:geometry()
|
||||
local bw = c.border_width or beautiful.border_width or 0
|
||||
local bc = c.border_color or beautiful.border_color
|
||||
|
||||
local l = wibox.layout.align.vertical()
|
||||
l.fill_space = true
|
||||
|
||||
local tbs = c._private and c._private.titlebars or {}
|
||||
|
||||
local map = {
|
||||
top = "set_first",
|
||||
left = "set_first",
|
||||
bottom = "set_third",
|
||||
right = "set_third",
|
||||
}
|
||||
|
||||
for _, position in ipairs{"top", "bottom"} do
|
||||
local tb = tbs[position]
|
||||
if tb then
|
||||
l[map[position]](l, wrap_titlebar(tb, c:geometry().width, tb.args.height or 16, args))
|
||||
end
|
||||
end
|
||||
|
||||
for _, position in ipairs{"left", "right"} do
|
||||
local tb = tbs[position]
|
||||
if tb then
|
||||
l[map[position]](l, wrap_titlebar(tb, tb.args.width or 16, c:geometry().height), args)
|
||||
end
|
||||
end
|
||||
|
||||
local l2 = wibox.layout.align.horizontal()
|
||||
l2.fill_space = true
|
||||
l:set_second(l2)
|
||||
l.forced_width = c.width
|
||||
l.forced_height = c.height
|
||||
|
||||
return wibox.widget {
|
||||
{
|
||||
l,
|
||||
{
|
||||
text = label or "",
|
||||
halign = "center",
|
||||
valign = "center",
|
||||
widget = wibox.widget.textbox
|
||||
},
|
||||
layout = wibox.layout.stack
|
||||
},
|
||||
border_width = bw,
|
||||
border_color = bc,
|
||||
shape_clip = true,
|
||||
border_strategy = "inner",
|
||||
opacity = c.opacity,
|
||||
fg = beautiful.fg_normal or "#000000",
|
||||
bg = col,
|
||||
shape = function(cr2, w, h)
|
||||
if c.shape then
|
||||
c.shape(cr2, w, h)
|
||||
else
|
||||
return shape.rounded_rect(cr2, w, h, args.radius or 5)
|
||||
end
|
||||
end,
|
||||
|
||||
forced_width = geo.width + 2*bw,
|
||||
forced_height = geo.height + 2*bw,
|
||||
widget = wibox.container.background,
|
||||
}
|
||||
end
|
||||
|
||||
-- Mock the c:content(), it cannot be shimmed because the "real" one uses
|
||||
-- a raw surface rather than a LGI one.
|
||||
function screenshot._screenshot_methods.client(self)
|
||||
local c = self.client
|
||||
local geo = c:geometry()
|
||||
|
||||
local wdg = client_widget(
|
||||
c, c.color or geo._color or beautiful.bg_normal, "", {}
|
||||
)
|
||||
|
||||
local sur = wibox.widget.draw_to_image_surface(wdg, geo.width, geo.height)
|
||||
|
||||
return sur, geo
|
||||
end
|
||||
|
||||
function screenshot._screenshot_methods.root()
|
||||
local w, h = root.size()
|
||||
|
||||
local img = cairo.ImageSurface.create(cairo.Format.ARGB32, 1, 1)
|
||||
|
||||
return img, {x=0,y=0,width=w,height=h}
|
||||
end
|
||||
|
||||
-- Run the test
|
||||
local args = loadfile(file_path)() or {}
|
||||
|
@ -97,98 +209,6 @@ local total_area = wibox.layout {
|
|||
layout = wibox.layout.manual,
|
||||
}
|
||||
|
||||
local function wrap_titlebar(tb, width, height)
|
||||
|
||||
local bg, fg
|
||||
|
||||
if args.honor_titlebar_colors then
|
||||
bg = tb.drawable.background_color or tb.args.bg_normal
|
||||
fg = tb.drawable.foreground_color or tb.args.fg_normal
|
||||
else
|
||||
bg, fg = tb.args.bg_normal, tb.args.fg_normal
|
||||
end
|
||||
|
||||
return wibox.widget {
|
||||
tb.drawable.widget,
|
||||
bg = bg,
|
||||
fg = fg,
|
||||
forced_width = width,
|
||||
forced_height = height,
|
||||
widget = wibox.container.background
|
||||
}
|
||||
end
|
||||
|
||||
local function client_widget(c, col, label)
|
||||
local geo = c:geometry()
|
||||
local bw = c.border_width or beautiful.border_width or 0
|
||||
local bc = c.border_color or beautiful.border_color
|
||||
|
||||
local l = wibox.layout.align.vertical()
|
||||
l.fill_space = true
|
||||
|
||||
local tbs = c._private and c._private.titlebars or {}
|
||||
|
||||
local map = {
|
||||
top = "set_first",
|
||||
left = "set_first",
|
||||
bottom = "set_third",
|
||||
right = "set_third",
|
||||
}
|
||||
|
||||
for _, position in ipairs{"top", "bottom"} do
|
||||
local tb = tbs[position]
|
||||
if tb then
|
||||
l[map[position]](l, wrap_titlebar(tb, c:geometry().width, tb.args.height or 16))
|
||||
end
|
||||
end
|
||||
|
||||
for _, position in ipairs{"left", "right"} do
|
||||
local tb = tbs[position]
|
||||
if tb then
|
||||
l[map[position]](l, wrap_titlebar(tb, tb.args.width or 16, c:geometry().height))
|
||||
end
|
||||
end
|
||||
|
||||
local l2 = wibox.layout.align.horizontal()
|
||||
l2.fill_space = true
|
||||
l:set_second(l2)
|
||||
l.forced_width = c.width
|
||||
l.forced_height = c.height
|
||||
|
||||
return wibox.widget {
|
||||
{
|
||||
l,
|
||||
{
|
||||
text = label or "",
|
||||
halign = "center",
|
||||
valign = "center",
|
||||
widget = wibox.widget.textbox
|
||||
},
|
||||
layout = wibox.layout.stack
|
||||
},
|
||||
border_width = bw,
|
||||
border_color = bc,
|
||||
shape_clip = true,
|
||||
border_strategy = "inner",
|
||||
opacity = c.opacity,
|
||||
fg = beautiful.fg_normal or "#000000",
|
||||
bg = col,
|
||||
shape = function(cr2, w, h)
|
||||
if c.shape then
|
||||
c.shape(cr2, w, h)
|
||||
else
|
||||
return shape.rounded_rect(cr2, w, h, args.radius or 5)
|
||||
end
|
||||
end,
|
||||
|
||||
forced_width = geo.width + 2*bw,
|
||||
forced_height = geo.height + 2*bw,
|
||||
widget = wibox.container.background,
|
||||
}
|
||||
end
|
||||
|
||||
-- Add all wiboxes
|
||||
|
||||
-- Fix the wibox geometries that have a dependency on their content
|
||||
for _, d in ipairs(drawin.get()) do
|
||||
local w = d.get_wibox and d:get_wibox() or nil
|
||||
|
@ -228,7 +248,7 @@ for _, c in ipairs(client.get()) do
|
|||
for _, geo in ipairs(c._old_geo) do
|
||||
if not geo._hide then
|
||||
total_area:add_at(
|
||||
client_widget(c, c.color or geo._color or beautiful.bg_normal, geo._label),
|
||||
client_widget(c, c.color or geo._color or beautiful.bg_normal, geo._label, args),
|
||||
{x=geo.x, y=geo.y}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -19,8 +19,10 @@ end
|
|||
|
||||
local function titlebar_meta(c)
|
||||
for _, position in ipairs {"top", "bottom", "left", "right" } do
|
||||
c["titlebar_"..position] = function(size) --luacheck: no unused
|
||||
return drawin{}
|
||||
c["titlebar_"..position] = function(_, size)
|
||||
local prop = "titlebar_"..position.."_size"
|
||||
c._private[prop] = c._private[prop] or size
|
||||
return drawin{}, c._private[prop] or 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
-- This test suite tests the various screenshot related APIs.
|
||||
--
|
||||
-- Credit: https://www.reddit.com/r/awesomewm/comments/i6nf7z/need_help_writing_a_color_picker_widget_using_lgi/
|
||||
local awful = require("awful")
|
||||
local wibox = require("wibox")
|
||||
local spawn = require("awful.spawn")
|
||||
local gsurface = require("gears.surface")
|
||||
local gdebug = require("gears.debug")
|
||||
local lgi = require('lgi')
|
||||
local cairo = lgi.cairo
|
||||
local gdk = lgi.require('Gdk', '3.0')
|
||||
local capi = {
|
||||
root = _G.root
|
||||
}
|
||||
|
||||
-- Dummy blue client for the client.content test
|
||||
-- the lua_executable portion may need to get ironed out. I need to specify 5.3
|
||||
local lua_executable = os.getenv("LUA")
|
||||
|
@ -33,6 +34,10 @@ Gtk:main{...}
|
|||
local tiny_client = { lua_executable, "-e", string.format(
|
||||
tiny_client_code_template, client_dim, client_dim)}
|
||||
|
||||
-- We need a directory to configure the screenshot library to use even through
|
||||
-- we never actually write a file.
|
||||
local fake_screenshot_dir = "/tmp"
|
||||
|
||||
-- Split in the screen into 2 distict screens.
|
||||
screen[1]:split()
|
||||
|
||||
|
@ -104,9 +109,15 @@ local function get_pixel(img, x, y)
|
|||
return "#" .. bytes:gsub('.', function(c) return ('%02x'):format(c:byte()) end)
|
||||
end
|
||||
|
||||
local snipper_success = nil
|
||||
local function snipper_cb(ss)
|
||||
local img = ss.surface
|
||||
snipper_success = img and get_pixel(img, 10, 10) == "#00ff00"
|
||||
end
|
||||
|
||||
local steps = {}
|
||||
|
||||
-- Check the whole root window.
|
||||
-- Check the whole root window with root.content()
|
||||
table.insert(steps, function()
|
||||
local root_width, root_height = root.size()
|
||||
local img = copy_to_image_surface(capi.root.content(), root_width,
|
||||
|
@ -127,17 +138,24 @@ table.insert(steps, function()
|
|||
return true
|
||||
end)
|
||||
|
||||
-- Check the screen.content
|
||||
-- Check screen.content
|
||||
table.insert(steps, function()
|
||||
for s in screen do
|
||||
|
||||
local geo = s.geometry
|
||||
local img = copy_to_image_surface(s.content, geo.width, geo.height)
|
||||
|
||||
assert(get_pixel(img, 4, 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.width - 4, 4) == "#ff0000")
|
||||
assert(get_pixel(img, 4, geo.height - 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.width - 4, geo.height - 4) == "#ff0000")
|
||||
|
||||
--[[
|
||||
assert(get_pixel(img, geo.x + 4, geo.y + 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.x + geo.width - 4, geo.y + 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.x + 4, geo.y + geo.height - 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.x + geo.width - 4, geo.y + geo.height - 4) == "#ff0000")
|
||||
--]]
|
||||
|
||||
end
|
||||
|
||||
|
@ -148,7 +166,7 @@ table.insert(steps, function()
|
|||
return true
|
||||
end)
|
||||
|
||||
-- Check the client.content
|
||||
-- Check client.content
|
||||
table.insert(steps, function()
|
||||
if #client.get() ~= 1 then return end
|
||||
local c = client.get()[1]
|
||||
|
@ -167,4 +185,332 @@ table.insert(steps, function()
|
|||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
--Make sure client from last test is gone
|
||||
if #client.get() ~= 0 then return end
|
||||
|
||||
local fake_screenshot_dir2 = string.gsub(fake_screenshot_dir, "/*$", "/", 1)
|
||||
|
||||
local ss = awful.screenshot { directory = "/tmp" }
|
||||
local name_prfx = fake_screenshot_dir2 .. "Screenshot-"
|
||||
|
||||
local f = string.find(ss.file_path, name_prfx)
|
||||
if f ~= 1 then
|
||||
error("Failed autogenerate filename: " .. ss.file_path .. " : " .. name_prfx)
|
||||
return false
|
||||
end
|
||||
|
||||
name_prfx = fake_screenshot_dir2 .. "MyShot.png"
|
||||
ss.file_path = name_prfx
|
||||
|
||||
if ss.file_path ~= name_prfx then
|
||||
error("Failed assign filename: " .. ss.file_path .. " : " .. name_prfx)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end)
|
||||
|
||||
--Check the root window with awful.screenshot.root() method
|
||||
table.insert(steps, function()
|
||||
|
||||
local root_width, root_height = root.size()
|
||||
local ss = awful.screenshot { directory = "/tmp" }
|
||||
ss:refresh()
|
||||
|
||||
local img = ss.surface
|
||||
assert(img)
|
||||
|
||||
assert(get_pixel(img, 100, 100) == "#00ff00")
|
||||
assert(get_pixel(img, 199, 199) == "#00ff00")
|
||||
assert(get_pixel(img, 201, 201) ~= "#00ff00")
|
||||
|
||||
assert(get_pixel(img, 2, 2) == "#ff0000")
|
||||
assert(get_pixel(img, root_width - 2, 2) == "#ff0000")
|
||||
assert(get_pixel(img, 2, root_height - 2) == "#ff0000")
|
||||
assert(get_pixel(img, root_width - 2, root_height - 2) == "#ff0000")
|
||||
|
||||
if ss.screen ~= nil or ss.client ~= nil then
|
||||
error("Returned non nil screen or client for root screenshot")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end)
|
||||
|
||||
-- Check the awful.screenshot.screen() method
|
||||
table.insert(steps, function()
|
||||
for s in screen do
|
||||
local geo = s.geometry
|
||||
local ss = awful.screenshot {screen = s, directory = "/tmp" }
|
||||
ss:refresh()
|
||||
|
||||
local img = ss.surface
|
||||
assert(img)
|
||||
|
||||
assert(get_pixel(img, 4, 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.width - 4, 4) == "#ff0000")
|
||||
assert(get_pixel(img, 4, geo.height - 4) == "#ff0000")
|
||||
assert(get_pixel(img, geo.width - 4, geo.height - 4) == "#ff0000")
|
||||
end
|
||||
|
||||
-- Spawn for the client.content test
|
||||
assert(#client.get() == 0)
|
||||
spawn(tiny_client)
|
||||
|
||||
return true
|
||||
|
||||
end)
|
||||
|
||||
-- Check the awful.screenshot.client() method
|
||||
table.insert(steps, function()
|
||||
|
||||
if #client.get() ~= 1 then return end
|
||||
|
||||
local c = client.get()[1]
|
||||
local geo = c:geometry()
|
||||
local ss = awful.screenshot {client = c, directory = "/tmp" }
|
||||
ss:refresh()
|
||||
local img = ss.surface
|
||||
assert(img)
|
||||
|
||||
if get_pixel(img, math.floor(geo.width / 2), math.floor(geo.height / 2)) ~= "#0000ff" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Make sure the process finishes. Just `c:kill()` only
|
||||
-- closes the window. Adding some handlers to the GTK "app"
|
||||
-- created some unwanted side effects in the CI.
|
||||
awesome.kill(c.pid, 9)
|
||||
|
||||
return true
|
||||
|
||||
end)
|
||||
|
||||
--Check the snipper toop with awful.screenshot.snipper() method
|
||||
table.insert(steps, function()
|
||||
--Make sure client from last test is gone
|
||||
if #client.get() ~= 0 then return end
|
||||
--Ensure mousegrabber is satisfied
|
||||
root.fake_input("button_press",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_release",1)
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
local ss = awful.screenshot { interactive = true, directory = "/tmp" }
|
||||
ss:refresh()
|
||||
|
||||
ss:connect_signal("snipping::success", snipper_cb)
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
mouse.coords {x = 110, y = 110}
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_press",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
mouse.coords {x = 190, y = 190}
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_release",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
if snipper_success == nil then return end
|
||||
|
||||
return snipper_success
|
||||
end)
|
||||
|
||||
|
||||
--Check the snipper collapse and cancel
|
||||
table.insert(steps, function()
|
||||
--Make sure client from last test is gone
|
||||
if #client.get() ~= 0 then return end
|
||||
--Ensure mousegrabber is satisfied
|
||||
root.fake_input("button_press",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_release",1)
|
||||
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
local ss = awful.screenshot { interactive = true, directory = "/tmp" }
|
||||
ss:connect_signal("snipping::success", snipper_cb)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
mouse.coords {x = 110, y = 110}
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_press",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_release",1)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
mouse.coords {x = 150, y = 150}
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
--Cause a rectangle collapse
|
||||
mouse.coords {x = 150, y = 110}
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
--Cancel snipper tool
|
||||
root.fake_input("button_press",3)
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
root.fake_input("button_release",3)
|
||||
awesome.sync()
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
local ss = awful.screenshot {
|
||||
geometry = {x = 100, y = 100, width = 100, height = 100}
|
||||
}
|
||||
ss:refresh()
|
||||
|
||||
local img = ss.surface
|
||||
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)
|
||||
|
||||
-- Test auto-save
|
||||
table.insert(steps, function()
|
||||
if not escape_works then
|
||||
root.fake_input("key_press","Escape")
|
||||
root.fake_input("key_release","Escape")
|
||||
return
|
||||
end
|
||||
|
||||
local called = false
|
||||
local save = awful.screenshot.save
|
||||
awful.screenshot.save = function() called = true end
|
||||
local ss = awful.screenshot { auto_save_delay = 0 }
|
||||
ss:connect_signal("snipping::cancelled", function() escape_works = true end)
|
||||
awful.screenshot.save = save
|
||||
|
||||
assert(called)
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
local timer_start, timer_tick, timer_timeout, saved = false, false, false, false
|
||||
|
||||
-- Test delayed auto-save
|
||||
table.insert(steps, function()
|
||||
local ss = awful.screenshot { auto_save_tick_duration = 0.05 }
|
||||
|
||||
ss:connect_signal("timer::started", function() timer_start = true end)
|
||||
ss:connect_signal("timer::tick", function() timer_tick = true end)
|
||||
ss:connect_signal("timer::timeout", function() timer_timeout = true end)
|
||||
ss:connect_signal("file::saved", function() saved = true end)
|
||||
|
||||
ss.auto_save_delay = 1
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
if not (timer_start and timer_tick and timer_timeout and saved) then return end
|
||||
return true
|
||||
end)
|
||||
|
||||
table.insert(steps, function()
|
||||
local ss = awful.screenshot { auto_save_delay = 10 }
|
||||
|
||||
-- Reach into some `if`.
|
||||
assert(not ss.selected_geometry)
|
||||
assert(not ss.surface)
|
||||
assert(#ss.surfaces == 0)
|
||||
ss.auto_save_delay = 0
|
||||
ss.minimum_size = 2
|
||||
ss.minimum_size = {width = 1, height = 1}
|
||||
ss.minimum_size = nil
|
||||
assert(ss.surface)
|
||||
assert(next(ss.surfaces) ~= nil)
|
||||
|
||||
local count = 0
|
||||
|
||||
local err = gdebug.print_error
|
||||
|
||||
function gdebug.print_error()
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
-- Cause some validation failures.
|
||||
ss.prefix = "//////"
|
||||
assert(count == 1)
|
||||
ss.directory = "/tmp/////"
|
||||
ss.directory = "/tmp"
|
||||
ss.directory = "/root/"
|
||||
assert(count == 2)
|
||||
|
||||
gdebug.print_error = err
|
||||
return true
|
||||
end)
|
||||
|
||||
require("_runner").run_steps(steps)
|
||||
|
|
Loading…
Reference in New Issue