-
Notifications
You must be signed in to change notification settings - Fork 44
feat(widgets): add CircularProgressBar widget #417
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
base: main
Are you sure you want to change the base?
Changes from all commits
76a9f1c
86c4a14
be60a51
eb20df0
78c6ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| CircularProgressBar | ||
| ------------------ | ||
|
|
||
| .. autoclass:: ignis.widgets.CircularProgressBar | ||
| :members: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| import math | ||
| from typing import Literal | ||
|
|
||
| import cairo | ||
| import gi | ||
| from gi.repository.Gdk import RGBA | ||
|
|
||
| from gi.repository import Gtk | ||
| from ignis.base_widget import BaseWidget | ||
| from ignis.gobject import IgnisProperty | ||
|
|
||
|
|
||
| LINE_STYLE_MAP = { | ||
| 'none': cairo.LineCap.BUTT, | ||
| 'butt': cairo.LineCap.BUTT, | ||
| 'round': cairo.LineCap.ROUND, | ||
| 'square': cairo.LineCap.SQUARE, | ||
| } | ||
|
|
||
|
|
||
| def clamp(value: float, min_val: float, max_val: float) -> float: | ||
| return max(min_val, min(value, max_val)) | ||
|
|
||
|
|
||
| class CircularProgressBar(Gtk.DrawingArea, BaseWidget): | ||
| """ | ||
| Bases: :class:`Gtk.DrawingArea` | ||
|
|
||
| A circular progress indicator widget. | ||
|
|
||
| Args: | ||
| **kwargs: Properties to set. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| widgets.CircularProgressBar( | ||
| value=0.75, | ||
| min_value=0, | ||
| max_value=100, | ||
| line_width=6, | ||
| line_style='round', | ||
| pie=False | ||
| ) | ||
| """ | ||
| __gtype_name__ = "IgnisCircularProgressBar" | ||
| __gproperties__ = {**BaseWidget.gproperties} | ||
|
|
||
| def __init__( | ||
| self, | ||
| value: float = 1.0, | ||
| min_value: float = 0.0, | ||
| child: Gtk.Widget | None = None, | ||
| max_value: float = 1.0, | ||
| start_angle: float = 0.0, | ||
| end_angle: float = 360.0, | ||
| line_width: int = 4, | ||
| line_style: Literal["none", "butt", "round", "square"] | cairo.LineCap = "round", | ||
| pie: bool = False, | ||
| invert: bool = False, | ||
| size: tuple[int, int] = (100, 100), | ||
| track_color: RGBA | None = None, | ||
| **kwargs, | ||
| ): | ||
| Gtk.DrawingArea.__init__(self) | ||
| self._value = value | ||
| self._min_value = min_value | ||
| self._max_value = max_value | ||
| self._line_width = line_width | ||
| self._line_style_value = line_style # store original string or LineCap | ||
| self._pie = pie | ||
| self._invert = invert | ||
| self.start_angle = start_angle | ||
| self.end_angle = end_angle | ||
|
Comment on lines
+72
to
+73
Member
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. Should start with the underscore, otherwise it sets the value through the GObject setter which triggers |
||
| self._track_color = track_color | ||
|
|
||
| BaseWidget.__init__(self, **kwargs) | ||
| self.set_size_request(size[0], size[1]) | ||
|
Member
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. There are already |
||
| self.set_draw_func(self.__on_draw) | ||
|
|
||
| @IgnisProperty | ||
| def value(self) -> float: | ||
| """The current progress value.""" | ||
| return self._value | ||
|
|
||
| @value.setter | ||
| def value(self, value: float) -> None: | ||
| self._value = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def min_value(self) -> float: | ||
| """Minimum value for this progress bar.""" | ||
| return self._min_value | ||
|
|
||
| @min_value.setter | ||
| def min_value(self, value: float) -> None: | ||
| self._min_value = clamp(value, 0.0, self._max_value) | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def max_value(self) -> float: | ||
| """Maximum value for this progress bar.""" | ||
| return self._max_value | ||
|
|
||
| @max_value.setter | ||
| def max_value(self, value: float) -> None: | ||
| if value == 0: | ||
| raise ValueError("max_value cannot be zero") | ||
| self._max_value = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def pie(self) -> bool: | ||
| """Whether to draw as a filled pie chart instead of a ring.""" | ||
| return self._pie | ||
|
|
||
| @pie.setter | ||
| def pie(self, value: bool) -> None: | ||
| self._pie = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def line_width(self) -> int: | ||
| """The width of the progress arc line.""" | ||
| return self._line_width | ||
|
|
||
| @line_width.setter | ||
| def line_width(self, value: int) -> None: | ||
| self._line_width = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def line_style(self) -> str | cairo.LineCap: | ||
| """The style of the progress arc line.""" | ||
| return self._line_style_value | ||
|
|
||
| @line_style.setter | ||
| def line_style( | ||
| self, line_style: Literal["none", "butt", "round", "square"] | cairo.LineCap | ||
| ) -> None: | ||
| self._line_style_value = line_style | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def start_angle(self) -> float: | ||
|
linkfrg marked this conversation as resolved.
|
||
| """The starting angle of the progress arc in degrees.""" | ||
| return self._start_angle | ||
|
|
||
| @start_angle.setter | ||
| def start_angle(self, value: float) -> None: | ||
| self._start_angle = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def end_angle(self) -> float: | ||
| """The ending angle of the progress arc in degrees.""" | ||
| return self._end_angle | ||
|
|
||
| @end_angle.setter | ||
| def end_angle(self, value: float) -> None: | ||
| self._end_angle = value | ||
| self.queue_draw() | ||
|
|
||
| @IgnisProperty | ||
| def invert(self) -> bool: | ||
| """Whether to invert the drawing direction of the progress arc.""" | ||
| return self._invert | ||
|
|
||
| @invert.setter | ||
| def invert(self, value: bool) -> None: | ||
| self._invert = value | ||
| self.queue_draw() | ||
|
|
||
| def __on_draw( | ||
| self, drawing_area, cr: cairo.Context, width: int, height: int, user_data=None | ||
| ): | ||
| progress_color = self.get_color() | ||
| track_color = self._track_color or RGBA(0.4, 0.4, 0.4, 1.0) | ||
|
|
||
| line_cap = ( | ||
| LINE_STYLE_MAP.get(self._line_style_value, cairo.LineCap.ROUND) | ||
| if isinstance(self._line_style_value, str) | ||
| else self._line_style_value | ||
| ) | ||
|
|
||
| line_width = self._line_width | ||
| center_x = width / 2 | ||
| center_y = height / 2 | ||
| radius = min(width, height) / 2 - line_width | ||
| if radius <= 0: | ||
| radius = 10 | ||
|
|
||
| cr.save() | ||
| cr.set_line_cap(line_cap) | ||
| cr.set_line_width(line_width) | ||
|
|
||
| cr.set_source_rgba(track_color.red, track_color.green, track_color.blue, track_color.alpha) | ||
| if self._pie: | ||
| cr.move_to(center_x, center_y) | ||
| cr.arc(center_x, center_y, radius, math.radians(self._start_angle), math.radians(self._end_angle)) | ||
| if self._pie: | ||
| cr.fill() | ||
| else: | ||
| cr.stroke() | ||
|
|
||
| normalized_value = clamp((self._value - self._min_value) / (self._max_value - self._min_value), 0.0, 1.0) | ||
| if normalized_value > 0: | ||
| cr.set_source_rgba(progress_color.red, progress_color.green, progress_color.blue, progress_color.alpha) | ||
| if self._pie: | ||
| cr.move_to(center_x, center_y) | ||
| cr.arc( | ||
| center_x, | ||
| center_y, | ||
| radius, | ||
| math.radians(self._start_angle), | ||
| math.radians(self._start_angle + normalized_value * (self._end_angle - self._start_angle)), | ||
| ) | ||
| if self._pie: | ||
| cr.fill() | ||
| else: | ||
| cr.stroke() | ||
| cr.restore() | ||
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.
I think we shouldn't define them in constructor arguments, because they are GObject properties and can be handled by
**kwargsautomatically. The constructor should explicitly define arguments only for read-only properties, that can be set only during the widget initialization. By the way, I don't seetrack_colorto be a GObject property, did you forget to add it?The default values can be set below:
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.
bind-able values would be good too