commit 3d2084e4c91ae625504e5b6dfc9631950a35a4af
Author: Sv. Lockal <lockalsash@gmail.com>
Date:   Wed Nov 26 22:33:49 2025 +0800

    Fix compatibility with pygobject-3.50
    
    Closes #256

diff --git a/gradia/backend/tool_config.py b/gradia/backend/tool_config.py
index 5d8ab08..ec206a7 100644
--- a/gradia/backend/tool_config.py
+++ b/gradia/backend/tool_config.py
@@ -21,6 +21,7 @@ from gradia.overlay.drawing_actions import DrawingMode
 import re
 import json
 from gradia.backend.settings import Settings
+from gradia.utils.colors import make_rgba, tuple_to_rgba
 
 class ToolOption:
     def __init__(
@@ -36,9 +37,14 @@ class ToolOption:
     ) -> None:
         self.mode = mode
         self._size = size
-        self._primary_color_str = self._rgba_to_str(primary_color or Gdk.RGBA(0,0,0,1))
-        self._fill_color_str = self._rgba_to_str(fill_color or Gdk.RGBA(1,1,1,1))
-        self._border_color_str = self._rgba_to_str(border_color or Gdk.RGBA(0,0,0,0))
+
+        default_primary = make_rgba(0.0, 0.0, 0.0, 1.0)
+        default_fill = make_rgba(1.0, 1.0, 1.0, 1.0)
+        default_border = make_rgba(0.0, 0.0, 0.0, 0.0)
+
+        self._primary_color_str = self._rgba_to_str(primary_color or default_primary)
+        self._fill_color_str = self._rgba_to_str(fill_color or default_fill)
+        self._border_color_str = self._rgba_to_str(border_color or default_border)
         self._font = font or "Adwaita Sans"
         self._on_change_callback = on_change_callback
         self._is_temporary = is_temporary
@@ -51,7 +57,7 @@ class ToolOption:
         if not m:
             return Gdk.RGBA(0,0,0,1)
         r, g, b, a = map(float, m.groups())
-        return Gdk.RGBA(r, g, b, a)
+        return make_rgba(r, g, b, a)
 
     def _notify_change(self):
         if self._on_change_callback and not self._is_temporary:
@@ -123,11 +129,6 @@ class ToolOption:
 
     @classmethod
     def deserialize(cls, json_str: str, on_change_callback: Optional[Callable[['ToolOption'], None]] = None) -> "ToolOption":
-        def tuple_to_rgba(t):
-            if not t or len(t) != 4:
-                return Gdk.RGBA(0, 0, 0, 1)
-            return Gdk.RGBA(t[0], t[1], t[2], t[3])
-
         data = json.loads(json_str)
         mode = DrawingMode[data.get("mode", "PEN")]
         return cls(
@@ -211,22 +212,22 @@ class ToolOptionsManager:
 class ToolConfig:
 
     TEXT_COLORS = [
-        (Gdk.RGBA(0.65,0.11,0.18, 1), _("Red")),
-        (Gdk.RGBA(0.15,0.64,0.41, 1), _("Green")),
-        (Gdk.RGBA(0.1,0.37,0.71, 1), _("Blue")),
-        (Gdk.RGBA(0.9,0.65,0.04, 1), _("Yellow")),
-        (Gdk.RGBA(0,0,0, 1), _("Black")),
-        (Gdk.RGBA(1.0, 1.0, 1.0, 1), _("White")),
+        (make_rgba(0.65, 0.11, 0.18, 1.0), _("Red")),
+        (make_rgba(0.15, 0.64, 0.41, 1.0), _("Green")),
+        (make_rgba(0.10, 0.37, 0.71, 1.0), _("Blue")),
+        (make_rgba(0.90, 0.65, 0.04, 1.0), _("Yellow")),
+        (make_rgba(0.00, 0.00, 0.00, 1.0), _("Black")),
+        (make_rgba(1.00, 1.00, 1.00, 1.0), _("White")),
     ]
 
     TEXT_BACKGROUND_COLORS = [
-        (Gdk.RGBA(0.96,0.38,0.32, 1), _("Red")),
-        (Gdk.RGBA(0.56,0.94,0.64, 1), _("Green")),
-        (Gdk.RGBA(0.6,0.76,0.95, 1), _("Blue")),
-        (Gdk.RGBA(0.98,0.94,0.42, 1), _("Yellow")),
-        (Gdk.RGBA(1.0, 1.0, 1.0, 1), _("White")),
-        (Gdk.RGBA(0,0,0, 1), _("Black")),
-        (Gdk.RGBA(0, 0, 0, 0), _("Transparent"))
+        (make_rgba(0.96, 0.38, 0.32, 1.0), _("Red")),
+        (make_rgba(0.56, 0.94, 0.64, 1.0), _("Green")),
+        (make_rgba(0.60, 0.76, 0.95, 1.0), _("Blue")),
+        (make_rgba(0.98, 0.94, 0.42, 1.0), _("Yellow")),
+        (make_rgba(1.00, 1.00, 1.00, 1.0), _("White")),
+        (make_rgba(0.00, 0.00, 0.00, 1.0), _("Black")),
+        (make_rgba(0.00, 0.00, 0.00, 0.0), _("Transparent"))
     ]
 
     def __init__(
@@ -256,10 +257,6 @@ class ToolConfig:
 
     @staticmethod
     def get_all_tools_positions():
-        black = Gdk.RGBA(red=0, green=0, blue=0, alpha=1)
-        white = Gdk.RGBA(red=1, green=1, blue=1, alpha=1)
-        transparent = Gdk.RGBA(red=0, green=0, blue=0, alpha=0)
-
         return [
             ToolConfig(
                 mode=DrawingMode.SELECT,
@@ -331,12 +328,12 @@ class ToolConfig:
                 has_scale=True,
                 has_primary_color=True,
                 primary_color_list=[
-                        (Gdk.RGBA(0.88, 0.11, 0.14, 0.4), _("Red")),
-                        (Gdk.RGBA(0.18, 0.76, 0.49, 0.4), _("Green")),
-                        (Gdk.RGBA(0.21, 0.52, 0.89, 0.4), _("Blue")),
-                        (Gdk.RGBA(0.96, 0.83, 0.18, 0.4), _("Yellow")),
-                        (Gdk.RGBA(0.51, 0.24, 0.61, 0.4), _("Purple")),
-                        (Gdk.RGBA(1.0, 1.0, 1.0, 0.4), _("White")),
+                        (make_rgba(0.88, 0.11, 0.14, 0.4), _("Red")),
+                        (make_rgba(0.18, 0.76, 0.49, 0.4), _("Green")),
+                        (make_rgba(0.21, 0.52, 0.89, 0.4), _("Blue")),
+                        (make_rgba(0.96, 0.83, 0.18, 0.4), _("Yellow")),
+                        (make_rgba(0.51, 0.24, 0.61, 0.4), _("Purple")),
+                        (make_rgba(1.0, 1.0, 1.0, 0.4), _("White")),
                     ]
             ),
             ToolConfig(
diff --git a/gradia/overlay/drawing_actions.py b/gradia/overlay/drawing_actions.py
index 6a8b872..59cc169 100644
--- a/gradia/overlay/drawing_actions.py
+++ b/gradia/overlay/drawing_actions.py
@@ -22,7 +22,7 @@ from gi.repository import Gtk, Gdk, Gio, Pango, PangoCairo, GdkPixbuf
 from enum import Enum
 import math
 from gradia.backend.logger import Logger
-from gradia.utils.colors import has_visible_color
+from gradia.utils.colors import rgba_to_tuple
 import time
 import unicodedata
 
@@ -162,7 +162,7 @@ class StrokeAction(DrawingAction):
         coords = [image_to_widget_coords(x, y) for x, y in self.stroke]
         line_width = self.options.size * scale
         self._build_path(cr, coords)
-        cr.set_source_rgba(*self.options.primary_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
         cr.set_line_width(line_width)
         cr.stroke()
 
@@ -240,7 +240,7 @@ class ArrowAction(DrawingAction):
 
         cr.set_line_width(width * scale)
         cr.set_line_cap(cairo.LINE_CAP_ROUND)
-        cr.set_source_rgba(*self.options.primary_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
 
         cr.move_to(start_x, start_y)
         cr.line_to(end_x, end_y)
@@ -412,23 +412,33 @@ class TextAction(DrawingAction):
         text_x_widget = x_widget - text_width_widget / 2
         text_y_widget = y_widget - text_height_widget
 
-        if self.options.fill_color and any(c > 0 for c in self.options.fill_color):
-            cr.set_source_rgba(*self.options.fill_color)
+        if self.options.fill_color and (
+            self.options.fill_color.red > 0 or
+            self.options.fill_color.green > 0 or
+            self.options.fill_color.blue > 0 or
+            self.options.fill_color.alpha > 0
+        ):
+            cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color))
             self.draw_per_line_background(cr, layout, text_x_widget, text_y_widget, scale)
 
         cr.move_to(text_x_widget, text_y_widget)
         if self.contains_emoji():
-            cr.set_source_rgba(*self.options.primary_color)
+            cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
             PangoCairo.show_layout(cr, layout)
         else:
             PangoCairo.layout_path(cr, layout)
-            if self.options.border_color and any(c > 0 for c in self.options.border_color):
-                cr.set_source_rgba(*self.options.border_color)
+            if self.options.border_color and (
+                self.options.border_color.red > 0 or
+                self.options.border_color.green > 0 or
+                self.options.border_color.blue > 0 or
+                self.options.border_color.alpha > 0
+            ):
+                cr.set_source_rgba(*rgba_to_tuple(self.options.border_color))
                 base_line_width = 2.0
                 adjusted_line_width = base_line_width * scale * (self.font_size / 14.0)
                 cr.set_line_width(adjusted_line_width)
                 cr.stroke_preserve()
-            cr.set_source_rgba(*self.options.primary_color)
+            cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
             cr.fill()
 
     def get_bounds(self) -> QuadBounds:
@@ -453,7 +463,12 @@ class TextAction(DrawingAction):
         x_img, y_img = self.position
 
         outline_padding = 0
-        if self.options.border_color and any(c > 0 for c in self.options.border_color):
+        if self.options.border_color and (
+            self.options.border_color.red > 0 or
+            self.options.border_color.green > 0 or
+            self.options.border_color.blue > 0 or
+            self.options.border_color.alpha > 0
+        ):
             outline_padding = int(2.0 * (self.font_size / 14.0)) + 1
 
         left_img = x_img - text_width_img // 2 - self.PADDING_X_IMG - outline_padding
@@ -485,7 +500,7 @@ class LineAction(ArrowAction):
         cr.set_line_width(line_width)
         cr.move_to(start_x, start_y)
         cr.line_to(end_x, end_y)
-        cr.set_source_rgba(*self.options.primary_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
         cr.stroke()
 
     def get_bounds(self) -> QuadBounds:
@@ -540,10 +555,10 @@ class RectAction(DrawingAction):
 
         if w > 0 and h > 0:
             if self.options.fill_color:
-                cr.set_source_rgba(*self.options.fill_color)
+                cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color))
                 cr.rectangle(x, y, w, h)
                 cr.fill()
-            cr.set_source_rgba(*self.options.primary_color)
+            cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
             cr.set_line_width(self.options.size * scale)
             cr.rectangle(x, y, w, h)
             cr.stroke()
@@ -599,9 +614,9 @@ class CircleAction(RectAction):
             cr.restore()
 
             if self.options.fill_color:
-                cr.set_source_rgba(*self.options.fill_color)
+                cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color))
                 cr.fill_preserve()
-            cr.set_source_rgba(*self.options.primary_color)
+            cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
             cr.set_line_width(self.options.size * scale)
             cr.stroke()
 
@@ -621,7 +636,7 @@ class HighlighterAction(StrokeAction):
             return
         coords = [image_to_widget_coords(x, y) for x, y in self.stroke]
         cr.set_operator(cairo.Operator.MULTIPLY)
-        cr.set_source_rgba(*self.options.primary_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
         cr.set_line_width(self.options.size * scale * 2)
         cr.set_line_cap(cairo.LineCap.BUTT)
         cr.move_to(*coords[0])
@@ -724,12 +739,12 @@ class NumberStampAction(DrawingAction):
         x_widget, y_widget = image_to_widget_coords(*self.position)
         r_widget = self.options.size * 2 * scale
 
-        cr.set_source_rgba(*self.options.fill_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color))
         cr.arc(x_widget, y_widget, r_widget, 0, 2 * math.pi)
         cr.fill_preserve()
 
         if self.options.border_color.alpha != 0 and self.options.fill_color.alpha != 0:
-            cr.set_source_rgba(*self.options.border_color)
+            cr.set_source_rgba(*rgba_to_tuple(self.options.border_color))
             cr.set_line_width(2.0 * scale)
             cr.stroke()
         else:
@@ -746,12 +761,17 @@ class NumberStampAction(DrawingAction):
         cr.move_to(tx, ty)
         cr.text_path(text)
 
-        if self.options.border_color and any(c > 0 for c in self.options.border_color):
-            cr.set_source_rgba(*self.options.border_color)
+        if self.options.border_color and (
+            self.options.border_color.red > 0 or
+            self.options.border_color.green > 0 or
+            self.options.border_color.blue > 0 or
+            self.options.border_color.alpha > 0
+        ):
+            cr.set_source_rgba(*rgba_to_tuple(self.options.border_color))
             cr.set_line_width(4 * scale)
             cr.stroke_preserve()
 
-        cr.set_source_rgba(*self.options.primary_color)
+        cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color))
         cr.fill()
 
     def contains_point(self, px_img: int, py_img: int) -> bool:
@@ -762,7 +782,14 @@ class NumberStampAction(DrawingAction):
 
     def get_bounds(self) -> QuadBounds:
         x_img, y_img = self.position
-        outline_padding = 2 if self.options.border_color and any(c > 0 for c in self.options.border_color) else 0
+        outline_padding = 0
+        if self.options.border_color and (
+            self.options.border_color.red > 0 or
+            self.options.border_color.green > 0 or
+            self.options.border_color.blue > 0 or
+            self.options.border_color.alpha > 0
+        ):
+            outline_padding = 2
         total_radius = self.options.size * 2 + outline_padding + 1
         return QuadBounds.from_rect(x_img - total_radius, y_img - total_radius, x_img + total_radius, y_img + total_radius)
 
diff --git a/gradia/overlay/drawing_overlay.py b/gradia/overlay/drawing_overlay.py
index f25b8e1..cc0c257 100644
--- a/gradia/overlay/drawing_overlay.py
+++ b/gradia/overlay/drawing_overlay.py
@@ -371,7 +371,7 @@ class DrawingOverlay(Gtk.DrawingArea):
         points = bounds.get_points()
         widget_points = [self._image_to_widget_coords(int(p[0]), int(p[1])) for p in points]
         accent = Adw.StyleManager.get_default().get_accent_color_rgba()
-        cr.set_source_rgba(*accent)
+        cr.set_source_rgba(accent.red, accent.green, accent.blue, accent.alpha)
         cr.set_line_width(2)
         cr.move_to(*widget_points[0])
         for point in widget_points[1:]:
@@ -384,7 +384,7 @@ class DrawingOverlay(Gtk.DrawingArea):
                 cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
                 cr.rectangle(handle_x, handle_y, HANDLE_SIZE, HANDLE_SIZE)
                 cr.fill()
-                cr.set_source_rgba(*accent)
+                cr.set_source_rgba(accent.red, accent.green, accent.blue, accent.alpha)
                 cr.rectangle(handle_x, handle_y, HANDLE_SIZE, HANDLE_SIZE)
                 cr.stroke()
 
@@ -769,7 +769,12 @@ class DrawingOverlay(Gtk.DrawingArea):
             action.draw(cr, self._image_to_widget_coords, scale)
 
         if self.is_drawing and self.options.mode != DrawingMode.TEXT and self.options.mode != DrawingMode.NUMBER:
-            cr.set_source_rgba(*self.options.primary_color)
+            cr.set_source_rgba(
+                self.options.primary_color.red,
+                self.options.primary_color.green,
+                self.options.primary_color.blue,
+                self.options.primary_color.alpha,
+            )
             if self.options.mode == DrawingMode.PEN and len(self.current_stroke) > 1:
                 StrokeAction(self.current_stroke, self.options.copy()).draw(cr, self._image_to_widget_coords, scale)
             elif self.options.mode == DrawingMode.HIGHLIGHTER and len(self.current_stroke) > 1:
diff --git a/gradia/ui/widget/quick_color_picker.py b/gradia/ui/widget/quick_color_picker.py
index 7e1d82a..fb93299 100644
--- a/gradia/ui/widget/quick_color_picker.py
+++ b/gradia/ui/widget/quick_color_picker.py
@@ -16,22 +16,22 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from gi.repository import Gtk, Adw, Gdk, GObject, Gio
-from gradia.utils.colors import is_light_color_rgba, rgba_to_hex
+from gradia.utils.colors import is_light_color_rgba, rgba_to_hex, make_rgba
+
 
 class ColorPickerMixin:
     def _get_base_colors(self, alpha=1.0, secondary=False):
         base_colors = [
-            (Gdk.RGBA(0.88, 0.11, 0.14, alpha), _("Red")),
-            (Gdk.RGBA(0.18, 0.76, 0.49, alpha), _("Green")),
-            (Gdk.RGBA(0.21, 0.52, 0.89, alpha), _("Blue")),
-            (Gdk.RGBA(0.96, 0.83, 0.18, alpha), _("Yellow")),
-            (Gdk.RGBA(0.0, 0.0, 0.0, alpha), _("Black")),
-            (Gdk.RGBA(1.0, 1.0, 1.0, alpha), _("White")),
-            ]
+            (make_rgba(0.88, 0.11, 0.14, alpha), _("Red")),
+            (make_rgba(0.18, 0.76, 0.49, alpha), _("Green")),
+            (make_rgba(0.21, 0.52, 0.89, alpha), _("Blue")),
+            (make_rgba(0.96, 0.83, 0.18, alpha), _("Yellow")),
+            (make_rgba(0.0, 0.0, 0.0, alpha), _("Black")),
+            (make_rgba(1.0, 1.0, 1.0, alpha), _("White")),
+        ]
 
         if secondary:
-            base_colors.append((Gdk.RGBA(0, 0, 0, 0), _("Transparent")))
-
+            base_colors.append((make_rgba(0.0, 0.0, 0.0, 0.0), _("Transparent")))
 
         return base_colors
 
diff --git a/gradia/utils/colors.py b/gradia/utils/colors.py
index 260a4dc..f0cb51e 100644
--- a/gradia/utils/colors.py
+++ b/gradia/utils/colors.py
@@ -20,6 +20,19 @@ from gi.repository import Gdk
 HexColor = str
 RGBTuple = tuple[int, int, int]
 
+def make_rgba(r: float, g: float, b: float, a: float) -> Gdk.RGBA:
+    """Utility to construct a Gdk.RGBA with the given components.
+
+    Gdk.RGBA does not take RGBA values as constructor arguments; you must
+    instantiate it and then assign the channels explicitly.
+    """
+    rgba = Gdk.RGBA()
+    rgba.red = r
+    rgba.green = g
+    rgba.blue = b
+    rgba.alpha = a
+    return rgba
+
 def hex_to_rgba(hex_color: HexColor, alpha: float | None = None) -> Gdk.RGBA:
     """
     Converts hexadecimal color code to `Gdk.RGBA` object.
@@ -58,6 +71,23 @@ def hex_to_rgb(hex_color: HexColor) -> RGBTuple:
     r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4))
     return (r, g, b)
 
+def rgba_to_tuple(color: Gdk.RGBA) -> tuple[float, float, float, float]:
+    return (color.red, color.green, color.blue, color.alpha)
+
+def tuple_to_rgba(color: tuple[float, float, float, float] | None) -> Gdk.RGBA:
+    rgba = Gdk.RGBA()
+    if not color or len(color) != 4:
+        rgba.red = 0.0
+        rgba.green = 0.0
+        rgba.blue = 0.0
+        rgba.alpha = 1.0
+        return rgba
+    rgba.red = float(color[0])
+    rgba.green = float(color[1])
+    rgba.blue = float(color[2])
+    rgba.alpha = float(color[3])
+    return rgba
+
 def has_visible_color(color):
     return any(c > 0 for c in color[:3]) or (len(color) > 3 and color[3] > 0)
 
