diff --git a/be.alexandervanhee.gradia.flatpak b/be.alexandervanhee.gradia.flatpak new file mode 100644 index 00000000..d6f3a460 Binary files /dev/null and b/be.alexandervanhee.gradia.flatpak differ diff --git a/data/be.alexandervanhee.gradia.gschema.xml b/data/be.alexandervanhee.gradia.gschema.xml index 4e4adc55..c018b68c 100644 --- a/data/be.alexandervanhee.gradia.gschema.xml +++ b/data/be.alexandervanhee.gradia.gschema.xml @@ -76,6 +76,19 @@ false Overwrite currently opened screenshot on close + + false + Close the window automatically after copying the image to the clipboard + + + false + Close windows opened with --screenshot when they lose focus + + + + 5.0 + Seconds to wait after losing focus before closing + '' Custom command to run when pressing its button diff --git a/data/ui/preferences_window.blp b/data/ui/preferences_window.blp index e0e68691..d531f362 100644 --- a/data/ui/preferences_window.blp +++ b/data/ui/preferences_window.blp @@ -73,6 +73,39 @@ template $GradiaPreferencesWindow: Adw.PreferencesDialog { activatable: true; } + Adw.SwitchRow close_after_copy_switch { + title: _("_Close After Copying"); + use-underline: true; + subtitle: _("Exit the app once the image is on the clipboard"); + tooltip-text: _("Close the window after copying the image to the clipboard"); + activatable: true; + } + + Adw.SwitchRow close_on_focus_loss_switch { + title: _("Close on _Focus Loss"); + use-underline: true; + subtitle: _("Exit when switching away from a window opened by the screenshot shortcut"); + tooltip-text: _("Close the window when it loses focus, if it was opened with the screenshot shortcut"); + activatable: true; + } + + Adw.SpinRow close_on_focus_loss_delay_row { + title: _("Focus Loss _Delay"); + use-underline: true; + subtitle: _("Seconds to wait before closing"); + numeric: true; + digits: 1; + sensitive: bind close_on_focus_loss_switch.active; + + adjustment: Gtk.Adjustment { + value: 5; + lower: 0.1; + upper: 3600; + step-increment: 0.1; + page-increment: 1; + }; + } + Adw.SwitchRow delete_screenshot_switch { title: _("_Trash Screenshots on Close"); use-underline: true; diff --git a/gradia/backend/settings.py b/gradia/backend/settings.py index 06ef0bce..eafd7ba6 100644 --- a/gradia/backend/settings.py +++ b/gradia/backend/settings.py @@ -104,6 +104,18 @@ def exit_method(self, value: str) -> None: def overwrite_screenshot(self) -> bool: return self._settings.get_boolean("overwrite-screenshot") + @property + def close_after_copy(self) -> bool: + return self._settings.get_boolean("close-after-copy") + + @property + def close_on_focus_loss(self) -> bool: + return self._settings.get_boolean("close-on-focus-loss") + + @property + def close_on_focus_loss_delay(self) -> float: + return self._settings.get_double("close-on-focus-loss-delay") + @property def custom_export_command(self) -> str: return self._settings.get_string("custom-export-command") @@ -299,13 +311,13 @@ def bind_scale(self, scale: Gtk.Scale, key: str) -> None: else: print(f"Warning: GSettings key '{key}' not found in schema.") - def bind_spin_row(self, spin_row: object, key: str) -> None: + def bind_spin_row(self, spin_row: object, key: str, flags: Gio.SettingsBindFlags = Gio.SettingsBindFlags.DEFAULT) -> None: if key in self._settings.list_keys(): self._settings.bind( key, spin_row, "value", - Gio.SettingsBindFlags.DEFAULT + flags ) else: print(f"Warning: GSettings key '{key}' not found in schema.") diff --git a/gradia/graphics/image.py b/gradia/graphics/image.py index 902880cc..721e9a90 100644 --- a/gradia/graphics/image.py +++ b/gradia/graphics/image.py @@ -162,6 +162,7 @@ def _on_preset_selected(self, path: str) -> None: self._load_image_async(path) def _on_preview_clicked(self, _gesture: Gtk.GestureClick, _n_press: int, _x: float, _y: float) -> None: + self.get_root().inhibit_focus_loss_close() self.open_image_dialog.open(self.get_root(), None, self._on_file_dialog_ready) def _on_image_drop(self, _drop_target: Gtk.DropTarget, file: Gio.File, _x: int, _y: int) -> bool: @@ -172,6 +173,7 @@ def _on_image_drop(self, _drop_target: Gtk.DropTarget, file: Gio.File, _x: int, return False def _on_file_dialog_ready(self, file_dialog: Gtk.FileDialog, result: Gio.AsyncResult) -> None: + self.get_root().release_focus_loss_close() try: file = file_dialog.open_finish(result) file_path = file.get_path() diff --git a/gradia/ui/image_exporters.py b/gradia/ui/image_exporters.py index 5a3477b2..fc56e993 100644 --- a/gradia/ui/image_exporters.py +++ b/gradia/ui/image_exporters.py @@ -160,9 +160,11 @@ def save_to_file(self, filetype: str = None) -> None: dialog.set_current_name(base_name + default_ext) dialog.connect("response", lambda d, r: self._on_dialog_response(d, r, target_format)) + self.window.inhibit_focus_loss_close() dialog.show() def _on_dialog_response(self, dialog, response_id, suggested_format): + self.window.release_focus_loss_close() if response_id == Gtk.ResponseType.ACCEPT: file = dialog.get_file() if file: @@ -293,6 +295,8 @@ def _on_task_complete(source_object, result, user_data): if not silent: self.window.show_close_confirmation = False self.window._show_notification(_("Image Copied")) + if self.window.settings.close_after_copy and self.window.settings.exit_method != "copy": + self.window.close() except Exception as e: self.window._show_notification(_("Failed to copy image to clipboard")) diff --git a/gradia/ui/image_loaders.py b/gradia/ui/image_loaders.py index 73709daf..a47f56c6 100644 --- a/gradia/ui/image_loaders.py +++ b/gradia/ui/image_loaders.py @@ -148,9 +148,11 @@ def open_file_dialog(self) -> None: filters.append(image_filter) file_dialog.set_filters(filters) + self.window.inhibit_focus_loss_close() file_dialog.open(self.window, None, self._on_file_selected) def _on_file_selected(self, dialog: Gtk.FileDialog, result: Gio.AsyncResult) -> None: + self.window.release_focus_loss_close() try: file = dialog.open_finish(result) if not file: diff --git a/gradia/ui/preferences/preferences_window.py b/gradia/ui/preferences/preferences_window.py index eb5acdbb..6fc17326 100644 --- a/gradia/ui/preferences/preferences_window.py +++ b/gradia/ui/preferences/preferences_window.py @@ -39,6 +39,9 @@ class PreferencesWindow(Adw.PreferencesDialog): save_format_group: Adw.PreferencesGroup = Gtk.Template.Child() delete_screenshot_switch: Adw.SwitchRow = Gtk.Template.Child() overwrite_screenshot_switch: Adw.SwitchRow = Gtk.Template.Child() + close_after_copy_switch: Adw.SwitchRow = Gtk.Template.Child() + close_on_focus_loss_switch: Adw.SwitchRow = Gtk.Template.Child() + close_on_focus_loss_delay_row: Adw.SpinRow = Gtk.Template.Child() confirm_upload_switch: Adw.SwitchRow = Gtk.Template.Child() save_format_combo: Adw.ComboRow = Gtk.Template.Child() provider_name: Gtk.Label = Gtk.Template.Child() @@ -161,6 +164,13 @@ def _bind_settings(self): self.settings.bind_switch(self.delete_screenshot_switch,"trash-screenshots-on-close") self.settings.bind_switch(self.confirm_upload_switch,"show-export-confirm-dialog") self.settings.bind_switch(self.overwrite_screenshot_switch,"overwrite-screenshot") + self.settings.bind_switch(self.close_after_copy_switch,"close-after-copy") + self.settings.bind_switch(self.close_on_focus_loss_switch,"close-on-focus-loss") + self.settings.bind_spin_row( + self.close_on_focus_loss_delay_row, + "close-on-focus-loss-delay", + Gio.SettingsBindFlags.DEFAULT | Gio.SettingsBindFlags.NO_SENSITIVITY + ) @Gtk.Template.Callback() def on_choose_provider_clicked(self, button: Gtk.Button) -> None: @@ -175,6 +185,7 @@ def handle_selection(name: str, command: str): def on_folder_row_clicked(self, row: Adw.ActionRow) -> None: file_dialog = Gtk.FileDialog() def on_folder_selected(dialog, result): + self.parent_window.release_focus_loss_close() try: folder = dialog.select_folder_finish(result) if folder: @@ -191,6 +202,7 @@ def on_folder_selected(dialog, result): except GLib.Error: pass + self.parent_window.inhibit_focus_loss_close() file_dialog.select_folder( parent=self.get_root(), cancellable=None, diff --git a/gradia/ui/window.py b/gradia/ui/window.py index 0ada46e6..b7c44b75 100644 --- a/gradia/ui/window.py +++ b/gradia/ui/window.py @@ -89,6 +89,9 @@ def __init__( self.processed_pixbuf: Optional[Gdk.Pixbuf] = None self.image_ready = False self.show_close_confirmation = False + self._closing = False + self._focus_close_inhibitors = 0 + self._focus_close_timeout_id: Optional[int] = None self.export_manager: ExportManager = ExportManager(self, temp_dir) self.import_manager: ImportManager = ImportManager(self, temp_dir, self.app) @@ -103,6 +106,7 @@ def __init__( self._setup() self.connect("close-request", self._on_close_request) + self.connect("notify::is-active", self._on_active_changed) self.welcome_content = None @@ -223,6 +227,8 @@ def _on_close_request(self, window) -> bool: return True def _finalize_close(self, copy: bool) -> None: + self._closing = True + self._cancel_focus_close_timeout() if self.settings.delete_screenshots_on_close: self.import_manager.delete_screenshots() @@ -252,6 +258,51 @@ def _on_confirm_close_ok(self) -> None: def _on_confirm_close_copy(self) -> None: self._finalize_close(copy=True) + def _on_active_changed(self, window, pspec) -> None: + if not self.start_screenshot: + return + + self._cancel_focus_close_timeout() + + if not self.is_active() and not self._closing: + delay_ms = int(self.settings.close_on_focus_loss_delay * 1000) + self._focus_close_timeout_id = GLib.timeout_add(delay_ms, self._close_if_focus_lost) + + def _cancel_focus_close_timeout(self) -> None: + if self._focus_close_timeout_id is not None: + GLib.source_remove(self._focus_close_timeout_id) + self._focus_close_timeout_id = None + + def _close_if_focus_lost(self) -> bool: + self._focus_close_timeout_id = None + + if ( + not self._closing + and self.settings.close_on_focus_loss + and not self.is_active() + and self.get_visible() + and self._focus_close_inhibitors == 0 + and not self._has_active_auxiliary_window() + ): + if self.settings.exit_method == "copy": + self._on_confirm_close_copy() + else: + self._on_confirm_close_ok() + return False + + def _has_active_auxiliary_window(self) -> bool: + return any( + window.is_active() + for window in Gtk.Window.list_toplevels() + if window is not self and not isinstance(window, GradiaMainWindow) + ) + + def inhibit_focus_loss_close(self) -> None: + self._focus_close_inhibitors += 1 + + def release_focus_loss_close(self) -> None: + self._focus_close_inhibitors = max(0, self._focus_close_inhibitors - 1) + """ Callbacks """