/* gnome-screenshot.c - Take a screenshot of the desktop * * Copyright (C) 2001 Jonathan Blandford * Copyright (C) 2006 Emmanuele Bassi * Copyright (C) 2008-2012 Cosimo Cecchi * Copyright (C) 2020 Philipp Wolfer * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA */ #include "config.h" #include #include #include #include #include #include #include #include #include "screenshot-application.h" #include "screenshot-area-selection.h" #include "screenshot-config.h" #include "screenshot-filename-builder.h" #include "screenshot-interactive-dialog.h" #include "screenshot-utils.h" #include "screenshot-dialog.h" #define LAST_SAVE_DIRECTORY_KEY "last-save-directory" static void screenshot_save_to_file (ScreenshotApplication *self); static void screenshot_show_interactive_dialog (ScreenshotApplication *self); struct _ScreenshotApplication { GtkApplication parent_instance; gchar *icc_profile_base64; GdkPixbuf *screenshot; gchar *save_uri; gboolean should_overwrite; GdkRectangle *rectangle; ScreenshotDialog *dialog; }; G_DEFINE_TYPE (ScreenshotApplication, screenshot_application, GTK_TYPE_APPLICATION) static void save_folder_to_settings (ScreenshotApplication *self) { g_autofree gchar *folder = screenshot_dialog_get_folder (self->dialog); g_settings_set_string (screenshot_config->settings, LAST_SAVE_DIRECTORY_KEY, folder); } static void set_recent_entry (ScreenshotApplication *self) { g_autofree gchar *app_exec = NULL; g_autoptr(GAppInfo) app = NULL; GtkRecentManager *recent; GtkRecentData recent_data; const char *exec_name = NULL; static char * groups[2] = { "Graphics", NULL }; app = g_app_info_get_default_for_type ("image/png", TRUE); if (!app) { /* return early, as this would be an useless recent entry anyway. */ return; } recent = gtk_recent_manager_get_default (); exec_name = g_app_info_get_executable (app); app_exec = g_strjoin (" ", exec_name, "%u", NULL); recent_data.display_name = NULL; recent_data.description = NULL; recent_data.mime_type = "image/png"; recent_data.app_name = "GNOME Screenshot"; recent_data.app_exec = app_exec; recent_data.groups = groups; recent_data.is_private = FALSE; gtk_recent_manager_add_full (recent, self->save_uri, &recent_data); } static void screenshot_close_interactive_dialog (ScreenshotApplication *self) { ScreenshotDialog *dialog = self->dialog; save_folder_to_settings (self); gtk_widget_destroy (GTK_WIDGET (dialog)); } static void save_pixbuf_handle_success (ScreenshotApplication *self) { set_recent_entry (self); if (screenshot_config->interactive) { screenshot_close_interactive_dialog (self); } else { g_application_release (G_APPLICATION (self)); } } static void save_pixbuf_handle_error (ScreenshotApplication *self, GError *error) { if (screenshot_config->interactive) { ScreenshotDialog *dialog = self->dialog; screenshot_dialog_set_busy (dialog, FALSE); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS) && !self->should_overwrite) { g_autofree gchar *folder = screenshot_dialog_get_folder (dialog); g_autofree gchar *folder_uri = g_path_get_basename (folder); g_autofree gchar *folder_name = g_uri_unescape_string (folder_uri, NULL); g_autofree gchar *file_name = screenshot_dialog_get_filename (dialog); g_autofree gchar *detail = g_strdup_printf (_("A file named “%s” already exists in “%s”"), file_name, folder_name); gint response = screenshot_show_dialog (GTK_WINDOW (dialog), GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, _("Overwrite existing file?"), detail); if (response == GTK_RESPONSE_YES) { self->should_overwrite = TRUE; screenshot_save_to_file (self); return; } } else { screenshot_show_dialog (GTK_WINDOW (dialog), GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Unable to capture a screenshot"), _("Error creating file. Please choose another location and retry.")); } gtk_widget_grab_focus (screenshot_dialog_get_filename_entry (dialog)); } else { g_critical ("Unable to save the screenshot: %s", error->message); screenshot_play_sound_effect ("dialog-error", _("Unable to capture a screenshot")); g_application_release (G_APPLICATION (self)); if (screenshot_config->file != NULL) exit (EXIT_FAILURE); } } static void save_pixbuf_ready_cb (GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; ScreenshotApplication *self = user_data; gdk_pixbuf_save_to_stream_finish (res, &error); if (error != NULL) { save_pixbuf_handle_error (self, error); return; } save_pixbuf_handle_success (self); } static void find_out_writable_format_by_extension (gpointer data, gpointer user_data) { GdkPixbufFormat *format = (GdkPixbufFormat*) data; gchar **name = (gchar **) user_data; g_auto(GStrv) extensions = gdk_pixbuf_format_get_extensions (format); gchar **ptr = extensions; while (*ptr != NULL) { if (g_strcmp0 (*ptr, *name) == 0 && gdk_pixbuf_format_is_writable (format) == TRUE) { g_free (*name); *name = gdk_pixbuf_format_get_name (format); break; } ptr++; } } static gboolean is_png (gchar *format) { if (g_strcmp0 (format, "png") == 0) return TRUE; else return FALSE; } static gboolean has_profile (ScreenshotApplication *self) { return (self->icc_profile_base64 != NULL); } static void save_with_description_and_profile (ScreenshotApplication *self, GFileOutputStream *os, gchar *format) { gdk_pixbuf_save_to_stream_async (self->screenshot, G_OUTPUT_STREAM (os), format, NULL, save_pixbuf_ready_cb, self, "icc-profile", self->icc_profile_base64, "tEXt::Software", "gnome-screenshot", NULL); } static void save_with_description (ScreenshotApplication *self, GFileOutputStream *os, gchar *format) { gdk_pixbuf_save_to_stream_async (self->screenshot, G_OUTPUT_STREAM (os), format, NULL, save_pixbuf_ready_cb, self, "tEXt::Software", "gnome-screenshot", NULL); } static void save_with_no_profile_or_description (ScreenshotApplication *self, GFileOutputStream *os, gchar *format) { gdk_pixbuf_save_to_stream_async (self->screenshot, G_OUTPUT_STREAM (os), format, NULL, save_pixbuf_ready_cb, self, NULL); } static void save_file_create_ready_cb (GObject *source, GAsyncResult *res, gpointer user_data) { ScreenshotApplication *self = user_data; g_autoptr(GFileOutputStream) os = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *basename = g_file_get_basename (G_FILE (source)); g_autofree gchar *format = NULL; gchar *extension = g_strrstr (basename, "."); GSList *formats = NULL; if (extension == NULL) extension = "png"; else extension++; format = g_strdup (extension); formats = gdk_pixbuf_get_formats(); g_slist_foreach (formats, find_out_writable_format_by_extension, (gpointer) &format); g_slist_free (formats); if (self->should_overwrite) os = g_file_replace_finish (G_FILE (source), res, &error); else os = g_file_create_finish (G_FILE (source), res, &error); if (error != NULL) { save_pixbuf_handle_error (self, error); return; } if (is_png (format)) { if (has_profile (self)) save_with_description_and_profile (self, os, format); else save_with_description (self, os, format); } else { save_with_no_profile_or_description (self, os, format); } } static void screenshot_save_to_file (ScreenshotApplication *self) { g_autoptr(GFile) target_file = NULL; if (self->dialog != NULL) screenshot_dialog_set_busy (self->dialog, TRUE); target_file = g_file_new_for_uri (self->save_uri); if (self->should_overwrite) { g_file_replace_async (target_file, NULL, FALSE, G_FILE_CREATE_NONE, G_PRIORITY_DEFAULT, NULL, save_file_create_ready_cb, self); } else { g_file_create_async (target_file, G_FILE_CREATE_NONE, G_PRIORITY_DEFAULT, NULL, save_file_create_ready_cb, self); } } static void screenshot_back (ScreenshotApplication *self) { screenshot_close_interactive_dialog (self); screenshot_show_interactive_dialog (self); } static void screenshot_save_to_clipboard (ScreenshotApplication *self) { GtkClipboard *clipboard; clipboard = gtk_clipboard_get_for_display (gdk_display_get_default (), GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_image (clipboard, self->screenshot); } static void save_clicked_cb (ScreenshotDialog *dialog, ScreenshotApplication *self) { /* update to the new URI */ g_free (self->save_uri); self->save_uri = screenshot_dialog_get_uri (self->dialog); screenshot_save_to_file (self); } static void copy_clicked_cb (ScreenshotDialog *dialog, ScreenshotApplication *self) { screenshot_save_to_clipboard (self); } static void back_clicked_cb (ScreenshotDialog *dialog, ScreenshotApplication *self) { screenshot_back (self); } static void build_filename_ready_cb (GObject *source, GAsyncResult *res, gpointer user_data) { ScreenshotApplication *self = user_data; g_autoptr(GError) error = NULL; g_autofree gchar *save_path = screenshot_build_filename_finish (res, &error); if (save_path != NULL) { g_autoptr(GFile) file = g_file_new_for_path (save_path); self->save_uri = g_file_get_uri (file); } else self->save_uri = NULL; /* now release the application */ g_application_release (G_APPLICATION (self)); if (error != NULL) { g_critical ("Impossible to find a valid location to save the screenshot: %s", error->message); if (screenshot_config->interactive) screenshot_show_dialog (NULL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Unable to capture a screenshot"), _("Error creating file")); else { screenshot_play_sound_effect ("dialog-error", _("Unable to capture a screenshot")); if (screenshot_config->file != NULL) exit (EXIT_FAILURE); } return; } screenshot_play_sound_effect ("screen-capture", _("Screenshot taken")); if (screenshot_config->interactive) { self->dialog = screenshot_dialog_new (GTK_APPLICATION (self), self->screenshot, self->save_uri); g_signal_connect_object (self->dialog, "save", G_CALLBACK (save_clicked_cb), self, 0); g_signal_connect_object (self->dialog, "copy", G_CALLBACK (copy_clicked_cb), self, 0); g_signal_connect_object (self->dialog, "back", G_CALLBACK (back_clicked_cb), self, 0); } else { g_application_hold (G_APPLICATION (self)); screenshot_save_to_file (self); } } static void finish_take_screenshot (ScreenshotApplication *self) { GdkPixbuf *screenshot; screenshot = screenshot_get_pixbuf (self->rectangle); g_clear_pointer (&self->rectangle, g_free); if (screenshot == NULL) { g_critical ("Unable to capture a screenshot of any window"); if (screenshot_config->interactive) screenshot_show_dialog (NULL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Unable to capture a screenshot"), _("All possible methods failed")); else screenshot_play_sound_effect ("dialog-error", _("Unable to capture a screenshot")); g_application_release (G_APPLICATION (self)); if (screenshot_config->file != NULL) exit (EXIT_FAILURE); return; } self->screenshot = screenshot; if (screenshot_config->copy_to_clipboard) { screenshot_save_to_clipboard (self); screenshot_play_sound_effect ("screen-capture", _("Screenshot taken")); if (screenshot_config->file == NULL) { g_application_release (G_APPLICATION (self)); return; } } /* FIXME: apply the ICC profile according to the preferences. * org.gnome.ColorManager.GetProfileForWindow() does not exist anymore, * so we probably need to fetch the color profile of the screen where * the area/window was. * * screenshot_ensure_icc_profile (window); */ if (screenshot_config->file != NULL) { self->save_uri = g_file_get_uri (screenshot_config->file); self->should_overwrite = TRUE; screenshot_save_to_file (self); } else screenshot_build_filename_async (screenshot_config->save_dir, NULL, build_filename_ready_cb, self); } static gboolean take_screenshot_timeout (gpointer user_data) { ScreenshotApplication *self = user_data; finish_take_screenshot (self); return FALSE; } static void start_screenshot_timeout (ScreenshotApplication *self) { guint delay = screenshot_config->delay * 1000; if (!screenshot_config->take_area_shot) /* hold the GApplication while doing the async screenshot op */ g_application_hold (G_APPLICATION (self)); /* HACK: give time to the dialog to actually disappear. * We don't have any way to tell when the compositor has finished * re-drawing. */ if (delay == 0 && screenshot_config->interactive) delay = 200; if (delay > 0) g_timeout_add (delay, take_screenshot_timeout, self); else g_idle_add (take_screenshot_timeout, self); } static void rectangle_found_cb (GdkRectangle *rectangle, gpointer user_data) { ScreenshotApplication *self = user_data; if (rectangle != NULL) { self->rectangle = g_memdup (rectangle, sizeof *rectangle); start_screenshot_timeout (self); } else { /* user dismissed the area selection, possibly show the dialog again */ g_application_release (G_APPLICATION (self)); if (screenshot_config->interactive) screenshot_show_interactive_dialog (self); } } static void screenshot_start (ScreenshotApplication *self) { if (screenshot_config->take_area_shot) { /* hold the GApplication while selecting the screen area */ g_application_hold (G_APPLICATION (self)); screenshot_select_area_async (rectangle_found_cb, self); } else { start_screenshot_timeout (self); } screenshot_save_config (); } static gboolean version_arg = FALSE; static const GOptionEntry entries[] = { { "clipboard", 'c', 0, G_OPTION_ARG_NONE, NULL, N_("Send the grab directly to the clipboard"), NULL }, { "window", 'w', 0, G_OPTION_ARG_NONE, NULL, N_("Grab a window instead of the entire screen"), NULL }, { "area", 'a', 0, G_OPTION_ARG_NONE, NULL, N_("Grab an area of the screen instead of the entire screen"), NULL }, { "include-border", 'b', 0, G_OPTION_ARG_NONE, NULL, N_("Include the window border with the screenshot. This option is deprecated and window border is always included"), NULL }, { "remove-border", 'B', 0, G_OPTION_ARG_NONE, NULL, N_("Remove the window border from the screenshot. This option is deprecated and window border is always included"), NULL }, { "include-pointer", 'p', 0, G_OPTION_ARG_NONE, NULL, N_("Include the pointer with the screenshot"), NULL }, { "delay", 'd', 0, G_OPTION_ARG_INT, NULL, N_("Take screenshot after specified delay [in seconds]"), N_("seconds") }, { "border-effect", 'e', 0, G_OPTION_ARG_STRING, NULL, N_("Effect to add to the border (‘shadow’, ‘border’, ‘vintage’ or ‘none’). Note: This option is deprecated and is assumed to be ‘none’"), N_("effect") }, { "interactive", 'i', 0, G_OPTION_ARG_NONE, NULL, N_("Interactively set options"), NULL }, { "file", 'f', 0, G_OPTION_ARG_FILENAME, NULL, N_("Save screenshot directly to this file"), N_("filename") }, { "version", 0, 0, G_OPTION_ARG_NONE, &version_arg, N_("Print version information and exit"), NULL }, { NULL }, }; static gint screenshot_application_handle_local_options (GApplication *app, GVariantDict *options) { if (version_arg) { g_print ("%s %s\n", g_get_application_name (), VERSION); exit (EXIT_SUCCESS); } /* Start headless instances in non-unique mode */ if (!g_variant_dict_contains (options, "interactive")) { GApplicationFlags old_flags; old_flags = g_application_get_flags (app); if ((old_flags & G_APPLICATION_IS_SERVICE) == 0) { g_application_set_flags (app, old_flags | G_APPLICATION_NON_UNIQUE); } } return -1; } static gint screenshot_application_command_line (GApplication *app, GApplicationCommandLine *command_line) { ScreenshotApplication *self = SCREENSHOT_APPLICATION (app); gboolean clipboard_arg = FALSE; gboolean window_arg = FALSE; gboolean area_arg = FALSE; gboolean include_border_arg = FALSE; gboolean disable_border_arg = FALSE; gboolean include_pointer_arg = FALSE; gboolean interactive_arg = FALSE; gchar *border_effect_arg = NULL; guint delay_arg = 0; gchar *file_arg = NULL; GVariantDict *options; gint exit_status = EXIT_SUCCESS; gboolean res; options = g_application_command_line_get_options_dict (command_line); g_variant_dict_lookup (options, "clipboard", "b", &clipboard_arg); g_variant_dict_lookup (options, "window", "b", &window_arg); g_variant_dict_lookup (options, "area", "b", &area_arg); g_variant_dict_lookup (options, "include-border", "b", &include_border_arg); g_variant_dict_lookup (options, "remove-border", "b", &disable_border_arg); g_variant_dict_lookup (options, "include-pointer", "b", &include_pointer_arg); g_variant_dict_lookup (options, "interactive", "b", &interactive_arg); g_variant_dict_lookup (options, "border-effect", "&s", &border_effect_arg); g_variant_dict_lookup (options, "delay", "i", &delay_arg); g_variant_dict_lookup (options, "file", "^&ay", &file_arg); res = screenshot_config_parse_command_line (clipboard_arg, window_arg, area_arg, include_border_arg, disable_border_arg, include_pointer_arg, border_effect_arg, delay_arg, interactive_arg, file_arg); if (!res) { exit_status = EXIT_FAILURE; goto out; } /* interactive mode: trigger the dialog and wait for the response */ if (screenshot_config->interactive) g_application_activate (app); else screenshot_start (self); out: return exit_status; } static void capture_clicked_cb (ScreenshotInteractiveDialog *dialog, ScreenshotApplication *self) { gtk_widget_destroy (GTK_WIDGET (dialog)); screenshot_start (self); } static void screenshot_show_interactive_dialog (ScreenshotApplication *self) { ScreenshotInteractiveDialog *dialog; dialog = screenshot_interactive_dialog_new (GTK_APPLICATION (self)); g_signal_connect_object (dialog, "capture", G_CALLBACK (capture_clicked_cb), self, 0); gtk_widget_show (GTK_WIDGET (dialog)); } static void action_quit (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GList *windows = gtk_application_get_windows (GTK_APPLICATION (user_data)); gtk_widget_destroy (g_list_nth_data (windows, 0)); } static void action_help (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GList *windows = gtk_application_get_windows (GTK_APPLICATION (user_data)); screenshot_display_help (g_list_nth_data (windows, 0)); } static void action_about (GSimpleAction *action, GVariant *parameter, gpointer user_data) { const gchar *authors[] = { "Emmanuele Bassi", "Jonathan Blandford", "Cosimo Cecchi", NULL }; const gchar *artists[] = { "Tobias Bernard", "Jakub Steiner", NULL }; GList *windows = gtk_application_get_windows (GTK_APPLICATION (user_data)); gtk_show_about_dialog (GTK_WINDOW (g_list_nth_data (windows, 0)), "version", VERSION, "authors", authors, "artists", artists, "program-name", _("Screenshot"), "comments", _("Save images of your screen or individual windows"), "logo-icon-name", SCREENSHOT_ICON_NAME, "translator-credits", _("translator-credits"), "license-type", GTK_LICENSE_GPL_2_0, "wrap-license", TRUE, NULL); } static void action_screen_shot (GSimpleAction *action, GVariant *parameter, gpointer user_data) { ScreenshotApplication *self = SCREENSHOT_APPLICATION (user_data); screenshot_config_parse_command_line (FALSE, /* clipboard */ FALSE, /* window */ FALSE, /* area */ FALSE, /* include border */ FALSE, /* disable border */ FALSE, /* include pointer */ NULL, /* border effect */ 0, /* delay */ FALSE, /* interactive */ NULL); /* file */ screenshot_start (self); } static void action_window_shot (GSimpleAction *action, GVariant *parameter, gpointer user_data) { ScreenshotApplication *self = SCREENSHOT_APPLICATION (user_data); screenshot_config_parse_command_line (FALSE, /* clipboard */ TRUE, /* window */ FALSE, /* area */ FALSE, /* include border */ FALSE, /* disable border */ FALSE, /* include pointer */ NULL, /* border effect */ 0, /* delay */ FALSE, /* interactive */ NULL); /* file */ screenshot_start (self); } static GActionEntry action_entries[] = { { "about", action_about, NULL, NULL, NULL }, { "help", action_help, NULL, NULL, NULL }, { "quit", action_quit, NULL, NULL, NULL }, { "screen-shot", action_screen_shot, NULL, NULL, NULL }, { "window-shot", action_window_shot, NULL, NULL, NULL } }; static void screenshot_application_startup (GApplication *app) { const gchar *help_accels[2] = { "F1", NULL }; const gchar *quit_accels[2] = { "q", NULL }; ScreenshotApplication *self = SCREENSHOT_APPLICATION (app); G_APPLICATION_CLASS (screenshot_application_parent_class)->startup (app); hdy_init (); screenshot_load_config (); g_set_application_name (_("Screenshot")); gtk_window_set_default_icon_name (SCREENSHOT_ICON_NAME); g_action_map_add_action_entries (G_ACTION_MAP (self), action_entries, G_N_ELEMENTS (action_entries), self); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "app.help", help_accels); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "app.quit", quit_accels); } static void screenshot_application_activate (GApplication *app) { GtkWindow *window; window = gtk_application_get_active_window (GTK_APPLICATION (app)); if (window != NULL) { gtk_window_present (GTK_WINDOW (window)); return; } screenshot_config->interactive = TRUE; screenshot_show_interactive_dialog (SCREENSHOT_APPLICATION (app)); } static void screenshot_application_finalize (GObject *object) { ScreenshotApplication *self = SCREENSHOT_APPLICATION (object); g_clear_object (&self->screenshot); g_free (self->icc_profile_base64); g_free (self->save_uri); g_clear_pointer (&self->rectangle, g_free); G_OBJECT_CLASS (screenshot_application_parent_class)->finalize (object); } static void screenshot_application_class_init (ScreenshotApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *application_class = G_APPLICATION_CLASS (klass); object_class->finalize = screenshot_application_finalize; application_class->handle_local_options = screenshot_application_handle_local_options; application_class->command_line = screenshot_application_command_line; application_class->startup = screenshot_application_startup; application_class->activate = screenshot_application_activate; } static void screenshot_application_init (ScreenshotApplication *self) { g_application_add_main_option_entries (G_APPLICATION (self), entries); } ScreenshotApplication * screenshot_application_new (void) { return g_object_new (SCREENSHOT_TYPE_APPLICATION, "application-id", "org.gnome.Screenshot", "flags", G_APPLICATION_HANDLES_COMMAND_LINE, NULL); }