Huge updates: ControlCenter: Wi-Fi, Bluetooth, Password Entry Dialog, Notification, CustomRevealer for notif

This commit is contained in:
sankalp committed 2025-08-01 07:54:43 +05:45
1 parent 195e2d0f82
commit 10f1b25264
31 files changed
+1418 -349

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import math
from typing import cast
import cairo
from gi.repository import Gtk
from fabric.widgets.image import Image
class CustomImage(Image):
def do_render_rectangle(
self, cr: cairo.Context, width: int, height: int, radius: int = 0
):
cr.move_to(radius, 0)
cr.line_to(width - radius, 0)
cr.arc(width - radius, radius, radius, -(math.pi / 2), 0)
cr.line_to(width, height - radius)
cr.arc(width - radius, height - radius, radius, 0, (math.pi / 2))
cr.line_to(radius, height)
cr.arc(radius, height - radius, radius, (math.pi / 2), math.pi)
cr.line_to(0, radius)
cr.arc(radius, radius, radius, math.pi, (3 * (math.pi / 2)))
cr.close_path()
def do_draw(self, cr: cairo.Context):
context = self.get_style_context()
width, height = self.get_allocated_width(), self.get_allocated_height()
cr.save()
self.do_render_rectangle(
cr,
width,
height,
cast(int, context.get_property("border-radius", Gtk.StateFlags.NORMAL)),
)
cr.clip()
Image.do_draw(self, cr)
cr.restore()
+255
View File
@@ -0,0 +1,255 @@
from gi.repository import GLib, Gtk
import gi
gi.require_version("Gtk", "3.0")
# TODO: UsE BETTER APPROACH IF POSSIBLE
class AnimationManager:
_instance = None
_animating_widgets = set()
_timer_id = None
_containers_to_redraw = set()
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def add_widget(self, widget):
self._animating_widgets.add(widget)
if self._timer_id is None:
self._timer_id = GLib.timeout_add(22, self._animate_all) # 45 FPS
def remove_widget(self, widget):
self._animating_widgets.discard(widget)
if not self._animating_widgets and self._timer_id:
# Stop timer when no widgets are animating
GLib.source_remove(self._timer_id)
self._timer_id = None
# Clear any pending redraws
self._containers_to_redraw.clear()
def _animate_all(self):
# Clear previous frame's redraw queue
self._containers_to_redraw.clear()
widgets_to_remove = []
for widget in list(self._animating_widgets):
if not widget._calculate_position():
widgets_to_remove.append(widget)
else:
container = widget._get_container_for_redraw()
if container:
self._containers_to_redraw.add(container)
# Apply all position changes at once
for widget in self._animating_widgets:
widget._apply_position()
# Single redraw call per container
for container in self._containers_to_redraw:
container.queue_draw()
# Remove completed animations
for widget in widgets_to_remove:
self.remove_widget(widget)
return len(self._animating_widgets) > 0 # Continue if widgets remain
def _get_optimal_interval(self):
"""Calculate optimal frame interval based on widget count"""
widget_count = len(self._animating_widgets)
if widget_count <= 1:
return 22 # 45 FPS for single widget
elif widget_count <= 3:
return 33 # 30 FPS for 2-3 widgets
elif widget_count <= 6:
return 50 # 20 FPS for 4-6 widgets
else:
return 67 # 15 FPS for 7+ widgets
def _start_timer(self):
interval = self._get_optimal_interval()
self._timer_id = GLib.timeout_add(interval, self._animate_all)
def _adjust_frame_rate(self):
if not self._timer_id:
return
new_interval = self._get_optimal_interval()
GLib.source_remove(self._timer_id)
self._timer_id = GLib.timeout_add(new_interval, self._animate_all)
class SlideRevealer(Gtk.Overlay):
def __init__(self, child: Gtk.Widget, direction="right", duration=300, size=None):
super().__init__()
self.child = child
self.direction = direction
self.duration = duration
self.fixed_size = size
self._revealed = False
self._animating = False
self._start_time = None
self._show_animation = False
self._pending_position = None
self._fixed = Gtk.Fixed()
self._fixed.set_has_window(False)
self._fixed.add(child)
self.add_overlay(self._fixed)
if self.fixed_size:
self.set_size_request(self.fixed_size[0], self.fixed_size[1])
child.hide()
self.show_all()
else:
child.connect("size-allocate", self._on_size_allocate)
child.hide()
self.show_all()
def _on_size_allocate(self, _widget, allocation):
if not self.fixed_size:
current_req = self.get_size_request()
if (
current_req[0] != allocation.width
or current_req[1] != allocation.height
):
self.set_size_request(allocation.width, allocation.height)
def set_reveal_child(self, reveal: bool):
if reveal:
self.reveal()
else:
self.hide()
def reveal(self):
if self._revealed or self._animating:
return
self._revealed = True
self._start_animation(show=True)
def hide(self):
if not self._revealed or self._animating:
return
self._revealed = False
self._start_animation(show=False)
def _start_animation(self, show: bool):
if self._animating:
AnimationManager.get_instance().remove_widget(self)
self._cached_dimensions = self._get_dimensions()
if self._cached_dimensions[0] == 0 or self._cached_dimensions[1] == 0:
self._animating = False
return
self._start_time = GLib.get_monotonic_time()
self._animating = True
self._show_animation = show
if show:
self.child.show()
self._fixed.move(self.child, *self._get_offscreen_pos_cached())
AnimationManager.get_instance().add_widget(self)
def _calculate_position(self):
if not self._animating:
return False
elapsed = (GLib.get_monotonic_time() - self._start_time) / 1000
t = min(elapsed / self.duration, 1.0)
if self._show_animation:
eased = 1 - (1 - t) ** 3 # Inline ease_out_cubic
else:
eased = t**3 # Ease in cubic for hide animation
self._pending_position = self._get_position_at_progress_cached(eased)
if t >= 1.0:
self._animating = False
self._cached_dimensions = None
if not self._show_animation:
GLib.idle_add(lambda: self.child.hide())
return False
return True
def _apply_position(self):
if self._pending_position:
x, y = self._pending_position
self._fixed.move(self.child, x, y)
self._pending_position = None
def _get_container_for_redraw(self):
return self
def _get_dimensions(self):
if self.fixed_size:
return self.fixed_size
else:
alloc = self.child.get_allocation()
return alloc.width, alloc.height
def _get_offscreen_pos_cached(self):
w, h = self._cached_dimensions
if self.direction == "left":
return -w, 0
elif self.direction == "right":
return w, 0
elif self.direction == "top":
return 0, -h
elif self.direction == "bottom":
return 0, h
return 0, 0
def _get_position_at_progress_cached(self, progress):
w, h = self._cached_dimensions
if self._show_animation:
# Showing animation: slide from offscreen to onscreen (0,0)
if self.direction == "left":
return int(-w + w * progress), 0
elif self.direction == "right":
return int(w - w * progress), 0
elif self.direction == "top":
return 0, int(-h + h * progress)
elif self.direction == "bottom":
return 0, int(h - h * progress)
else:
# Hiding animation: slide from onscreen (0,0) to offscreen
if self.direction == "left":
return int(-w * progress), 0 # Slide left (negative x)
elif self.direction == "right":
return int(w * progress), 0 # Slide right (positive x)
elif self.direction == "top":
return 0, int(-h * progress) # Slide up (negative y)
elif self.direction == "bottom":
return 0, int(h * progress) # Slide down (positive y)
return 0, 0
def set_slide_direction(self, direction):
self.direction = direction
def is_revealed(self):
return self._revealed
def is_animating(self):
return self._animating
def get_child_revealed(self):
return self._revealed
def stop_animation(self):
if self._animating:
AnimationManager.get_instance().remove_widget(self)
self._animating = False
self._cached_dimensions = None
def destroy(self):
self.stop_animation()
super().destroy()
-1
View File
@@ -1,7 +1,6 @@
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.eventbox import EventBox
from utils.roam import modus_service
from widgets.popup_window import PopupWindow
+3 -3
View File
@@ -1,12 +1,12 @@
from typing import Any
import cairo
from gi.repository import GtkLayerShell, GLib # type: ignore
import cairo
from gi.repository import GLib, GtkLayerShell # type: ignore
from fabric.widgets.eventbox import EventBox
from widgets.wayland import WaylandWindow as Window
from fabric.widgets.widget import Widget
from utils.roam import modus_service
from widgets.wayland import WaylandWindow as Window
class MouseCapture(Window):
+3 -2
View File
@@ -5,11 +5,12 @@ from typing import Literal, cast
import cairo
import gi
from gi.repository import Gdk, GObject, Gtk
from loguru import logger
from fabric.core.service import Property
from fabric.utils.helpers import extract_css_values, get_enum_member
from fabric.widgets.window import Window
from gi.repository import Gdk, GObject, Gtk
from loguru import logger
gi.require_version("Gtk", "3.0")
+282
View File
@@ -0,0 +1,282 @@
import gi
from gi.repository import Gdk, GLib
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.image import Image
from fabric.widgets.label import Label
from widgets.wayland import WaylandWindow as Window
gi.require_version("Gtk", "3.0")
class WiFiPasswordDialog(Window):
def __init__(
self,
ssid: str,
on_connect_callback=None,
on_cancel_callback=None,
on_dialog_closed=None,
**kwargs,
):
super().__init__(
title="modus",
layer="overlay",
anchor="center",
keyboard_mode="on-demand",
visible=False,
name="wifi-password-dialog",
**kwargs,
)
self.ssid = ssid
self.on_connect_callback = on_connect_callback
self.on_cancel_callback = on_cancel_callback
self.on_dialog_closed = on_dialog_closed
self.is_connecting = False
self.connection_timeout_id = None
self._create_dialog_content()
self.connect("key-press-event", self._on_key_press)
self.connect("notify::visible", self._on_visibility_changed)
def _create_dialog_content(self):
self.wifi_icon = Image(
icon_name="network-wireless-symbolic", size=20, name="wifi-dialog-icon"
)
self.title_label = Label(
label=f'The Wi-Fi network "{self.ssid}" requires a WPA2 password.',
name="wifi-dialog-title",
h_align="start",
wrap=True,
max_width_chars=40,
)
self.title_container = Box(
orientation="h",
spacing=8,
children=[self.wifi_icon, self.title_label],
name="wifi-dialog-title-container",
h_align="center",
)
self.error_label = Label(
label="Incorrect password. Please try again.",
name="wifi-dialog-error",
h_align="center",
visible=False,
)
self.password_label = Label(
label="Password:", name="wifi-dialog-password-label", h_align="start"
)
self.password_entry = Entry(
placeholder_text="Enter password",
name="wifi-dialog-password-entry",
visibility=False,
h_expand=True,
)
self.password_entry.connect("activate", lambda *_: self._on_join_clicked())
self.password_entry.connect("changed", self._on_password_changed)
self.password_visible = False
self.show_password_button = Button(
image=Image(icon_name="view-conceal-symbolic", size=16),
name="wifi-dialog-show-password-button",
on_clicked=self._on_show_password_clicked,
)
self.show_password_label = Label(
label="Show password", name="wifi-dialog-show-password-label"
)
self.show_password_box = Box(
orientation="h",
spacing=8,
children=[self.show_password_button, self.show_password_label],
name="wifi-dialog-show-password-box",
)
self.cancel_button = Button(
label="Cancel",
name="wifi-dialog-cancel-button",
on_clicked=self._on_cancel_clicked,
)
self.join_button = Button(
label="Join",
name="wifi-dialog-join-button",
on_clicked=self._on_join_clicked,
)
self.button_box = Box(
orientation="h",
spacing=12,
children=[self.cancel_button, self.join_button],
name="wifi-dialog-button-box",
h_align="end",
)
self.password_container = Box(
orientation="v",
spacing=6,
children=[self.password_label, self.password_entry, self.show_password_box],
name="wifi-dialog-password-container",
)
self.content_box = Box(
orientation="v",
spacing=12,
children=[
self.title_container,
self.error_label,
self.password_container,
self.button_box,
],
name="wifi-dialog-content",
h_align="center",
v_align="center",
)
self.dialog_background = Box(
children=[self.content_box],
name="wifi-dialog-background",
h_align="center",
v_align="center",
)
self.children = self.dialog_background
self._update_join_button_state()
def _on_password_changed(self, entry):
self._update_join_button_state()
def _update_join_button_state(self):
password = self.password_entry.get_text().strip()
has_password = len(password) > 0
if has_password:
self.join_button.set_opacity(1.0)
self.join_button.remove_style_class("disabled")
else:
self.join_button.set_opacity(0.5)
self.join_button.add_style_class("disabled")
def _on_show_password_clicked(self, *args):
self.password_visible = not self.password_visible
self.password_entry.set_visibility(self.password_visible)
icon_name = (
"view-reveal-symbolic" if self.password_visible else "view-conceal-symbolic"
)
self.show_password_button.get_image().set_property("icon-name", icon_name)
def _on_key_press(self, widget, event):
keyval = event.keyval
if keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter:
self._on_join_clicked()
return True
elif keyval == Gdk.KEY_Escape:
self._on_cancel_clicked()
return True
return False
def _on_visibility_changed(self, widget, *args):
"""Handle visibility changes"""
if self.get_visible():
GLib.timeout_add(100, lambda: self.password_entry.grab_focus())
def _on_cancel_clicked(self, *args):
self.hide()
if self.on_cancel_callback:
self.on_cancel_callback()
if self.on_dialog_closed:
self.on_dialog_closed()
def _on_join_clicked(self, *args):
if self.is_connecting:
return
password = self.password_entry.get_text().strip()
if not password:
self.password_entry.grab_focus()
return
self.is_connecting = True
self.join_button.set_sensitive(False)
self.connection_timeout_id = GLib.timeout_add(5000, self._connection_timeout)
self.error_label.set_visible(False)
self.hide()
if self.on_connect_callback:
self.on_connect_callback(self.ssid, password)
if self.on_dialog_closed:
self.on_dialog_closed()
def _connection_timeout(self):
if self.is_connecting:
self.is_connecting = False
self.join_button.set_sensitive(True)
self.show_error("Connection timeout. Please try again.")
return False
def show_dialog(self):
if self.connection_timeout_id:
GLib.source_remove(self.connection_timeout_id)
self.connection_timeout_id = None
self.show_all()
self.password_entry.set_text("")
self.error_label.set_visible(False)
self.password_visible = False
self.password_entry.set_visibility(False)
self.show_password_button.get_image().set_property(
"icon-name", "view-conceal-symbolic"
)
self.is_connecting = False
self.join_button.set_sensitive(True)
self._update_join_button_state()
def show_error(self, message="Incorrect password. Please try again."):
if self.connection_timeout_id:
GLib.source_remove(self.connection_timeout_id)
self.connection_timeout_id = None
self.is_connecting = False
self.join_button.set_sensitive(True)
if not self.get_visible():
self.error_label.set_text(message)
self.error_label.set_visible(True)
self.show_all()
GLib.timeout_add(10, lambda: self._focus_and_select_password())
else:
self.error_label.set_text(message)
self.error_label.set_visible(True)
self._focus_and_select_password()
def _focus_and_select_password(self):
try:
self.password_entry.grab_focus()
self.password_entry.select_region(0, -1)
return False
except:
return False
def get_password(self):
return self.password_entry.get_text()
def destroy_dialog(self):
self.hide()
self.destroy()