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
+4
-8
@@ -1,13 +1,9 @@
|
||||
import subprocess
|
||||
import re
|
||||
import gi # type: ignore
|
||||
import subprocess
|
||||
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.box import Box
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from fabric.utils.helpers import exec_shell_command_async, get_relative_path
|
||||
from gi.repository import Gtk, GdkPixbuf # type: ignore
|
||||
from gi.repository import GdkPixbuf, Gtk # type: ignore
|
||||
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
|
||||
|
||||
def read_dmi(field):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from fabric.bluetooth import BluetoothClient, BluetoothDevice
|
||||
from fabric.widgets.box import Box
|
||||
@@ -7,7 +7,6 @@ from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
|
||||
|
||||
class BluetoothDeviceSlot(CenterBox):
|
||||
@@ -68,8 +67,6 @@ class BluetoothConnections(Box):
|
||||
|
||||
self.client = BluetoothClient(on_device_added=self.on_device_added)
|
||||
|
||||
|
||||
|
||||
self.title = Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
@@ -95,10 +92,7 @@ class BluetoothConnections(Box):
|
||||
)
|
||||
|
||||
# Connect scanning state changes to update scan button
|
||||
self.client.connect(
|
||||
"notify::scanning",
|
||||
lambda *_: self.update_scan_label()
|
||||
)
|
||||
self.client.connect("notify::scanning", lambda *_: self.update_scan_label())
|
||||
|
||||
self.not_paired = Box(spacing=2, orientation="vertical")
|
||||
self.paired = Box(spacing=2, orientation="vertical")
|
||||
@@ -108,13 +102,13 @@ class BluetoothConnections(Box):
|
||||
name="bluetooth-refresh-indicator",
|
||||
label="↓ Pull to scan for devices",
|
||||
h_align="center",
|
||||
visible=False
|
||||
visible=False,
|
||||
)
|
||||
|
||||
self.device_box = Box(
|
||||
spacing=2,
|
||||
orientation="vertical",
|
||||
children=[self.refresh_indicator, self.paired, self.not_paired]
|
||||
children=[self.refresh_indicator, self.paired, self.not_paired],
|
||||
)
|
||||
|
||||
# Create scrolled window with pull-to-refresh
|
||||
@@ -143,8 +137,6 @@ class BluetoothConnections(Box):
|
||||
# Connect cleanup on destroy
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Cleanup animations when widget is destroyed"""
|
||||
self.stop_bounce_animation()
|
||||
@@ -165,8 +157,6 @@ class BluetoothConnections(Box):
|
||||
self.bounce_duration = 60 # Total frames for animation
|
||||
self.bounce_amplitude = 15 # Maximum bounce height in pixels
|
||||
|
||||
|
||||
|
||||
# Connect to scroll events
|
||||
self.scrolled_window.connect("scroll-event", self.on_scroll_event)
|
||||
self.scrolled_window.connect("button-press-event", self.on_button_press)
|
||||
@@ -175,10 +165,10 @@ class BluetoothConnections(Box):
|
||||
|
||||
# Enable events
|
||||
self.scrolled_window.set_events(
|
||||
Gdk.EventMask.SCROLL_MASK |
|
||||
Gdk.EventMask.BUTTON_PRESS_MASK |
|
||||
Gdk.EventMask.BUTTON_RELEASE_MASK |
|
||||
Gdk.EventMask.POINTER_MOTION_MASK
|
||||
Gdk.EventMask.SCROLL_MASK
|
||||
| Gdk.EventMask.BUTTON_PRESS_MASK
|
||||
| Gdk.EventMask.BUTTON_RELEASE_MASK
|
||||
| Gdk.EventMask.POINTER_MOTION_MASK
|
||||
)
|
||||
|
||||
def on_scroll_event(self, widget, event):
|
||||
@@ -243,22 +233,22 @@ class BluetoothConnections(Box):
|
||||
|
||||
def ease_out_bounce(self, t):
|
||||
"""Smooth bounce easing function (0 to 1)"""
|
||||
import math
|
||||
if t < 1/2.75:
|
||||
if t < 1 / 2.75:
|
||||
return 7.5625 * t * t
|
||||
elif t < 2/2.75:
|
||||
t -= 1.5/2.75
|
||||
elif t < 2 / 2.75:
|
||||
t -= 1.5 / 2.75
|
||||
return 7.5625 * t * t + 0.75
|
||||
elif t < 2.5/2.75:
|
||||
t -= 2.25/2.75
|
||||
elif t < 2.5 / 2.75:
|
||||
t -= 2.25 / 2.75
|
||||
return 7.5625 * t * t + 0.9375
|
||||
else:
|
||||
t -= 2.625/2.75
|
||||
t -= 2.625 / 2.75
|
||||
return 7.5625 * t * t + 0.984375
|
||||
|
||||
def ease_out_elastic(self, t):
|
||||
"""Elastic easing function for smoother bounce"""
|
||||
import math
|
||||
|
||||
if t == 0 or t == 1:
|
||||
return t
|
||||
|
||||
@@ -327,8 +317,6 @@ class BluetoothConnections(Box):
|
||||
self.bounce_frame += 1
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def stop_bounce_animation(self):
|
||||
"""Stop any ongoing animations"""
|
||||
if self.bounce_timeout_id:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from gi.repository import Gdk, GLib
|
||||
|
||||
from fabric.utils import idle_add
|
||||
from fabric.utils.helpers import (
|
||||
get_relative_path,
|
||||
@@ -8,9 +10,6 @@ from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scale import Scale
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.image import Image
|
||||
from gi.repository import Gdk, GLib
|
||||
|
||||
from modules.controlcenter.bluetooth import BluetoothConnections
|
||||
from modules.controlcenter.wifi import WifiConnections
|
||||
from services.brightness import Brightness
|
||||
@@ -37,7 +36,7 @@ class ModusControlCenter(Window):
|
||||
visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
self.focus_mode = False
|
||||
self.focus_mode = modus_service.dont_disturb
|
||||
self._updating_brightness = False
|
||||
self._updating_volume = False
|
||||
|
||||
@@ -50,6 +49,7 @@ class ModusControlCenter(Window):
|
||||
|
||||
audio_service.connect("changed", self.audio_changed)
|
||||
audio_service.connect("changed", self.volume_changed)
|
||||
modus_service.connect("dont-disturb-changed", self.dnd_changed)
|
||||
|
||||
self.wlan_label = Label(wlan, name="wifi-widget-label", h_align="start")
|
||||
if bluetooth != "disabled":
|
||||
@@ -178,7 +178,11 @@ class ModusControlCenter(Window):
|
||||
|
||||
self.focus_icon = Svg(
|
||||
name="focus-icon",
|
||||
svg_file=get_relative_path("../../config/assets/icons/applets/dnd-off.svg"),
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/applets/dnd.svg"
|
||||
if self.focus_mode
|
||||
else "../../config/assets/icons/applets/dnd-off.svg"
|
||||
),
|
||||
size=46,
|
||||
)
|
||||
|
||||
@@ -364,9 +368,13 @@ class ModusControlCenter(Window):
|
||||
parts = wlan.split(":")
|
||||
if len(parts) >= 2:
|
||||
wifi_name = parts[1]
|
||||
GLib.idle_add(lambda: self.wlan_label.set_property("label", wifi_name))
|
||||
GLib.idle_add(
|
||||
lambda: self.wlan_label.set_property("label", wifi_name)
|
||||
)
|
||||
else:
|
||||
GLib.idle_add(lambda: self.wlan_label.set_property("label", "Connected"))
|
||||
GLib.idle_add(
|
||||
lambda: self.wlan_label.set_property("label", "Connected")
|
||||
)
|
||||
else:
|
||||
GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan))
|
||||
else:
|
||||
@@ -385,9 +393,13 @@ class ModusControlCenter(Window):
|
||||
parts = bluetooth.split(":")
|
||||
if len(parts) >= 2:
|
||||
device_name = parts[1]
|
||||
GLib.idle_add(lambda: self.bluetooth_label.set_property("label", device_name))
|
||||
GLib.idle_add(
|
||||
lambda: self.bluetooth_label.set_property("label", device_name)
|
||||
)
|
||||
else:
|
||||
GLib.idle_add(lambda: self.bluetooth_label.set_property("label", "Connected"))
|
||||
GLib.idle_add(
|
||||
lambda: self.bluetooth_label.set_property("label", "Connected")
|
||||
)
|
||||
elif bluetooth == "enabled":
|
||||
GLib.idle_add(lambda: self.bluetooth_label.set_property("label", "On"))
|
||||
else:
|
||||
@@ -398,6 +410,16 @@ class ModusControlCenter(Window):
|
||||
def audio_changed(self, *_):
|
||||
pass
|
||||
|
||||
def dnd_changed(self, _, dnd_state):
|
||||
self.focus_mode = dnd_state
|
||||
self.focus_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../config/assets/icons/applets/dnd.svg"
|
||||
if self.focus_mode
|
||||
else "../../config/assets/icons/applets/dnd-off.svg"
|
||||
)
|
||||
)
|
||||
|
||||
def _init_mousecapture(self, mousecapture):
|
||||
self._mousecapture_parent = mousecapture
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from services.network import NetworkService
|
||||
from widgets.wifi_password_dialog import WiFiPasswordDialog
|
||||
|
||||
|
||||
class WifiNetworkSlot(CenterBox):
|
||||
def __init__(self, network_data: dict, wifi_service, **kwargs):
|
||||
def __init__(self, network_data: dict, wifi_service, parent=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.network_data = network_data
|
||||
self.wifi_service = wifi_service
|
||||
self.parent = parent # Reference to control center
|
||||
|
||||
# Get network info
|
||||
self.ssid = network_data.get("ssid", "Unknown")
|
||||
@@ -43,6 +45,9 @@ class WifiNetworkSlot(CenterBox):
|
||||
style_classes=" ".join(self.styles),
|
||||
)
|
||||
|
||||
# Initialize password dialog
|
||||
self.password_dialog = None
|
||||
|
||||
self.start_children = [
|
||||
Button(
|
||||
image=self.dimage,
|
||||
@@ -79,11 +84,10 @@ class WifiNetworkSlot(CenterBox):
|
||||
# Reset state if disconnect failed
|
||||
self._reset_disconnect_state()
|
||||
else:
|
||||
# Show connecting state
|
||||
# Try to connect without password first (for open networks or saved connections)
|
||||
self.dimage.set_property("icon-name", "network-wireless-acquiring-symbolic")
|
||||
self.dimage.add_style_class("connecting")
|
||||
|
||||
# Connect to network (try without password first, could be enhanced for password input)
|
||||
success = self.wifi_service.connect_network(self.ssid)
|
||||
if success:
|
||||
self.is_connected = True
|
||||
@@ -92,8 +96,9 @@ class WifiNetworkSlot(CenterBox):
|
||||
|
||||
GLib.timeout_add(500, lambda: self._reset_connect_state())
|
||||
else:
|
||||
# Reset state if connect failed
|
||||
# Connection failed, likely needs password - show password dialog
|
||||
self._reset_connect_state()
|
||||
self._show_password_dialog()
|
||||
|
||||
# Update display after connection attempt
|
||||
self.on_changed()
|
||||
@@ -133,6 +138,67 @@ class WifiNetworkSlot(CenterBox):
|
||||
self.dimage.set_property("style-classes", " ".join(self.styles))
|
||||
return
|
||||
|
||||
def _show_password_dialog(self):
|
||||
"""Show the WiFi password dialog"""
|
||||
# Close the control center first
|
||||
if self.parent and hasattr(self.parent, "hide_controlcenter"):
|
||||
self.parent.hide_controlcenter()
|
||||
|
||||
# Create a new dialog each time to ensure clean state
|
||||
if self.password_dialog:
|
||||
self.password_dialog.destroy_dialog()
|
||||
|
||||
self.password_dialog = WiFiPasswordDialog(
|
||||
ssid=self.ssid,
|
||||
on_connect_callback=self._on_password_connect,
|
||||
on_cancel_callback=self._on_password_cancel,
|
||||
)
|
||||
|
||||
self.password_dialog.show_dialog()
|
||||
|
||||
def _on_password_connect(self, ssid, password):
|
||||
"""Handle password dialog connect action"""
|
||||
if password.strip():
|
||||
# Show connecting state
|
||||
self.dimage.set_property("icon-name", "network-wireless-acquiring-symbolic")
|
||||
self.dimage.add_style_class("connecting")
|
||||
|
||||
# Try to connect with password
|
||||
try:
|
||||
success = self.wifi_service.connect_network(ssid, password)
|
||||
if success:
|
||||
self.is_connected = True
|
||||
# Remove connecting state after a short delay
|
||||
from gi.repository import GLib
|
||||
|
||||
GLib.timeout_add(500, lambda: self._reset_connect_state())
|
||||
else:
|
||||
# Connection failed - show error in dialog immediately
|
||||
self._reset_connect_state()
|
||||
if self.password_dialog:
|
||||
# Show error immediately without delay
|
||||
self._show_connection_error()
|
||||
except Exception:
|
||||
# Handle any connection errors gracefully
|
||||
self._reset_connect_state()
|
||||
if self.password_dialog:
|
||||
# Show error immediately
|
||||
self._show_connection_error("Connection failed. Please try again.")
|
||||
|
||||
# Update display after connection attempt
|
||||
self.on_changed()
|
||||
|
||||
def _show_connection_error(self, message="Incorrect password. Please try again."):
|
||||
"""Show connection error in a separate thread to prevent UI blocking"""
|
||||
if self.password_dialog:
|
||||
self.password_dialog.show_error(message)
|
||||
return False # Don't repeat if called from GLib.timeout_add
|
||||
|
||||
def _on_password_cancel(self):
|
||||
"""Handle password dialog cancel action"""
|
||||
# Reset any connecting state
|
||||
self._reset_connect_state()
|
||||
|
||||
|
||||
class WifiConnections(Box):
|
||||
def __init__(self, parent, **kwargs):
|
||||
@@ -261,7 +327,9 @@ class WifiConnections(Box):
|
||||
access_points = self.wifi_service.access_points
|
||||
for ap_data in access_points:
|
||||
if ap_data.get("ssid") and ap_data.get("ssid") != "Unknown":
|
||||
network_slot = WifiNetworkSlot(ap_data, self.wifi_service)
|
||||
network_slot = WifiNetworkSlot(
|
||||
ap_data, self.wifi_service, parent=self.parent
|
||||
)
|
||||
self.available_networks.add(network_slot)
|
||||
|
||||
# Update all network connection states
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.shapes import Corner
|
||||
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -2,6 +2,8 @@ import json
|
||||
import logging
|
||||
|
||||
import cairo
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
import config.data as data
|
||||
from fabric.hyprland.widgets import get_hyprland_connection
|
||||
from fabric.utils import (
|
||||
@@ -17,13 +19,11 @@ from fabric.widgets.button import Button
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
from modules.corners import MyCorner
|
||||
from utils.icon_resolver import IconResolver
|
||||
from utils.occlusion import check_occlusion
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from modules.corners import MyCorner
|
||||
|
||||
# TODO: use modus_service
|
||||
|
||||
|
||||
|
||||
+123
-45
@@ -1,42 +1,35 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import cast
|
||||
|
||||
from fabric import Application
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.button import Button
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from gi.repository import GdkPixbuf, GLib # type: ignore
|
||||
|
||||
from config.data import NOTIFICATION_TIMEOUT
|
||||
from fabric.notifications import (
|
||||
NotificationAction,
|
||||
Notifications,
|
||||
Notification,
|
||||
NotificationAction,
|
||||
NotificationCloseReason,
|
||||
NotificationImagePixmap,
|
||||
Notifications,
|
||||
)
|
||||
import os
|
||||
from fabric.utils import invoke_repeater, get_relative_path
|
||||
|
||||
from fabric.utils import invoke_repeater
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.label import Label
|
||||
from utils.roam import modus_service
|
||||
from widgets.custom_image import CustomImage
|
||||
from widgets.customrevealer import SlideRevealer
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from utils.custom_image import CustomImage
|
||||
|
||||
from gi.repository import GdkPixbuf # type: ignore
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
NOTIFICATION_WIDTH = 360
|
||||
NOTIFICATION_IMAGE_SIZE = 48
|
||||
NOTIFICATION_TIMEOUT = 10 * 1000
|
||||
|
||||
|
||||
# Improved animation smoothness and consistent behavior for rapid notifications.
|
||||
|
||||
def smooth_revealer_animation(revealer: Revealer, duration: int = 300):
|
||||
"""
|
||||
Adjusts the Revealer's animation properties for smoother transitions.
|
||||
"""
|
||||
revealer.set_property("transition-duration", duration)
|
||||
revealer.set_property("transition-type", "slide-left")
|
||||
def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 250):
|
||||
revealer.duration = duration
|
||||
|
||||
|
||||
def get_notification_image_pixbuf(
|
||||
@@ -74,7 +67,6 @@ class NotificationWidget(Box):
|
||||
self.image = notification.app_icon
|
||||
body_container = Box(name="noti-image", spacing=4, orientation="h")
|
||||
|
||||
# Load and show image (from app_icon, image_file, or pixbuf)
|
||||
pixbuf = get_notification_image_pixbuf(notification)
|
||||
image_file = None
|
||||
|
||||
@@ -129,7 +121,6 @@ class NotificationWidget(Box):
|
||||
),
|
||||
Label(
|
||||
label=self._notification.body, # type: ignore
|
||||
# line_wrap="word-char",
|
||||
v_align="start",
|
||||
h_align="start",
|
||||
ellipsization="end",
|
||||
@@ -187,39 +178,103 @@ class NotificationWidget(Box):
|
||||
self.destroy()
|
||||
|
||||
|
||||
class NotificationRevealer(Revealer):
|
||||
class NotificationRevealer(SlideRevealer):
|
||||
def __init__(self, notification: Notification, on_transition_end=None, **kwargs):
|
||||
self.notif_box = NotificationWidget(notification)
|
||||
self.notification = notification
|
||||
self.on_transition_end = on_transition_end
|
||||
super().__init__(
|
||||
child=Box(
|
||||
children=[self.notif_box],
|
||||
),
|
||||
transition_duration=300,
|
||||
transition_type="slide-left",
|
||||
|
||||
# Add swipe detection variables
|
||||
self._drag_start_x = 0
|
||||
self._drag_start_y = 0
|
||||
self._is_dragging = False
|
||||
self._swipe_threshold = 100 # pixels to trigger swipe dismiss
|
||||
|
||||
# Wrap notification in EventBox for swipe detection
|
||||
self.event_box = EventBox(
|
||||
events=[
|
||||
"button-press-event",
|
||||
"button-release-event",
|
||||
"motion-notify-event",
|
||||
],
|
||||
child=self.notif_box,
|
||||
)
|
||||
|
||||
self.connect(
|
||||
"notify::child-revealed",
|
||||
self._on_child_revealed,
|
||||
self.event_box.connect("button-press-event", self._on_button_press)
|
||||
self.event_box.connect("button-release-event", self._on_button_release)
|
||||
self.event_box.connect("motion-notify-event", self._on_motion)
|
||||
|
||||
super().__init__(
|
||||
child=self.event_box,
|
||||
direction="right",
|
||||
duration=250,
|
||||
)
|
||||
|
||||
smooth_revealer_animation(self)
|
||||
|
||||
# Disconnect the destroy_noti handler from the notification widget
|
||||
# to prevent immediate destruction that interferes with slide animation
|
||||
self.notification.disconnect_by_func(self.notif_box.destroy_noti)
|
||||
|
||||
# Connect our own handler that manages the slide animation
|
||||
self.notification.connect("closed", self.on_resolved)
|
||||
|
||||
def _on_child_revealed(self, *args):
|
||||
if not self.get_child_revealed():
|
||||
def _on_animation_complete(self, is_hiding=False):
|
||||
if is_hiding:
|
||||
# Manually destroy the notification widget since we disconnected its handler
|
||||
self.notif_box.destroy_noti()
|
||||
|
||||
if self.on_transition_end:
|
||||
self.on_transition_end()
|
||||
self.destroy()
|
||||
|
||||
def on_resolved(
|
||||
self,
|
||||
notification: Notification,
|
||||
_notification: Notification,
|
||||
reason: NotificationCloseReason,
|
||||
):
|
||||
self.set_property("transition-type", "slide-right")
|
||||
self.set_reveal_child(False)
|
||||
# Use left-to-right slide for auto-dismiss (expired), right-to-left for manual close
|
||||
if reason == "expired":
|
||||
# Left-to-right slide for auto-dismiss
|
||||
self.set_slide_direction("left")
|
||||
else:
|
||||
# Right-to-left slide for manual close
|
||||
self.set_slide_direction("right")
|
||||
|
||||
self.hide()
|
||||
GLib.timeout_add(self.duration + 50, lambda: self._on_animation_complete(True))
|
||||
|
||||
def _on_button_press(self, _widget, event):
|
||||
if event.button == 1:
|
||||
self._drag_start_x = event.x
|
||||
self._drag_start_y = event.y
|
||||
self._is_dragging = True
|
||||
return False
|
||||
|
||||
def _on_button_release(self, _widget, event):
|
||||
if self._is_dragging and event.button == 1:
|
||||
self._is_dragging = False
|
||||
|
||||
# Calculate swipe distance
|
||||
dx = event.x - self._drag_start_x
|
||||
dy = abs(event.y - self._drag_start_y)
|
||||
|
||||
if dx > self._swipe_threshold and dy < 50: # 50px vertical tolerance
|
||||
# Follow the swipe gesture direction
|
||||
self.set_slide_direction("right")
|
||||
self.hide()
|
||||
# Schedule cleanup after animation
|
||||
GLib.timeout_add(
|
||||
self.duration + 50, lambda: self._on_animation_complete(True)
|
||||
)
|
||||
return False
|
||||
|
||||
def _on_motion(self, _widget, event):
|
||||
if self._is_dragging:
|
||||
# TODO: Add visual feedback during swipe
|
||||
# Could add translation or opacity changes here
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class ModusNoti(Window):
|
||||
@@ -244,6 +299,29 @@ class ModusNoti(Window):
|
||||
|
||||
def on_new_notification(self, fabric_notif, id):
|
||||
notification: Notification = fabric_notif.get_notification_from_id(id)
|
||||
new_box = NotificationRevealer(notification, on_transition_end=lambda: self.notifications.remove(new_box))
|
||||
self.notifications.add(new_box)
|
||||
new_box.set_reveal_child(True)
|
||||
|
||||
# Check if Do Not Disturb is enabled
|
||||
if modus_service.dont_disturb:
|
||||
# Still cache the notification for notification center, but don't show it
|
||||
modus_service.cache_notification(notification)
|
||||
# Close the notification immediately to prevent it from showing
|
||||
notification.close("dismissed-by-user")
|
||||
return
|
||||
|
||||
new_box = NotificationRevealer(
|
||||
notification, on_transition_end=lambda: self.notifications.remove(new_box)
|
||||
)
|
||||
|
||||
# Insert at the beginning (index 0) to show new notifications at the top
|
||||
current_children = list(self.notifications.children)
|
||||
current_children.insert(0, new_box)
|
||||
self.notifications.children = current_children
|
||||
|
||||
|
||||
# This ensures each notification's container is fully laid out before animation
|
||||
def start_animation():
|
||||
if new_box.get_parent(): # Only animate if still in the tree
|
||||
new_box.reveal()
|
||||
return False
|
||||
|
||||
GLib.timeout_add(32, start_animation)
|
||||
+5
-5
@@ -1,16 +1,15 @@
|
||||
import math
|
||||
import time
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import utils.icons as icons
|
||||
from gi.repository import GLib, GObject
|
||||
|
||||
from fabric.audio import Audio
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.scale import Scale, ScaleMark
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from gi.repository import GLib, GObject
|
||||
import math
|
||||
from services.brightness import Brightness
|
||||
from utils.animator import Animator
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
@@ -410,6 +409,7 @@ class OSD(Window):
|
||||
super().__init__(
|
||||
layer="overlay",
|
||||
anchor="bottom",
|
||||
title="modus",
|
||||
child=self.main_box,
|
||||
visible=False,
|
||||
pass_through=True,
|
||||
|
||||
@@ -6,9 +6,10 @@ including fallback mechanisms for file paths and common icon locations.
|
||||
"""
|
||||
|
||||
import os
|
||||
from gi.repository import Gtk, GdkPixbuf
|
||||
from fabric.system_tray.widgets import SystemTrayItem
|
||||
|
||||
from gi.repository import GdkPixbuf, Gtk
|
||||
|
||||
from fabric.system_tray.widgets import SystemTrayItem
|
||||
|
||||
# FIX: the tooltip should show application names instead of unknown
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from fabric.utils import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.svg import Svg
|
||||
from services.network import NetworkService
|
||||
from services.battery import Battery
|
||||
from services.network import NetworkService
|
||||
from utils.roam import modus_service
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ class BluetoothIndicator(Box):
|
||||
self.bt_icon = Svg(
|
||||
name="bt-icon",
|
||||
size=20,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/applets/bluetooth-clear.svg"),
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
),
|
||||
)
|
||||
|
||||
self.bt_button = Button(name="bt-button", child=self.bt_icon)
|
||||
@@ -34,14 +36,18 @@ class BluetoothIndicator(Box):
|
||||
def update_state(self):
|
||||
if not self.bluetooth.enabled:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/applets/bluetooth-off-clear.svg")
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-off-clear.svg"
|
||||
)
|
||||
)
|
||||
tooltip = "Bluetooth disabled"
|
||||
else:
|
||||
connected_devices = self.bluetooth.connected_devices
|
||||
if connected_devices:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/applets/bluetooth-clear.svg")
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
)
|
||||
)
|
||||
if len(connected_devices) == 1:
|
||||
device = connected_devices[0]
|
||||
@@ -52,7 +58,9 @@ class BluetoothIndicator(Box):
|
||||
tooltip = f"Connected to {len(connected_devices)} devices"
|
||||
else:
|
||||
self.bt_icon.set_from_file(
|
||||
get_relative_path("../../../config/assets/icons/applets/bluetooth-clear.svg")
|
||||
get_relative_path(
|
||||
"../../../config/assets/icons/applets/bluetooth-clear.svg"
|
||||
)
|
||||
)
|
||||
tooltip = "No devices connected"
|
||||
|
||||
@@ -104,7 +112,9 @@ class NetworkIndicator(Box):
|
||||
self.network_icon = Svg(
|
||||
name="network-icon",
|
||||
size=22,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/applets/wifi-clear.svg"),
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/applets/wifi-clear.svg"
|
||||
),
|
||||
)
|
||||
|
||||
self.network_button = Button(name="network-button", child=self.network_icon)
|
||||
@@ -227,7 +237,9 @@ class BatteryIndicator(Box):
|
||||
self.battery_icon = Svg(
|
||||
name="battery-icon",
|
||||
size=22,
|
||||
svg_file=get_relative_path("../../../config/assets/icons/battery/battery-100.svg"),
|
||||
svg_file=get_relative_path(
|
||||
"../../../config/assets/icons/battery/battery-100.svg"
|
||||
),
|
||||
)
|
||||
|
||||
self.battery_button = Button(name="battery-button", child=self.battery_icon)
|
||||
|
||||
@@ -7,7 +7,6 @@ from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from modules.about import About
|
||||
from utils.roam import modus_service
|
||||
from widgets.dropdown import ModusDropdown, dropdown_divider
|
||||
|
||||
@@ -4,22 +4,19 @@ from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.datetime import DateTime
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.svg import Svg
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from modules.controlcenter.main import ModusControlCenter
|
||||
from modules.panel.components.enhanced_system_tray import apply_enhanced_system_tray
|
||||
from modules.panel.components.indicators import (
|
||||
BatteryIndicator,
|
||||
BluetoothIndicator,
|
||||
NetworkIndicator,
|
||||
BatteryIndicator,
|
||||
)
|
||||
from modules.panel.components.menubar import MenuBar
|
||||
from modules.panel.components.recording_indicator import RecordingIndicator
|
||||
|
||||
from modules.controlcenter.main import ModusControlCenter
|
||||
from widgets.mousecapture import MouseCapture
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
# Apply enhanced system tray icon handling
|
||||
apply_enhanced_system_tray()
|
||||
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
import json
|
||||
|
||||
from gi.repository import Gdk
|
||||
|
||||
import config.data as data
|
||||
from fabric.hyprland.widgets import get_hyprland_connection
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
from gi.repository import Gdk
|
||||
from utils.icon_resolver import IconResolver
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
Reference in new issue
Block a user