diff --git a/be.alexandervanhee.gradia.json b/be.alexandervanhee.gradia.json
index b5708c7..7f16183 100644
--- a/be.alexandervanhee.gradia.json
+++ b/be.alexandervanhee.gradia.json
@@ -36,6 +36,26 @@
}
]
},
+ {
+ "name":"libportal",
+ "buildsystem":"meson",
+ "config-opts":[
+ "-Ddocs=false",
+ "-Dbackend-gtk4=enabled"
+ ],
+ "sources":[
+ {
+ "type":"archive",
+ "url":"https://github.com/flatpak/libportal/archive/refs/tags/0.9.1.tar.gz",
+ "sha256":"ea422b789ae487e04194d387bea031fd7485bf88a18aef8c767f7d1c29496a4e",
+ "x-checker-data":{
+ "type":"anitya",
+ "project-id":230124,
+ "url-template":"https://github.com/flatpak/libportal/archive/refs/tags/$version.tar.gz"
+ }
+ }
+ ]
+ },
{
"name": "gradia",
"builddir": true,
diff --git a/data/icons/scalable/actions/screenshooter-symbolic.svg b/data/icons/scalable/actions/screenshooter-symbolic.svg
new file mode 100644
index 0000000..7b0b2e0
--- /dev/null
+++ b/data/icons/scalable/actions/screenshooter-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/resources.data.gresource.xml.in b/data/resources.data.gresource.xml.in
index 8b28ee5..1a78f4c 100644
--- a/data/resources.data.gresource.xml.in
+++ b/data/resources.data.gresource.xml.in
@@ -10,6 +10,7 @@
icons/scalable/actions/draw-line-symbolic.svg
icons/scalable/actions/text-insert2-symbolic.svg
icons/scalable/actions/pointer-primary-click-symbolic.svg
+ icons/scalable/actions/screenshooter-symbolic.svg
diff --git a/gradia/gradia.in b/gradia/gradia.in
index 84416db..3da870b 100755
--- a/gradia/gradia.in
+++ b/gradia/gradia.in
@@ -41,6 +41,7 @@ if __name__ == '__main__':
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
+ gi.require_version('Xdp', '1.0')
from gi.repository import Gio
diff --git a/gradia/main.py b/gradia/main.py
index 18db02f..981d47a 100644
--- a/gradia/main.py
+++ b/gradia/main.py
@@ -32,14 +32,25 @@ class GradiaApp(Adw.Application):
def __init__(self, version: str):
super().__init__(
application_id="be.alexandervanhee.gradia",
- flags=Gio.ApplicationFlags.HANDLES_OPEN
+ flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
)
self.temp_dir = tempfile.mkdtemp()
self.version = version
- self.file_to_open = None
+ self.init_with_screenshot = False
+
+ def do_command_line(self, command_line: Gio.ApplicationCommandLine) -> int:
+ args = command_line.get_arguments()[1:]
+ self.init_with_screenshot = "--screenshot" in args
+ self.activate()
+ return 0
def do_activate(self):
- self.ui = GradientWindow(self.temp_dir, version=self.version, application=self)
+ self.ui = GradientWindow(
+ self.temp_dir,
+ version=self.version,
+ application=self,
+ init_with_screenshot=self.init_with_screenshot
+ )
self.ui.build_ui()
self.ui.show()
@@ -55,7 +66,6 @@ def do_shutdown(self):
finally:
Gio.Application.do_shutdown(self)
-
def main(version: str):
try:
app = GradiaApp(version=version)
@@ -63,3 +73,4 @@ def main(version: str):
except Exception as e:
print('Application closed with an exception:', e)
return 1
+
diff --git a/gradia/ui/image_loaders.py b/gradia/ui/image_loaders.py
index 6467a29..662b10b 100644
--- a/gradia/ui/image_loaders.py
+++ b/gradia/ui/image_loaders.py
@@ -15,9 +15,10 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
+
import os
from typing import Optional, Tuple
-from gi.repository import Gtk, Gio, Gdk
+from gi.repository import Gtk, Gio, Gdk, GLib, Xdp
from gradia.clipboard import save_texture_to_file
ImportFormat = Tuple[str, str]
@@ -176,6 +177,63 @@ def _handle_clipboard_texture(
self.window._set_loading_state(False)
+class ScreenshotImageLoader(BaseImageLoader):
+ """Handles loading images through screenshot capture"""
+
+ def __init__(self, window: Gtk.ApplicationWindow, temp_dir: str) -> None:
+ super().__init__(window, temp_dir)
+ self.portal = Xdp.Portal()
+
+ def take_screenshot(self) -> None:
+ """Initiate screenshot capture"""
+ try:
+ self.portal.take_screenshot(
+ None,
+ Xdp.ScreenshotFlags.INTERACTIVE,
+ None,
+ self._on_screenshot_taken,
+ None
+ )
+ except Exception as e:
+ print(f"Failed to initiate screenshot: {e}")
+ self.window._show_notification(_("Failed to take screenshot"))
+
+ def _on_screenshot_taken(self, portal_object, result, user_data) -> None:
+ """Handle screenshot completion"""
+ try:
+ uri = self.portal.take_screenshot_finish(result)
+ self._handle_screenshot_uri(uri)
+ except GLib.Error as e:
+ print(f"Screenshot error: {e}")
+ self.window._show_notification(_("Screenshot cancelled"))
+
+ def _handle_screenshot_uri(self, uri: str) -> None:
+ """Process the screenshot URI and convert to local file"""
+ try:
+ file = Gio.File.new_for_uri(uri)
+ success, contents, _unused = file.load_contents(None)
+ if not success or not contents:
+ raise Exception("Failed to load screenshot data")
+
+ temp_filename = f"screenshot_{os.urandom(6).hex()}.png"
+ temp_path = os.path.join(self.temp_dir, temp_filename)
+
+ with open(temp_path, 'wb') as f:
+ f.write(contents)
+
+ filename = _("Screenshot")
+ location = _("Screenshot")
+
+ self._set_image_and_update_ui(temp_path, filename, location)
+ self.window._show_notification(_("Screenshot captured!"))
+
+ except Exception as e:
+ print(f"Error processing screenshot: {e}")
+ self.window._show_notification(_("Failed to process screenshot"))
+ finally:
+ self.window._set_loading_state(False)
+
+
class ImportManager:
def __init__(self, window: Gtk.ApplicationWindow, temp_dir: str) -> None:
self.window: Gtk.ApplicationWindow = window
@@ -184,6 +242,7 @@ def __init__(self, window: Gtk.ApplicationWindow, temp_dir: str) -> None:
self.file_loader: FileDialogImageLoader = FileDialogImageLoader(window, temp_dir)
self.drag_drop_loader: DragDropImageLoader = DragDropImageLoader(window, temp_dir)
self.clipboard_loader: ClipboardImageLoader = ClipboardImageLoader(window, temp_dir)
+ self.screenshot_loader: ScreenshotImageLoader = ScreenshotImageLoader(window, temp_dir)
def open_file_dialog(self) -> None:
self.file_loader.open_file_dialog()
@@ -197,3 +256,5 @@ def _on_drop_action(self, action: Optional[object], param: object) -> None:
def load_from_clipboard(self) -> None:
self.clipboard_loader.load_from_clipboard()
+ def take_screenshot(self) -> None:
+ self.screenshot_loader.take_screenshot()
diff --git a/gradia/ui/ui_parts.py b/gradia/ui/ui_parts.py
index 29f4a0c..9d9135a 100644
--- a/gradia/ui/ui_parts.py
+++ b/gradia/ui/ui_parts.py
@@ -31,12 +31,12 @@ def create_header_bar() -> Adw.HeaderBar:
open_btn.set_action_name("app.open")
header_bar.pack_start(open_btn)
- # Copy from clipboard button
- copy_btn = Gtk.Button.new_from_icon_name("clipboard-symbolic")
- copy_btn.get_style_context().add_class("flat")
- copy_btn.set_tooltip_text(_("Paste from Clipboard"))
- copy_btn.set_action_name("app.paste")
- header_bar.pack_start(copy_btn)
+ # Screenshot button
+ screenshot_btn = Gtk.Button.new_from_icon_name("screenshooter-symbolic")
+ screenshot_btn.get_style_context().add_class("flat")
+ screenshot_btn.set_tooltip_text(_("Take a screenshot"))
+ screenshot_btn.set_action_name("app.screenshot")
+ header_bar.pack_start(screenshot_btn)
# About menu button with popover menu
about_menu_btn = Gtk.MenuButton(icon_name="open-menu-symbolic")
@@ -170,6 +170,17 @@ def create_spinner_widget() -> Gtk.Widget:
return spinner_box, spinner
def create_status_page() -> Gtk.Widget:
+ screenshot_btn = Gtk.Button.new_with_label("_Take a screenshot…")
+ screenshot_btn.set_use_underline(True)
+ screenshot_btn.set_halign(Gtk.Align.CENTER)
+
+ style_context = screenshot_btn.get_style_context()
+ style_context.add_class("pill")
+ style_context.add_class("text-button")
+ style_context.add_class("suggested-action")
+
+ screenshot_btn.set_action_name("app.screenshot")
+
open_status_btn = Gtk.Button.new_with_label("_Open Image…")
open_status_btn.set_use_underline(True)
open_status_btn.set_halign(Gtk.Align.CENTER)
@@ -177,15 +188,19 @@ def create_status_page() -> Gtk.Widget:
style_context = open_status_btn.get_style_context()
style_context.add_class("pill")
style_context.add_class("text-button")
- style_context.add_class("suggested-action")
open_status_btn.set_action_name("app.open")
+ button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
+ button_box.set_halign(Gtk.Align.CENTER)
+ button_box.append(screenshot_btn)
+ button_box.append(open_status_btn)
+
status_page = Adw.StatusPage.new()
status_page.set_icon_name("image-x-generic-symbolic")
status_page.set_title("No Image Loaded")
status_page.set_description("Drag and drop one here")
- status_page.set_child(open_status_btn)
+ status_page.set_child(button_box)
return status_page
diff --git a/gradia/ui/window.py b/gradia/ui/window.py
index 3a553e9..0924f73 100644
--- a/gradia/ui/window.py
+++ b/gradia/ui/window.py
@@ -45,7 +45,7 @@ class GradientWindow(Adw.ApplicationWindow):
# Temp file names
TEMP_PROCESSED_FILENAME: str = "processed.png"
- def __init__(self, temp_dir: str, version: str, **kwargs) -> None:
+ def __init__(self, temp_dir: str, version: str, init_with_screenshot: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
self.app: Adw.Application = kwargs['application']
@@ -77,6 +77,7 @@ def __init__(self, temp_dir: str, version: str, **kwargs) -> None:
self.create_action("open", lambda *_: self.import_manager.open_file_dialog(), ["o"])
self.create_action("load-drop", self.import_manager._on_drop_action)
self.create_action("paste", lambda *_: self.import_manager.load_from_clipboard(), ["v"])
+ self.create_action("screenshot", lambda *_: self.import_manager.take_screenshot(), ["a"])
self.create_action("save", lambda *_: self.export_manager.save_to_file(), ["s"], enabled=False)
self.create_action("copy", lambda *_: self.export_manager.copy_to_clipboard(), ["c"], enabled=False)
@@ -87,13 +88,15 @@ def __init__(self, temp_dir: str, version: str, **kwargs) -> None:
self.create_action("undo", lambda *_: self.drawing_overlay.undo(), ["z"])
self.create_action("redo", lambda *_: self.drawing_overlay.redo(), ["z"])
self.create_action("clear", lambda *_: self.drawing_overlay.clear_drawing())
- self.create_action("draw-mode", lambda *_: self.drawing_overlay.set_drawing_mode(mode))
self.create_action_with_param("draw-mode", lambda action, param: self.drawing_overlay.set_drawing_mode(DrawingMode(param.get_string())))
self.create_action_with_param("pen-color", lambda action, param: self._set_pen_color_from_string(param.get_string()))
self.create_action_with_param("fill-color", lambda action, param: self._set_fill_color_from_string(param.get_string()))
self.create_action("del-selected", lambda *_: self.drawing_overlay.remove_selected_action(), ["x", "Delete"])
+ if init_with_screenshot:
+ self.import_manager.take_screenshot()
+
def create_action(self, name: str, callback: Callable[..., None], shortcuts: Optional[list[str]] = None, enabled: bool = True) -> None:
action: Gio.SimpleAction = Gio.SimpleAction.new(name, None)