Huge updates: ControlCenter: Wi-Fi, Bluetooth, Password Entry Dialog, Notification, CustomRevealer for notif
This commit is contained in:
31 files changed
+1418
-349
No files matched your search
@@ -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()
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user