🔧 chore(workspaces): update Workspaces and WorkspaceButton instances, remove Hyprland import
This commit is contained in:
8 files changed
+219
-467
No files matched your search
@@ -111,7 +111,15 @@ class DockComponents(Box):
|
||||
|
||||
for component_name, widget in components.items():
|
||||
if component_name in self.component_visibility:
|
||||
widget.set_visible(self.component_visibility[component_name])
|
||||
# Special handling for applications component - let it manage its own visibility
|
||||
if component_name == "applications":
|
||||
# Only apply config visibility if the component has content
|
||||
if hasattr(widget, '_update_visibility_based_on_content'):
|
||||
widget._update_visibility_based_on_content()
|
||||
else:
|
||||
widget.set_visible(self.component_visibility[component_name])
|
||||
else:
|
||||
widget.set_visible(self.component_visibility[component_name])
|
||||
|
||||
def toggle_component_visibility(self, component_name):
|
||||
components = {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from gi.repository import GLib, Gdk
|
||||
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.button import Button
|
||||
from services.battery import Battery as BatteryService
|
||||
import utils.icons as icons
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.label import Label
|
||||
from gi.repository import Gdk, GLib
|
||||
from services.battery import Battery as BatteryService
|
||||
|
||||
|
||||
class Battery(Box):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from fabric.audio.service import Audio
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
@@ -5,11 +7,7 @@ from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.scale import Scale
|
||||
from gi.repository import Gdk, GLib
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from services.brightness import Brightness as BrightnessService
|
||||
|
||||
|
||||
@@ -22,17 +20,19 @@ class Brightness(Box):
|
||||
return
|
||||
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="button-brightness", size=28, line_width=2,
|
||||
start_angle=150, end_angle=390,
|
||||
name="button-brightness",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
)
|
||||
self.brightness_label = Label(
|
||||
name="brightness-label", markup=icons.brightness_high
|
||||
)
|
||||
self.brightness_label = Label(name="brightness-label", markup=icons.brightness_high)
|
||||
self.brightness_button = Button(child=self.brightness_label)
|
||||
self.event_box = EventBox(
|
||||
events=["scroll", "smooth-scroll"],
|
||||
child=Overlay(
|
||||
child=self.progress_bar,
|
||||
overlays=self.brightness_button
|
||||
),
|
||||
child=Overlay(child=self.progress_bar, overlays=self.brightness_button),
|
||||
)
|
||||
self.event_box.connect("scroll-event", self.on_scroll)
|
||||
self.add(self.event_box)
|
||||
@@ -49,22 +49,20 @@ class Brightness(Box):
|
||||
def on_scroll(self, _, event):
|
||||
if self.brightness.max_screen == -1:
|
||||
return
|
||||
|
||||
|
||||
current_brightness = self.brightness.screen_brightness
|
||||
max_brightness = self.brightness.max_screen
|
||||
step = max(1, int(max_brightness * 0.01)) # 1% of max brightness
|
||||
|
||||
|
||||
if event.direction == Gdk.ScrollDirection.SMOOTH:
|
||||
if hasattr(event, 'delta_y') and abs(event.delta_y) > 0:
|
||||
if hasattr(event, "delta_y") and abs(event.delta_y) > 0:
|
||||
new_brightness = current_brightness - int(event.delta_y * step)
|
||||
self.brightness.screen_brightness = new_brightness
|
||||
# Remove delta_x handling
|
||||
elif event.direction == Gdk.ScrollDirection.UP:
|
||||
self.brightness.screen_brightness = current_brightness + step
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
self.brightness.screen_brightness = current_brightness - step
|
||||
|
||||
|
||||
def on_progress_value_changed(self, widget, pspec):
|
||||
if self._updating_from_brightness:
|
||||
return
|
||||
@@ -72,10 +70,15 @@ class Brightness(Box):
|
||||
new_brightness = int(new_norm * self.brightness.max_screen)
|
||||
self._pending_value = new_brightness
|
||||
if self._update_source_id is None:
|
||||
self._update_source_id = GLib.timeout_add(50, self._update_brightness_callback)
|
||||
self._update_source_id = GLib.timeout_add(
|
||||
50, self._update_brightness_callback
|
||||
)
|
||||
|
||||
def _update_brightness_callback(self):
|
||||
if self._pending_value is not None and self._pending_value != self.brightness.screen_brightness:
|
||||
if (
|
||||
self._pending_value is not None
|
||||
and self._pending_value != self.brightness.screen_brightness
|
||||
):
|
||||
self.brightness.screen_brightness = self._pending_value
|
||||
self._pending_value = None
|
||||
return True
|
||||
@@ -105,13 +108,17 @@ class Brightness(Box):
|
||||
GLib.source_remove(self._update_source_id)
|
||||
super().destroy()
|
||||
|
||||
|
||||
class Volume(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-vol", **kwargs)
|
||||
self.audio = Audio()
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="button-volume", size=28, line_width=2,
|
||||
start_angle=150, end_angle=390,
|
||||
name="button-volume",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
)
|
||||
self.vol_label = Label(name="vol-label", markup=icons.vol_high)
|
||||
self.vol_button = Button(on_clicked=self.toggle_mute, child=self.vol_label)
|
||||
@@ -148,12 +155,12 @@ class Volume(Box):
|
||||
def on_scroll(self, _, event):
|
||||
if not self.audio.speaker:
|
||||
return
|
||||
|
||||
|
||||
current_volume = self.audio.speaker.volume
|
||||
step = 1
|
||||
|
||||
|
||||
if event.direction == Gdk.ScrollDirection.SMOOTH:
|
||||
if hasattr(event, 'delta_y') and abs(event.delta_y) > 0:
|
||||
if hasattr(event, "delta_y") and abs(event.delta_y) > 0:
|
||||
new_volume = current_volume - int(event.delta_y * step)
|
||||
self.audio.speaker.volume = new_volume
|
||||
# Remove delta_x handling
|
||||
@@ -165,7 +172,7 @@ class Volume(Box):
|
||||
def on_speaker_changed(self, *_):
|
||||
if not self.audio.speaker:
|
||||
return
|
||||
|
||||
|
||||
vol_high_icon = icons.vol_high
|
||||
vol_medium_icon = icons.vol_medium
|
||||
vol_mute_icon = icons.vol_off
|
||||
@@ -178,7 +185,7 @@ class Volume(Box):
|
||||
vol_off_icon = icons.bluetooth_disconnected
|
||||
|
||||
self.progress_bar.value = self.audio.speaker.volume / 100
|
||||
|
||||
|
||||
if self.audio.speaker.muted:
|
||||
self.vol_label.set_markup(vol_mute_icon)
|
||||
self.progress_bar.add_style_class("muted")
|
||||
@@ -196,13 +203,17 @@ class Volume(Box):
|
||||
else:
|
||||
self.vol_label.set_markup(vol_off_icon)
|
||||
|
||||
|
||||
class MicroPhone(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-mic", **kwargs)
|
||||
self.audio = Audio()
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="button-mic", size=28, line_width=2,
|
||||
start_angle=150, end_angle=390,
|
||||
name="button-mic",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
)
|
||||
self.mic_label = Label(name="mic-label", markup=icons.mic)
|
||||
self.mic_button = Button(on_clicked=self.toggle_mute, child=self.mic_label)
|
||||
@@ -240,9 +251,8 @@ class MicroPhone(Box):
|
||||
if not self.audio.microphone:
|
||||
return
|
||||
if event.direction == Gdk.ScrollDirection.SMOOTH:
|
||||
if hasattr(event, 'delta_y') and abs(event.delta_y) > 0:
|
||||
if hasattr(event, "delta_y") and abs(event.delta_y) > 0:
|
||||
self.audio.microphone.volume -= event.delta_y
|
||||
# Remove delta_x handling
|
||||
elif event.direction == Gdk.ScrollDirection.UP:
|
||||
self.audio.microphone.volume += 1
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
@@ -251,9 +261,9 @@ class MicroPhone(Box):
|
||||
def on_microphone_changed(self, *_):
|
||||
if not self.audio.microphone:
|
||||
return
|
||||
|
||||
|
||||
self.progress_bar.value = self.audio.microphone.volume / 100
|
||||
|
||||
|
||||
if self.audio.microphone.muted:
|
||||
self.mic_label.set_markup(icons.mic_mute)
|
||||
self.progress_bar.add_style_class("muted")
|
||||
@@ -271,11 +281,12 @@ class MicroPhone(Box):
|
||||
else:
|
||||
self.mic_label.set_markup(icons.mic_mute)
|
||||
|
||||
|
||||
class Controls(Box):
|
||||
def __init__(self, **kwargs):
|
||||
brightness = BrightnessService.get_initial()
|
||||
children = []
|
||||
if (brightness.screen_brightness != -1):
|
||||
if brightness.screen_brightness != -1:
|
||||
children.append(Brightness())
|
||||
children.extend([Volume(), MicroPhone()])
|
||||
super().__init__(
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from services.network import NetworkClient
|
||||
from fabric.bluetooth import BluetoothClient
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from services.network import NetworkClient
|
||||
|
||||
|
||||
class WifiIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-network", **kwargs)
|
||||
|
||||
# Initialize NetworkClient
|
||||
self.network = NetworkClient()
|
||||
# Create progress bar and wifi label
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="button-network",
|
||||
size=28,
|
||||
@@ -26,13 +23,10 @@ class WifiIndicator(Button):
|
||||
)
|
||||
self.wifi_label = Label(name="network-label", markup=icons.loader)
|
||||
|
||||
# Create overlay with progress bar and label
|
||||
self.add(Overlay(child=self.progress_bar, overlays=self.wifi_label))
|
||||
|
||||
# Connect signals
|
||||
self.network.connect("ready", self.on_network_ready)
|
||||
|
||||
# If already ready, set up signals immediately
|
||||
if self.network.is_ready:
|
||||
self.setup_signals()
|
||||
|
||||
@@ -40,27 +34,23 @@ class WifiIndicator(Button):
|
||||
self.setup_signals()
|
||||
|
||||
def setup_signals(self):
|
||||
# Connect to NetworkClient signals
|
||||
self.network.connect("changed", self.on_network_changed)
|
||||
self.network.connect("wifi-device-added", self.on_wifi_device_added)
|
||||
self.network.connect("wifi-device-removed", self.on_wifi_device_removed)
|
||||
self.network.connect("ethernet-device-added", self.on_ethernet_device_added)
|
||||
self.network.connect("ethernet-device-removed", self.on_ethernet_device_removed)
|
||||
|
||||
# Connect to WiFi device if available
|
||||
if self.network.wifi_device:
|
||||
self.network.wifi_device.connect("changed", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-added", self.on_wifi_changed)
|
||||
self.network.wifi_device.connect("ap-removed", self.on_wifi_changed)
|
||||
|
||||
# Connect to Ethernet device if available
|
||||
if self.network.ethernet_device:
|
||||
self.network.ethernet_device.connect("changed", self.on_ethernet_changed)
|
||||
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
# Check ethernet first
|
||||
if (
|
||||
self.network.ethernet_device
|
||||
and self.network.ethernet_device.state == "activated"
|
||||
@@ -71,7 +61,6 @@ class WifiIndicator(Button):
|
||||
self.set_tooltip_text(tooltip)
|
||||
return
|
||||
|
||||
# Then check wifi
|
||||
if not self.network.wifi_device:
|
||||
self.wifi_label.set_markup(icons.cloud_off)
|
||||
self.progress_bar.value = 0
|
||||
@@ -132,19 +121,15 @@ class BluetoothIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="button-bar-bluetooth", **kwargs)
|
||||
|
||||
# Initialize BluetoothClient
|
||||
self.bluetooth = BluetoothClient()
|
||||
|
||||
# Create bluetooth label
|
||||
self.bt_label = Label(name="bluetooth-label", markup=icons.loader)
|
||||
self.add(self.bt_label)
|
||||
|
||||
# Connect signals
|
||||
self.bluetooth.connect("changed", self.on_bluetooth_changed)
|
||||
self.bluetooth.connect("device-added", self.on_device_added)
|
||||
self.bluetooth.connect("device-removed", self.on_device_removed)
|
||||
|
||||
# Initial state update
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
import config.data as data
|
||||
import psutil
|
||||
import utils.icons as icons
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
@@ -10,16 +11,8 @@ from fabric.widgets.label import Label
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from gi.repository import GLib
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MetricsProvider:
|
||||
"""
|
||||
Class responsible for obtaining centralized CPU, memory, disk usage, and battery metrics.
|
||||
It updates periodically so that all widgets querying it display the same values.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.gpu = []
|
||||
self.cpu = 0.0
|
||||
@@ -47,13 +40,11 @@ class MetricsProvider:
|
||||
return True
|
||||
|
||||
def _start_gpu_update_async(self):
|
||||
"""Starts a new GLib thread to run nvtop in the background."""
|
||||
self._gpu_update_running = True
|
||||
|
||||
GLib.Thread.new("nvtop-thread", lambda _: self._run_nvtop_in_thread(), None)
|
||||
|
||||
def _run_nvtop_in_thread(self):
|
||||
"""Runs nvtop via subprocess in a separate GLib thread."""
|
||||
output = None
|
||||
error_message = None
|
||||
try:
|
||||
@@ -61,34 +52,29 @@ class MetricsProvider:
|
||||
output = result
|
||||
except FileNotFoundError:
|
||||
error_message = "nvtop command not found."
|
||||
logger.warning(error_message)
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_message = f"nvtop failed with exit code {e.returncode}: {e.stderr.strip()}"
|
||||
logger.error(error_message)
|
||||
error_message = (
|
||||
f"nvtop failed with exit code {e.returncode}: {e.stderr.strip()}"
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
error_message = "nvtop command timed out."
|
||||
logger.error(error_message)
|
||||
except Exception as e:
|
||||
error_message = f"Unexpected error running nvtop: {e}"
|
||||
logger.error(error_message)
|
||||
|
||||
GLib.idle_add(self._process_gpu_output, output, error_message)
|
||||
self._gpu_update_running = False
|
||||
|
||||
def _process_gpu_output(self, output, error_message):
|
||||
"""Process nvtop JSON output on the main loop."""
|
||||
try:
|
||||
if error_message:
|
||||
logger.error(f"GPU update failed: {error_message}")
|
||||
self.gpu = []
|
||||
elif output:
|
||||
try:
|
||||
info = json.loads(output)
|
||||
if not info or not isinstance(info, list):
|
||||
logger.warning("Invalid nvtop data format")
|
||||
self.gpu = []
|
||||
return False
|
||||
|
||||
|
||||
gpu_utils = []
|
||||
for gpu in info:
|
||||
if not isinstance(gpu, dict):
|
||||
@@ -102,19 +88,15 @@ class MetricsProvider:
|
||||
gpu_utils.append(util_value)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
|
||||
self.gpu = gpu_utils if gpu_utils else []
|
||||
except (KeyError, ValueError, TypeError, AttributeError) as e:
|
||||
logger.error(f"Failed parsing nvtop JSON: {e}")
|
||||
except (KeyError, ValueError, TypeError, AttributeError):
|
||||
self.gpu = []
|
||||
else:
|
||||
logger.warning("nvtop returned no output.")
|
||||
self.gpu = []
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON decode error: {e}")
|
||||
except json.JSONDecodeError:
|
||||
self.gpu = []
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing nvtop output: {e}")
|
||||
except Exception:
|
||||
self.gpu = []
|
||||
|
||||
return False
|
||||
@@ -130,45 +112,42 @@ class MetricsProvider:
|
||||
result = subprocess.check_output(["nvtop", "-s"], text=True, timeout=5)
|
||||
if not result.strip():
|
||||
return []
|
||||
|
||||
|
||||
info = json.loads(result)
|
||||
if not info or not isinstance(info, list):
|
||||
return []
|
||||
|
||||
|
||||
valid_gpus = []
|
||||
for gpu in info:
|
||||
if not isinstance(gpu, dict):
|
||||
continue
|
||||
|
||||
|
||||
gpu_data = {
|
||||
'device_name': gpu.get('device_name', 'GPU'),
|
||||
'gpu_util': gpu.get('gpu_util', '0%'),
|
||||
'mem_util': gpu.get('mem_util', '0%'),
|
||||
'temperature': gpu.get('temperature', '0 C'),
|
||||
'power': gpu.get('power', '0 W')
|
||||
"device_name": gpu.get("device_name", "GPU"),
|
||||
"gpu_util": gpu.get("gpu_util", "0%"),
|
||||
"mem_util": gpu.get("mem_util", "0%"),
|
||||
"temperature": gpu.get("temperature", "0 C"),
|
||||
"power": gpu.get("power", "0 W"),
|
||||
}
|
||||
valid_gpus.append(gpu_data)
|
||||
|
||||
|
||||
return valid_gpus
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning("nvtop not found; GPU info unavailable.")
|
||||
return []
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"nvtop init sync failed: {e}")
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("nvtop init call timed out.")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Init JSON parse error: {e}")
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during GPU init: {e}")
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
shared_provider = MetricsProvider()
|
||||
|
||||
|
||||
class SingularMetric:
|
||||
def __init__(self, id, name, icon):
|
||||
self.name_markup = name
|
||||
@@ -205,6 +184,7 @@ class SingularMetric:
|
||||
def markup(self):
|
||||
return f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}"
|
||||
|
||||
|
||||
class Metrics(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="metrics", **kwargs)
|
||||
@@ -216,17 +196,53 @@ class Metrics(Button):
|
||||
all_visible=True,
|
||||
)
|
||||
|
||||
visible = getattr(data, "METRICS_VISIBLE", {'cpu': True, 'ram': True, 'swap': True, 'disk': True, 'gpu': True})
|
||||
disks = [SingularMetric("disk", f"DISK ({path})" if len(data.METRICS_DISKS) != 1 else "DISK", icons.disk)
|
||||
for path in data.METRICS_DISKS] if visible.get('disk', True) else []
|
||||
visible = getattr(
|
||||
data,
|
||||
"METRICS_VISIBLE",
|
||||
{"cpu": True, "ram": True, "swap": True, "disk": True, "gpu": True},
|
||||
)
|
||||
disks = (
|
||||
[
|
||||
SingularMetric(
|
||||
"disk",
|
||||
f"DISK ({path})" if len(data.METRICS_DISKS) != 1 else "DISK",
|
||||
icons.disk,
|
||||
)
|
||||
for path in data.METRICS_DISKS
|
||||
]
|
||||
if visible.get("disk", True)
|
||||
else []
|
||||
)
|
||||
|
||||
gpu_info = shared_provider.get_gpu_info()
|
||||
gpus = [SingularMetric(f"gpu", f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU", icons.gpu)
|
||||
for v in gpu_info] if visible.get('gpu', True) else []
|
||||
gpus = (
|
||||
[
|
||||
SingularMetric(
|
||||
"gpu",
|
||||
f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU",
|
||||
icons.gpu,
|
||||
)
|
||||
for v in gpu_info
|
||||
]
|
||||
if visible.get("gpu", True)
|
||||
else []
|
||||
)
|
||||
|
||||
self.cpu = SingularMetric("cpu", "CPU", icons.cpu) if visible.get('cpu', True) else None
|
||||
self.ram = SingularMetric("ram", "RAM", icons.memory) if visible.get('ram', True) else None
|
||||
self.swap = SingularMetric("swap", "SWAP", icons.swap) if visible.get('swap', True) else None
|
||||
self.cpu = (
|
||||
SingularMetric("cpu", "CPU", icons.cpu)
|
||||
if visible.get("cpu", True)
|
||||
else None
|
||||
)
|
||||
self.ram = (
|
||||
SingularMetric("ram", "RAM", icons.memory)
|
||||
if visible.get("ram", True)
|
||||
else None
|
||||
)
|
||||
self.swap = (
|
||||
SingularMetric("swap", "SWAP", icons.swap)
|
||||
if visible.get("swap", True)
|
||||
else None
|
||||
)
|
||||
self.disk = disks
|
||||
self.gpu = gpus
|
||||
|
||||
@@ -259,9 +275,12 @@ class Metrics(Button):
|
||||
def on_click(self, widget):
|
||||
if not data.VERTICAL:
|
||||
self.labels_visible = not self.labels_visible
|
||||
if self.cpu: self.cpu.revealer.set_reveal_child(self.labels_visible)
|
||||
if self.ram: self.ram.revealer.set_reveal_child(self.labels_visible)
|
||||
if self.swap: self.swap.revealer.set_reveal_child(self.labels_visible)
|
||||
if self.cpu:
|
||||
self.cpu.revealer.set_reveal_child(self.labels_visible)
|
||||
if self.ram:
|
||||
self.ram.revealer.set_reveal_child(self.labels_visible)
|
||||
if self.swap:
|
||||
self.swap.revealer.set_reveal_child(self.labels_visible)
|
||||
for disk in self.disk:
|
||||
disk.revealer.set_reveal_child(self.labels_visible)
|
||||
for gpu in self.gpu:
|
||||
@@ -289,11 +308,20 @@ class Metrics(Button):
|
||||
gpu.level.set_label(self._format_percentage(int(gpus[i])))
|
||||
|
||||
tooltip_metrics = []
|
||||
if self.disk: tooltip_metrics.extend(self.disk)
|
||||
if self.ram: tooltip_metrics.append(self.ram)
|
||||
if self.swap: tooltip_metrics.append(self.swap)
|
||||
if self.cpu: tooltip_metrics.append(self.cpu)
|
||||
if self.gpu: tooltip_metrics.extend(self.gpu)
|
||||
self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics]))
|
||||
if self.disk:
|
||||
tooltip_metrics.extend(self.disk)
|
||||
if self.ram:
|
||||
tooltip_metrics.append(self.ram)
|
||||
if self.swap:
|
||||
tooltip_metrics.append(self.swap)
|
||||
if self.cpu:
|
||||
tooltip_metrics.append(self.cpu)
|
||||
if self.gpu:
|
||||
tooltip_metrics.extend(self.gpu)
|
||||
self.set_tooltip_markup(
|
||||
(" - " if not data.VERTICAL else "\n").join(
|
||||
[v.markup() for v in tooltip_metrics]
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,39 +1,34 @@
|
||||
import os
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
|
||||
import config.data as data
|
||||
import requests
|
||||
import utils.icons as icons
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.stack import Stack
|
||||
from gi.repository import GLib, Gdk, GdkPixbuf
|
||||
from gi.repository import Gdk, GdkPixbuf, GLib
|
||||
from services.mpris import MediaManager
|
||||
from utils.circle_image import CircleImage
|
||||
from utils.audio_visualizer import SpectrumRender
|
||||
import urllib.parse
|
||||
import os
|
||||
import requests
|
||||
import tempfile
|
||||
from utils.circle_image import CircleImage
|
||||
|
||||
|
||||
class MusicPlayer(EventBox):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="music-player",
|
||||
events=["button-press"],
|
||||
**kwargs
|
||||
)
|
||||
super().__init__(name="music-player", events=["button-press"], **kwargs)
|
||||
|
||||
orientation = "v" if data.VERTICAL else "h"
|
||||
|
||||
# Create main container box
|
||||
self.main_container = Box(
|
||||
name="music-player-container",
|
||||
spacing=4,
|
||||
orientation=orientation,
|
||||
)
|
||||
|
||||
# Initialize media manager
|
||||
self._media_manager = MediaManager()
|
||||
self._media_manager.connect("player-appeared", self._on_player_changed)
|
||||
self._media_manager.connect("player-vanished", self._on_player_changed)
|
||||
@@ -41,84 +36,62 @@ class MusicPlayer(EventBox):
|
||||
self._current_player = None
|
||||
self._manually_selected_player = None
|
||||
self._update_timeout_id = None
|
||||
self._current_player_signals = [] # Track signal connections for current player
|
||||
self._current_player_signals = []
|
||||
|
||||
# Marquee animation variables
|
||||
self._marquee_timeout_id = None
|
||||
self._marquee_position = 0
|
||||
self._original_title = ""
|
||||
self._title_needs_marquee = False
|
||||
|
||||
# Thumbnail spinning animation variables
|
||||
self._spin_timeout_id = None
|
||||
self._spin_angle = 0
|
||||
|
||||
# Visualizer state
|
||||
self._show_visualizer = False
|
||||
|
||||
# Create music player view with stack for visualizer/text switching
|
||||
self._create_music_player_view()
|
||||
|
||||
# Set the main container as child
|
||||
self.add(self.main_container)
|
||||
|
||||
# Connect click handler for the entire music player
|
||||
self.connect("button-press-event", self._on_music_player_clicked)
|
||||
|
||||
# Hide initially - will show when media players become available
|
||||
self.set_visible(False)
|
||||
|
||||
# Delay initial update to allow MediaManager to initialize properly
|
||||
GLib.timeout_add(100, self._delayed_initial_update)
|
||||
|
||||
def _create_music_player_view(self):
|
||||
"""Create the music player view with stack for visualizer/text switching like PlayerSmall"""
|
||||
orientation = "v" if data.VERTICAL else "h"
|
||||
|
||||
# Create CircleImage for album art thumbnails (clickable to toggle visualizer)
|
||||
self.album_thumbnail = CircleImage(
|
||||
name="music-album-thumbnail",
|
||||
size=34, # Appropriate size for dock
|
||||
size=34,
|
||||
v_align="center",
|
||||
visible=True, # Always visible, will show default or album art
|
||||
visible=True,
|
||||
)
|
||||
|
||||
# Make album thumbnail clickable to toggle visualizer
|
||||
self.album_thumbnail.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
|
||||
self.album_thumbnail.connect("button-press-event", self._on_thumbnail_clicked)
|
||||
|
||||
# Create track info display
|
||||
self.track_label = Label(
|
||||
name="music-track",
|
||||
label="No Media",
|
||||
ellipsize=0, # No ellipsize for marquee
|
||||
ellipsize=0,
|
||||
h_align="center",
|
||||
)
|
||||
# Fixed width to prevent box size changes
|
||||
self.track_label.set_size_request(100, -1)
|
||||
self.track_label.set_hexpand(False)
|
||||
self.track_label.set_vexpand(False)
|
||||
|
||||
# Create spectrum visualizer lazily to save memory
|
||||
self.cavalcade = None
|
||||
self.cavalcade_box = None
|
||||
|
||||
# Create stack for crossfade between visualizer and text
|
||||
self.center_stack = Stack(
|
||||
name="music-player-stack",
|
||||
transition_type="crossfade",
|
||||
transition_duration=100,
|
||||
v_align="center",
|
||||
v_expand=False,
|
||||
children=[self.track_label] # Start with just track label
|
||||
children=[self.track_label],
|
||||
)
|
||||
# Start with track label visible
|
||||
self.center_stack.set_visible_child(self.track_label)
|
||||
|
||||
# Create control buttons
|
||||
self._create_controls()
|
||||
|
||||
# Create main music player layout using CenterBox like PlayerSmall
|
||||
self.music_player_box = CenterBox(
|
||||
name="music-player-centerbox",
|
||||
orientation=orientation,
|
||||
@@ -131,13 +104,10 @@ class MusicPlayer(EventBox):
|
||||
end_children=self.controls_box,
|
||||
)
|
||||
|
||||
# Add to main container
|
||||
self.main_container.add(self.music_player_box)
|
||||
|
||||
def _load_album_art(self, art_url):
|
||||
"""Load album art from URL and set it to the CircleImage"""
|
||||
# Cache the last loaded URL to avoid reloading the same image
|
||||
if hasattr(self, '_last_art_url') and self._last_art_url == art_url:
|
||||
if hasattr(self, "_last_art_url") and self._last_art_url == art_url:
|
||||
return
|
||||
|
||||
if not art_url:
|
||||
@@ -146,9 +116,8 @@ class MusicPlayer(EventBox):
|
||||
return
|
||||
|
||||
try:
|
||||
if art_url.startswith('file://'):
|
||||
# Handle local file URLs
|
||||
file_path = urllib.parse.unquote(art_url[7:]) # Remove 'file://' prefix
|
||||
if art_url.startswith("file://"):
|
||||
file_path = urllib.parse.unquote(art_url[7:])
|
||||
if os.path.exists(file_path):
|
||||
self.album_thumbnail.set_image_from_file(file_path)
|
||||
self._show_album_art()
|
||||
@@ -156,22 +125,20 @@ class MusicPlayer(EventBox):
|
||||
else:
|
||||
self._load_fallback_image()
|
||||
self._last_art_url = None
|
||||
elif art_url.startswith(('http://', 'https://')):
|
||||
# Handle remote URLs by downloading them first
|
||||
elif art_url.startswith(("http://", "https://")):
|
||||
response = requests.get(art_url)
|
||||
if response.status_code == 200:
|
||||
# Create a temporary file
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".jpg"
|
||||
) as temp_file:
|
||||
temp_file.write(response.content)
|
||||
temp_path = temp_file.name
|
||||
|
||||
# Load the image from the temporary file
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(temp_path)
|
||||
self.album_thumbnail.set_image_from_pixbuf(pixbuf)
|
||||
self._show_album_art()
|
||||
self._last_art_url = art_url
|
||||
|
||||
# Clean up the temporary file
|
||||
os.unlink(temp_path)
|
||||
else:
|
||||
self._load_fallback_image()
|
||||
@@ -184,7 +151,6 @@ class MusicPlayer(EventBox):
|
||||
self._last_art_url = None
|
||||
|
||||
def _load_fallback_image(self):
|
||||
"""Load fallback image from current wallpaper when no album art is available"""
|
||||
fallback = os.path.expanduser("~/.current.wall")
|
||||
|
||||
try:
|
||||
@@ -197,67 +163,54 @@ class MusicPlayer(EventBox):
|
||||
self._hide_album_art()
|
||||
|
||||
def _show_album_art(self):
|
||||
"""Show album art thumbnail"""
|
||||
self.album_thumbnail.set_visible(True)
|
||||
# Start spinning animation when showing album art
|
||||
self._start_spin_animation()
|
||||
|
||||
def _hide_album_art(self):
|
||||
"""Hide album art thumbnail"""
|
||||
self.album_thumbnail.set_visible(False)
|
||||
# Stop spinning animation when hiding album art
|
||||
self._stop_spin_animation()
|
||||
|
||||
def _start_spin_animation(self):
|
||||
"""Start the thumbnail spinning animation"""
|
||||
if not self._spin_timeout_id:
|
||||
self._spin_timeout_id = GLib.timeout_add(100, self._animate_spin) # 100ms to save CPU
|
||||
self._spin_timeout_id = GLib.timeout_add(
|
||||
100, self._animate_spin
|
||||
) # 100ms to save CPU
|
||||
|
||||
def _stop_spin_animation(self):
|
||||
"""Stop the thumbnail spinning animation"""
|
||||
if self._spin_timeout_id:
|
||||
GLib.source_remove(self._spin_timeout_id)
|
||||
self._spin_timeout_id = None
|
||||
# Reset angle to 0
|
||||
self._spin_angle = 0
|
||||
self.album_thumbnail.angle = 0
|
||||
|
||||
def _animate_spin(self):
|
||||
"""Animate the thumbnail spinning"""
|
||||
if not self._current_player or not self.album_thumbnail.get_visible():
|
||||
return False
|
||||
|
||||
# Only spin when music is playing
|
||||
if self._current_player.status == "playing":
|
||||
self._spin_angle = (self._spin_angle + 5) % 360 # Rotate 2 degrees per frame
|
||||
self._spin_angle = (self._spin_angle + 5) % 360
|
||||
self.album_thumbnail.angle = self._spin_angle
|
||||
|
||||
return True # Continue animation
|
||||
return True
|
||||
|
||||
def _update_player_indicators(self):
|
||||
"""Update the player indicators to show all available players"""
|
||||
# Get all available players
|
||||
available_players = self._media_manager.players
|
||||
|
||||
# Only show indicators if there are multiple players to save memory
|
||||
if len(available_players) <= 1:
|
||||
# Clear indicators if only one or no players
|
||||
for child in self.player_indicators_box.get_children():
|
||||
self.player_indicators_box.remove(child)
|
||||
return
|
||||
|
||||
# Clear existing indicators
|
||||
for child in self.player_indicators_box.get_children():
|
||||
self.player_indicators_box.remove(child)
|
||||
|
||||
current_player_name = self._current_player.player_name if self._current_player else "None"
|
||||
current_player_name = (
|
||||
self._current_player.player_name if self._current_player else "None"
|
||||
)
|
||||
|
||||
# Create indicators for each player (only when multiple players exist)
|
||||
for player in available_players:
|
||||
player_name = player.player_name or "Unknown"
|
||||
player_icon_markup = self._get_player_icon(player_name)
|
||||
|
||||
# Create clickable button with indicator label
|
||||
indicator_button = Button(
|
||||
name="music-player-indicator-button",
|
||||
child=Label(
|
||||
@@ -267,8 +220,7 @@ class MusicPlayer(EventBox):
|
||||
can_focus=False,
|
||||
)
|
||||
|
||||
# Set tooltip with player info
|
||||
is_current_player = (player.player_name == current_player_name)
|
||||
is_current_player = player.player_name == current_player_name
|
||||
tooltip_text = f"Player: {player_name}"
|
||||
if is_current_player:
|
||||
tooltip_text += " (Active)"
|
||||
@@ -276,29 +228,26 @@ class MusicPlayer(EventBox):
|
||||
tooltip_text += " - Click to switch"
|
||||
indicator_button.set_tooltip_text(tooltip_text)
|
||||
|
||||
# Add visual distinction for active player (you can style this with CSS)
|
||||
if is_current_player:
|
||||
indicator_button.get_child().set_name("music-player-indicator-active")
|
||||
else:
|
||||
indicator_button.get_child().set_name("music-player-indicator")
|
||||
|
||||
# Connect click handler with player reference
|
||||
indicator_button.connect("clicked", self._on_player_indicator_clicked, player)
|
||||
indicator_button.connect(
|
||||
"clicked", self._on_player_indicator_clicked, player
|
||||
)
|
||||
|
||||
self.player_indicators_box.add(indicator_button)
|
||||
|
||||
# Force refresh the indicators container
|
||||
self.player_indicators_box.show_all()
|
||||
|
||||
def _create_controls(self):
|
||||
"""Create the media control buttons"""
|
||||
self.controls_box = Box(
|
||||
name="music-controls",
|
||||
orientation="h" if not data.VERTICAL else "v",
|
||||
spacing=2,
|
||||
)
|
||||
|
||||
# Previous button
|
||||
self.prev_button = Button(
|
||||
name="music-prev",
|
||||
child=Label(markup=icons.prev),
|
||||
@@ -307,7 +256,6 @@ class MusicPlayer(EventBox):
|
||||
self.prev_button.connect("clicked", self._on_previous_clicked)
|
||||
self.prev_button.set_tooltip_text("Previous Track")
|
||||
|
||||
# Play/Pause button
|
||||
self.play_pause_button = Button(
|
||||
name="music-play-pause",
|
||||
child=Label(name="play-pause-icon", markup=icons.play),
|
||||
@@ -316,7 +264,6 @@ class MusicPlayer(EventBox):
|
||||
self.play_pause_button.connect("clicked", self._on_play_pause_clicked)
|
||||
self.play_pause_button.set_tooltip_text("Play/Pause")
|
||||
|
||||
# Next button
|
||||
self.next_button = Button(
|
||||
name="music-next",
|
||||
child=Label(markup=icons.next),
|
||||
@@ -325,34 +272,32 @@ class MusicPlayer(EventBox):
|
||||
self.next_button.connect("clicked", self._on_next_clicked)
|
||||
self.next_button.set_tooltip_text("Next Track")
|
||||
|
||||
# Player indicators container
|
||||
self.player_indicators_box = Box(
|
||||
name="music-player-indicators",
|
||||
orientation="h" if not data.VERTICAL else "v",
|
||||
spacing=2,
|
||||
)
|
||||
|
||||
# Add buttons and indicators to controls
|
||||
self.controls_box.add(self.prev_button)
|
||||
self.controls_box.add(self.play_pause_button)
|
||||
self.controls_box.add(self.next_button)
|
||||
self.controls_box.add(self.player_indicators_box)
|
||||
|
||||
def _delayed_initial_update(self):
|
||||
"""Delayed initial update to allow MediaManager to initialize"""
|
||||
self._update_display()
|
||||
return False # Don't repeat this timeout
|
||||
return False
|
||||
|
||||
def _connect_player_signals(self, player):
|
||||
"""Connect to player signals for immediate updates"""
|
||||
if player:
|
||||
# Connect to metadata changes for immediate track/thumbnail updates
|
||||
metadata_signal = player.connect("metadata_changed", self._on_metadata_changed)
|
||||
status_signal = player.connect("playback_status_changed", self._on_playback_status_changed)
|
||||
metadata_signal = player.connect(
|
||||
"metadata_changed", self._on_metadata_changed
|
||||
)
|
||||
status_signal = player.connect(
|
||||
"playback_status_changed", self._on_playback_status_changed
|
||||
)
|
||||
self._current_player_signals = [metadata_signal, status_signal]
|
||||
|
||||
def _disconnect_player_signals(self):
|
||||
"""Disconnect from current player signals"""
|
||||
if self._current_player and self._current_player_signals:
|
||||
for signal_id in self._current_player_signals:
|
||||
try:
|
||||
@@ -362,50 +307,41 @@ class MusicPlayer(EventBox):
|
||||
self._current_player_signals = []
|
||||
|
||||
def _on_metadata_changed(self, *_args):
|
||||
"""Handle immediate metadata changes (track, artist, album art)"""
|
||||
GLib.idle_add(self._update_display)
|
||||
|
||||
def _on_playback_status_changed(self, *_args):
|
||||
"""Handle immediate playback status changes"""
|
||||
GLib.idle_add(self._update_display)
|
||||
|
||||
def _on_player_changed(self, *_args):
|
||||
"""Handle player appearance/disappearance"""
|
||||
GLib.idle_add(self._update_display)
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the display based on current player state"""
|
||||
|
||||
# Store previous player to detect changes
|
||||
previous_player = self._current_player
|
||||
|
||||
# Determine current player (manual selection takes priority)
|
||||
if self._manually_selected_player:
|
||||
# Check if manually selected player is still available
|
||||
manually_selected_available = next(
|
||||
(p for p in self._media_manager.players
|
||||
if p.player_name == self._manually_selected_player.player_name),
|
||||
None
|
||||
(
|
||||
p
|
||||
for p in self._media_manager.players
|
||||
if p.player_name == self._manually_selected_player.player_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if manually_selected_available:
|
||||
self._current_player = manually_selected_available
|
||||
self._manually_selected_player = manually_selected_available
|
||||
else:
|
||||
# Manual player no longer available, clear selection
|
||||
self._manually_selected_player = None
|
||||
self._current_player = self._media_manager.current_player
|
||||
else:
|
||||
self._current_player = self._media_manager.current_player
|
||||
|
||||
# Connect to new player signals if player changed
|
||||
if self._current_player != previous_player:
|
||||
self._disconnect_player_signals()
|
||||
if self._current_player:
|
||||
self._connect_player_signals(self._current_player)
|
||||
|
||||
|
||||
if not self._current_player:
|
||||
# No players available, hide the component
|
||||
self.set_visible(False)
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
@@ -413,42 +349,33 @@ class MusicPlayer(EventBox):
|
||||
if self._marquee_timeout_id:
|
||||
GLib.source_remove(self._marquee_timeout_id)
|
||||
self._marquee_timeout_id = None
|
||||
# Stop spinning animation when no player
|
||||
self._stop_spin_animation()
|
||||
return False
|
||||
|
||||
self.set_visible(True)
|
||||
|
||||
# Try to load album art, use fallback if not available
|
||||
album_art_url = self._current_player.album_image_url
|
||||
if album_art_url:
|
||||
self._load_album_art(album_art_url)
|
||||
else:
|
||||
# No album art available, use fallback image
|
||||
self._load_fallback_image()
|
||||
|
||||
# Update stack display (text vs visualizer)
|
||||
self._update_stack_display()
|
||||
|
||||
# Update player indicators to show all available players
|
||||
self._update_player_indicators()
|
||||
|
||||
# Update play/pause button
|
||||
play_pause_icon = self.play_pause_button.get_child()
|
||||
if self._current_player.status == "playing":
|
||||
play_pause_icon.set_markup(icons.pause)
|
||||
else:
|
||||
play_pause_icon.set_markup(icons.play)
|
||||
|
||||
# Update track info with marquee animation
|
||||
track_title = self._current_player.track_title or "Unknown Track"
|
||||
max_display_chars = 12
|
||||
|
||||
self._original_title = track_title
|
||||
self._title_needs_marquee = len(track_title) > max_display_chars
|
||||
|
||||
# Reset marquee if title changed
|
||||
if self._original_title != getattr(self, '_last_original_title', ''):
|
||||
if self._original_title != getattr(self, "_last_original_title", ""):
|
||||
self._last_original_title = self._original_title
|
||||
self._marquee_position = 0
|
||||
self._start_marquee_animation()
|
||||
@@ -456,12 +383,10 @@ class MusicPlayer(EventBox):
|
||||
self._update_marquee_text()
|
||||
self._update_tooltips()
|
||||
|
||||
# Update button sensitivity
|
||||
self.prev_button.set_sensitive(self._current_player.can_go_previous)
|
||||
self.next_button.set_sensitive(self._current_player.can_go_next)
|
||||
self.play_pause_button.set_sensitive(self._current_player.can_pause)
|
||||
|
||||
# Remove all polling - rely entirely on signals for updates
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
self._update_timeout_id = None
|
||||
@@ -469,8 +394,6 @@ class MusicPlayer(EventBox):
|
||||
return True
|
||||
|
||||
def _start_marquee_animation(self):
|
||||
"""Start or restart the marquee animation"""
|
||||
# Stop existing marquee animation
|
||||
if self._marquee_timeout_id:
|
||||
GLib.source_remove(self._marquee_timeout_id)
|
||||
self._marquee_timeout_id = None
|
||||
@@ -479,13 +402,12 @@ class MusicPlayer(EventBox):
|
||||
self._marquee_timeout_id = GLib.timeout_add(250, self._animate_marquee)
|
||||
|
||||
def _animate_marquee(self):
|
||||
"""Animate the marquee scrolling"""
|
||||
if not self._current_player:
|
||||
return False
|
||||
|
||||
self._marquee_position += 1
|
||||
title_length = len(self._original_title)
|
||||
loop_length = title_length + 3 # Add spacing between loops
|
||||
loop_length = title_length + 3
|
||||
|
||||
if self._marquee_position >= loop_length:
|
||||
self._marquee_position = 0
|
||||
@@ -494,7 +416,6 @@ class MusicPlayer(EventBox):
|
||||
return True
|
||||
|
||||
def _update_marquee_text(self):
|
||||
"""Update the displayed text based on marquee position"""
|
||||
if not self._current_player:
|
||||
return
|
||||
|
||||
@@ -509,7 +430,7 @@ class MusicPlayer(EventBox):
|
||||
display_title = extended_text[start_pos:end_pos]
|
||||
else:
|
||||
part1 = extended_text[start_pos:]
|
||||
part2 = self._original_title[:end_pos - len(extended_text)]
|
||||
part2 = self._original_title[: end_pos - len(extended_text)]
|
||||
display_title = (part1 + part2)[:max_display_chars]
|
||||
else:
|
||||
display_title = self._original_title[:max_display_chars]
|
||||
@@ -517,7 +438,6 @@ class MusicPlayer(EventBox):
|
||||
self.track_label.set_label(display_title)
|
||||
|
||||
def _get_player_icon(self, player_name):
|
||||
"""Get the appropriate icon for a player based on its name"""
|
||||
if not player_name:
|
||||
return icons.disc
|
||||
|
||||
@@ -527,102 +447,82 @@ class MusicPlayer(EventBox):
|
||||
return icons.firefox
|
||||
elif "spotify" in player_name_lower:
|
||||
return icons.spotify
|
||||
elif any(browser in player_name_lower for browser in [
|
||||
"chromium", "chrome", "edge", "brave", "opera", "vivaldi"
|
||||
]):
|
||||
elif any(
|
||||
browser in player_name_lower
|
||||
for browser in ["chromium", "chrome", "edge", "brave", "opera", "vivaldi"]
|
||||
):
|
||||
return icons.chromium
|
||||
else:
|
||||
return icons.disc
|
||||
|
||||
def _update_tooltips(self):
|
||||
"""Update tooltips with full track information"""
|
||||
if not self._current_player:
|
||||
return
|
||||
|
||||
# Get full track information for tooltip
|
||||
full_track_title = self._current_player.track_title or "Unknown Track"
|
||||
track_artist = self._current_player.track_artist or "Unknown Artist"
|
||||
player_name = self._current_player.player_name or "Media Player"
|
||||
|
||||
# Create tooltip text with full information
|
||||
tooltip_text = f"{full_track_title}\nby {track_artist}\n\nPlayer: {player_name}"
|
||||
self.track_label.set_tooltip_text(tooltip_text)
|
||||
|
||||
def _on_play_pause_clicked(self, _button):
|
||||
"""Handle play/pause button click"""
|
||||
if self._current_player:
|
||||
self._current_player.play_pause()
|
||||
# No need for manual update - signal will trigger it
|
||||
|
||||
def _on_previous_clicked(self, _button):
|
||||
"""Handle previous button click"""
|
||||
if self._current_player:
|
||||
self._current_player.previous()
|
||||
# No need for manual update - signal will trigger it
|
||||
|
||||
def _on_next_clicked(self, _button):
|
||||
"""Handle next button click"""
|
||||
if self._current_player:
|
||||
self._current_player.next()
|
||||
# No need for manual update - signal will trigger it
|
||||
|
||||
def _on_player_indicator_clicked(self, _button, player):
|
||||
"""Handle player indicator click to switch players"""
|
||||
if player:
|
||||
self._manually_selected_player = player
|
||||
self._update_display()
|
||||
|
||||
def _on_thumbnail_clicked(self, _widget, _event):
|
||||
"""Handle thumbnail click to toggle visualizer"""
|
||||
if self._current_player:
|
||||
self._show_visualizer = not self._show_visualizer
|
||||
self._update_stack_display()
|
||||
return True # Stop event propagation
|
||||
return True
|
||||
|
||||
def _on_music_player_clicked(self, _widget, _event):
|
||||
"""Handle music player click to toggle between text and visualizer view"""
|
||||
if self._current_player:
|
||||
self._show_visualizer = not self._show_visualizer
|
||||
self._update_stack_display()
|
||||
return True
|
||||
|
||||
def _create_visualizer_if_needed(self):
|
||||
"""Create visualizer lazily to save memory"""
|
||||
if not self.cavalcade:
|
||||
self.cavalcade = SpectrumRender()
|
||||
self.cavalcade_box = self.cavalcade.get_spectrum_box()
|
||||
self.center_stack.add(self.cavalcade_box)
|
||||
|
||||
def _destroy_visualizer(self):
|
||||
"""Destroy visualizer to free memory when not needed"""
|
||||
if self.cavalcade_box:
|
||||
self.center_stack.remove(self.cavalcade_box)
|
||||
self.cavalcade_box = None
|
||||
if self.cavalcade and hasattr(self.cavalcade, 'cava'):
|
||||
if self.cavalcade and hasattr(self.cavalcade, "cava"):
|
||||
self.cavalcade.cava.close()
|
||||
self.cavalcade = None
|
||||
|
||||
def _update_stack_display(self):
|
||||
"""Update stack to show either text or visualizer"""
|
||||
if self._show_visualizer and self._current_player:
|
||||
# Create visualizer only when needed
|
||||
self._create_visualizer_if_needed()
|
||||
if self.cavalcade_box:
|
||||
self.cavalcade_box.set_visible(True)
|
||||
self.cavalcade_box.show_all()
|
||||
self.center_stack.set_visible_child(self.cavalcade_box)
|
||||
else:
|
||||
# Show text label in stack
|
||||
self.center_stack.set_visible_child(self.track_label)
|
||||
# Destroy visualizer to save memory when not in use
|
||||
self._destroy_visualizer()
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup when component is destroyed"""
|
||||
# Disconnect player signals
|
||||
self._disconnect_player_signals()
|
||||
|
||||
# Clean up all timeouts
|
||||
if self._update_timeout_id:
|
||||
GLib.source_remove(self._update_timeout_id)
|
||||
if self._marquee_timeout_id:
|
||||
@@ -630,5 +530,4 @@ class MusicPlayer(EventBox):
|
||||
if self._spin_timeout_id:
|
||||
GLib.source_remove(self._spin_timeout_id)
|
||||
|
||||
# Clean up visualizer
|
||||
self._destroy_visualizer()
|
||||
@@ -1,170 +0,0 @@
|
||||
import config.data as data
|
||||
from gi.repository import Gdk, GdkPixbuf, GLib, Gray, Gtk
|
||||
from fabric.widgets.box import Box
|
||||
import logging
|
||||
import gi
|
||||
|
||||
gi.require_version("Gray", "0.1")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemTray(Box):
|
||||
def __init__(
|
||||
self, pixel_size: int = 20, refresh_interval: int = 1, **kwargs
|
||||
) -> None:
|
||||
orientation = (
|
||||
Gtk.Orientation.HORIZONTAL
|
||||
if not data.VERTICAL
|
||||
else Gtk.Orientation.VERTICAL
|
||||
)
|
||||
super().__init__(name="tray", orientation=orientation, spacing=8, **kwargs)
|
||||
self.enabled = True
|
||||
super().set_visible(False)
|
||||
self.pixel_size = pixel_size
|
||||
self.refresh_interval = refresh_interval
|
||||
|
||||
self.buttons_by_id = {}
|
||||
self.items_by_id = {}
|
||||
|
||||
self.watcher = Gray.Watcher()
|
||||
self.watcher.connect("item-added", self.on_watcher_item_added)
|
||||
|
||||
GLib.timeout_add_seconds(self.refresh_interval, self._refresh_all_items)
|
||||
|
||||
def set_visible(self, visible: bool):
|
||||
self.enabled = visible
|
||||
self._update_visibility()
|
||||
|
||||
def _update_visibility(self):
|
||||
has = len(self.get_children()) > 0
|
||||
super().set_visible(self.enabled and has)
|
||||
|
||||
def _get_item_pixbuf(self, item: Gray.Item) -> GdkPixbuf.Pixbuf:
|
||||
try:
|
||||
pm = Gray.get_pixmap_for_pixmaps(item.get_icon_pixmaps(), self.pixel_size)
|
||||
if pm:
|
||||
return pm.as_pixbuf(self.pixel_size, GdkPixbuf.InterpType.HYPER)
|
||||
|
||||
name = item.get_icon_name()
|
||||
theme = Gtk.IconTheme.new()
|
||||
path = item.get_icon_theme_path()
|
||||
if path:
|
||||
theme.prepend_search_path(path)
|
||||
return theme.load_icon(
|
||||
name, self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
except GLib.Error as e:
|
||||
logger.debug(f"Icon load error {e}")
|
||||
return Gtk.IconTheme.get_default().load_icon(
|
||||
"image-missing", self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
|
||||
def _refresh_item_ui(self, identifier: str, item: Gray.Item, button: Gtk.Button):
|
||||
pixbuf = self._get_item_pixbuf(item)
|
||||
img = button.get_image()
|
||||
if isinstance(img, Gtk.Image):
|
||||
img.set_from_pixbuf(pixbuf)
|
||||
else:
|
||||
new = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
button.set_image(new)
|
||||
new.show()
|
||||
tip = None
|
||||
if hasattr(item, "get_tooltip_text"):
|
||||
tip = item.get_tooltip_text()
|
||||
elif hasattr(item, "get_title"):
|
||||
tip = item.get_title()
|
||||
if tip:
|
||||
button.set_tooltip_text(tip)
|
||||
else:
|
||||
button.set_has_tooltip(False)
|
||||
|
||||
def _refresh_all_items(self) -> bool:
|
||||
for ident, item in self.items_by_id.items():
|
||||
btn = self.buttons_by_id.get(ident)
|
||||
if btn:
|
||||
self._refresh_item_ui(ident, item, btn)
|
||||
return True
|
||||
|
||||
def on_watcher_item_added(self, _, identifier: str):
|
||||
item = self.watcher.get_item_for_identifier(identifier)
|
||||
if not item:
|
||||
return
|
||||
|
||||
if identifier in self.buttons_by_id:
|
||||
self.buttons_by_id[identifier].destroy()
|
||||
del self.buttons_by_id[identifier]
|
||||
del self.items_by_id[identifier]
|
||||
|
||||
btn = self.do_bake_item_button(item)
|
||||
self.buttons_by_id[identifier] = btn
|
||||
self.items_by_id[identifier] = item
|
||||
|
||||
item.connect(
|
||||
"notify::icon-pixmaps",
|
||||
lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn),
|
||||
)
|
||||
item.connect(
|
||||
"notify::icon-name",
|
||||
lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn),
|
||||
)
|
||||
|
||||
try:
|
||||
item.connect(
|
||||
"updated", lambda itm: self._refresh_item_ui(identifier, itm, btn)
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
item.connect(
|
||||
"removed", lambda itm: self.on_item_instance_removed(identifier, itm)
|
||||
)
|
||||
|
||||
self.add(btn)
|
||||
btn.show_all()
|
||||
self._update_visibility()
|
||||
|
||||
def do_bake_item_button(self, item: Gray.Item) -> Gtk.Button:
|
||||
btn = Gtk.Button()
|
||||
btn.connect("button-press-event", lambda b, e: self.on_button_click(b, item, e))
|
||||
img = Gtk.Image.new_from_pixbuf(self._get_item_pixbuf(item))
|
||||
btn.set_image(img)
|
||||
tip = (
|
||||
item.get_tooltip_text()
|
||||
if hasattr(item, "get_tooltip_text")
|
||||
else getattr(item, "get_title", lambda: None)()
|
||||
)
|
||||
if tip:
|
||||
btn.set_tooltip_text(tip)
|
||||
return btn
|
||||
|
||||
def on_item_instance_removed(self, identifier: str, removed_item: Gray.Item):
|
||||
if self.items_by_id.get(identifier) is removed_item:
|
||||
btn = self.buttons_by_id.pop(identifier, None)
|
||||
self.items_by_id.pop(identifier, None)
|
||||
if btn:
|
||||
btn.destroy()
|
||||
self._update_visibility()
|
||||
|
||||
def on_button_click(
|
||||
self, button: Gtk.Button, item: Gray.Item, event: Gdk.EventButton
|
||||
):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
try:
|
||||
item.activate(int(event.x_root), int(event.y_root))
|
||||
except Exception as e:
|
||||
logger.error(f"Activate error: {e}")
|
||||
elif event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu = getattr(item, "get_menu", lambda: None)()
|
||||
if isinstance(menu, Gtk.Menu):
|
||||
menu.popup_at_widget(
|
||||
button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event
|
||||
)
|
||||
else:
|
||||
cm = getattr(item, "context_menu", None)
|
||||
if cm:
|
||||
try:
|
||||
cm(int(event.x_root), int(event.y_root))
|
||||
except Exception as e:
|
||||
logger.error(f"ContextMenu error: {e}")
|
||||
@@ -1,21 +1,19 @@
|
||||
import json
|
||||
import subprocess
|
||||
from fabric.hyprland.service import Hyprland
|
||||
from fabric.hyprland.widgets import Workspaces, WorkspaceButton
|
||||
from fabric.widgets.button import Button
|
||||
import config.data as data
|
||||
from fabric.widgets.box import Box
|
||||
|
||||
import config.data as data
|
||||
from fabric.hyprland.service import Hyprland
|
||||
from fabric.hyprland.widgets import WorkspaceButton, Workspaces
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
|
||||
CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"]
|
||||
|
||||
|
||||
class Workspace(Button):
|
||||
def __init__(self):
|
||||
# Determine if invert style should be applied
|
||||
should_invert = (
|
||||
data.DOCK_THEME in ["Dense", "Edge"] or
|
||||
(data.DOCK_THEME == "Pills" and data.DOCK_POSITION in ["Left", "Right"])
|
||||
should_invert = data.DOCK_THEME in ["Dense", "Edge"] or (
|
||||
data.DOCK_THEME == "Pills" and data.DOCK_POSITION in ["Left", "Right"]
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -41,7 +39,6 @@ class Workspace(Button):
|
||||
return json.loads(active.decode("utf-8"))["name"]
|
||||
|
||||
def _get_active_workspaces(self):
|
||||
"""Get list of workspaces that have windows"""
|
||||
clients_data = self.connection.send_command("j/clients").reply
|
||||
clients = json.loads(clients_data.decode("utf-8"))
|
||||
|
||||
@@ -56,11 +53,9 @@ class Workspace(Button):
|
||||
)
|
||||
|
||||
def _on_click(self, _widget):
|
||||
"""Handle button click to cycle through workspaces"""
|
||||
self._cycle_workspace()
|
||||
|
||||
def _cycle_workspace(self):
|
||||
"""Core logic to cycle through workspaces"""
|
||||
active_workspaces = self._get_active_workspaces()
|
||||
|
||||
if not active_workspaces:
|
||||
@@ -87,7 +82,6 @@ class Workspace(Button):
|
||||
self.set_label(str(self.activeWorkspace))
|
||||
|
||||
|
||||
# Create workspace components using Workspaces widget
|
||||
workspaces = Workspaces(
|
||||
name="workspaces",
|
||||
invert_scroll=True,
|
||||
@@ -132,10 +126,8 @@ workspaces_num = Workspaces(
|
||||
)
|
||||
|
||||
|
||||
# Create the workspace button instance
|
||||
workspaceactive = Workspace()
|
||||
|
||||
# Export the workspace widget based on configuration
|
||||
workspace = Box(
|
||||
name="workspaces-container",
|
||||
children=[
|
||||
|
||||
Reference in new issue
Block a user