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)