gnome-shell/src/shell-screenshot.c
Jonas Dreßler c1bfdd74d8 screenshot: Fix slow audiovisual feedback on when taking screenshot
Add a "screenshot-taken" signal from the screenshot service's internal C
implementation, and use that to trigger the camera flash visual effect
and the click sound, allowing them to run in parallel with the PNG
compression instead of waiting until the file is complete to start.

This significantly improves perceived latency on high res setups such as
4K, 5K, or dual 4K screens.

Fixes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/512

Co-authored-by: Brion Vibber <bvibber@wikimedia.org>
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1658>
2021-02-11 18:51:40 +00:00

812 lines
25 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#include <clutter/clutter.h>
#include <cogl/cogl.h>
#include <meta/display.h>
#include <meta/util.h>
#include <meta/meta-plugin.h>
#include <meta/meta-cursor-tracker.h>
#include <st/st.h>
#include "shell-global.h"
#include "shell-screenshot.h"
#include "shell-util.h"
typedef enum _ShellScreenshotFlag
{
SHELL_SCREENSHOT_FLAG_NONE,
SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR,
} ShellScreenshotFlag;
typedef enum _ShellScreenshotMode
{
SHELL_SCREENSHOT_SCREEN,
SHELL_SCREENSHOT_WINDOW,
SHELL_SCREENSHOT_AREA,
} ShellScreenshotMode;
enum
{
SCREENSHOT_TAKEN,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0, };
typedef struct _ShellScreenshotPrivate ShellScreenshotPrivate;
struct _ShellScreenshot
{
GObject parent_instance;
ShellScreenshotPrivate *priv;
};
struct _ShellScreenshotPrivate
{
ShellGlobal *global;
GOutputStream *stream;
ShellScreenshotFlag flags;
ShellScreenshotMode mode;
GDateTime *datetime;
cairo_surface_t *image;
cairo_rectangle_int_t screenshot_area;
gboolean include_frame;
};
G_DEFINE_TYPE_WITH_PRIVATE (ShellScreenshot, shell_screenshot, G_TYPE_OBJECT);
static void
shell_screenshot_class_init (ShellScreenshotClass *screenshot_class)
{
signals[SCREENSHOT_TAKEN] =
g_signal_new ("screenshot-taken",
G_TYPE_FROM_CLASS(screenshot_class),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1,
META_TYPE_RECTANGLE);
}
static void
shell_screenshot_init (ShellScreenshot *screenshot)
{
screenshot->priv = shell_screenshot_get_instance_private (screenshot);
screenshot->priv->global = shell_global_get ();
}
static void
on_screenshot_written (GObject *source,
GAsyncResult *task,
gpointer user_data)
{
ShellScreenshot *screenshot = SHELL_SCREENSHOT (source);
ShellScreenshotPrivate *priv = screenshot->priv;
GTask *result = user_data;
g_task_return_boolean (result, g_task_propagate_boolean (G_TASK (task), NULL));
g_object_unref (result);
g_clear_pointer (&priv->image, cairo_surface_destroy);
g_clear_object (&priv->stream);
g_clear_pointer (&priv->datetime, g_date_time_unref);
}
static void
write_screenshot_thread (GTask *result,
gpointer object,
gpointer task_data,
GCancellable *cancellable)
{
ShellScreenshot *screenshot = SHELL_SCREENSHOT (object);
ShellScreenshotPrivate *priv;
g_autoptr (GOutputStream) stream = NULL;
g_autoptr(GdkPixbuf) pixbuf = NULL;
g_autofree char *creation_time = NULL;
GError *error = NULL;
g_assert (screenshot != NULL);
priv = screenshot->priv;
stream = g_object_ref (priv->stream);
pixbuf = gdk_pixbuf_get_from_surface (priv->image,
0, 0,
cairo_image_surface_get_width (priv->image),
cairo_image_surface_get_height (priv->image));
creation_time = g_date_time_format (priv->datetime, "%c");
if (!creation_time)
creation_time = g_date_time_format (priv->datetime, "%FT%T%z");
gdk_pixbuf_save_to_stream (pixbuf, stream, "png", NULL, &error,
"tEXt::Software", "gnome-screenshot",
"tEXt::Creation Time", creation_time,
NULL);
if (error)
g_task_return_error (result, error);
else
g_task_return_boolean (result, TRUE);
}
static void
do_grab_screenshot (ShellScreenshot *screenshot,
int x,
int y,
int width,
int height,
ShellScreenshotFlag flags)
{
ShellScreenshotPrivate *priv = screenshot->priv;
ClutterStage *stage = shell_global_get_stage (priv->global);
cairo_rectangle_int_t screenshot_rect = { x, y, width, height };
int image_width;
int image_height;
float scale;
cairo_surface_t *image;
ClutterPaintFlag paint_flags = CLUTTER_PAINT_FLAG_NONE;
g_autoptr (GError) error = NULL;
clutter_stage_get_capture_final_size (stage, &screenshot_rect,
&image_width,
&image_height,
&scale);
image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
image_width, image_height);
if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR)
paint_flags |= CLUTTER_PAINT_FLAG_FORCE_CURSORS;
else
paint_flags |= CLUTTER_PAINT_FLAG_NO_CURSORS;
if (!clutter_stage_paint_to_buffer (stage, &screenshot_rect, scale,
cairo_image_surface_get_data (image),
cairo_image_surface_get_stride (image),
CLUTTER_CAIRO_FORMAT_ARGB32,
paint_flags,
&error))
{
cairo_surface_destroy (image);
g_warning ("Failed to take screenshot: %s", error->message);
return;
}
priv->image = image;
priv->datetime = g_date_time_new_now_local ();
}
static void
draw_cursor_image (cairo_surface_t *surface,
cairo_rectangle_int_t area)
{
CoglTexture *texture;
int width, height;
int stride;
guint8 *data;
MetaDisplay *display;
MetaCursorTracker *tracker;
cairo_surface_t *cursor_surface;
cairo_region_t *screenshot_region;
cairo_t *cr;
int x, y;
int xhot, yhot;
double xscale, yscale;
graphene_point_t point;
display = shell_global_get_display (shell_global_get ());
tracker = meta_cursor_tracker_get_for_display (display);
texture = meta_cursor_tracker_get_sprite (tracker);
if (!texture)
return;
screenshot_region = cairo_region_create_rectangle (&area);
meta_cursor_tracker_get_pointer (tracker, &point, NULL);
x = point.x;
y = point.y;
if (!cairo_region_contains_point (screenshot_region, point.x, point.y))
{
cairo_region_destroy (screenshot_region);
return;
}
meta_cursor_tracker_get_hot (tracker, &xhot, &yhot);
width = cogl_texture_get_width (texture);
height = cogl_texture_get_height (texture);
stride = 4 * width;
data = g_new (guint8, stride * height);
cogl_texture_get_data (texture, CLUTTER_CAIRO_FORMAT_ARGB32, stride, data);
/* FIXME: cairo-gl? */
cursor_surface = cairo_image_surface_create_for_data (data,
CAIRO_FORMAT_ARGB32,
width, height,
stride);
cairo_surface_get_device_scale (surface, &xscale, &yscale);
if (xscale != 1.0 || yscale != 1.0)
{
int monitor;
float monitor_scale;
MetaRectangle cursor_rect = {
.x = x, .y = y, .width = width, .height = height
};
monitor = meta_display_get_monitor_index_for_rect (display, &cursor_rect);
monitor_scale = meta_display_get_monitor_scale (display, monitor);
cairo_surface_set_device_scale (cursor_surface, monitor_scale, monitor_scale);
}
cr = cairo_create (surface);
cairo_set_source_surface (cr,
cursor_surface,
x - xhot - area.x,
y - yhot - area.y);
cairo_paint (cr);
cairo_destroy (cr);
cairo_surface_destroy (cursor_surface);
cairo_region_destroy (screenshot_region);
g_free (data);
}
static void
grab_screenshot (ShellScreenshot *screenshot,
ShellScreenshotFlag flags,
GTask *result)
{
ShellScreenshotPrivate *priv = screenshot->priv;
MetaDisplay *display;
int width, height;
GTask *task;
display = shell_global_get_display (priv->global);
meta_display_get_size (display, &width, &height);
do_grab_screenshot (screenshot,
0, 0, width, height,
flags);
priv->screenshot_area.x = 0;
priv->screenshot_area.y = 0;
priv->screenshot_area.width = width;
priv->screenshot_area.height = height;
task = g_task_new (screenshot, NULL, on_screenshot_written, result);
g_task_run_in_thread (task, write_screenshot_thread);
g_object_unref (task);
}
static void
grab_window_screenshot (ShellScreenshot *screenshot,
ShellScreenshotFlag flags,
GTask *result)
{
ShellScreenshotPrivate *priv = screenshot->priv;
GTask *task;
MetaDisplay *display = shell_global_get_display (priv->global);
MetaWindow *window = meta_display_get_focus_window (display);
ClutterActor *window_actor;
gfloat actor_x, actor_y;
MetaRectangle rect;
window_actor = CLUTTER_ACTOR (meta_window_get_compositor_private (window));
clutter_actor_get_position (window_actor, &actor_x, &actor_y);
meta_window_get_frame_rect (window, &rect);
if (!priv->include_frame)
meta_window_frame_rect_to_client_rect (window, &rect, &rect);
priv->screenshot_area = rect;
priv->image = meta_window_actor_get_image (META_WINDOW_ACTOR (window_actor),
NULL);
if (!priv->image)
{
g_task_report_new_error (screenshot, on_screenshot_written, result, NULL,
G_IO_ERROR, G_IO_ERROR_FAILED,
"Capturing window failed");
return;
}
priv->datetime = g_date_time_new_now_local ();
if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR)
{
if (meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_WAYLAND)
{
float resource_scale;
resource_scale = clutter_actor_get_resource_scale (window_actor);
cairo_surface_set_device_scale (priv->image, resource_scale, resource_scale);
}
draw_cursor_image (priv->image, priv->screenshot_area);
}
g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0, &rect);
task = g_task_new (screenshot, NULL, on_screenshot_written, result);
g_task_run_in_thread (task, write_screenshot_thread);
g_object_unref (task);
}
static gboolean
finish_screenshot (ShellScreenshot *screenshot,
GAsyncResult *result,
cairo_rectangle_int_t **area,
GError **error)
{
ShellScreenshotPrivate *priv = screenshot->priv;
if (!g_task_propagate_boolean (G_TASK (result), error))
return FALSE;
if (area)
*area = &priv->screenshot_area;
return TRUE;
}
static void
on_after_paint (ClutterStage *stage,
ClutterStageView *view,
GTask *result)
{
ShellScreenshot *screenshot = g_task_get_task_data (result);
ShellScreenshotPrivate *priv = screenshot->priv;
MetaDisplay *display = shell_global_get_display (priv->global);
GTask *task;
g_signal_handlers_disconnect_by_func (stage, on_after_paint, result);
if (priv->mode == SHELL_SCREENSHOT_AREA)
{
do_grab_screenshot (screenshot,
priv->screenshot_area.x,
priv->screenshot_area.y,
priv->screenshot_area.width,
priv->screenshot_area.height,
priv->flags);
task = g_task_new (screenshot, NULL, on_screenshot_written, result);
g_task_run_in_thread (task, write_screenshot_thread);
}
else
{
grab_screenshot (screenshot, priv->flags, result);
}
g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0,
(cairo_rectangle_int_t *) &priv->screenshot_area);
meta_enable_unredirect_for_display (display);
}
/**
* shell_screenshot_screenshot:
* @screenshot: the #ShellScreenshot
* @include_cursor: Whether to include the cursor or not
* @stream: The stream for the screenshot
* @callback: (scope async): function to call returning success or failure
* of the async grabbing
* @user_data: the data to pass to callback function
*
* Takes a screenshot of the whole screen
* in @stream as png image.
*
*/
void
shell_screenshot_screenshot (ShellScreenshot *screenshot,
gboolean include_cursor,
GOutputStream *stream,
GAsyncReadyCallback callback,
gpointer user_data)
{
ShellScreenshotPrivate *priv;
GTask *result;
ShellScreenshotFlag flags;
g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot));
g_return_if_fail (G_IS_OUTPUT_STREAM (stream));
priv = screenshot->priv;
if (priv->stream != NULL) {
if (callback)
g_task_report_new_error (screenshot,
callback,
user_data,
shell_screenshot_screenshot,
G_IO_ERROR,
G_IO_ERROR_PENDING,
"Only one screenshot operation at a time "
"is permitted");
return;
}
result = g_task_new (screenshot, NULL, callback, user_data);
g_task_set_source_tag (result, shell_screenshot_screenshot);
g_task_set_task_data (result, screenshot, NULL);
priv->stream = g_object_ref (stream);
flags = SHELL_SCREENSHOT_FLAG_NONE;
if (include_cursor)
flags |= SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR;
if (meta_is_wayland_compositor ())
{
grab_screenshot (screenshot, flags, result);
g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0,
(cairo_rectangle_int_t *) &priv->screenshot_area);
}
else
{
MetaDisplay *display = shell_global_get_display (priv->global);
ClutterStage *stage = shell_global_get_stage (priv->global);
meta_disable_unredirect_for_display (display);
clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
priv->flags = flags;
priv->mode = SHELL_SCREENSHOT_SCREEN;
g_signal_connect (stage, "after-paint",
G_CALLBACK (on_after_paint), result);
}
}
/**
* shell_screenshot_screenshot_finish:
* @screenshot: the #ShellScreenshot
* @result: the #GAsyncResult that was provided to the callback
* @area: (out) (transfer none): the area that was grabbed in screen coordinates
* @error: #GError for error reporting
*
* Finish the asynchronous operation started by shell_screenshot_screenshot()
* and obtain its result.
*
* Returns: whether the operation was successful
*
*/
gboolean
shell_screenshot_screenshot_finish (ShellScreenshot *screenshot,
GAsyncResult *result,
cairo_rectangle_int_t **area,
GError **error)
{
g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE);
g_return_val_if_fail (G_IS_TASK (result), FALSE);
g_return_val_if_fail (g_async_result_is_tagged (result,
shell_screenshot_screenshot),
FALSE);
return finish_screenshot (screenshot, result, area, error);
}
/**
* shell_screenshot_screenshot_area:
* @screenshot: the #ShellScreenshot
* @x: The X coordinate of the area
* @y: The Y coordinate of the area
* @width: The width of the area
* @height: The height of the area
* @stream: The stream for the screenshot
* @callback: (scope async): function to call returning success or failure
* of the async grabbing
* @user_data: the data to pass to callback function
*
* Takes a screenshot of the passed in area and saves it
* in @stream as png image.
*
*/
void
shell_screenshot_screenshot_area (ShellScreenshot *screenshot,
int x,
int y,
int width,
int height,
GOutputStream *stream,
GAsyncReadyCallback callback,
gpointer user_data)
{
ShellScreenshotPrivate *priv;
GTask *result;
g_autoptr (GTask) task = NULL;
g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot));
g_return_if_fail (G_IS_OUTPUT_STREAM (stream));
priv = screenshot->priv;
if (priv->stream != NULL) {
if (callback)
g_task_report_new_error (screenshot,
callback,
NULL,
shell_screenshot_screenshot_area,
G_IO_ERROR,
G_IO_ERROR_PENDING,
"Only one screenshot operation at a time "
"is permitted");
return;
}
result = g_task_new (screenshot, NULL, callback, user_data);
g_task_set_source_tag (result, shell_screenshot_screenshot_area);
g_task_set_task_data (result, screenshot, NULL);
priv->stream = g_object_ref (stream);
priv->screenshot_area.x = x;
priv->screenshot_area.y = y;
priv->screenshot_area.width = width;
priv->screenshot_area.height = height;
if (meta_is_wayland_compositor ())
{
do_grab_screenshot (screenshot,
priv->screenshot_area.x,
priv->screenshot_area.y,
priv->screenshot_area.width,
priv->screenshot_area.height,
SHELL_SCREENSHOT_FLAG_NONE);
g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0,
(cairo_rectangle_int_t *) &priv->screenshot_area);
task = g_task_new (screenshot, NULL, on_screenshot_written, result);
g_task_run_in_thread (task, write_screenshot_thread);
}
else
{
MetaDisplay *display = shell_global_get_display (priv->global);
ClutterStage *stage = shell_global_get_stage (priv->global);
meta_disable_unredirect_for_display (display);
clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
priv->flags = SHELL_SCREENSHOT_FLAG_NONE;
priv->mode = SHELL_SCREENSHOT_AREA;
g_signal_connect (stage, "after-paint",
G_CALLBACK (on_after_paint), result);
}
}
/**
* shell_screenshot_screenshot_area_finish:
* @screenshot: the #ShellScreenshot
* @result: the #GAsyncResult that was provided to the callback
* @area: (out) (transfer none): the area that was grabbed in screen coordinates
* @error: #GError for error reporting
*
* Finish the asynchronous operation started by shell_screenshot_screenshot_area()
* and obtain its result.
*
* Returns: whether the operation was successful
*
*/
gboolean
shell_screenshot_screenshot_area_finish (ShellScreenshot *screenshot,
GAsyncResult *result,
cairo_rectangle_int_t **area,
GError **error)
{
g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE);
g_return_val_if_fail (G_IS_TASK (result), FALSE);
g_return_val_if_fail (g_async_result_is_tagged (result,
shell_screenshot_screenshot_area),
FALSE);
return finish_screenshot (screenshot, result, area, error);
}
/**
* shell_screenshot_screenshot_window:
* @screenshot: the #ShellScreenshot
* @include_frame: Whether to include the frame or not
* @include_cursor: Whether to include the cursor or not
* @stream: The stream for the screenshot
* @callback: (scope async): function to call returning success or failure
* of the async grabbing
* @user_data: the data to pass to callback function
*
* Takes a screenshot of the focused window (optionally omitting the frame)
* in @stream as png image.
*
*/
void
shell_screenshot_screenshot_window (ShellScreenshot *screenshot,
gboolean include_frame,
gboolean include_cursor,
GOutputStream *stream,
GAsyncReadyCallback callback,
gpointer user_data)
{
ShellScreenshotPrivate *priv;
MetaDisplay *display;
MetaWindow *window;
GTask *result;
g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot));
g_return_if_fail (G_IS_OUTPUT_STREAM (stream));
priv = screenshot->priv;
display = shell_global_get_display (priv->global);
window = meta_display_get_focus_window (display);
if (priv->stream != NULL || !window) {
if (callback)
g_task_report_new_error (screenshot,
callback,
NULL,
shell_screenshot_screenshot_window,
G_IO_ERROR,
G_IO_ERROR_PENDING,
"Only one screenshot operation at a time "
"is permitted");
return;
}
result = g_task_new (screenshot, NULL, callback, user_data);
g_task_set_source_tag (result, shell_screenshot_screenshot_window);
priv->stream = g_object_ref (stream);
priv->include_frame = include_frame;
grab_window_screenshot (screenshot, include_cursor, result);
}
/**
* shell_screenshot_screenshot_window_finish:
* @screenshot: the #ShellScreenshot
* @result: the #GAsyncResult that was provided to the callback
* @area: (out) (transfer none): the area that was grabbed in screen coordinates
* @error: #GError for error reporting
*
* Finish the asynchronous operation started by shell_screenshot_screenshot_window()
* and obtain its result.
*
* Returns: whether the operation was successful
*
*/
gboolean
shell_screenshot_screenshot_window_finish (ShellScreenshot *screenshot,
GAsyncResult *result,
cairo_rectangle_int_t **area,
GError **error)
{
g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE);
g_return_val_if_fail (G_IS_TASK (result), FALSE);
g_return_val_if_fail (g_async_result_is_tagged (result,
shell_screenshot_screenshot_window),
FALSE);
return finish_screenshot (screenshot, result, area, error);
}
/**
* shell_screenshot_pick_color:
* @screenshot: the #ShellScreenshot
* @x: The X coordinate to pick
* @y: The Y coordinate to pick
* @callback: (scope async): function to call returning success or failure
* of the async grabbing
*
* Picks the pixel at @x, @y and returns its color as #ClutterColor.
*
*/
void
shell_screenshot_pick_color (ShellScreenshot *screenshot,
int x,
int y,
GAsyncReadyCallback callback,
gpointer user_data)
{
ShellScreenshotPrivate *priv;
g_autoptr (GTask) result = NULL;
g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot));
result = g_task_new (screenshot, NULL, callback, user_data);
g_task_set_source_tag (result, shell_screenshot_pick_color);
priv = screenshot->priv;
priv->screenshot_area.x = x;
priv->screenshot_area.y = y;
priv->screenshot_area.width = 1;
priv->screenshot_area.height = 1;
do_grab_screenshot (screenshot,
priv->screenshot_area.x,
priv->screenshot_area.y,
1,
1,
SHELL_SCREENSHOT_FLAG_NONE);
g_task_return_boolean (result, TRUE);
}
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
#define INDEX_A 3
#define INDEX_R 2
#define INDEX_G 1
#define INDEX_B 0
#else
#define INDEX_A 0
#define INDEX_R 1
#define INDEX_G 2
#define INDEX_B 3
#endif
/**
* shell_screenshot_pick_color_finish:
* @screenshot: the #ShellScreenshot
* @result: the #GAsyncResult that was provided to the callback
* @color: (out caller-allocates): the picked color
* @error: #GError for error reporting
*
* Finish the asynchronous operation started by shell_screenshot_pick_color()
* and obtain its result.
*
* Returns: whether the operation was successful
*
*/
gboolean
shell_screenshot_pick_color_finish (ShellScreenshot *screenshot,
GAsyncResult *result,
ClutterColor *color,
GError **error)
{
ShellScreenshotPrivate *priv;
g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE);
g_return_val_if_fail (G_IS_TASK (result), FALSE);
g_return_val_if_fail (color != NULL, FALSE);
g_return_val_if_fail (g_async_result_is_tagged (result,
shell_screenshot_pick_color),
FALSE);
if (!g_task_propagate_boolean (G_TASK (result), error))
return FALSE;
priv = screenshot->priv;
/* protect against mutter changing the format used for stage captures */
g_assert (cairo_image_surface_get_format (priv->image) == CAIRO_FORMAT_ARGB32);
if (color)
{
uint8_t *data = cairo_image_surface_get_data (priv->image);
color->alpha = data[INDEX_A];
color->red = data[INDEX_R];
color->green = data[INDEX_G];
color->blue = data[INDEX_B];
}
return TRUE;
}
#undef INDEX_A
#undef INDEX_R
#undef INDEX_G
#undef INDEX_B
ShellScreenshot *
shell_screenshot_new (void)
{
return g_object_new (SHELL_TYPE_SCREENSHOT, NULL);
}