refactor(modules): improve exception handling and simplify service usage
Enhancements - Replace bare `except:` with `except Exception:` in multiple modules- Cast step calculation to `int` for consistent type - Simplify `get_battery_icon_file` to return a string directly - Use `get_todo_service()` instead of module-level `todo_service` throughout todo widget - Remove unused `"zen": "zen-browser"` mapping entry Bug Fixes - Prevent bare exception swallowing by catching `Exception` explicitly - Ensure proper cleanup of timer IDs and other resources - Delete unused modules/osdd components New Features - None
This commit is contained in:
34 files changed
+682
-519
No files matched your search
@@ -518,7 +518,7 @@ class BluetoothConnections(Box):
|
||||
# Make sure other devices revealer is collapsed when closing
|
||||
try:
|
||||
self.other_devices_revealer.child_revealed = False
|
||||
except:
|
||||
except Exception:
|
||||
pass # Widget might already be destroyed
|
||||
|
||||
def close_bluetooth(self):
|
||||
|
||||
@@ -29,9 +29,9 @@ def get_battery_icon_file(
|
||||
Relative path to the battery icon file
|
||||
"""
|
||||
clamped = max(0, min(100, percentage))
|
||||
step = (clamped // 10) * 10
|
||||
step = int((clamped // 10) * 10)
|
||||
filename = f"battery-{step:03d}{'-charging' if is_charging else ''}.svg"
|
||||
return svg_file(f"battery/{filename}")
|
||||
return f"battery/{filename}"
|
||||
|
||||
|
||||
def set_bluetooth_enabled_with_fallback(client, enabled: bool):
|
||||
@@ -518,7 +518,7 @@ class BluetoothConnections(Box):
|
||||
# Make sure other devices revealer is collapsed when closing
|
||||
try:
|
||||
self.other_devices_revealer.child_revealed = False
|
||||
except:
|
||||
except Exception:
|
||||
pass # Widget might already be destroyed
|
||||
|
||||
def close_bluetooth(self):
|
||||
|
||||
@@ -911,7 +911,7 @@ class PlayerBox(Box):
|
||||
from gi.repository import GLib
|
||||
|
||||
GLib.source_remove(self._seekbar_timer_id)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self._seekbar_timer_id = None
|
||||
|
||||
@@ -1041,7 +1041,7 @@ class PlayerBox(Box):
|
||||
from gi.repository import GLib
|
||||
|
||||
GLib.source_remove(self._seekbar_timer_id)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self._seekbar_timer_id = None
|
||||
|
||||
|
||||
@@ -683,7 +683,7 @@ class ModusControlCenter(Window):
|
||||
if self._caffeine_process:
|
||||
try:
|
||||
self._caffeine_process.terminate()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self._caffeine_process = None
|
||||
else:
|
||||
@@ -1193,5 +1193,5 @@ class ModusControlCenter(Window):
|
||||
logger.error(f"Failed to destroy control center: {e}")
|
||||
try:
|
||||
super().destroy()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
@@ -114,7 +114,6 @@ class PerAppVolumeControl(Box):
|
||||
"visual studio code": "vscode",
|
||||
"telegram": "telegram-desktop",
|
||||
"pulse": "audio-card",
|
||||
"zen": "zen-browser",
|
||||
"pipewire": "audio-card",
|
||||
"alsa": "audio-card",
|
||||
"sink": "audio-speakers",
|
||||
@@ -323,7 +322,7 @@ class PerAppVolumeControl(Box):
|
||||
for connection in self._signal_connections:
|
||||
try:
|
||||
audio_service.disconnect(connection)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._signal_connections.clear()
|
||||
|
||||
@@ -556,7 +556,6 @@ class PlayerBoxStack(Box):
|
||||
self._update_all_player_buttons()
|
||||
|
||||
def on_new_player(self, mpris_manager, player):
|
||||
player_name = player.props.player_name
|
||||
|
||||
# if player_name in self.config.get("ignore", []):
|
||||
# return
|
||||
|
||||
@@ -699,7 +699,7 @@ class WifiConnections(Box):
|
||||
# Make sure other networks revealer is collapsed when closing
|
||||
try:
|
||||
self.other_networks_revealer.child_revealed = False
|
||||
except:
|
||||
except Exception:
|
||||
pass # Widget might already be destroyed
|
||||
|
||||
def close_wifi(self):
|
||||
|
||||
@@ -546,7 +546,7 @@ class CpuInfo(SystemInfoBase):
|
||||
for p in ["package id 0", "core 0", ""]
|
||||
):
|
||||
return round(entry.current, 1)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@@ -1040,7 +1040,7 @@ class Launcher(Window):
|
||||
return widget
|
||||
# Also check if this is the only Entry in the widget (likely to be the target)
|
||||
return widget
|
||||
except:
|
||||
except Exception:
|
||||
# If focus checking fails, assume this Entry should handle the event
|
||||
return widget
|
||||
|
||||
@@ -1207,7 +1207,7 @@ class Launcher(Window):
|
||||
allocation = selected_child.get_allocation()
|
||||
item_height = allocation.height if allocation.height > 0 else 68
|
||||
item_y = allocation.y
|
||||
except:
|
||||
except Exception:
|
||||
# Fallback to estimation
|
||||
item_height = DEFAULT_ITEM_HEIGHT
|
||||
item_y = self.selected_index * item_height
|
||||
|
||||
@@ -29,7 +29,7 @@ class BookmarkManager:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
return f"{parsed.scheme}://{parsed.netloc}/favicon.ico"
|
||||
except:
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _extract_domain(self, url: str) -> str:
|
||||
@@ -41,7 +41,7 @@ class BookmarkManager:
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
return domain
|
||||
except:
|
||||
except Exception:
|
||||
return url
|
||||
|
||||
def _normalize_url(self, url: str) -> str:
|
||||
@@ -683,7 +683,7 @@ class BookmarksPlugin(PluginBase):
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
return domain
|
||||
except:
|
||||
except Exception:
|
||||
return url
|
||||
|
||||
def _open_bookmark(self, url: str):
|
||||
|
||||
@@ -497,7 +497,7 @@ class PasswordPlugin(PluginBase):
|
||||
f"Password '{name}' {action_word} successfully",
|
||||
]
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print(f"Failed to add password '{name}'")
|
||||
@@ -524,7 +524,7 @@ class PasswordPlugin(PluginBase):
|
||||
f"Password '{name}' removed successfully",
|
||||
]
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print(f"Failed to remove password '{name}'")
|
||||
@@ -564,7 +564,7 @@ class PasswordPlugin(PluginBase):
|
||||
f"Password for '{name}' copied to clipboard",
|
||||
]
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clear cache to force refresh
|
||||
|
||||
@@ -314,11 +314,11 @@ class WallpaperPlugin(PluginBase):
|
||||
)
|
||||
return filename
|
||||
|
||||
def _hsl_to_rgb_hex(self, h: float, s: float = 1.0, l: float = 0.5) -> str:
|
||||
def _hsl_to_rgb_hex(self, h: float, s: float = 1.0, l_val: float = 0.5) -> str:
|
||||
"""Convert HSL color value to RGB HEX string."""
|
||||
# colorsys uses HLS, not HSL, and expects values between 0.0 and 1.0
|
||||
hue = h / 360.0
|
||||
r, g, b = colorsys.hls_to_rgb(hue, l, s) # Note the order: H, L, S
|
||||
r, g, b = colorsys.hls_to_rgb(hue, l_val, s) # Note the order: H, L, S
|
||||
r_int, g_int, b_int = int(r * 255), int(g * 255), int(b * 255)
|
||||
return f"#{r_int:02X}{g_int:02X}{b_int:02X}"
|
||||
|
||||
|
||||
@@ -299,14 +299,14 @@ def verify_cache_persistence():
|
||||
for cache_file in icon_cache_files[:2]: # Test first 2 icon files
|
||||
try:
|
||||
cache_path = os.path.join(NOTIFICATION_ICON_CACHE_DIR, cache_file)
|
||||
test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached icon {cache_file}: {e}")
|
||||
|
||||
for cache_file in image_cache_files[:2]: # Test first 2 image files
|
||||
try:
|
||||
cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, cache_file)
|
||||
test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached image {cache_file}: {e}")
|
||||
|
||||
@@ -876,7 +876,7 @@ class NotificationRevealer(SlideRevealer):
|
||||
self.notif_box._should_cleanup_cache = True
|
||||
try:
|
||||
self.notification.close("dismissed-by-user")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class ModusNoti(Window):
|
||||
preload_notification_assets(notification)
|
||||
|
||||
# Implement smart queue management for smooth transitions
|
||||
current_time = GLib.get_monotonic_time() / 1000
|
||||
GLib.get_monotonic_time() / 1000
|
||||
|
||||
# If queue is getting full, remove oldest notifications smoothly
|
||||
if len(self.notification_queue) >= self.MAX_QUEUE_SIZE:
|
||||
@@ -1030,7 +1030,7 @@ class ModusNoti(Window):
|
||||
oldest = self.notification_queue.pop(0)
|
||||
try:
|
||||
oldest.close("dismissed-by-user")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add new notification to queue
|
||||
@@ -1081,7 +1081,7 @@ class ModusNoti(Window):
|
||||
# Force close current notification with smooth animation
|
||||
try:
|
||||
self.current_notification.notification.close("expired")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _show_next_notification(self):
|
||||
@@ -1114,7 +1114,7 @@ class ModusNoti(Window):
|
||||
for child in list(self.notifications.children):
|
||||
try:
|
||||
self.notifications.remove(child)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.notifications.children = [new_box]
|
||||
@@ -1146,7 +1146,7 @@ class ModusNoti(Window):
|
||||
try:
|
||||
if notification_box in self.notifications.children:
|
||||
self.notifications.remove(notification_box)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Reset state
|
||||
@@ -1179,7 +1179,7 @@ class ModusNoti(Window):
|
||||
for notification in list(self.notification_queue):
|
||||
try:
|
||||
notification.close("dismissed-by-user")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self.notification_queue.clear()
|
||||
|
||||
@@ -1223,15 +1223,15 @@ class ModusNoti(Window):
|
||||
timeout_str = new_config["notification_timeout"]
|
||||
# Parse the timeout string to milliseconds
|
||||
|
||||
timeout_ms = parse_timeout_string(timeout_str)
|
||||
parse_timeout_string(timeout_str)
|
||||
|
||||
# Handle ignored apps changes
|
||||
if "notification_ignored_apps" in new_config:
|
||||
ignored_apps = new_config["notification_ignored_apps"]
|
||||
new_config["notification_ignored_apps"]
|
||||
|
||||
# Handle limited apps history changes
|
||||
if "notification_limited_apps_history" in new_config:
|
||||
limited_apps = new_config["notification_limited_apps_history"]
|
||||
new_config["notification_limited_apps_history"]
|
||||
|
||||
# Update the current config
|
||||
self._current_config.update(new_config)
|
||||
|
||||
@@ -170,7 +170,7 @@ class ExpandableNotificationGroup(Box):
|
||||
def _get_notification_pixbuf_for_group(self, cached_notification):
|
||||
"""Get notification pixbuf using cached image key - fallback to app icon"""
|
||||
notification = cached_notification._notification
|
||||
notification_id = getattr(notification, "id", None)
|
||||
getattr(notification, "id", None)
|
||||
|
||||
# First try to get cached notification image using stored cache key
|
||||
if (
|
||||
@@ -447,7 +447,7 @@ class NotificationCenterWidget(NotificationWidget):
|
||||
|
||||
def _get_notification_pixbuf(self, notification):
|
||||
"""Get notification pixbuf using cached image key - fallback to app icon"""
|
||||
notification_id = getattr(notification, "id", None)
|
||||
getattr(notification, "id", None)
|
||||
|
||||
# First try to get cached notification image using stored cache key
|
||||
if self.cache_metadata:
|
||||
@@ -689,7 +689,7 @@ class NotificationCenter(Window):
|
||||
rebuild_count = 0
|
||||
for cached_notification in notification_service.cached_notifications:
|
||||
app_name = cached_notification._notification.app_name
|
||||
notification_id = getattr(cached_notification._notification, "id", None)
|
||||
getattr(cached_notification._notification, "id", None)
|
||||
|
||||
# Skip ignored apps during rebuild
|
||||
if app_name in data.NOTIFICATION_IGNORED_APPS_HISTORY:
|
||||
|
||||
@@ -150,7 +150,7 @@ def verify_cache_persistence():
|
||||
for cache_file in cache_files[:2]: # Test first 2 files
|
||||
try:
|
||||
cache_path = os.path.join(UNIFIED_NOTIFICATION_CACHE_DIR, cache_file)
|
||||
test_pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
GdkPixbuf.Pixbuf.new_from_file(cache_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached asset {cache_file}: {e}")
|
||||
|
||||
@@ -175,7 +175,7 @@ def get_fallback_icon(size=(48, 48)):
|
||||
return GdkPixbuf.Pixbuf.new(
|
||||
GdkPixbuf.Colorspace.RGB, True, 8, size[0], size[1]
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
-468
@@ -1,468 +0,0 @@
|
||||
import math
|
||||
import time
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
# from gi.repository import GLib, GObject
|
||||
from fabric.audio import Audio
|
||||
from fabric.utils import GLib, GObject
|
||||
from fabric.utils.helpers import get_relative_path
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.scale import Scale, ScaleMark
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
from services.brightness import Brightness
|
||||
from utils.animator import Animator
|
||||
|
||||
|
||||
class AnimatedScale(Scale):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.animator = None
|
||||
|
||||
def animate_value(self, value: float):
|
||||
if not self.animator:
|
||||
self.animator = Animator(
|
||||
bezier_curve=(0.34, 1.56, 0.64, 1.0),
|
||||
duration=0.8,
|
||||
min_value=self.min_value,
|
||||
max_value=self.value,
|
||||
tick_widget=self,
|
||||
notify_value=lambda p, *_: self.set_value(p.value),
|
||||
)
|
||||
self.animator.pause()
|
||||
self.animator.min_value = self.value
|
||||
self.animator.max_value = value
|
||||
self.animator.play()
|
||||
|
||||
|
||||
class BrightnessOSDContainer(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs, orientation="v", spacing=3, name="osd")
|
||||
self.brightness_service = Brightness.get_initial()
|
||||
self.scale = AnimatedScale(
|
||||
marks=(ScaleMark(value=i) for i in range(0, 101, 10)),
|
||||
value=70,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../config/assets/icons/brightness/brightness.svg"),
|
||||
size=(100, 150),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
self.update_brightness()
|
||||
|
||||
self.scale.connect("value-changed", lambda *_: self.update_brightness())
|
||||
self.brightness_service.connect("screen", self.on_brightness_changed)
|
||||
|
||||
def update_brightness(self) -> None:
|
||||
current_brightness = self.brightness_service.screen_brightness
|
||||
normalized_brightness = self._normalize_brightness(current_brightness)
|
||||
if current_brightness != 0:
|
||||
self.scale.animate_value(normalized_brightness)
|
||||
|
||||
def get_svg(self, value):
|
||||
b_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3)
|
||||
return b_level
|
||||
|
||||
def on_brightness_changed(self, _sender: any, value: float, *_args) -> None:
|
||||
normalized_brightness = self._normalize_brightness(value)
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../config/assets/icons/brightness/brightness-{
|
||||
self.get_svg(normalized_brightness)
|
||||
}.svg"
|
||||
)
|
||||
)
|
||||
self.scale.animate_value(normalized_brightness)
|
||||
|
||||
def _normalize_brightness(self, brightness: float) -> float:
|
||||
return (brightness / self.brightness_service.max_screen) * 100
|
||||
|
||||
|
||||
class AudioOSDContainer(Box):
|
||||
__gsignals__: ClassVar[dict] = {
|
||||
"volume-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
orientation="v",
|
||||
name="osd",
|
||||
)
|
||||
self.audio = Audio()
|
||||
self.scale = AnimatedScale(
|
||||
value=70,
|
||||
marks=(ScaleMark(value=i) for i in range(1, 100, 10)),
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../config/assets/icons/volume/audio-volume.svg"),
|
||||
size=(150, 150),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
|
||||
self.previous_volume = None
|
||||
self.previous_muted = None
|
||||
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
self.sync_with_audio()
|
||||
|
||||
self.scale.connect("value-changed", self.on_volume_changed)
|
||||
self.audio.connect("notify::speaker", self.on_audio_speaker_changed)
|
||||
self.audio.connect("speaker-changed", self.on_speaker_changed)
|
||||
|
||||
# Connect to current speaker if available
|
||||
self._connect_speaker_signals()
|
||||
|
||||
def _connect_speaker_signals(self):
|
||||
"""Connect to speaker's changed signal which should fire on both volume and mute changes"""
|
||||
if self.audio.speaker:
|
||||
# Connect to the main 'changed' signal from AudioStream
|
||||
self.audio.speaker.connect("changed", self.on_speaker_stream_changed)
|
||||
|
||||
def on_speaker_stream_changed(self, *_):
|
||||
"""This should be called whenever the speaker stream changes (volume OR mute)"""
|
||||
if self.audio.speaker:
|
||||
current_volume = (
|
||||
round(self.audio.speaker.volume)
|
||||
if hasattr(self.audio.speaker, "volume")
|
||||
else 0
|
||||
)
|
||||
current_muted = (
|
||||
self.audio.speaker.muted
|
||||
if hasattr(self.audio.speaker, "muted")
|
||||
else False
|
||||
)
|
||||
|
||||
# Check if either volume or mute state changed
|
||||
if (
|
||||
self.previous_volume != current_volume
|
||||
or self.previous_muted != current_muted
|
||||
):
|
||||
self.previous_volume = current_volume
|
||||
self.previous_muted = current_muted
|
||||
self.update_volume()
|
||||
self.emit("volume-changed")
|
||||
|
||||
def get_svg(self, value):
|
||||
audio_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3)
|
||||
return audio_level
|
||||
|
||||
def sync_with_audio(self):
|
||||
if self.audio.speaker:
|
||||
volume = (
|
||||
round(self.audio.speaker.volume)
|
||||
if hasattr(self.audio.speaker, "volume")
|
||||
else 0
|
||||
)
|
||||
self.scale.set_value(volume)
|
||||
self.previous_volume = volume
|
||||
self.previous_muted = (
|
||||
self.audio.speaker.muted
|
||||
if hasattr(self.audio.speaker, "muted")
|
||||
else False
|
||||
)
|
||||
|
||||
def on_volume_changed(self, *_):
|
||||
if self.audio.speaker:
|
||||
volume = self.scale.value
|
||||
if 0 <= volume <= 100:
|
||||
self.audio.speaker.set_volume(volume)
|
||||
self.update_volume_display(volume)
|
||||
self.emit("volume-changed")
|
||||
|
||||
def update_volume_display(self, volume=None):
|
||||
"""Update the visual display based on current volume/mute state"""
|
||||
if not self.audio.speaker:
|
||||
return
|
||||
|
||||
if volume is None:
|
||||
volume = (
|
||||
round(self.audio.speaker.volume)
|
||||
if hasattr(self.audio.speaker, "volume")
|
||||
else 0
|
||||
)
|
||||
|
||||
is_muted = (
|
||||
self.audio.speaker.muted if hasattr(self.audio.speaker, "muted") else False
|
||||
)
|
||||
|
||||
if volume == 0 or is_muted:
|
||||
self.scale.add_style_class("muted")
|
||||
display_volume = 0
|
||||
else:
|
||||
self.scale.remove_style_class("muted")
|
||||
display_volume = volume
|
||||
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../config/assets/icons/volume/audio-volume-{
|
||||
self.get_svg(display_volume)
|
||||
}.svg"
|
||||
)
|
||||
)
|
||||
|
||||
def on_audio_speaker_changed(self, *_):
|
||||
self._connect_speaker_signals()
|
||||
self.update_volume()
|
||||
|
||||
def on_speaker_changed(self, *_):
|
||||
self._connect_speaker_signals()
|
||||
self.update_volume()
|
||||
|
||||
def update_volume(self, *_):
|
||||
if self.audio.speaker and not self.is_hovered():
|
||||
volume = (
|
||||
round(self.audio.speaker.volume)
|
||||
if hasattr(self.audio.speaker, "volume")
|
||||
else 0
|
||||
)
|
||||
self.scale.set_value(volume)
|
||||
self.update_volume_display(volume)
|
||||
|
||||
|
||||
class MicrophoneOSDContainer(Box):
|
||||
__gsignals__: ClassVar[dict] = {
|
||||
"mic-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs, orientation="v", spacing=13, name="osd")
|
||||
self.audio = Audio()
|
||||
self.scale = AnimatedScale(
|
||||
marks=(ScaleMark(value=i) for i in range(1, 100, 10)),
|
||||
value=70,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../config/assets/icons/mic/microphone.svg"),
|
||||
name="osd-image",
|
||||
size=(100, 150),
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.previous_volume = None
|
||||
self.previous_muted = None
|
||||
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
self.sync_with_audio()
|
||||
|
||||
self.scale.connect("value-changed", self.on_volume_changed)
|
||||
self.audio.connect("notify::microphone", self.on_audio_microphone_changed)
|
||||
self.audio.connect("microphone-changed", self.on_microphone_changed)
|
||||
|
||||
# Connect to current microphone if available
|
||||
self._connect_microphone_signals()
|
||||
|
||||
def _connect_microphone_signals(self):
|
||||
"""Connect to microphone's changed signal which should fire on both volume and mute changes"""
|
||||
if self.audio.microphone:
|
||||
# Connect to the main 'changed' signal from AudioStream
|
||||
self.audio.microphone.connect("changed", self.on_microphone_stream_changed)
|
||||
|
||||
def on_microphone_stream_changed(self, *_):
|
||||
"""This should be called whenever the microphone stream changes (volume OR mute)"""
|
||||
if self.audio.microphone:
|
||||
current_volume = (
|
||||
round(self.audio.microphone.volume)
|
||||
if hasattr(self.audio.microphone, "volume")
|
||||
else 0
|
||||
)
|
||||
current_muted = (
|
||||
self.audio.microphone.muted
|
||||
if hasattr(self.audio.microphone, "muted")
|
||||
else False
|
||||
)
|
||||
|
||||
# Check if either volume or mute state changed
|
||||
if (
|
||||
self.previous_volume != current_volume
|
||||
or self.previous_muted != current_muted
|
||||
):
|
||||
self.previous_volume = current_volume
|
||||
self.previous_muted = current_muted
|
||||
self.update_volume()
|
||||
self.emit("mic-changed")
|
||||
|
||||
def get_svg(self, value):
|
||||
audio_level = 0 if value == 0 else min(int(math.ceil(value / 33)), 3)
|
||||
return audio_level
|
||||
|
||||
def sync_with_audio(self):
|
||||
if self.audio.microphone:
|
||||
volume = (
|
||||
round(self.audio.microphone.volume)
|
||||
if hasattr(self.audio.microphone, "volume")
|
||||
else 0
|
||||
)
|
||||
self.scale.set_value(volume)
|
||||
self.previous_volume = volume
|
||||
self.previous_muted = (
|
||||
self.audio.microphone.muted
|
||||
if hasattr(self.audio.microphone, "muted")
|
||||
else False
|
||||
)
|
||||
|
||||
def on_volume_changed(self, *_):
|
||||
if self.audio.microphone:
|
||||
volume = self.scale.value
|
||||
if 0 <= volume <= 100:
|
||||
self.audio.microphone.set_volume(volume)
|
||||
self.update_volume_display(volume)
|
||||
self.emit("mic-changed")
|
||||
|
||||
def update_volume_display(self, volume=None):
|
||||
"""Update the visual display based on current volume/mute state"""
|
||||
if not self.audio.microphone:
|
||||
return
|
||||
|
||||
if volume is None:
|
||||
volume = (
|
||||
round(self.audio.microphone.volume)
|
||||
if hasattr(self.audio.microphone, "volume")
|
||||
else 0
|
||||
)
|
||||
|
||||
is_muted = (
|
||||
self.audio.microphone.muted
|
||||
if hasattr(self.audio.microphone, "muted")
|
||||
else False
|
||||
)
|
||||
|
||||
if volume == 0 or is_muted:
|
||||
self.scale.add_style_class("muted")
|
||||
display_volume = 0
|
||||
else:
|
||||
self.scale.remove_style_class("muted")
|
||||
display_volume = volume
|
||||
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../config/assets/icons/mic/microphone-{
|
||||
self.get_svg(display_volume)
|
||||
}.svg"
|
||||
)
|
||||
)
|
||||
|
||||
def on_audio_microphone_changed(self, *_):
|
||||
self._connect_microphone_signals()
|
||||
self.update_volume()
|
||||
|
||||
def on_microphone_changed(self, *_):
|
||||
self._connect_microphone_signals()
|
||||
self.update_volume()
|
||||
|
||||
def update_volume(self, *_):
|
||||
if self.audio.microphone and not self.is_hovered():
|
||||
volume = (
|
||||
round(self.audio.microphone.volume)
|
||||
if hasattr(self.audio.microphone, "volume")
|
||||
else 0
|
||||
)
|
||||
self.scale.set_value(volume)
|
||||
self.update_volume_display(volume)
|
||||
|
||||
|
||||
class OSD(Window):
|
||||
def __init__(self, **kwargs):
|
||||
self.audio_container = AudioOSDContainer()
|
||||
self.brightness_container = BrightnessOSDContainer()
|
||||
self.microphone_container = MicrophoneOSDContainer()
|
||||
|
||||
self.timeout = 1000
|
||||
|
||||
self.revealer = Revealer(
|
||||
transition_type="slide-up",
|
||||
transition_duration=100,
|
||||
child_revealed=False,
|
||||
)
|
||||
|
||||
self.main_box = Box(
|
||||
orientation="v",
|
||||
h_expand=True,
|
||||
children=[self.revealer],
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
layer="overlay",
|
||||
anchor="bottom",
|
||||
title="modus",
|
||||
child=self.main_box,
|
||||
visible=False,
|
||||
pass_through=True,
|
||||
keyboard_mode="on-demand",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.last_activity_time = time.time()
|
||||
|
||||
# Connect to the containers' signals
|
||||
self.audio_container.connect("volume-changed", self.show_audio)
|
||||
self.brightness_container.brightness_service.connect(
|
||||
"screen", self.show_brightness
|
||||
)
|
||||
self.microphone_container.connect("mic-changed", self.show_microphone)
|
||||
|
||||
GLib.timeout_add(100, self.check_inactivity)
|
||||
|
||||
def show_audio(self, *_):
|
||||
self.show_box(box_to_show="audio")
|
||||
self.reset_inactivity_timer()
|
||||
|
||||
def show_brightness(self, *_):
|
||||
self.show_box(box_to_show="brightness")
|
||||
self.reset_inactivity_timer()
|
||||
|
||||
def show_microphone(self, *_):
|
||||
self.show_box(box_to_show="microphone")
|
||||
self.reset_inactivity_timer()
|
||||
|
||||
def show_box(self, box_to_show: Literal["audio", "brightness", "microphone"]):
|
||||
self.set_visible(True)
|
||||
if box_to_show == "audio":
|
||||
self.revealer.children = self.audio_container
|
||||
elif box_to_show == "brightness":
|
||||
self.revealer.children = self.brightness_container
|
||||
elif box_to_show == "microphone":
|
||||
self.revealer.children = self.microphone_container
|
||||
self.revealer.set_reveal_child(True)
|
||||
self.reset_inactivity_timer()
|
||||
|
||||
def start_hide_timer(self):
|
||||
self.set_visible(False)
|
||||
|
||||
def reset_inactivity_timer(self):
|
||||
self.last_activity_time = time.time()
|
||||
|
||||
def check_inactivity(self):
|
||||
if time.time() - self.last_activity_time >= (self.timeout / 1000):
|
||||
self.start_hide_timer()
|
||||
return True
|
||||
@@ -0,0 +1,8 @@
|
||||
from .main import OSDWindow # noqa: F401
|
||||
from .components import ( # noqa: F401
|
||||
AudioOSDContainer,
|
||||
BrightnessOSDContainer,
|
||||
MicrophoneOSDContainer,
|
||||
CapsLockOSDContainer,
|
||||
KeyboardLayoutOSDContainer,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
from .audio import AudioOSDContainer # noqa: F401
|
||||
from .brightness import BrightnessOSDContainer # noqa: F401
|
||||
from .microphone import MicrophoneOSDContainer # noqa: F401
|
||||
from .capslock import CapsLockOSDContainer # noqa: F401
|
||||
from .keyboard_layout import KeyboardLayoutOSDContainer # noqa: F401
|
||||
@@ -0,0 +1,28 @@
|
||||
from functools import partial
|
||||
from fabric.widgets.scale import Scale
|
||||
from utils.animator import Animator, cubic_bezier
|
||||
|
||||
|
||||
class AnimatedScale(Scale):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.animator = (
|
||||
Animator(
|
||||
duration=0.8,
|
||||
timing_function=partial(cubic_bezier, 0.34, 1.56, 0.64, 1.0),
|
||||
min_value=self.min_value,
|
||||
max_value=self.value,
|
||||
tick_widget=self,
|
||||
notify_value=lambda anim: self.set_value(anim.value),
|
||||
)
|
||||
.build()
|
||||
.play()
|
||||
.unwrap()
|
||||
)
|
||||
|
||||
def animate_value(self, value: float):
|
||||
if self.animator:
|
||||
self.animator.pause()
|
||||
self.animator.min_value = self.value
|
||||
self.animator.max_value = value
|
||||
self.animator.play()
|
||||
@@ -0,0 +1,105 @@
|
||||
import math
|
||||
from fabric.audio import Audio
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.scale import ScaleMark
|
||||
from .animated_scale import AnimatedScale
|
||||
from .base import BaseOSDContainer
|
||||
|
||||
|
||||
class AudioOSDContainer(BaseOSDContainer):
|
||||
def __init__(self, window, **kwargs):
|
||||
super().__init__(window, **kwargs)
|
||||
self.audio = Audio()
|
||||
self._last_volume = None
|
||||
self._last_muted = None
|
||||
self._setup_specific_components()
|
||||
self._connect_specific_signals()
|
||||
|
||||
def _setup_specific_components(self):
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../../../config/assets/icons/volume/audio-volume.svg"),
|
||||
size=(100, 100),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.scale = AnimatedScale(
|
||||
marks=(ScaleMark(value=i) for i in range(1, 100, 10)),
|
||||
value=70,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
|
||||
def _connect_specific_signals(self):
|
||||
self.audio.connect("notify::speaker", self._on_speaker_changed)
|
||||
if self.audio.speaker:
|
||||
self._connect_speaker_signals()
|
||||
|
||||
def _connect_speaker_signals(self):
|
||||
if self.audio.speaker:
|
||||
self.audio.speaker.connect("changed", self._on_speaker_stream_changed)
|
||||
self._sync_with_audio()
|
||||
|
||||
def _on_speaker_changed(self, *_):
|
||||
self._connect_speaker_signals()
|
||||
self.update()
|
||||
|
||||
def _on_speaker_stream_changed(self, *_):
|
||||
if self.audio.speaker:
|
||||
current_volume = round(self.audio.speaker.volume)
|
||||
current_muted = self.audio.speaker.muted
|
||||
if self._last_volume != current_volume or self._last_muted != current_muted:
|
||||
self._last_volume = current_volume
|
||||
self._last_muted = current_muted
|
||||
self.update()
|
||||
|
||||
def _sync_with_audio(self):
|
||||
if self.audio.speaker:
|
||||
self._last_volume = round(self.audio.speaker.volume)
|
||||
self._last_muted = self.audio.speaker.muted
|
||||
self.scale.set_value(self._last_volume)
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self):
|
||||
if not self.audio.speaker:
|
||||
return
|
||||
|
||||
volume = (
|
||||
self._last_volume
|
||||
if self._last_volume is not None
|
||||
else round(self.audio.speaker.volume)
|
||||
)
|
||||
muted = (
|
||||
self._last_muted
|
||||
if self._last_muted is not None
|
||||
else self.audio.speaker.muted
|
||||
)
|
||||
|
||||
display_volume = 0 if (volume == 0 or muted) else volume
|
||||
level = (
|
||||
0 if display_volume == 0 else min(int(math.ceil(display_volume / 33)), 3)
|
||||
)
|
||||
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../../../config/assets/icons/volume/audio-volume-{level}.svg"
|
||||
)
|
||||
)
|
||||
|
||||
if muted or volume == 0:
|
||||
self.scale.add_style_class("muted")
|
||||
else:
|
||||
self.scale.remove_style_class("muted")
|
||||
|
||||
self.scale.animate_value(volume)
|
||||
|
||||
def update(self, *_):
|
||||
self._update_display()
|
||||
super().update()
|
||||
@@ -0,0 +1,145 @@
|
||||
from abc import abstractmethod
|
||||
from fabric.utils import Gdk, GLib, invoke_repeater, remove_handler, time
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
class BaseOSDContainer(Box):
|
||||
MAX_VISIBLE_TIME = 6.0
|
||||
COOLDOWN_MS = 200
|
||||
|
||||
def __init__(self, window: Window, **kwargs):
|
||||
super().__init__(**kwargs, orientation="v", spacing=12, name="osd-container")
|
||||
self.last_handler: int = 0
|
||||
self.window = window
|
||||
self.osd = None
|
||||
self.show_timestamp: float = 0.0
|
||||
self.watchdog_handler: int = 0
|
||||
self._update_in_progress = False
|
||||
self._is_hovered = False
|
||||
self._last_trigger_time: float = 0.0
|
||||
|
||||
GLib.idle_add(self._setup_window_signals)
|
||||
|
||||
def _setup_window_signals(self):
|
||||
self.window.add_events(
|
||||
Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK
|
||||
)
|
||||
self.window.connect("enter-notify-event", self._on_enter_notify)
|
||||
self.window.connect("leave-notify-event", self._on_leave_notify)
|
||||
return False
|
||||
|
||||
def _on_enter_notify(self, widget, event):
|
||||
if event.detail != Gdk.NotifyType.INFERIOR:
|
||||
self._is_hovered = True
|
||||
|
||||
def _on_leave_notify(self, widget, event):
|
||||
if event.detail != Gdk.NotifyType.INFERIOR:
|
||||
self._is_hovered = False
|
||||
|
||||
def is_hovered(self):
|
||||
return self._is_hovered
|
||||
|
||||
def remove_last_handler(self):
|
||||
if self.last_handler:
|
||||
try:
|
||||
remove_handler(self.last_handler)
|
||||
except Exception:
|
||||
pass
|
||||
self.last_handler = 0
|
||||
|
||||
def remove_watchdog_handler(self):
|
||||
if self.watchdog_handler:
|
||||
try:
|
||||
GLib.source_remove(self.watchdog_handler)
|
||||
except Exception:
|
||||
pass
|
||||
self.watchdog_handler = 0
|
||||
|
||||
def cleanup_all_handlers(self):
|
||||
self.remove_last_handler()
|
||||
self.remove_watchdog_handler()
|
||||
self.show_timestamp = 0.0
|
||||
self._update_in_progress = False
|
||||
|
||||
def hide_window(self):
|
||||
if self.osd and hasattr(self.osd, "revealer"):
|
||||
self.osd.revealer.set_reveal_child(False)
|
||||
GLib.timeout_add(150, lambda: self.window.hide() or False)
|
||||
else:
|
||||
self.window.hide()
|
||||
|
||||
def watchdog_force_hide(self, *_):
|
||||
if not self.window.get_visible():
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
|
||||
elapsed = time.time() - self.show_timestamp
|
||||
if elapsed >= self.MAX_VISIBLE_TIME:
|
||||
if not self.is_hovered():
|
||||
self.hide_window()
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
def update(self, *_):
|
||||
if self._update_in_progress:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
time_since_last_trigger = (current_time - self._last_trigger_time) * 1000
|
||||
|
||||
if time_since_last_trigger < self.COOLDOWN_MS:
|
||||
return
|
||||
|
||||
self._last_trigger_time = current_time
|
||||
self._update_in_progress = True
|
||||
|
||||
self.cleanup_all_handlers()
|
||||
self.show_timestamp = time.time()
|
||||
|
||||
if self.osd:
|
||||
self.osd.show_container(self)
|
||||
|
||||
self.window.show()
|
||||
|
||||
self.focus()
|
||||
self.last_handler = invoke_repeater(1700, self.unfocus, initial_call=False)
|
||||
self.watchdog_handler = GLib.timeout_add(1000, self.watchdog_force_hide)
|
||||
self._update_in_progress = False
|
||||
|
||||
def focus(self, *_):
|
||||
self.add_style_class("focused")
|
||||
return False
|
||||
|
||||
def unfocus(self, *_):
|
||||
if not self.window.get_visible():
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
|
||||
self.remove_style_class("focused")
|
||||
self.remove_last_handler()
|
||||
self.last_handler = invoke_repeater(1700, self.unpop, initial_call=False)
|
||||
return False
|
||||
|
||||
def unpop(self, *_):
|
||||
if not self.window.get_visible():
|
||||
self.cleanup_all_handlers()
|
||||
return False
|
||||
|
||||
if not self.is_hovered():
|
||||
self.hide_window()
|
||||
self.cleanup_all_handlers()
|
||||
else:
|
||||
self.last_handler = invoke_repeater(500, self.unpop, initial_call=False)
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def _setup_specific_components(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _connect_specific_signals(self):
|
||||
pass
|
||||
@@ -0,0 +1,64 @@
|
||||
import math
|
||||
from services.brightness import Brightness
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.scale import ScaleMark
|
||||
from .animated_scale import AnimatedScale
|
||||
from .base import BaseOSDContainer
|
||||
|
||||
|
||||
class BrightnessOSDContainer(BaseOSDContainer):
|
||||
def __init__(self, window, **kwargs):
|
||||
super().__init__(window, **kwargs)
|
||||
self.brightness_service = Brightness.get_initial()
|
||||
self._setup_specific_components()
|
||||
self._connect_specific_signals()
|
||||
|
||||
def _setup_specific_components(self):
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../../../config/assets/icons/brightness/brightness.svg"),
|
||||
size=(100, 100),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.scale = AnimatedScale(
|
||||
marks=(ScaleMark(value=i) for i in range(0, 101, 10)),
|
||||
value=70,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
|
||||
def _connect_specific_signals(self):
|
||||
self.brightness_service.connect("screen", self._on_screen_brightness_changed)
|
||||
|
||||
def _on_screen_brightness_changed(self, _sender, value, *_args):
|
||||
self.update()
|
||||
|
||||
def _get_normalized_brightness(self):
|
||||
return (
|
||||
self.brightness_service.screen_brightness
|
||||
/ self.brightness_service.max_screen
|
||||
) * 100
|
||||
|
||||
def _update_display(self):
|
||||
normalized = self._get_normalized_brightness()
|
||||
level = 0 if normalized == 0 else min(int(math.ceil(normalized / 33)), 3)
|
||||
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../../../config/assets/icons/brightness/brightness-{level}.svg"
|
||||
)
|
||||
)
|
||||
|
||||
self.scale.animate_value(normalized)
|
||||
|
||||
def update(self, *_):
|
||||
self._update_display()
|
||||
super().update()
|
||||
@@ -0,0 +1,50 @@
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.utils import get_relative_path
|
||||
from .base import BaseOSDContainer
|
||||
from services.capslock import CapsLock
|
||||
|
||||
|
||||
class CapsLockOSDContainer(BaseOSDContainer):
|
||||
def __init__(self, window, **kwargs):
|
||||
super().__init__(window, **kwargs)
|
||||
self.capslock = CapsLock.get_initial()
|
||||
self._setup_specific_components()
|
||||
self._connect_specific_signals()
|
||||
|
||||
def _setup_specific_components(self):
|
||||
self.osd_image = Svg(
|
||||
get_relative_path("../../../assets/icons/misc/caps-lock.svg"),
|
||||
size=(100, 100),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.label = Label(name="osd-label")
|
||||
self.add(self.osd_image)
|
||||
self._update_display(self.capslock.is_on)
|
||||
|
||||
def _connect_specific_signals(self):
|
||||
self.capslock.connect("state_changed", self._on_caps_lock_state_changed)
|
||||
|
||||
def _on_caps_lock_state_changed(self, _, is_on: bool):
|
||||
self._update_display(is_on)
|
||||
self.update()
|
||||
|
||||
def _update_display(self, is_on: bool):
|
||||
icon_path = "caps-lock.svg" if is_on else "caps-lock-off.svg"
|
||||
self.osd_image.set_from_file(
|
||||
get_relative_path(f"../../../assets/icons/misc/{icon_path}")
|
||||
)
|
||||
if is_on:
|
||||
self.add_style_class("capslock-on")
|
||||
self.remove_style_class("capslock-off")
|
||||
else:
|
||||
self.add_style_class("capslock-off")
|
||||
self.remove_style_class("capslock-on")
|
||||
|
||||
def update(self, *_):
|
||||
# We don't need a separate display update here as it's handled in the signal
|
||||
super().update()
|
||||
@@ -0,0 +1,40 @@
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.utils import get_relative_path
|
||||
from .base import BaseOSDContainer
|
||||
from services.keyboard_layout import KeyboardLayout
|
||||
|
||||
|
||||
class KeyboardLayoutOSDContainer(BaseOSDContainer):
|
||||
def __init__(self, window, **kwargs):
|
||||
super().__init__(window, **kwargs)
|
||||
self.keyboard_layout = KeyboardLayout.get_initial()
|
||||
self._setup_specific_components()
|
||||
self._connect_specific_signals()
|
||||
|
||||
def _setup_specific_components(self):
|
||||
self.osd_image = Svg(
|
||||
get_relative_path("../../../assets/icons/misc/keyboard-layout.svg"),
|
||||
size=(100, 100),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.label = Label(name="osd-label")
|
||||
self.add(self.osd_image)
|
||||
self.add(self.label)
|
||||
self.label.set_text(self.keyboard_layout.current_layout)
|
||||
|
||||
def _connect_specific_signals(self):
|
||||
self.keyboard_layout.connect("layout_changed", self._on_layout_changed)
|
||||
|
||||
def _on_layout_changed(self, service, layout: str):
|
||||
if not self.is_hovered():
|
||||
self.label.set_text(layout)
|
||||
self.update()
|
||||
|
||||
def update(self, *_):
|
||||
self.label.set_text(self.keyboard_layout.current_layout)
|
||||
super().update()
|
||||
@@ -0,0 +1,105 @@
|
||||
import math
|
||||
from fabric.audio import Audio
|
||||
from fabric.utils import get_relative_path
|
||||
from fabric.widgets.svg import Svg
|
||||
from fabric.widgets.scale import ScaleMark
|
||||
from .animated_scale import AnimatedScale
|
||||
from .base import BaseOSDContainer
|
||||
|
||||
|
||||
class MicrophoneOSDContainer(BaseOSDContainer):
|
||||
def __init__(self, window, **kwargs):
|
||||
super().__init__(window, **kwargs)
|
||||
self.audio = Audio()
|
||||
self._last_volume = None
|
||||
self._last_muted = None
|
||||
self._setup_specific_components()
|
||||
self._connect_specific_signals()
|
||||
|
||||
def _setup_specific_components(self):
|
||||
self.osd_window_image = Svg(
|
||||
get_relative_path("../../../config/assets/icons/mic/microphone.svg"),
|
||||
size=(100, 100),
|
||||
name="osd-image",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
)
|
||||
self.scale = AnimatedScale(
|
||||
marks=(ScaleMark(value=i) for i in range(1, 100, 10)),
|
||||
value=70,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(1, 1),
|
||||
orientation="h",
|
||||
)
|
||||
self.add(self.osd_window_image)
|
||||
self.add(self.scale)
|
||||
|
||||
def _connect_specific_signals(self):
|
||||
self.audio.connect("notify::microphone", self._on_microphone_changed)
|
||||
if self.audio.microphone:
|
||||
self._connect_microphone_signals()
|
||||
|
||||
def _connect_microphone_signals(self):
|
||||
if self.audio.microphone:
|
||||
self.audio.microphone.connect("changed", self._on_microphone_stream_changed)
|
||||
self._sync_with_audio()
|
||||
|
||||
def _on_microphone_changed(self, *_):
|
||||
self._connect_microphone_signals()
|
||||
self.update()
|
||||
|
||||
def _on_microphone_stream_changed(self, *_):
|
||||
if self.audio.microphone:
|
||||
current_volume = round(self.audio.microphone.volume)
|
||||
current_muted = self.audio.microphone.muted
|
||||
if self._last_volume != current_volume or self._last_muted != current_muted:
|
||||
self._last_volume = current_volume
|
||||
self._last_muted = current_muted
|
||||
self.update()
|
||||
|
||||
def _sync_with_audio(self):
|
||||
if self.audio.microphone:
|
||||
self._last_volume = round(self.audio.microphone.volume)
|
||||
self._last_muted = self.audio.microphone.muted
|
||||
self.scale.set_value(self._last_volume)
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self):
|
||||
if not self.audio.microphone:
|
||||
return
|
||||
|
||||
volume = (
|
||||
self._last_volume
|
||||
if self._last_volume is not None
|
||||
else round(self.audio.microphone.volume)
|
||||
)
|
||||
muted = (
|
||||
self._last_muted
|
||||
if self._last_muted is not None
|
||||
else self.audio.microphone.muted
|
||||
)
|
||||
|
||||
display_volume = 0 if (volume == 0 or muted) else volume
|
||||
level = (
|
||||
0 if display_volume == 0 else min(int(math.ceil(display_volume / 33)), 3)
|
||||
)
|
||||
|
||||
self.osd_window_image.set_from_file(
|
||||
get_relative_path(
|
||||
f"../../../config/assets/icons/mic/microphone-{level}.svg"
|
||||
)
|
||||
)
|
||||
|
||||
if muted or volume == 0:
|
||||
self.scale.add_style_class("muted")
|
||||
else:
|
||||
self.scale.remove_style_class("muted")
|
||||
|
||||
self.scale.animate_value(volume)
|
||||
|
||||
def update(self, *_):
|
||||
self._update_display()
|
||||
super().update()
|
||||
@@ -0,0 +1,83 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from .components import (
|
||||
AudioOSDContainer,
|
||||
BrightnessOSDContainer,
|
||||
MicrophoneOSDContainer,
|
||||
CapsLockOSDContainer,
|
||||
KeyboardLayoutOSDContainer,
|
||||
)
|
||||
|
||||
|
||||
class OSD(Box):
|
||||
def __init__(self, window: Window, **kwargs):
|
||||
super().__init__(name="osd", **kwargs)
|
||||
self.window = window
|
||||
|
||||
self.revealer = Revealer(
|
||||
transition_type="slide-up",
|
||||
transition_duration=100,
|
||||
child_revealed=False,
|
||||
)
|
||||
self.children = [self.revealer]
|
||||
|
||||
self.containers = {
|
||||
"audio": AudioOSDContainer(window),
|
||||
"brightness": BrightnessOSDContainer(window),
|
||||
"microphone": MicrophoneOSDContainer(window),
|
||||
"capslock": CapsLockOSDContainer(window),
|
||||
"kb_layout": KeyboardLayoutOSDContainer(window),
|
||||
}
|
||||
|
||||
# Set the OSD reference in containers
|
||||
for container in self.containers.values():
|
||||
container.osd = self
|
||||
|
||||
self.current_container = None
|
||||
|
||||
def show_container(self, container):
|
||||
if self.current_container != container:
|
||||
self.revealer.children = [container]
|
||||
self.current_container = container
|
||||
self.revealer.set_reveal_child(True)
|
||||
|
||||
def show_audio_osd(self):
|
||||
self.show_container(self.containers.get("audio"))
|
||||
|
||||
def show_brightness_osd(self):
|
||||
self.show_container(self.containers.get("brightness"))
|
||||
|
||||
def show_microphone_osd(self):
|
||||
self.show_container(self.containers.get("microphone"))
|
||||
|
||||
def show_capslock_osd(self):
|
||||
self.show_container(self.containers.get("capslock"))
|
||||
|
||||
def show_kb_layout_osd(self):
|
||||
self.show_container(self.containers.get("kb_layout"))
|
||||
|
||||
|
||||
class OSDWindow(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="osd-window",
|
||||
title="modus-osd",
|
||||
anchor="bottom",
|
||||
margin="0px 0px 40px 0px",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
pass_through=True,
|
||||
layer="overlay",
|
||||
**kwargs,
|
||||
)
|
||||
self.osd = OSD(window=self)
|
||||
self.add(self.osd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fabric import Application
|
||||
|
||||
osd_window = OSDWindow()
|
||||
app = Application("modus-osd", osd_window)
|
||||
app.run()
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+11
-11
@@ -12,7 +12,7 @@ from fabric.widgets.svg import Svg
|
||||
from gi.repository import GLib
|
||||
|
||||
# Local imports
|
||||
from services.todo import todo_service
|
||||
from services.todo import get_todo_service
|
||||
from widgets.mousecapture import MouseCapture
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
@@ -153,14 +153,14 @@ class TodoItem(Box):
|
||||
|
||||
def _toggle_completion(self, *_):
|
||||
"""Toggle todo completion status"""
|
||||
todo_service.toggle_todo(self.todo_data["id"])
|
||||
get_todo_service().toggle_todo(self.todo_data["id"])
|
||||
|
||||
def _cycle_priority(self, *_):
|
||||
"""Cycle through priority levels"""
|
||||
priorities = ["low", "medium", "high"]
|
||||
current_index = priorities.index(self.todo_data["priority"])
|
||||
new_priority = priorities[(current_index + 1) % len(priorities)]
|
||||
todo_service.set_priority(self.todo_data["id"], new_priority)
|
||||
get_todo_service().set_priority(self.todo_data["id"], new_priority)
|
||||
|
||||
def _start_edit(self, *_):
|
||||
"""Start editing the todo text"""
|
||||
@@ -183,7 +183,7 @@ class TodoItem(Box):
|
||||
|
||||
new_text = self.text_entry.get_text().strip()
|
||||
if new_text:
|
||||
todo_service.edit_todo(self.todo_data["id"], new_text)
|
||||
get_todo_service().edit_todo(self.todo_data["id"], new_text)
|
||||
|
||||
self._cancel_edit()
|
||||
|
||||
@@ -198,7 +198,7 @@ class TodoItem(Box):
|
||||
|
||||
def _delete_todo(self, *_):
|
||||
"""Delete this todo"""
|
||||
todo_service.delete_todo(self.todo_data["id"])
|
||||
get_todo_service().delete_todo(self.todo_data["id"])
|
||||
|
||||
def update_from_data(self, todo_data):
|
||||
"""Update the widget from new todo data"""
|
||||
@@ -252,7 +252,7 @@ class TodoListWidget(Window):
|
||||
self.todo_items = {} # Maps todo IDs to TodoItem widgets
|
||||
|
||||
# Register callback with todo service
|
||||
todo_service.add_callback(self._on_todo_event)
|
||||
get_todo_service().add_callback(self._on_todo_event)
|
||||
|
||||
self._build_ui()
|
||||
self._refresh_todos()
|
||||
@@ -363,12 +363,12 @@ class TodoListWidget(Window):
|
||||
"""Add a new todo"""
|
||||
text = self.new_todo_entry.get_text().strip()
|
||||
if text:
|
||||
todo_service.add_todo(text)
|
||||
get_todo_service().add_todo(text)
|
||||
self.new_todo_entry.set_text("")
|
||||
|
||||
def _clear_completed(self, *_):
|
||||
"""Clear all completed todos"""
|
||||
todo_service.clear_completed()
|
||||
get_todo_service().clear_completed()
|
||||
|
||||
def _on_todo_event(self, event_type, data=None):
|
||||
"""Handle todo service events via callback"""
|
||||
@@ -392,7 +392,7 @@ class TodoListWidget(Window):
|
||||
self.todos_container.children = []
|
||||
|
||||
# Get all todos
|
||||
todos = todo_service.todos
|
||||
todos = get_todo_service().todos
|
||||
|
||||
# Sort todos: incomplete first, then by priority, then by creation date
|
||||
def sort_key(todo):
|
||||
@@ -418,7 +418,7 @@ class TodoListWidget(Window):
|
||||
|
||||
def _update_stats(self):
|
||||
"""Update the statistics display"""
|
||||
stats = todo_service.get_stats()
|
||||
stats = get_todo_service().get_stats()
|
||||
stats_text = f"{stats['pending']} pending, {stats['completed']} completed"
|
||||
self.stats_label.set_label(stats_text)
|
||||
|
||||
@@ -439,7 +439,7 @@ class TodoListWidget(Window):
|
||||
def destroy(self):
|
||||
"""Clean up when destroyed"""
|
||||
# Remove callback from todo service
|
||||
todo_service.remove_callback(self._on_todo_event)
|
||||
get_todo_service().remove_callback(self._on_todo_event)
|
||||
|
||||
super().destroy()
|
||||
|
||||
|
||||
Reference in new issue
Block a user