Skip to content
Open
Show file tree
Hide file tree
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 Jan 25, 2026
7cd722b
Change File Name: Initial Commit to change file name. TBD: Update the…
Joas329 Jan 25, 2026
1bfec07
change file name: I forgot that database has a watcher, it updates it…
Joas329 Jan 25, 2026
a09fe3d
EditView: Load an ImageWidget within the EditView's overlay.
Joas329 Jan 26, 2026
400b586
image_viewer_widget: Allow disabling and enabling of zoom gesture on …
Joas329 Jan 26, 2026
56be030
image_viewer_widget: Implement reseting of the viewed picture.
Joas329 Jan 26, 2026
54ecf5a
crop_overlay: Implement overlay widget that acts as a simple crop ui …
Joas329 Jan 26, 2026
3447217
edit_view: Implement the crop overlay widget when crop button pressed.
Joas329 Jan 26, 2026
4f2a293
FuriOSMediaTools: Implement cropping function for pictures only so far.
Joas329 Jan 26, 2026
ccf6cb5
edit_view: Add popup that lets the user decide between saving a copy …
Joas329 Jan 26, 2026
eb4261a
Reset edit viewer picture once cropped.
Joas329 Jan 26, 2026
f5a403e
DrawOverlay: Simple widget with stroke controls using Gsk.Path and Gs…
Joas329 Jan 26, 2026
89140d0
edit_view: Use DrawOverlay in edit view with its own custom control b…
Joas329 Jan 26, 2026
05206f3
draw_overlay: GSK path strokes are in widget coordinates, not on imag…
Joas329 Jan 27, 2026
b0c258a
DrawOverlay: Apply coords transformations when storing strokes on ges…
Joas329 Jan 27, 2026
8921231
DrawOverlay: Also scale stroke width based on coordinate frame.
Joas329 Jan 27, 2026
b42fc5a
furios_media_toos: Implement function to Rasterize strokes via Cairo …
Joas329 Jan 27, 2026
5f01029
edit_view: Implement same popup style as crop mode. TBD: Make this co…
Joas329 Jan 27, 2026
3080dd0
filters_overlay: Implement a simple filters overlay widget that apply…
Joas329 Jan 28, 2026
c35db8b
edit_view: Create the filters bar and connect signals.
Joas329 Jan 28, 2026
6f297bc
furios_media_tools: Implement basic contrast function. RGB vectors ar…
Joas329 Jan 28, 2026
f8542cf
furios_media_tools: Use ITU-R BT.601 standards for RGB <-> YCrCb tras…
Joas329 Jan 28, 2026
6254f9f
furios_media_tools: Add contrast luma transform.
Joas329 Jan 28, 2026
2a5c432
furios_media_tools: Add naive and luma brightness operators.
Joas329 Jan 28, 2026
758f1c9
furios_media_tools: Add greyscale operator.
Joas329 Jan 28, 2026
6d4981f
furios_media_tools: Add gaussian blur operator.
Joas329 Jan 28, 2026
1850c2c
furios_media_tools: Add invert opertaor.
Joas329 Jan 28, 2026
11bfd08
furios_media_tools: Add Sepia Transform.
Joas329 Jan 28, 2026
fbd1848
furios_media_tools: Add saturation transfrom.
Joas329 Jan 28, 2026
1164aa1
furios_media_tools: Add function to apply image transformations into …
Joas329 Jan 29, 2026
b98912c
filters_overlay: Remove cool-fitler.
Joas329 Jan 29, 2026
9db1167
image_transformations_overlay: Implement a widget bar that allows the…
Joas329 Jan 29, 2026
cb3fa3a
edit_view: Add the imageTransforamtionsOverlay class to editView.
Joas329 Jan 29, 2026
5b194fc
custom_filters: Connect buttons to save to disk image transformations…
Joas329 Jan 30, 2026
595c4b4
edit_view: Make apply btn reusable and use it all four menu options.
Joas329 Jan 30, 2026
73c6a70
crop: Move bar creation to CropOverlay.
Joas329 Jan 30, 2026
511710e
filters: Move filters to FiltersOverlay.
Joas329 Jan 30, 2026
da9f942
image transformations: Move IT to ImageTransformationsOverlay.
Joas329 Jan 30, 2026
e944206
drawing: Move Drawing bar creation to DrawingOverlay.
Joas329 Jan 30, 2026
d42ad77
treewide: Move all editing software into its own directory.
Joas329 Jan 30, 2026
2dfc5cb
ui: Offload all ui to new file within edit_view.
Joas329 Jan 30, 2026
9801e06
media_watcher: Implement a watcher that reacts on file created within…
Joas329 Jan 30, 2026
4940c7e
furios_gallery: Create an instance of media_watcher for top_down sign…
Joas329 Jan 30, 2026
452d90b
grid_view: Handle case when there is only one item in the album.
Joas329 Jan 30, 2026
5daa56e
tree_wide: Update all UI pages if a new file has been added or created.
Joas329 Jan 30, 2026
e6bc107
mediaView: Only show edit view button if its an image. There is no su…
Joas329 Jan 30, 2026
9c39361
image_viewer_widget: Respect Exif data orientation.
Joas329 Feb 4, 2026
64b3ea7
gallery_window: Update both album view and grid views everytime a new…
Joas329 Feb 5, 2026
7612dac
media_watcher: Wait for signal of CHANGES_DONE_HINT, so videos can be…
Joas329 Feb 6, 2026
07f7a3c
grid_view: On single item case, only add if the database return the o…
Joas329 Feb 8, 2026
90c2ebb
gallery_window: Check for the instance of the object raather than the…
Joas329 Feb 8, 2026
711626c
treewide: Fix guards with the correct length of the media_paths array.
Joas329 Feb 8, 2026
58e8ccf
ui: Make the button creation reusable and use it accordingly.
Joas329 Feb 8, 2026
bbef439
debian/control: Add python3-cairo to deps.
Joas329 Feb 8, 2026
9b2efbb
furios_gallery: If application exist, return already created window i…
Joas329 Feb 9, 2026
2661580
edit_view/ui: Reduce draw_overlay spacing.
Joas329 Feb 15, 2026
5cd7269
edit_view: Clamp gaussian blur to a small kernel for now. Bigger kern…
Joas329 Feb 23, 2026
a413892
media_view: Use main app member variables rather than getting them fr…
Joas329 Feb 24, 2026
7943791
grid_view: Delete from gridview based on pathname, not media index. T…
Joas329 Feb 24, 2026
5c285f7
Drop Image Transformations until I find a better way to compute them.
Joas329 Feb 28, 2026
0a99718
filters: drop warm filter due to Sepia misconception.
Joas329 Feb 28, 2026
e298852
GridView: Add furios-tight-grid CSS styling
Joas329 Feb 28, 2026
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
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Depends: ${misc:Depends},
gstreamer1.0-gtk4,
ffmpeg,
python3-av,
python3-cairo,
Description: Media gallery application for FuriOS
4 changes: 2 additions & 2 deletions furios_gallery/albums_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@ def on_album_selected(self, flowbox):
self.app_window.header.set_title_widget(Adw.WindowTitle(title=album_name))

# Create grid view page for the album
grid_view_page = self.app_window.create_grid_view_page(album_name)
self.app_window.grid_view_page = self.app_window.create_grid_view_page(album_name)

# Push grid view to navigation view
self.app_window.navigation_view.push(grid_view_page)
self.app_window.navigation_view.push(self.app_window.grid_view_page)

self.flowbox.unselect_all()

Expand Down
264 changes: 264 additions & 0 deletions furios_gallery/edit_view/crop_overlay.py
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):

Copy link
Copy Markdown
Contributor

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?

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize on_cancel and on_apply and set to None in __init__ and use if self.on_cancel and if self.on_apply


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)
Loading