Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added be.alexandervanhee.gradia.flatpak
Binary file not shown.
13 changes: 13 additions & 0 deletions data/be.alexandervanhee.gradia.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@
<default>false</default>
<summary>Overwrite currently opened screenshot on close</summary>
</key>
<key name="close-after-copy" type="b">
<default>false</default>
<summary>Close the window automatically after copying the image to the clipboard</summary>
</key>
<key name="close-on-focus-loss" type="b">
<default>false</default>
<summary>Close windows opened with --screenshot when they lose focus</summary>
</key>
<key name="close-on-focus-loss-delay" type="d">
<range min="0.1" max="3600"/>
<default>5.0</default>
<summary>Seconds to wait after losing focus before closing</summary>
</key>
<key name="custom-export-command" type="s">
<default>''</default>
<summary>Custom command to run when pressing its button</summary>
Expand Down
33 changes: 33 additions & 0 deletions data/ui/preferences_window.blp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions gradia/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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.")
Expand Down
2 changes: 2 additions & 0 deletions gradia/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions gradia/ui/image_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"))
Expand Down
2 changes: 2 additions & 0 deletions gradia/ui/image_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions gradia/ui/preferences/preferences_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions gradia/ui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
"""
Expand Down