Skip to content
Merged
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
20 changes: 20 additions & 0 deletions be.alexandervanhee.gradia.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions data/icons/scalable/actions/screenshooter-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions data/resources.data.gresource.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<file preprocess="xml-stripblanks">icons/scalable/actions/draw-line-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/text-insert2-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/pointer-primary-click-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/screenshooter-symbolic.svg</file>

</gresource>
</gresources>
Expand Down
1 change: 1 addition & 0 deletions gradia/gradia.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 15 additions & 4 deletions gradia/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -55,11 +66,11 @@ def do_shutdown(self):
finally:
Gio.Application.do_shutdown(self)


def main(version: str):
try:
app = GradiaApp(version=version)
return app.run(sys.argv)
except Exception as e:
print('Application closed with an exception:', e)
return 1

63 changes: 62 additions & 1 deletion gradia/ui/image_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
31 changes: 23 additions & 8 deletions gradia/ui/ui_parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -170,22 +170,37 @@ 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)

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

Expand Down
7 changes: 5 additions & 2 deletions gradia/ui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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(), ["<Primary>o"])
self.create_action("load-drop", self.import_manager._on_drop_action)
self.create_action("paste", lambda *_: self.import_manager.load_from_clipboard(), ["<Primary>v"])
self.create_action("screenshot", lambda *_: self.import_manager.take_screenshot(), ["<Primary>a"])

self.create_action("save", lambda *_: self.export_manager.save_to_file(), ["<Primary>s"], enabled=False)
self.create_action("copy", lambda *_: self.export_manager.copy_to_clipboard(), ["<Primary>c"], enabled=False)
Expand All @@ -87,13 +88,15 @@ def __init__(self, temp_dir: str, version: str, **kwargs) -> None:
self.create_action("undo", lambda *_: self.drawing_overlay.undo(), ["<Primary>z"])
self.create_action("redo", lambda *_: self.drawing_overlay.redo(), ["<Primary><Shift>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(), ["<Primary>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)
Expand Down