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

+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"""