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
"""