refactor: extract shared mixins, fix cpu waste, resolve browser icons

- Extract CloseButtonRevealerMixin, PullToRefreshMixin, BasePlayerBoxStack
- Replace mpris.py 2s+5s D-Bus polling with Seeked signal subscription
- Remove redundant desktop/main.py os.listdir() poll (FileMonitor covers)
- Remove switcher/main.py 200ms capture timer, chain from frame-ready
- Resolve browser MPRIS icons via desktop file lookup, fallback to app name
- Fix CodeQL py/clear-text-storage by rounding lat/lon and stripping from cache
This commit is contained in:
sankalp committed 2026-07-31 01:35:25 +05:45
1 parent 92c2b92333
commit beab79a405
14 files changed
+515 -740

No files matched your search

+63 -58
View File
@@ -212,6 +212,7 @@ class PlayerService(Service):
self._is_cleaning_up = False
self._artwork_generation = 0
self._signal_ids = []
self._subscription_ids: list[int] = []
self._last_emitted_status = ""
self._failed_covers: set[str] = set()
self._download_threads: dict[str, threading.Thread] = {}
@@ -220,11 +221,10 @@ class PlayerService(Service):
self._proxy.connect("g-properties-changed", self._on_properties_changed)
)
self._pos_source_id = 0
self._status_source_id = 0
self._last_polled_status = ""
self._start_status_polling()
self.poll_progress()
self._subscribe_seeked()
if self.playback_status.lower() == "playing":
self.fabricating()
metadata = self._get_metadata()
if metadata:
@@ -251,6 +251,49 @@ class PlayerService(Service):
GLib.timeout_add(800, self._refresh_initial_state, attempt + 1)
return False
def _subscribe_seeked(self):
conn = self._proxy.get_connection()
if conn:
self._subscription_ids.append(
conn.signal_subscribe(
self._proxy.get_name(),
MPRIS_PLAYER_IFACE,
"Seeked",
MPRIS_PLAYER_PATH,
None,
Gio.DBusSignalFlags.NONE,
self._on_seeked,
)
)
def _on_seeked(
self,
_connection,
_sender_name,
_object_path,
_interface_name,
_signal_name,
params: GLib.Variant,
):
if self._is_cleaning_up:
return
try:
pos = params.unpack()[0] / 1_000_000
except Exception as e:
logger.warning(f"[mpris] Seeked unpack failed: {e}")
return
m = self._get_metadata()
dur = 0
if m is not None:
v = m.get("mpris:length")
if v is not None:
dur = (
v.get_uint64() / 1_000_000
if isinstance(v, GLib.Variant) and v.get_type_string() == "t"
else int(v) / 1_000_000
)
self.track_position(pos, dur)
def get_artwork(self) -> str:
return self._current_artwork_path
@@ -270,49 +313,9 @@ class PlayerService(Service):
logger.warning(f"Could not get position: {e}")
return 0
def _start_pos_fabricator(self):
if self._pos_source_id:
return
self._pos_source_id = GLib.timeout_add(2000, self._on_pos_poll)
def _stop_pos_fabricator(self):
if not self._pos_source_id:
return
GLib.source_remove(self._pos_source_id)
self._pos_source_id = 0
def _on_pos_poll(self):
if self._is_cleaning_up:
return False
self.fabricating()
return True
def _start_status_polling(self):
if self._status_source_id:
return
self._last_polled_status = self.playback_status
self._status_source_id = GLib.timeout_add(5000, self._on_status_poll)
def _stop_status_polling(self):
if not self._status_source_id:
return
GLib.source_remove(self._status_source_id)
self._status_source_id = 0
def _on_status_poll(self):
if self._is_cleaning_up:
return False
status = self.playback_status
if status != self._last_polled_status:
self._last_polled_status = status
self.poll_progress()
self._notify_playback(status)
return True
def seek_position(self, pos: float):
if self._is_cleaning_up:
return
self._stop_pos_fabricator()
try:
self._proxy.call_sync(
"SetPosition",
@@ -335,13 +338,11 @@ class PlayerService(Service):
except GLib.Error as e:
logger.error(f"seek_position fallback failed: {e}")
finally:
if self.playback_status.lower() == "playing":
self._start_pos_fabricator()
self.poll_progress()
def seek(self, offset: float):
if self._is_cleaning_up:
return
self._stop_pos_fabricator()
try:
self._proxy.call_sync(
"Seek",
@@ -353,16 +354,13 @@ class PlayerService(Service):
except GLib.Error as e:
logger.error(f"Failed to seek: {e}")
finally:
if self.playback_status.lower() == "playing":
self._start_pos_fabricator()
self.poll_progress()
def poll_progress(self):
if self._is_cleaning_up:
return
if self.playback_status.lower() == "playing":
self._start_pos_fabricator()
else:
self._stop_pos_fabricator()
self.fabricating()
def fabricating(self, metadata=None):
if self._is_cleaning_up:
@@ -556,15 +554,22 @@ class PlayerService(Service):
self._is_cleaning_up = True
self._artwork_generation += 1
self._stop_pos_fabricator()
self._stop_status_polling()
for signal_id in self._signal_ids:
for sid in self._signal_ids:
try:
self._proxy.disconnect(signal_id)
self._proxy.disconnect(sid)
except Exception as e:
logger.warning(f"Error disconnecting signal: {e}")
self._signal_ids.clear()
conn = self._proxy.get_connection()
for sid in self._subscription_ids:
try:
if conn:
conn.signal_unsubscribe(sid)
except Exception as e:
logger.warning(f"Error unsubscribing signal: {e}")
self._subscription_ids.clear()
self._current_artwork_path = ""
self._failed_covers.clear()
self._download_threads.clear()
+9 -4
View File
@@ -409,8 +409,8 @@ class Weather(Service):
return
result = raw["results"][0]
location_data = {
"lat": result["latitude"],
"lon": result["longitude"],
"lat": round(result["latitude"], 1),
"lon": round(result["longitude"], 1),
"city": result.get("name", "Unknown"),
"country": result.get("country", ""),
"config_location": target_loc,
@@ -421,8 +421,8 @@ class Weather(Service):
GLib.idle_add(self._set_error, "Location unavailable")
return
location_data = {
"lat": raw["lat"],
"lon": raw["lon"],
"lat": round(raw["lat"], 1),
"lon": round(raw["lon"], 1),
"city": raw.get("city", "Unknown"),
"country": raw.get("country", ""),
"config_location": "",
@@ -450,6 +450,11 @@ class Weather(Service):
GLib.idle_add(self._set_error, "Weather unavailable")
return
else:
weather_data = {
k: v
for k, v in weather_data.items()
if k not in ("latitude", "longitude")
}
self._weather_cache.set(weather_data)
GLib.idle_add(self._update_properties, weather_data, location_data)
@@ -0,0 +1,51 @@
from fabric.utils import Gdk, GLib
from gi.repository import Gtk
class CloseButtonRevealerMixin:
_close_button_hide_timeout_id: int | None
_hovered: bool
_is_close_button_hovered: bool
close_button_revealer: Gtk.Revealer
def _schedule_close_button_hide(self):
self._cancel_close_button_hide()
self._close_button_hide_timeout_id = GLib.timeout_add(
80, self._do_hide_close_button
)
def _cancel_close_button_hide(self):
if self._close_button_hide_timeout_id is not None:
GLib.source_remove(self._close_button_hide_timeout_id)
self._close_button_hide_timeout_id = None
def _do_hide_close_button(self):
self._close_button_hide_timeout_id = None
if not self._hovered and not self._is_close_button_hovered:
self.close_button_revealer.set_reveal_child(False)
return False
def _on_hover_enter(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._hovered = True
self._cancel_close_button_hide()
self.close_button_revealer.set_reveal_child(True)
return False
def _on_hover_leave(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._hovered = False
self._schedule_close_button_hide()
return False
def _on_close_button_enter(self, widget, event):
self._is_close_button_hovered = True
self._cancel_close_button_hide()
return False
def _on_close_button_leave(self, widget, event):
self._is_close_button_hovered = False
self._schedule_close_button_hide()
return False
+188
View File
@@ -0,0 +1,188 @@
from fabric.utils import bulk_connect, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
class BasePlayerBoxStack(Box):
def __init__(self, mpris_manager, **kwargs):
super().__init__(
orientation="v", name="media", children=[self.player_stack], **kwargs
)
self.player_stack.children = [self.no_media_box]
self.set_visible(True)
self.mpris_manager = mpris_manager
self.connect("map", self._on_map)
if self.mpris_manager is not None:
self._init_signals()
def _init_signals(self):
try:
connections = bulk_connect(
self.mpris_manager,
{
"new-player": self.on_new_player,
"player-vanish": self.on_lost_player,
},
)
for handler_id in connections:
self._signal_connections.append((self.mpris_manager, handler_id))
for name, player in self.mpris_manager.get_all_services().items():
self.on_new_player(self.mpris_manager, name, player)
except Exception as e:
logger.error(f"Failed to initialize PlayerBoxStack signals: {e}")
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
for obj, handler_id in self._signal_connections:
try:
obj.disconnect(handler_id)
except Exception as e:
logger.error(f"An error occurred: {e}")
self._signal_connections.clear()
super().destroy()
def _find_playing_player_index(self):
players = self.player_stack.get_children()
for i, player_box in enumerate(players):
if (
hasattr(player_box, "player")
and str(player_box.player.playback_status).lower() == "playing"
):
return i
return None
def _check_and_update_playing_state(self):
children = self.player_stack.get_children()
players = [p for p in children if p != self.no_media_box]
if len(players) > 0:
if self.no_media_box in children:
self.player_stack.remove(self.no_media_box)
playing_index = self._find_playing_player_index()
if playing_index is not None:
self.on_player_clicked_by_index(playing_index)
return
if len(children) == 1 and children[0] == self.no_media_box:
return
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_stack.set_visible_child(self.no_media_box)
def on_player_playback_changed(self, player_box, status):
status = str(status).lower()
if status == "playing":
players = self.player_stack.get_children()
for i, pb in enumerate(players):
if pb == player_box and i != self.current_stack_pos:
self.on_player_clicked_by_index(i)
break
elif status in ["paused", "stopped"]:
self._check_and_update_playing_state()
def _update_all_player_buttons(self):
show_buttons = len(self.player_buttons) > 1
for child in self.player_stack.get_children():
if hasattr(child, "update_buttons"):
child.update_buttons(self.player_buttons, show_buttons)
def on_player_clicked_by_index(self, index):
if 0 <= index < len(self.player_buttons):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
self.current_stack_pos = index
self.player_buttons[self.current_stack_pos].add_style_class("active")
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def on_new_player(self, mpris_manager, name, player):
children = self.player_stack.get_children()
if len(children) == 1 and children[0] == self.no_media_box:
self.player_stack.children = []
self.current_stack_pos = 0
self.set_visible(True)
new_player_box = self._create_player_box_widget(player)
self.player_stack.children = [*self.player_stack.children, new_player_box]
self.make_new_player_button(new_player_box)
if self.player_buttons:
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self._check_and_update_playing_state()
self._update_all_player_buttons()
def _create_player_box_widget(self, player):
from window.controlcenter.player import PlayerBox
return PlayerBox(player=player, player_stack=self)
def on_lost_player(self, mpris_manager, bus_name):
children = self.player_stack.get_children()
player_box_to_remove = None
for player_box in children:
if (
hasattr(player_box, "player")
and getattr(player_box.player, "player_name", None) == bus_name
):
player_box_to_remove = player_box
break
if player_box_to_remove:
player_box_to_remove.destroy()
remaining = [
c for c in self.player_stack.get_children() if c != player_box_to_remove
]
if len(remaining) == 0:
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_buttons = []
return
if self.current_stack_pos >= len(remaining):
self.current_stack_pos = max(0, len(remaining) - 1)
if self.player_buttons and self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self.player_stack.set_visible_child(remaining[self.current_stack_pos])
self._update_all_player_buttons()
def make_new_player_button(self, player_box):
new_button = Button(name="player-stack-button")
def on_player_button_click(button: Button):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
if button in self.player_buttons:
self.current_stack_pos = self.player_buttons.index(button)
button.add_style_class("active")
self.player_stack.set_visible_child(player_box)
new_button.connect("clicked", on_player_button_click)
self.player_buttons.append(new_button)
player_box.connect(
"destroy",
lambda *_: (
self.player_buttons.remove(new_button)
if new_button in self.player_buttons
else None
),
)
def suspend(self):
for child in self.player_stack.get_children():
if hasattr(child, "suspend"):
child.suspend()
def resume(self):
for child in self.player_stack.get_children():
if hasattr(child, "resume"):
child.resume()
def _create_no_media_box(self):
raise NotImplementedError
+100
View File
@@ -0,0 +1,100 @@
from fabric.utils import Gdk, logger
class PullToRefreshMixin:
def setup_pull_to_refresh(self, scrolled_widget):
self._scrolled_widget = scrolled_widget
self.vadjustment = scrolled_widget.get_vadjustment()
self.pull_start_y = 0
self.is_pulling = False
self.pull_threshold = 50
self._anim_finished_handler = None
scrolled_widget.connect("scroll-event", self.on_scroll_event)
scrolled_widget.connect("button-press-event", self.on_button_press)
scrolled_widget.connect("button-release-event", self.on_button_release)
scrolled_widget.connect("motion-notify-event", self.on_motion_notify)
scrolled_widget.set_events(
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):
if self.vadjustment.get_value() <= 0:
if event.direction == Gdk.ScrollDirection.UP:
self._trigger_scan()
self._trigger_refresh()
return True
return False
def on_button_press(self, widget, event):
if self.vadjustment.get_value() <= 0:
self.pull_start_y = event.y
self.is_pulling = True
return False
def on_button_release(self, widget, event):
if self.is_pulling:
pull_distance = event.y - self.pull_start_y
if pull_distance > self.pull_threshold:
self._trigger_scan()
self._trigger_refresh()
self.refresh_indicator.set_visible(False)
self.refresh_indicator.remove_style_class("ready-to-refresh")
self.is_pulling = False
return False
def on_motion_notify(self, widget, event):
if self.is_pulling and self.vadjustment.get_value() <= 0:
pull_distance = event.y - self.pull_start_y
if pull_distance > 0:
self.refresh_indicator.set_visible(True)
if pull_distance >= self.pull_threshold:
self.refresh_indicator.set_label(self._get_pull_ready_label())
self.refresh_indicator.add_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_label(self._get_pull_normal_label())
self.refresh_indicator.remove_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_visible(False)
return False
def _cancel_pending_refresh(self):
if hasattr(self, "_anim_finished_handler") and self._anim_finished_handler:
try:
self._scrolled_widget.height_animator.disconnect(
self._anim_finished_handler
)
except Exception as e:
logger.warning(
f"[pull-to-refresh] height_animator.disconnect(...) failed: {e}"
)
self._anim_finished_handler = None
def _refresh_after_animation(self):
if self._destroyed:
return False
anim = self._scrolled_widget.height_animator
if anim.playing:
self._anim_finished_handler = anim.connect(
"finished",
lambda *_: self._trigger_refresh() if not self._destroyed else None,
)
else:
self._trigger_refresh()
return False
def _get_pull_normal_label(self):
return "↓ Pull to refresh"
def _get_pull_ready_label(self):
return "↑ Release to refresh"
def _trigger_scan(self):
raise NotImplementedError
def _trigger_refresh(self):
raise NotImplementedError
+17 -103
View File
@@ -1,6 +1,6 @@
from enum import Enum, auto
from fabric.utils import Gdk, GLib, Gtk, idle_add, logger
from fabric.utils import GLib, Gtk, idle_add, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
@@ -9,6 +9,7 @@ from fabric.widgets.label import Label
from fabric.widgets.separator import Separator
from services.bluetooth import BluetoothClient, BluetoothDevice
from shared.widgets.pull_to_refresh import PullToRefreshMixin
from shared.widgets.smooth_switch import SmoothSwitch
from shared.window.animated_scrollwindow import AnimatedScrollable
from utils.functions import spawn_detached
@@ -304,7 +305,7 @@ class BluetoothDeviceSlot(Box):
logger.warning(f"[Bluetooth] Failed to remove device: {e}")
class BluetoothConnections(Box):
class BluetoothConnections(PullToRefreshMixin, Box):
def __init__(
self, parent, show_hidden_devices: bool = False, show_back_button=True, **kwargs
):
@@ -422,7 +423,7 @@ class BluetoothConnections(Box):
self.other_devices.set_visible(False)
# Add pull-to-refresh functionality to scrolled window
self.setup_pull_to_refresh()
self.setup_pull_to_refresh(self.other_devices_scrolled)
self.more_settings_button = Button(
child=Label("More Settings", h_align="start"),
@@ -507,31 +508,6 @@ class BluetoothConnections(Box):
# Defer refresh until after the height animation completes
idle_add(self._refresh_after_animation)
def _cancel_pending_refresh(self):
if hasattr(self, "_anim_finished_handler") and self._anim_finished_handler:
try:
self.other_devices_scrolled.height_animator.disconnect(
self._anim_finished_handler
)
except Exception as e:
logger.warning(
f"[bluetooth] self.other_devices_scrolled.height_animator.disconnect( s... failed: {e}"
)
self._anim_finished_handler = None
def _refresh_after_animation(self):
if self._destroyed:
return False
anim = self.other_devices_scrolled.height_animator
if anim.playing:
self._anim_finished_handler = anim.connect(
"finished",
lambda *_: self.force_device_refresh() if not self._destroyed else None,
)
else:
self.force_device_refresh()
return False
def open_bluetooth_settings(self, *_):
"""Open Blueman bluetooth manager"""
try:
@@ -692,84 +668,22 @@ class BluetoothConnections(Box):
self.other_devices.set_visible(False)
self.other_devices_scrolled.snap_to_size(0)
def setup_pull_to_refresh(self):
"""Setup pull-to-refresh gesture for the scrolled window"""
self.vadjustment = self.other_devices_scrolled.get_vadjustment()
def _trigger_scan(self):
if not self.client.scanning and hasattr(self.client, "scan"):
self.client.scan()
# Track gesture state
self.pull_start_y = 0
self.is_pulling = False
self.pull_threshold = 50 # pixels to trigger refresh
def _trigger_refresh(self):
self.force_device_refresh()
# Connect to scroll events
self.other_devices_scrolled.connect("scroll-event", self.on_scroll_event)
self.other_devices_scrolled.connect("button-press-event", self.on_button_press)
self.other_devices_scrolled.connect(
"button-release-event", self.on_button_release
)
self.other_devices_scrolled.connect(
"motion-notify-event", self.on_motion_notify
)
def _get_pull_normal_label(self):
if self.client.scanning:
return "↓ Pull to stop scanning"
return "↓ Pull to scan for devices"
# Enable events
self.other_devices_scrolled.set_events(
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):
"""Handle scroll events for pull-to-refresh"""
if self.vadjustment.get_value() <= 0:
if event.direction == Gdk.ScrollDirection.UP:
if not self.client.scanning and hasattr(self.client, "scan"):
self.client.scan()
self.force_device_refresh()
return True
return False
def on_button_press(self, widget, event):
"""Handle button press for touch/drag gestures"""
if self.vadjustment.get_value() <= 0:
self.pull_start_y = event.y
self.is_pulling = True
return False
def on_button_release(self, widget, event):
"""Handle button release for touch/drag gestures"""
if self.is_pulling:
pull_distance = event.y - self.pull_start_y
if pull_distance > self.pull_threshold:
if not self.client.scanning and hasattr(self.client, "scan"):
self.client.scan()
self.force_device_refresh()
self.refresh_indicator.set_visible(False)
self.refresh_indicator.remove_style_class("ready-to-refresh")
self.is_pulling = False
return False
def on_motion_notify(self, widget, event):
"""Handle motion events for visual feedback during pull"""
if self.is_pulling and self.vadjustment.get_value() <= 0:
pull_distance = event.y - self.pull_start_y
if pull_distance > 0:
self.refresh_indicator.set_visible(True)
if pull_distance >= self.pull_threshold:
if self.client.scanning:
self.refresh_indicator.set_label("↑ Release to stop scanning")
else:
self.refresh_indicator.set_label("↑ Release to scan")
self.refresh_indicator.add_style_class("ready-to-refresh")
else:
if self.client.scanning:
self.refresh_indicator.set_label("↓ Pull to stop scanning")
else:
self.refresh_indicator.set_label("↓ Pull to scan for devices")
self.refresh_indicator.remove_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_visible(False)
return False
def _get_pull_ready_label(self):
if self.client.scanning:
return "↑ Release to stop scanning"
return "↑ Release to scan"
def on_device_added(self, client: BluetoothClient, address: str):
"""Handle when a new device is added"""
+7 -162
View File
@@ -1,5 +1,4 @@
import os
import weakref
from fabric.utils import GLib, bulk_connect, logger
from fabric.widgets.box import Box
@@ -12,11 +11,13 @@ from fabric.widgets.stack import Stack
from fabric.widgets.wayland import WaylandWindow as Window
from services.mpris import PlayerManager, PlayerService
from shared.widgets.player_box_stack import BasePlayerBoxStack
from utils.gtk_utils import svg_file
from window.controlcenter.player import (
PLAYER_FALLBACK_ART,
apply_player_art,
get_shared_mpris_manager,
resolve_mpris_player_icon,
)
@@ -55,42 +56,18 @@ class EmbeddedExpandedPlayer(Box):
super().destroy()
class PlayerBoxStack(Box):
class PlayerBoxStack(BasePlayerBoxStack):
def __init__(self, mpris_manager: PlayerManager, **kwargs):
self.player_stack = Stack(name="player-stack", transition_type="none")
self.current_stack_pos = 0
self.player_buttons: list[Button] = []
self._player_widgets = weakref.WeakValueDictionary()
self._signal_connections = []
self.no_media_box = self._create_no_media_box()
super().__init__(orientation="v", name="media", children=[self.player_stack])
self.player_stack.children = [self.no_media_box]
self.set_visible(True)
self.mpris_manager = mpris_manager
self.connect("map", self._on_map)
super().__init__(mpris_manager, **kwargs)
connections = bulk_connect(
self.mpris_manager,
{"new-player": self.on_new_player, "player-vanish": self.on_lost_player},
)
for handler_id in connections:
self._signal_connections.append((self.mpris_manager, handler_id))
for name, player in self.mpris_manager.get_all_services().items():
self.on_new_player(self.mpris_manager, name, player)
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
for obj, handler_id in self._signal_connections:
try:
obj.disconnect(handler_id)
except Exception as e:
logger.error(f"An error occurred: {e}")
self._signal_connections.clear()
super().destroy()
def _create_player_box_widget(self, player):
return PlayerBox(player=player, player_stack=self)
def _create_no_media_box(self):
fallback_cover_path = PLAYER_FALLBACK_ART
@@ -139,138 +116,6 @@ class PlayerBoxStack(Box):
],
)
def _find_playing_player_index(self):
players = self.player_stack.get_children()
for i, player_box in enumerate(players):
if (
hasattr(player_box, "player")
and str(player_box.player.playback_status).lower() == "playing"
):
return i
return None
def _check_and_update_playing_state(self):
players = [
p for p in self.player_stack.get_children() if p != self.no_media_box
]
if len(players) > 0:
if self.no_media_box in self.player_stack.get_children():
self.player_stack.remove(self.no_media_box)
playing_index = self._find_playing_player_index()
if playing_index is not None:
self.on_player_clicked_by_index(playing_index)
return
if (
len(self.player_stack.get_children()) == 1
and self.player_stack.get_children()[0] == self.no_media_box
):
return
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_stack.set_visible_child(self.no_media_box)
def on_player_playback_changed(self, player_box, status):
status = str(status).lower()
if status == "playing":
players = self.player_stack.get_children()
for i, pb in enumerate(players):
if pb == player_box and i != self.current_stack_pos:
self.on_player_clicked_by_index(i)
break
elif status in ["paused", "stopped"]:
self._check_and_update_playing_state()
def on_player_clicked_by_index(self, index):
if 0 <= index < len(self.player_buttons):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
self.current_stack_pos = index
self.player_buttons[self.current_stack_pos].add_style_class("active")
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def on_new_player(self, mpris_manager, name, player):
if (
len(self.player_stack.get_children()) == 1
and self.player_stack.get_children()[0] == self.no_media_box
):
self.player_stack.children = []
self.current_stack_pos = 0
self.set_visible(True)
new_player_box = PlayerBox(player=player, player_stack=self)
self.player_stack.children = [*self.player_stack.children, new_player_box]
self.make_new_player_button(new_player_box)
if self.player_buttons:
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self._check_and_update_playing_state()
self._update_all_player_buttons()
def on_lost_player(self, mpris_manager, player_name):
player_box_to_remove = None
for player_box in self.player_stack.get_children():
if (
hasattr(player_box, "player")
and player_box.player.player_name == player_name
):
player_box_to_remove = player_box
break
if player_box_to_remove:
player_box_to_remove.destroy()
remaining_players = [
p for p in self.player_stack.get_children() if p != player_box_to_remove
]
if len(remaining_players) == 0:
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_buttons = []
return
if self.current_stack_pos >= len(self.player_stack.get_children()):
self.current_stack_pos = max(0, len(self.player_stack.get_children()) - 1)
if self.player_buttons and self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def make_new_player_button(self, player_box):
new_button = Button(name="player-stack-button")
def on_player_button_click(button: Button):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
if button in self.player_buttons:
self.current_stack_pos = self.player_buttons.index(button)
button.add_style_class("active")
self.player_stack.set_visible_child(player_box)
new_button.connect("clicked", on_player_button_click)
self.player_buttons.append(new_button)
player_box.connect(
"destroy",
lambda *_: (
self.player_buttons.remove(new_button)
if new_button in self.player_buttons
else None
),
)
def _update_all_player_buttons(self):
players = self.player_stack.get_children()
for player_box in players:
if hasattr(player_box, "update_buttons"):
player_box.update_buttons(self.player_buttons, len(players) > 1)
class PlayerBox(Box):
def __init__(self, player: PlayerService, player_stack=None, **kwargs):
@@ -303,7 +148,7 @@ class PlayerBox(Box):
overlays=[
Box(
children=Image(
icon_name=self.player.player_name,
icon_name=resolve_mpris_player_icon(self.player.player_name),
name="player-app-icon",
icon_size=20,
),
+51 -180
View File
@@ -9,16 +9,60 @@ from fabric.widgets.stack import Stack
import shared.data as data
from services.mpris import PlayerManager, PlayerService
from shared.widgets.player_box_stack import BasePlayerBoxStack
from utils.gtk_utils import svg_file
CACHE_DIR = f"{data.CACHE_DIR}/media"
PLAYER_FALLBACK_ART = get_relative_path("../../assets/icons/music.svg")
MPRIS_ICON_FALLBACK = "application-x-executable-symbolic"
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)
_shared_mpris_manager = None
def resolve_mpris_player_icon(player_name: str) -> str:
"""Resolve MPRIS player icon from .desktop file."""
raw = player_name or ""
app_name = raw.removeprefix("org.mpris.MediaPlayer2.")
search_dirs = [
os.path.join(os.path.expanduser("~/.local/share"), "applications"),
"/usr/share/applications",
"/usr/local/share/applications",
]
for d in os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":"):
p = os.path.join(d, "applications")
if p not in search_dirs:
search_dirs.append(p)
candidates = [app_name]
parts = app_name.split("-")
for i in range(len(parts) - 1, 0, -1):
shorter = "-".join(parts[:i])
if shorter not in candidates:
candidates.append(shorter)
for base in search_dirs:
for name in candidates:
dp = os.path.join(base, name + ".desktop")
if os.path.exists(dp):
try:
with open(dp) as f:
for line in f:
if line.startswith("Icon="):
return line[5:].strip()
except OSError:
pass
# No desktop file found — try app_name as direct icon name
for name in candidates:
if name:
return name
return MPRIS_ICON_FALLBACK
def get_shared_mpris_manager():
global _shared_mpris_manager
if _shared_mpris_manager is None:
@@ -61,7 +105,7 @@ def apply_player_art(album_cover_widget, player, service=None, path=None):
return None
class PlayerBoxStack(Box):
class PlayerBoxStack(BasePlayerBoxStack):
def __init__(
self, mpris_manager: PlayerManager = None, control_center=None, **kwargs
):
@@ -72,53 +116,12 @@ class PlayerBoxStack(Box):
self._signal_connections = []
self.no_media_box = self._create_no_media_box()
super().__init__(orientation="v", name="media", children=[self.player_stack])
self.player_stack.children = [self.no_media_box]
self.set_visible(True)
self.mpris_manager = mpris_manager or get_shared_mpris_manager()
self.connect("map", self._on_map)
super().__init__(mpris_manager or get_shared_mpris_manager(), **kwargs)
if self.mpris_manager is not None:
self._init_mpris_manager()
def _init_mpris_manager(self):
try:
connections = bulk_connect(
self.mpris_manager,
{
"new-player": self.on_new_player,
"player-vanish": self.on_lost_player,
},
)
for handler_id in connections:
self._signal_connections.append((self.mpris_manager, handler_id))
for name, player in self.mpris_manager.get_all_services().items():
self.on_new_player(self.mpris_manager, name, player)
except Exception as e:
logger.error(f"Failed to initialize PlayerBoxStack signals: {e}")
def _on_map(self, *_):
self._check_and_update_playing_state()
def destroy(self):
for obj, handler_id in self._signal_connections:
try:
obj.disconnect(handler_id)
except Exception as e:
logger.error(f"An error occurred: {e}")
self._signal_connections.clear()
super().destroy()
def suspend(self):
for child in self.player_stack.get_children():
if hasattr(child, "suspend"):
child.suspend()
def resume(self):
for child in self.player_stack.get_children():
if hasattr(child, "resume"):
child.resume()
def _create_player_box_widget(self, player):
return PlayerBox(
player=player, player_stack=self, control_center=self.control_center
)
def _create_no_media_box(self):
fallback_cover_path = get_relative_path("../../assets/icons/music.svg")
@@ -172,138 +175,6 @@ class PlayerBoxStack(Box):
],
)
def _find_playing_player_index(self):
players = self.player_stack.get_children()
for i, player_box in enumerate(players):
if (
hasattr(player_box, "player")
and str(player_box.player.playback_status).lower() == "playing"
):
return i
return None
def _check_and_update_playing_state(self):
players = [
p for p in self.player_stack.get_children() if p != self.no_media_box
]
if len(players) > 0:
if self.no_media_box in self.player_stack.get_children():
self.player_stack.remove(self.no_media_box)
playing_index = self._find_playing_player_index()
if playing_index is not None:
self.on_player_clicked_by_index(playing_index)
return
if (
len(self.player_stack.get_children()) == 1
and self.player_stack.get_children()[0] == self.no_media_box
):
return
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_stack.set_visible_child(self.no_media_box)
def on_player_playback_changed(self, player_box, status):
status = str(status).lower()
if status == "playing":
players = self.player_stack.get_children()
for i, pb in enumerate(players):
if pb == player_box and i != self.current_stack_pos:
self.on_player_clicked_by_index(i)
break
elif status in ["paused", "stopped"]:
self._check_and_update_playing_state()
def _update_all_player_buttons(self):
show_buttons = len(self.player_buttons) > 1
for child in self.player_stack.get_children():
if hasattr(child, "update_buttons"):
child.update_buttons(self.player_buttons, show_buttons)
def on_player_clicked_by_index(self, index):
if 0 <= index < len(self.player_buttons):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
self.current_stack_pos = index
self.player_buttons[self.current_stack_pos].add_style_class("active")
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def on_new_player(self, mpris_manager, name, player):
if (
len(self.player_stack.get_children()) == 1
and self.player_stack.get_children()[0] == self.no_media_box
):
self.player_stack.children = []
self.current_stack_pos = 0
new_player_box = PlayerBox(
player=player, player_stack=self, control_center=self.control_center
)
self.player_stack.children = [*self.player_stack.children, new_player_box]
self.make_new_player_button(new_player_box)
if self.player_buttons:
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self._check_and_update_playing_state()
self._update_all_player_buttons()
def on_lost_player(self, mpris_manager, bus_name):
player_box_to_remove = None
for player_box in self.player_stack.get_children():
if (
hasattr(player_box, "player")
and getattr(player_box.player, "player_name", None) == bus_name
):
player_box_to_remove = player_box
break
if player_box_to_remove:
player_box_to_remove.destroy()
remaining_players = [
p for p in self.player_stack.get_children() if p != player_box_to_remove
]
if len(remaining_players) == 0:
self.player_stack.children = [self.no_media_box]
self.current_stack_pos = 0
self.player_buttons = []
return
if self.current_stack_pos >= len(self.player_stack.get_children()):
self.current_stack_pos = max(0, len(self.player_stack.get_children()) - 1)
if self.player_buttons and self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].set_style_classes(["active"])
self.player_stack.set_visible_child(
self.player_stack.get_children()[self.current_stack_pos]
)
self._update_all_player_buttons()
def make_new_player_button(self, player_box):
new_button = Button(name="player-stack-button")
def on_player_button_click(button: Button):
if self.current_stack_pos < len(self.player_buttons):
self.player_buttons[self.current_stack_pos].remove_style_class("active")
if button in self.player_buttons:
self.current_stack_pos = self.player_buttons.index(button)
button.add_style_class("active")
self.player_stack.set_visible_child(player_box)
new_button.connect("clicked", on_player_button_click)
self.player_buttons.append(new_button)
player_box.connect(
"destroy",
lambda *_: (
self.player_buttons.remove(new_button)
if new_button in self.player_buttons
else None
),
)
class PlayerBox(Box):
def __init__(
@@ -327,7 +198,7 @@ class PlayerBox(Box):
overlays=[
Box(
children=Image(
icon_name=self.player.player_name,
icon_name=resolve_mpris_player_icon(self.player.player_name),
name="player-app-icon",
icon_size=20,
),
+13 -99
View File
@@ -1,4 +1,4 @@
from fabric.utils import Gdk, GLib, idle_add, logger
from fabric.utils import GLib, idle_add, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
@@ -9,6 +9,7 @@ from fabric.widgets.separator import Separator
from services import dns as dns_service
from services.network import NetworkClient
from shared.dialogs.sysauth_dialog import run_auth_dialog
from shared.widgets.pull_to_refresh import PullToRefreshMixin
from shared.widgets.smooth_switch import SmoothSwitch
from shared.window.animated_scrollwindow import AnimatedScrollable
from utils.functions import (
@@ -402,7 +403,7 @@ class DnsSwitcher:
self._load_current_dns()
class WifiConnections(Box):
class WifiConnections(PullToRefreshMixin, Box):
def __init__(self, parent, show_back_button=True, network_service=None, **kwargs):
super().__init__(
spacing=8,
@@ -506,7 +507,7 @@ class WifiConnections(Box):
self.other_networks.set_visible(False)
# Add pull-to-refresh functionality to scrolled window
self.setup_pull_to_refresh()
self.setup_pull_to_refresh(self.other_networks_scrolled)
# Create More Settings button (same style as Other Networks button)
self.more_settings_button = Button(
@@ -574,33 +575,6 @@ class WifiConnections(Box):
# Defer refresh until after the height animation completes
idle_add(self._refresh_after_animation)
def _cancel_pending_refresh(self):
if hasattr(self, "_anim_finished_handler") and self._anim_finished_handler:
try:
self.other_networks_scrolled.height_animator.disconnect(
self._anim_finished_handler
)
except Exception as e:
logger.warning(
f"[wifi] self.other_networks_scrolled.height_animator.disconnect( ... failed: {e}"
)
self._anim_finished_handler = None
def _refresh_after_animation(self):
if self._destroyed:
return False
anim = self.other_networks_scrolled.height_animator
if anim.playing:
self._anim_finished_handler = anim.connect(
"finished",
lambda *_: (
self.force_network_refresh() if not self._destroyed else None
),
)
else:
self.force_network_refresh()
return False
def on_network_ready(self, *_):
"""Called when network service is ready"""
if self._network_ready_fired and self.wifi_service:
@@ -812,78 +786,18 @@ class WifiConnections(Box):
except Exception as e:
logger.error(f"[WiFi] Error during forced network refresh: {e}")
def setup_pull_to_refresh(self):
"""Setup pull-to-refresh gesture for the scrolled window"""
self.vadjustment = self.other_networks_scrolled.get_vadjustment()
def _trigger_scan(self):
if self.wifi_service:
self.wifi_service.scan()
# Track gesture state
self.pull_start_y = 0
self.is_pulling = False
self.pull_threshold = 50 # pixels to trigger refresh
def _trigger_refresh(self):
self.force_network_refresh()
# Connect to scroll events
self.other_networks_scrolled.connect("scroll-event", self.on_scroll_event)
self.other_networks_scrolled.connect("button-press-event", self.on_button_press)
self.other_networks_scrolled.connect(
"button-release-event", self.on_button_release
)
self.other_networks_scrolled.connect(
"motion-notify-event", self.on_motion_notify
)
def _get_pull_normal_label(self):
return "↓ Pull to scan for networks"
# Enable events
self.other_networks_scrolled.set_events(
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):
"""Handle scroll events for pull-to-refresh"""
if self.vadjustment.get_value() <= 0:
if event.direction == Gdk.ScrollDirection.UP:
if self.wifi_service:
self.wifi_service.scan()
self.force_network_refresh()
return True
return False
def on_button_press(self, widget, event):
"""Handle button press for touch/drag gestures"""
if self.vadjustment.get_value() <= 0:
self.pull_start_y = event.y
self.is_pulling = True
return False
def on_button_release(self, widget, event):
"""Handle button release for touch/drag gestures"""
if self.is_pulling:
pull_distance = event.y - self.pull_start_y
if pull_distance > self.pull_threshold:
if self.wifi_service:
self.wifi_service.scan()
self.force_network_refresh()
self.refresh_indicator.set_visible(False)
self.refresh_indicator.remove_style_class("ready-to-refresh")
self.is_pulling = False
return False
def on_motion_notify(self, widget, event):
"""Handle motion events for visual feedback during pull"""
if self.is_pulling and self.vadjustment.get_value() <= 0:
pull_distance = event.y - self.pull_start_y
if pull_distance > 0:
self.refresh_indicator.set_visible(True)
if pull_distance >= self.pull_threshold:
self.refresh_indicator.set_label("↑ Release to scan")
self.refresh_indicator.add_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_label("↓ Pull to scan for networks")
self.refresh_indicator.remove_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_visible(False)
return False
def _get_pull_ready_label(self):
return "↑ Release to scan"
def on_destroy(self, widget):
"""Cleanup when widget is destroyed"""
-27
View File
@@ -194,8 +194,6 @@ class DesktopWidgetWindow(WaylandWindow):
self.connect("button-press-event", self._on_window_button_press)
self._setup_drag_dest()
self._setup_file_monitor()
self._last_user_files: set[str] = set()
GLib.timeout_add(2000, self._poll_user_widgets)
GLib.timeout_add(50, self._initial_build)
@@ -251,31 +249,6 @@ class DesktopWidgetWindow(WaylandWindow):
# fallback poll — catches deletions that Gio file monitor misses
def _poll_user_widgets(self) -> bool:
if not os.path.isdir(_CONFIG_DESKTOP_DIR):
return True
current = {
f[:-3]
for f in os.listdir(_CONFIG_DESKTOP_DIR)
if f.endswith(".py") and not f.startswith("_")
}
if current != self._last_user_files:
self._last_user_files = current
stale = {k for k, f in _user_key_to_file.items() if f not in current}
if stale:
logger.info(f"[DesktopWidgets] poll found stale: {stale}")
for key in stale:
mod_name = _user_key_to_file.pop(key, None)
if mod_name:
sys.modules.pop(f"user_widget_{mod_name}", None)
_user_widget_modules.pop(mod_name, None)
DesktopWidgetRegistry.unregister_user_keys(stale)
_load_user_widgets()
self.rebuild()
return True
# file monitor — live-rebuild when config/desktop/ changes
def _setup_file_monitor(self) -> None:
if not os.path.isdir(_CONFIG_DESKTOP_DIR):
os.makedirs(_CONFIG_DESKTOP_DIR, exist_ok=True)
+5 -46
View File
@@ -19,6 +19,7 @@ import shared.data as data
from services.config import get_config, on_config_change
from services.modus import notification_service
from shared.widgets.clipping_box import ClippingBox
from shared.widgets.close_button_revealer import CloseButtonRevealerMixin
from shared.widgets.custom_image import CustomImage
from shared.widgets.customrevealer import SlideRevealer
from utils.gtk_utils import setup_cursor_hover, svg_file
@@ -847,7 +848,7 @@ class NotificationWidget(Box):
return data.NOTIFICATION_TIMEOUT
class NotificationRevealer(SlideRevealer):
class NotificationRevealer(CloseButtonRevealerMixin, SlideRevealer):
def __init__(
self,
notification: Notification,
@@ -872,7 +873,7 @@ class NotificationRevealer(SlideRevealer):
setup_cursor_hover(self.close_button)
self._is_close_button_hovered = False
self._is_popup_hovered = False
self._hovered = False
self._close_button_hide_timeout_id = None
self.close_button_event_box = EventBox(
@@ -927,8 +928,8 @@ class NotificationRevealer(SlideRevealer):
],
child=self.notif_box,
)
self.event_box.connect("enter-notify-event", self._on_popup_enter)
self.event_box.connect("leave-notify-event", self._on_popup_leave)
self.event_box.connect("enter-notify-event", self._on_hover_enter)
self.event_box.connect("leave-notify-event", self._on_hover_leave)
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_notify)
@@ -956,48 +957,6 @@ class NotificationRevealer(SlideRevealer):
"""Smoother easing function for better animation quality"""
return 1 - pow(1 - t, 3)
def _on_popup_enter(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._is_popup_hovered = True
self._cancel_close_button_hide()
self.close_button_revealer.set_reveal_child(True)
return False
def _on_popup_leave(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._is_popup_hovered = False
self._schedule_close_button_hide()
return False
def _on_close_button_enter(self, widget, event):
self._is_close_button_hovered = True
self._cancel_close_button_hide()
return False
def _on_close_button_leave(self, widget, event):
self._is_close_button_hovered = False
self._schedule_close_button_hide()
return False
def _schedule_close_button_hide(self):
self._cancel_close_button_hide()
self._close_button_hide_timeout_id = GLib.timeout_add(
80, self._do_hide_close_button
)
def _cancel_close_button_hide(self):
if self._close_button_hide_timeout_id is not None:
GLib.source_remove(self._close_button_hide_timeout_id)
self._close_button_hide_timeout_id = None
def _do_hide_close_button(self):
self._close_button_hide_timeout_id = None
if not self._is_popup_hovered and not self._is_close_button_hovered:
self.close_button_revealer.set_reveal_child(False)
return False
def _on_button_press(self, widget, event):
if event.button != 1:
return False
+8 -49
View File
@@ -13,6 +13,7 @@ from fabric.widgets.scrolledwindow import ScrolledWindow
import shared.data as data
from services.modus import notification_service
from shared.widgets.clipping_box import ClippingBox
from shared.widgets.close_button_revealer import CloseButtonRevealerMixin
from shared.widgets.custom_image import CustomImage
from shared.window.applet_window import AppletWindow
from utils.functions import clear_children, escape_markup_text
@@ -33,7 +34,7 @@ from window.notification.unified_cache import (
)
class ExpandableNotificationGroup(Box):
class ExpandableNotificationGroup(CloseButtonRevealerMixin, Box):
def __init__(self, app_name, notifications, **kwargs):
super().__init__(
name="notification-group", orientation="v", spacing=0, **kwargs
@@ -457,7 +458,7 @@ class ExpandableNotificationGroup(Box):
return True
class NotificationCenterWidget(NotificationWidget):
class NotificationCenterWidget(CloseButtonRevealerMixin, NotificationWidget):
def __init__(self, notification, **kwargs):
self.notification_id = notification.cache_id
self.cache_metadata = getattr(notification, "cache_metadata", {})
@@ -539,7 +540,7 @@ class NotificationCenterWidget(NotificationWidget):
setup_cursor_hover(self.close_button)
self._is_close_button_hovered = False
self._is_content_hovered = False
self._hovered = False
self._close_button_hide_timeout_id = None
close_event_box = EventBox(
@@ -549,7 +550,7 @@ class NotificationCenterWidget(NotificationWidget):
close_event_box.connect("enter-notify-event", self._on_close_button_enter)
close_event_box.connect("leave-notify-event", self._on_close_button_leave)
self.close_revealer = Gtk.Revealer(
self.close_button_revealer = Gtk.Revealer(
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
transition_duration=200,
child=close_event_box,
@@ -618,12 +619,12 @@ class NotificationCenterWidget(NotificationWidget):
],
child=content,
)
content_event_box.connect("enter-notify-event", self._on_content_enter)
content_event_box.connect("leave-notify-event", self._on_content_leave)
content_event_box.connect("enter-notify-event", self._on_hover_enter)
content_event_box.connect("leave-notify-event", self._on_hover_leave)
overlay = Gtk.Overlay()
overlay.add(content_event_box)
overlay.add_overlay(self.close_revealer)
overlay.add_overlay(self.close_button_revealer)
overlay.show_all()
return overlay
@@ -697,48 +698,6 @@ class NotificationCenterWidget(NotificationWidget):
except Exception as e:
logger.error(f"Error removing notification {self.notification_id}: {e}")
def _on_content_enter(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._is_content_hovered = True
self._cancel_close_button_hide()
self.close_revealer.set_reveal_child(True)
return False
def _on_content_leave(self, widget, event):
if hasattr(event, "detail") and event.detail == Gdk.NotifyType.INFERIOR:
return False
self._is_content_hovered = False
self._schedule_close_button_hide()
return False
def _on_close_button_enter(self, widget, event):
self._is_close_button_hovered = True
self._cancel_close_button_hide()
return False
def _on_close_button_leave(self, widget, event):
self._is_close_button_hovered = False
self._schedule_close_button_hide()
return False
def _schedule_close_button_hide(self):
self._cancel_close_button_hide()
self._close_button_hide_timeout_id = GLib.timeout_add(
80, self._do_hide_close_button
)
def _cancel_close_button_hide(self):
if self._close_button_hide_timeout_id is not None:
GLib.source_remove(self._close_button_hide_timeout_id)
self._close_button_hide_timeout_id = None
def _do_hide_close_button(self):
self._close_button_hide_timeout_id = None
if not self._is_content_hovered and not self._is_close_button_hovered:
self.close_revealer.set_reveal_child(False)
return False
# Override to disable timeout functionality
def start_timeout(self):
pass
+1 -10
View File
@@ -574,16 +574,7 @@ class ApplicationSwitcher(Window):
item.preview_image.set_from_pixbuf(pixbuf)
if self.get_visible() and config().get("switcher_live_preview", True):
fps_delay = config().get("switcher.live_preview_delay_ms", 200)
invoke_repeater(
fps_delay,
lambda: (
self._capture.capture_by_handle(full_addr, 300, 168, True)
if self.get_visible()
else False
),
initial_call=False,
)
self._capture.capture_by_handle(full_addr, 300, 168, True)
except Exception:
logger.exception("_on_frame_ready error")
Generated
+2 -2
View File
@@ -136,7 +136,7 @@ wheels = [
[[package]]
name = "fabric"
version = "0.0.2"
source = { git = "https://github.com/Fabric-Development/fabric.git#85cacf660b1a324525a4dd9b512521e55ef7f89b" }
source = { git = "https://github.com/Fabric-Development/fabric.git?rev=85cacf660b1a324525a4dd9b512521e55ef7f89b#85cacf660b1a324525a4dd9b512521e55ef7f89b" }
dependencies = [
{ name = "click" },
{ name = "loguru" },
@@ -268,7 +268,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "fabric", git = "https://github.com/Fabric-Development/fabric.git" },
{ name = "fabric", git = "https://github.com/Fabric-Development/fabric.git?rev=85cacf660b1a324525a4dd9b512521e55ef7f89b" },
{ name = "keyring", specifier = "==25.7.0" },
{ name = "keyrings-alt", specifier = "==5.0.2" },
{ name = "psutil", specifier = "==7.2.2" },