-
Notifications
You must be signed in to change notification settings - Fork 4
Add editing media features into the app gallery #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Joas329
wants to merge
62
commits into
forky
Choose a base branch
from
feature/forky/edit-media
base: forky
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
62 commits
Select commit
Hold shift + click to select a range
daf54fe
Inital commit for implementing editing view:
Joas329 7cd722b
Change File Name: Initial Commit to change file name. TBD: Update the…
Joas329 1bfec07
change file name: I forgot that database has a watcher, it updates it…
Joas329 a09fe3d
EditView: Load an ImageWidget within the EditView's overlay.
Joas329 400b586
image_viewer_widget: Allow disabling and enabling of zoom gesture on …
Joas329 56be030
image_viewer_widget: Implement reseting of the viewed picture.
Joas329 54ecf5a
crop_overlay: Implement overlay widget that acts as a simple crop ui …
Joas329 3447217
edit_view: Implement the crop overlay widget when crop button pressed.
Joas329 4f2a293
FuriOSMediaTools: Implement cropping function for pictures only so far.
Joas329 ccf6cb5
edit_view: Add popup that lets the user decide between saving a copy …
Joas329 eb4261a
Reset edit viewer picture once cropped.
Joas329 f5a403e
DrawOverlay: Simple widget with stroke controls using Gsk.Path and Gs…
Joas329 89140d0
edit_view: Use DrawOverlay in edit view with its own custom control b…
Joas329 05206f3
draw_overlay: GSK path strokes are in widget coordinates, not on imag…
Joas329 b0c258a
DrawOverlay: Apply coords transformations when storing strokes on ges…
Joas329 8921231
DrawOverlay: Also scale stroke width based on coordinate frame.
Joas329 b42fc5a
furios_media_toos: Implement function to Rasterize strokes via Cairo …
Joas329 5f01029
edit_view: Implement same popup style as crop mode. TBD: Make this co…
Joas329 3080dd0
filters_overlay: Implement a simple filters overlay widget that apply…
Joas329 c35db8b
edit_view: Create the filters bar and connect signals.
Joas329 6f297bc
furios_media_tools: Implement basic contrast function. RGB vectors ar…
Joas329 f8542cf
furios_media_tools: Use ITU-R BT.601 standards for RGB <-> YCrCb tras…
Joas329 6254f9f
furios_media_tools: Add contrast luma transform.
Joas329 2a5c432
furios_media_tools: Add naive and luma brightness operators.
Joas329 758f1c9
furios_media_tools: Add greyscale operator.
Joas329 6d4981f
furios_media_tools: Add gaussian blur operator.
Joas329 1850c2c
furios_media_tools: Add invert opertaor.
Joas329 11bfd08
furios_media_tools: Add Sepia Transform.
Joas329 fbd1848
furios_media_tools: Add saturation transfrom.
Joas329 1164aa1
furios_media_tools: Add function to apply image transformations into …
Joas329 b98912c
filters_overlay: Remove cool-fitler.
Joas329 9db1167
image_transformations_overlay: Implement a widget bar that allows the…
Joas329 cb3fa3a
edit_view: Add the imageTransforamtionsOverlay class to editView.
Joas329 5b194fc
custom_filters: Connect buttons to save to disk image transformations…
Joas329 595c4b4
edit_view: Make apply btn reusable and use it all four menu options.
Joas329 73c6a70
crop: Move bar creation to CropOverlay.
Joas329 511710e
filters: Move filters to FiltersOverlay.
Joas329 da9f942
image transformations: Move IT to ImageTransformationsOverlay.
Joas329 e944206
drawing: Move Drawing bar creation to DrawingOverlay.
Joas329 d42ad77
treewide: Move all editing software into its own directory.
Joas329 2dfc5cb
ui: Offload all ui to new file within edit_view.
Joas329 9801e06
media_watcher: Implement a watcher that reacts on file created within…
Joas329 4940c7e
furios_gallery: Create an instance of media_watcher for top_down sign…
Joas329 452d90b
grid_view: Handle case when there is only one item in the album.
Joas329 5daa56e
tree_wide: Update all UI pages if a new file has been added or created.
Joas329 e6bc107
mediaView: Only show edit view button if its an image. There is no su…
Joas329 9c39361
image_viewer_widget: Respect Exif data orientation.
Joas329 64b3ea7
gallery_window: Update both album view and grid views everytime a new…
Joas329 7612dac
media_watcher: Wait for signal of CHANGES_DONE_HINT, so videos can be…
Joas329 07f7a3c
grid_view: On single item case, only add if the database return the o…
Joas329 90c2ebb
gallery_window: Check for the instance of the object raather than the…
Joas329 711626c
treewide: Fix guards with the correct length of the media_paths array.
Joas329 58e8ccf
ui: Make the button creation reusable and use it accordingly.
Joas329 bbef439
debian/control: Add python3-cairo to deps.
Joas329 9b2efbb
furios_gallery: If application exist, return already created window i…
Joas329 2661580
edit_view/ui: Reduce draw_overlay spacing.
Joas329 5cd7269
edit_view: Clamp gaussian blur to a small kernel for now. Bigger kern…
Joas329 a413892
media_view: Use main app member variables rather than getting them fr…
Joas329 7943791
grid_view: Delete from gridview based on pathname, not media index. T…
Joas329 5c285f7
Drop Image Transformations until I find a better way to compute them.
Joas329 0a99718
filters: drop warm filter due to Sepia misconception.
Joas329 e298852
GridView: Add furios-tight-grid CSS styling
Joas329 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| # SPDX-License-Identifier: GPL-2.0 | ||
| # Copyright (C) 2026 Furi Labs | ||
| # | ||
| # Authors: | ||
| # Joaquin Philco <joaquin@furilabs.com> | ||
|
|
||
| import gi | ||
| gi.require_version("Gtk", "4.0") | ||
| gi.require_version("Gdk", "4.0") | ||
|
|
||
| from gi.repository import Gtk, Gdk, Graphene, Gsk | ||
| from .ui import (create_main_bar_body, create_cancel_btn, create_crop_btn) | ||
|
|
||
| class CropOverlay(Gtk.Widget): | ||
| HANDLE = 12 | ||
| HIT = 18 | ||
| MIN_SIZE = 32 | ||
|
|
||
| def __init__(self, picture_widget: Gtk.Widget, texture: Gdk.Texture): | ||
| super().__init__() | ||
| self.picture_widget = picture_widget | ||
| self.texture = texture | ||
|
|
||
| self.initial_h = self.texture.get_height() | ||
| self.initial_w = self.texture.get_width() | ||
|
|
||
| self.set_hexpand(True) | ||
| self.set_vexpand(True) | ||
| self.set_halign(Gtk.Align.FILL) | ||
| self.set_valign(Gtk.Align.FILL) | ||
| self.set_can_target(True) | ||
|
|
||
| self.rect = [0.0, 0.0, 0.0, 0.0] | ||
|
|
||
| self.mode = None | ||
| self.drag_start = (0.0, 0.0) | ||
| self.rect_start = None | ||
|
|
||
| drag = Gtk.GestureDrag.new() | ||
| drag.connect("drag-begin", self.on_drag_begin) | ||
| drag.connect("drag-update", self.on_drag_update) | ||
| drag.connect("drag-end", self.on_drag_end) | ||
| self.add_controller(drag) | ||
|
|
||
| self.bar = self.build_crop_bar() | ||
| self.bar.set_can_target(True) | ||
|
|
||
| def get_bar_widget(self) -> Gtk.Widget: | ||
| return self.bar | ||
|
|
||
| def build_crop_bar(self): | ||
| if getattr(self, "crop_bar", None): | ||
| return | ||
|
|
||
| bar = create_main_bar_body(12, 12, 12, 12, 6, "horizontal") | ||
|
|
||
| cancel = create_cancel_btn(self.on_cancel_clicked) | ||
|
|
||
| crop = create_crop_btn(self.on_apply_clicked) | ||
|
|
||
| bar.append(cancel) | ||
| bar.append(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)) | ||
| bar.append(crop) | ||
|
|
||
| return bar | ||
|
|
||
| def on_cancel_clicked(self, btn=None): | ||
| if callable(getattr(self, "on_cancel", None)): | ||
| self.on_cancel() | ||
|
|
||
| def on_apply_clicked(self, btn=None): | ||
| if callable(getattr(self, "on_apply", None)): | ||
| self.on_apply(getattr(self, "selected_filter", "filter-original")) | ||
|
Comment on lines
+67
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. initialize on_cancel and on_apply and set to None in |
||
|
|
||
| def image_rect_in_widget(self) -> tuple[float, float, float, float]: | ||
| w = float(self.get_allocated_width()) | ||
| h = float(self.get_allocated_height()) | ||
|
|
||
| tw = float(self.texture.get_width()) | ||
| th = float(self.texture.get_height()) | ||
|
|
||
| if w <= 0 or h <= 0 or tw <= 0 or th <= 0: | ||
| return (0.0, 0.0, w, h) | ||
|
|
||
| scale = min(w / tw, h / th) | ||
| iw = tw * scale | ||
| ih = th * scale | ||
| ix = (w - iw) * 0.5 | ||
| iy = (h - ih) * 0.5 | ||
| return (ix, iy, iw, ih) | ||
|
|
||
| def clamp_rect_to_image(self, rect): | ||
| ix, iy, iw, ih = self.image_rect_in_widget() | ||
| x, y, w, h = rect | ||
|
|
||
| w = max(w, self.MIN_SIZE) | ||
| h = max(h, self.MIN_SIZE) | ||
|
|
||
| w = min(w, iw) | ||
| h = min(h, ih) | ||
|
|
||
| x = max(ix, min(x, ix + iw - w)) | ||
| y = max(iy, min(y, iy + ih - h)) | ||
|
|
||
| return [x, y, w, h] | ||
|
|
||
| def init_default_rect(self): | ||
| ix, iy, iw, ih = self.image_rect_in_widget() | ||
| w = iw * 0.6 | ||
| h = ih * 0.6 | ||
| x = ix + (iw - w) * 0.5 | ||
| y = iy + (ih - h) * 0.5 | ||
| self.rect = self.clamp_rect_to_image([x, y, w, h]) | ||
| self.queue_draw() | ||
|
|
||
| def handle_points(self): | ||
| x, y, w, h = self.rect | ||
| return { | ||
| "nw": (x, y), | ||
| "ne": (x + w, y), | ||
| "sw": (x, y + h), | ||
| "se": (x + w, y + h), | ||
| } | ||
|
|
||
| def hit_test(self, px: float, py: float) -> str | None: | ||
| for name, (hx, hy) in self.handle_points().items(): | ||
| if (px - hx) ** 2 + (py - hy) ** 2 <= (self.HIT ** 2): | ||
| return name | ||
|
|
||
| x, y, w, h = self.rect | ||
| if x <= px <= x + w and y <= py <= y + h: | ||
| return "move" | ||
| return None | ||
|
|
||
| def snapshot_circle(self, snapshot: Gtk.Snapshot, cx: float, cy: float, r: float, color: Gdk.RGBA): | ||
| rect = Graphene.Rect().init(cx - r, cy - r, 2*r, 2*r) | ||
|
|
||
| rr = Gsk.RoundedRect() | ||
| rad = Graphene.Size() | ||
| rad.init(r, r) | ||
|
|
||
| rr.init(rect, rad, rad, rad, rad) | ||
|
|
||
| snapshot.push_rounded_clip(rr) | ||
| snapshot.append_color(color, rect) | ||
| snapshot.pop() | ||
|
|
||
| def do_snapshot(self, snapshot: Gtk.Snapshot): | ||
| w = float(self.get_allocated_width()) | ||
| h = float(self.get_allocated_height()) | ||
|
|
||
| if self.rect[2] <= 0 or self.rect[3] <= 0: | ||
| self.init_default_rect() | ||
|
|
||
| x, y, rw, rh = self.rect | ||
|
|
||
| dim = Gdk.RGBA(); dim.parse("rgba(0,0,0,0.45)") | ||
| white = Gdk.RGBA(); white.parse("rgba(255,255,255,0.90)") | ||
|
|
||
| # dim around crop rect | ||
| snapshot.append_color(dim, Graphene.Rect().init(0, 0, w, y)) | ||
| snapshot.append_color(dim, Graphene.Rect().init(0, y + rh, w, max(0.0, h - (y + rh)))) | ||
| snapshot.append_color(dim, Graphene.Rect().init(0, y, x, rh)) | ||
| snapshot.append_color(dim, Graphene.Rect().init(x + rw, y, max(0.0, w - (x + rw)), rh)) | ||
|
|
||
| # border | ||
| t = 2.0 | ||
| snapshot.append_color(white, Graphene.Rect().init(x, y, rw, t)) | ||
| snapshot.append_color(white, Graphene.Rect().init(x, y + rh - t, rw, t)) | ||
| snapshot.append_color(white, Graphene.Rect().init(x, y, t, rh)) | ||
| snapshot.append_color(white, Graphene.Rect().init(x + rw - t, y, t, rh)) | ||
|
|
||
| # circular handles | ||
| fill = Gdk.RGBA(); fill.parse("rgba(255,255,255,0.25)") | ||
| stroke = Gdk.RGBA(); stroke.parse("rgba(255,255,255,0.95)") | ||
|
Comment on lines
+174
to
+175
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one statement per line |
||
|
|
||
| r_outer = float(self.HANDLE) * 0.5 | ||
| r_inner = max(1.0, r_outer - 2.0) | ||
|
|
||
| for _, (hx, hy) in self.handle_points().items(): | ||
| self.snapshot_circle(snapshot, hx, hy, r_outer, stroke) | ||
| self.snapshot_circle(snapshot, hx, hy, r_inner, fill) | ||
|
|
||
| def on_drag_begin(self, gesture, start_x, start_y): | ||
| if self.rect[2] <= 0 or self.rect[3] <= 0: | ||
| self.init_default_rect() | ||
|
|
||
| mode = self.hit_test(float(start_x), float(start_y)) | ||
| if mode is None: | ||
| ix, iy, iw, ih = self.image_rect_in_widget() | ||
| if not (ix <= start_x <= ix + iw and iy <= start_y <= iy + ih): | ||
| self.mode = None | ||
| return | ||
| self.rect = self.clamp_rect_to_image([start_x - 80, start_y - 80, 160, 160]) | ||
| self.queue_draw() | ||
| mode = "move" | ||
|
|
||
| self.mode = mode | ||
| self.drag_start = (float(start_x), float(start_y)) | ||
| self.rect_start = self.rect.copy() | ||
|
|
||
| def on_drag_update(self, gesture, offset_x, offset_y): | ||
| if not self.mode or self.rect_start is None: | ||
| return | ||
|
|
||
| dx = float(offset_x) | ||
| dy = float(offset_y) | ||
|
|
||
| x0, y0, w0, h0 = self.rect_start | ||
|
|
||
| if self.mode == "move": | ||
| new_rect = [x0 + dx, y0 + dy, w0, h0] | ||
| else: | ||
| x, y, w, h = x0, y0, w0, h0 | ||
|
|
||
| if self.mode == "nw": | ||
| x = x0 + dx | ||
| y = y0 + dy | ||
| w = (x0 + w0) - x | ||
| h = (y0 + h0) - y | ||
| elif self.mode == "ne": | ||
| y = y0 + dy | ||
| w = w0 + dx | ||
| h = (y0 + h0) - y | ||
| elif self.mode == "sw": | ||
| x = x0 + dx | ||
| w = (x0 + w0) - x | ||
| h = h0 + dy | ||
| elif self.mode == "se": | ||
| w = w0 + dx | ||
| h = h0 + dy | ||
|
|
||
| new_rect = [x, y, w, h] | ||
|
|
||
| self.rect = self.clamp_rect_to_image(new_rect) | ||
| self.queue_draw() | ||
|
|
||
| def on_drag_end(self, *_args): | ||
| self.mode = None | ||
| self.rect_start = None | ||
|
|
||
| def get_crop_in_image_pixels(self) -> tuple[int, int, int, int]: | ||
| ix, iy, iw, ih = self.image_rect_in_widget() | ||
| x, y, w, h = self.rect | ||
|
|
||
| nx = (x - ix) / iw | ||
| ny = (y - iy) / ih | ||
| nw = w / iw | ||
| nh = h / ih | ||
|
|
||
| tw = self.texture.get_width() | ||
| th = self.texture.get_height() | ||
|
|
||
| x_px = int(nx * tw) | ||
| y_px = int(ny * th) | ||
| w_px = int(nw * tw) | ||
| h_px = int(nh * th) | ||
|
|
||
| x_px = max(0, min(x_px, tw - 1)) | ||
| y_px = max(0, min(y_px, th - 1)) | ||
| w_px = max(1, min(w_px, tw - x_px)) | ||
| h_px = max(1, min(h_px, th - y_px)) | ||
|
|
||
| return (x_px, y_px, w_px, h_px) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is self.crop_bar defined/used? if defined at all, why not initialize it to a default value in
__init__and check for if self.crop_bar?