feat: control center
This commit is contained in:
34
sims/main.py
34
sims/main.py
@@ -25,10 +25,12 @@ from .modules.launcher.clipboard import ClipboardMenu
|
|||||||
from .modules.launcher.power import PowerMenu
|
from .modules.launcher.power import PowerMenu
|
||||||
from .modules.launcher.screenrec import ScreenrecMenu
|
from .modules.launcher.screenrec import ScreenrecMenu
|
||||||
from .modules.launcher.screenshot import ScreenshotMenu
|
from .modules.launcher.screenshot import ScreenshotMenu
|
||||||
from .modules.notification_center import NotificationCenter
|
from .modules.calendar import CalendarService
|
||||||
|
from .modules.control_center import ControlCenter
|
||||||
from .modules.notifications import NotificationToasts
|
from .modules.notifications import NotificationToasts
|
||||||
from .modules.stylix import get_stylix_css_path
|
from .modules.stylix import get_stylix_css_path
|
||||||
from .config import NOTIFICATIONS, POWER, SCREENREC, STYLIX
|
from .modules.vinyl import VinylButton
|
||||||
|
from .config import CALENDAR, NOTIFICATIONS, POWER, SCREENREC, STYLIX, VINYL
|
||||||
from .services.fenster import get_i3_connection
|
from .services.fenster import get_i3_connection
|
||||||
from .services.notification_history import NotificationHistoryService
|
from .services.notification_history import NotificationHistoryService
|
||||||
from .services.screenrec import ScreenrecService
|
from .services.screenrec import ScreenrecService
|
||||||
@@ -57,7 +59,6 @@ if SCREENREC.get("enable", False):
|
|||||||
notifications_service: Notifications | None = None
|
notifications_service: Notifications | None = None
|
||||||
notification_history: NotificationHistoryService | None = None
|
notification_history: NotificationHistoryService | None = None
|
||||||
notification_toasts: NotificationToasts | None = None
|
notification_toasts: NotificationToasts | None = None
|
||||||
notification_center: NotificationCenter | None = None
|
|
||||||
if NOTIFICATIONS.get("enable", False):
|
if NOTIFICATIONS.get("enable", False):
|
||||||
notifications_service = Notifications()
|
notifications_service = Notifications()
|
||||||
notification_history = NotificationHistoryService(
|
notification_history = NotificationHistoryService(
|
||||||
@@ -73,8 +74,18 @@ if NOTIFICATIONS.get("enable", False):
|
|||||||
width=NOTIFICATIONS.get("width", 360),
|
width=NOTIFICATIONS.get("width", 360),
|
||||||
timeout_ms=NOTIFICATIONS.get("timeout_ms", 10_000),
|
timeout_ms=NOTIFICATIONS.get("timeout_ms", 10_000),
|
||||||
)
|
)
|
||||||
notification_center = NotificationCenter(
|
|
||||||
notification_history,
|
vinyl_button: VinylButton | None = VinylButton() if VINYL.get("enable", False) else None
|
||||||
|
calendar_service: CalendarService | None = (
|
||||||
|
CalendarService(update_interval=120000) if CALENDAR.get("enable", True) else None
|
||||||
|
)
|
||||||
|
|
||||||
|
control_center: ControlCenter | None = None
|
||||||
|
if notification_history is not None:
|
||||||
|
control_center = ControlCenter(
|
||||||
|
history=notification_history,
|
||||||
|
calendar_service=calendar_service,
|
||||||
|
vinyl_button=vinyl_button,
|
||||||
monitor=0,
|
monitor=0,
|
||||||
width=NOTIFICATIONS.get("center_width", 380),
|
width=NOTIFICATIONS.get("center_width", 380),
|
||||||
)
|
)
|
||||||
@@ -87,8 +98,8 @@ if screenrec_menu is not None:
|
|||||||
_app_windows.append(screenrec_menu)
|
_app_windows.append(screenrec_menu)
|
||||||
if notification_toasts is not None:
|
if notification_toasts is not None:
|
||||||
_app_windows.append(notification_toasts)
|
_app_windows.append(notification_toasts)
|
||||||
if notification_center is not None:
|
if control_center is not None:
|
||||||
_app_windows.append(notification_center)
|
_app_windows.append(control_center)
|
||||||
app = Application("sims", *_app_windows)
|
app = Application("sims", *_app_windows)
|
||||||
|
|
||||||
|
|
||||||
@@ -148,9 +159,9 @@ def screenrec_stop():
|
|||||||
|
|
||||||
|
|
||||||
@Application.action()
|
@Application.action()
|
||||||
def toggle_notification_center():
|
def toggle_control_center():
|
||||||
if notification_center is not None:
|
if control_center is not None:
|
||||||
notification_center.toggle()
|
control_center.toggle()
|
||||||
|
|
||||||
|
|
||||||
def _set_all_bars_rounded(rounded: bool):
|
def _set_all_bars_rounded(rounded: bool):
|
||||||
@@ -210,8 +221,7 @@ def spawn_bars():
|
|||||||
tray=tray if i == 0 else None,
|
tray=tray if i == 0 else None,
|
||||||
monitor=i,
|
monitor=i,
|
||||||
screenrec_service=screenrec_service if i == 0 else None,
|
screenrec_service=screenrec_service if i == 0 else None,
|
||||||
notification_history=notification_history if i == 0 else None,
|
control_center=control_center if i == 0 else None,
|
||||||
notification_center=notification_center if i == 0 else None,
|
|
||||||
)
|
)
|
||||||
bar_windows.append(bar)
|
bar_windows.append(bar)
|
||||||
if i == 0 and bar.notmuch:
|
if i == 0 and bar.notmuch:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import psutil
|
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.label import Label
|
||||||
from fabric.widgets.image import Image
|
from fabric.widgets.image import Image
|
||||||
@@ -6,25 +5,20 @@ from fabric.widgets.overlay import Overlay
|
|||||||
from fabric.widgets.datetime import DateTime
|
from fabric.widgets.datetime import DateTime
|
||||||
from fabric.widgets.centerbox import CenterBox
|
from fabric.widgets.centerbox import CenterBox
|
||||||
from sims.modules.player import Player
|
from sims.modules.player import Player
|
||||||
from sims.modules.vinyl import VinylButton
|
|
||||||
from sims.modules.quick_menu import QuickMenuOpener
|
|
||||||
from sims.modules.battery import Battery
|
from sims.modules.battery import Battery
|
||||||
from sims.modules.calendar import CalendarService, CalendarPopup
|
from sims.modules.control_center import ControlCenter
|
||||||
from sims.modules.notification_center import NotificationCenter
|
|
||||||
from sims.modules.notmuch import NotmuchWidget
|
from sims.modules.notmuch import NotmuchWidget
|
||||||
from sims.modules.screenrec import ScreenrecWidget
|
from sims.modules.screenrec import ScreenrecWidget
|
||||||
from sims.services.notification_history import NotificationHistoryService
|
|
||||||
from sims.widgets.notification_bell import NotificationBell
|
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.system_tray.widgets import SystemTray
|
from fabric.system_tray.widgets import SystemTray
|
||||||
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
from sims.widgets.fenster import FensterWorkspaces, FensterActiveWindow
|
||||||
from sims.services.fenster import get_i3_connection
|
|
||||||
from sims.services.screenrec import ScreenrecService
|
from sims.services.screenrec import ScreenrecService
|
||||||
from sims.services.smart_corners import get_smart_corners_service
|
from sims.services.smart_corners import get_smart_corners_service
|
||||||
|
from fabric.widgets.button import Button
|
||||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||||
from sims.services.system_stats import SystemStatsService
|
from sims.services.system_stats import SystemStatsService
|
||||||
|
|
||||||
from sims.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
from sims.config import BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(Window):
|
class StatusBar(Window):
|
||||||
@@ -34,8 +28,7 @@ class StatusBar(Window):
|
|||||||
tray: SystemTray | None = None,
|
tray: SystemTray | None = None,
|
||||||
monitor: int = 1,
|
monitor: int = 1,
|
||||||
screenrec_service: ScreenrecService | None = None,
|
screenrec_service: ScreenrecService | None = None,
|
||||||
notification_history: NotificationHistoryService | None = None,
|
control_center: ControlCenter | None = None,
|
||||||
notification_center: NotificationCenter | None = None,
|
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name="sims",
|
name="sims",
|
||||||
@@ -49,29 +42,23 @@ class StatusBar(Window):
|
|||||||
)
|
)
|
||||||
self.output = display
|
self.output = display
|
||||||
self._corners_rounded = False
|
self._corners_rounded = False
|
||||||
|
self._right_flat = False
|
||||||
|
|
||||||
self.workspaces = FensterWorkspaces(
|
self.workspaces = FensterWorkspaces(
|
||||||
output=display,
|
output=display,
|
||||||
name="workspaces",
|
name="workspaces",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
)
|
)
|
||||||
# Create calendar components (refresh every 2 minutes)
|
|
||||||
self.calendar_service = CalendarService(update_interval=120000)
|
|
||||||
self.calendar_popup = CalendarPopup()
|
|
||||||
self.calendar_popup_visible = False
|
|
||||||
|
|
||||||
# Create clickable datetime widget
|
|
||||||
from fabric.widgets.button import Button
|
|
||||||
datetime_widget = DateTime(name="date-time", formatters="%d %b - %H:%M")
|
datetime_widget = DateTime(name="date-time", formatters="%d %b - %H:%M")
|
||||||
self.date_time = Button(
|
self.date_time = Button(
|
||||||
name="date-time-button",
|
name="date-time-button",
|
||||||
child=datetime_widget,
|
child=datetime_widget,
|
||||||
on_clicked=self.toggle_calendar,
|
on_clicked=self._on_date_time_clicked,
|
||||||
style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;"
|
style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;",
|
||||||
)
|
)
|
||||||
|
self.control_center = control_center
|
||||||
|
|
||||||
# Connect calendar service to popup
|
|
||||||
self.calendar_service.connect("events-changed", self.update_calendar_display)
|
|
||||||
self.system_tray = tray
|
self.system_tray = tray
|
||||||
|
|
||||||
self.active_window = FensterActiveWindow(
|
self.active_window = FensterActiveWindow(
|
||||||
@@ -95,15 +82,6 @@ class StatusBar(Window):
|
|||||||
overlays=[self.cpu_progress_bar, self.progress_label],
|
overlays=[self.cpu_progress_bar, self.progress_label],
|
||||||
)
|
)
|
||||||
self.player = Player()
|
self.player = Player()
|
||||||
self.vinyl = None
|
|
||||||
if VINYL["enable"]:
|
|
||||||
self.vinyl = VinylButton()
|
|
||||||
|
|
||||||
# Create quick menu button
|
|
||||||
self.quick_menu = QuickMenuOpener(icon_name="open-menu-symbolic")
|
|
||||||
# Setup audio section with vinyl if enabled
|
|
||||||
if self.vinyl:
|
|
||||||
self.quick_menu.get_menu().setup_audio_section(vinyl_service=self.vinyl)
|
|
||||||
|
|
||||||
self.battery = None
|
self.battery = None
|
||||||
if BATTERY["enable"]:
|
if BATTERY["enable"]:
|
||||||
@@ -117,13 +95,6 @@ class StatusBar(Window):
|
|||||||
if screenrec_service is not None:
|
if screenrec_service is not None:
|
||||||
self.screenrec = ScreenrecWidget(screenrec_service)
|
self.screenrec = ScreenrecWidget(screenrec_service)
|
||||||
|
|
||||||
self.notification_bell = None
|
|
||||||
if notification_history is not None and notification_center is not None:
|
|
||||||
self.notification_bell = NotificationBell(
|
|
||||||
notification_history,
|
|
||||||
on_clicked=notification_center.toggle,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.status_container = Box(
|
self.status_container = Box(
|
||||||
name="widgets-container",
|
name="widgets-container",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
@@ -146,11 +117,6 @@ class StatusBar(Window):
|
|||||||
if self.screenrec:
|
if self.screenrec:
|
||||||
end_container_children.append(self.screenrec)
|
end_container_children.append(self.screenrec)
|
||||||
|
|
||||||
if self.notification_bell:
|
|
||||||
end_container_children.append(self.notification_bell)
|
|
||||||
|
|
||||||
# Add quick menu button next to time
|
|
||||||
end_container_children.append(self.quick_menu)
|
|
||||||
end_container_children.append(self.date_time)
|
end_container_children.append(self.date_time)
|
||||||
|
|
||||||
center_children = []
|
center_children = []
|
||||||
@@ -183,23 +149,19 @@ class StatusBar(Window):
|
|||||||
)
|
)
|
||||||
self.children = self.inner
|
self.children = self.inner
|
||||||
|
|
||||||
# Create system stats service with signal-based updates
|
|
||||||
self.system_stats_service = SystemStatsService(update_interval=3000)
|
self.system_stats_service = SystemStatsService(update_interval=3000)
|
||||||
self.system_stats_service.connect("stats-changed", self.update_progress_bars)
|
self.system_stats_service.connect("stats-changed", self.update_progress_bars)
|
||||||
|
|
||||||
# Set the bar height
|
|
||||||
self.set_size_request(-1, BAR_HEIGHT)
|
self.set_size_request(-1, BAR_HEIGHT)
|
||||||
|
|
||||||
smart_corners = get_smart_corners_service()
|
smart_corners = get_smart_corners_service()
|
||||||
smart_corners.connect("state-changed", self._on_smart_corners_changed)
|
smart_corners.connect("state-changed", self._on_smart_corners_changed)
|
||||||
self.set_corners_rounded(not smart_corners.get(display))
|
self.set_corners_rounded(not smart_corners.get(display))
|
||||||
|
|
||||||
self.show_all()
|
if self.control_center is not None:
|
||||||
|
self.control_center.add_visibility_listener(self.set_right_flat)
|
||||||
|
|
||||||
def __del__(self):
|
self.show_all()
|
||||||
"""Cleanup when bar is destroyed"""
|
|
||||||
if hasattr(self, 'calendar_service'):
|
|
||||||
self.calendar_service.stop_monitoring()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def corners_rounded(self) -> bool:
|
def corners_rounded(self) -> bool:
|
||||||
@@ -219,30 +181,19 @@ class StatusBar(Window):
|
|||||||
return
|
return
|
||||||
self.set_corners_rounded(not active)
|
self.set_corners_rounded(not active)
|
||||||
|
|
||||||
|
def set_right_flat(self, flat: bool) -> None:
|
||||||
|
if flat == self._right_flat:
|
||||||
|
return
|
||||||
|
if flat:
|
||||||
|
self.inner.set_style("border-radius: 0 0 0 28px;")
|
||||||
|
else:
|
||||||
|
self.inner.set_style("")
|
||||||
|
self._right_flat = flat
|
||||||
|
|
||||||
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
||||||
"""Update progress bars when system stats change"""
|
|
||||||
self.cpu_progress_bar.value = cpu_percent
|
self.cpu_progress_bar.value = cpu_percent
|
||||||
self.ram_progress_bar.value = memory_percent
|
self.ram_progress_bar.value = memory_percent
|
||||||
|
|
||||||
def toggle_calendar(self, button=None):
|
def _on_date_time_clicked(self, _button=None):
|
||||||
"""Toggle the calendar popup when datetime is clicked"""
|
if self.control_center is not None:
|
||||||
from loguru import logger
|
self.control_center.toggle()
|
||||||
logger.info(f"[Calendar] DateTime clicked, popup_visible: {self.calendar_popup_visible}")
|
|
||||||
|
|
||||||
if self.calendar_popup_visible:
|
|
||||||
logger.info("[Calendar] Hiding calendar popup")
|
|
||||||
self.calendar_popup.set_visible(False)
|
|
||||||
self.calendar_popup_visible = False
|
|
||||||
else:
|
|
||||||
logger.info("[Calendar] Showing calendar popup")
|
|
||||||
# Use cached events - no need to refresh on click
|
|
||||||
cached_events = self.calendar_service.get_cached_events()
|
|
||||||
logger.info(f"[Calendar] Using {len(cached_events)} cached events")
|
|
||||||
self.calendar_popup.update_events_display(cached_events)
|
|
||||||
self.calendar_popup.set_visible(True)
|
|
||||||
self.calendar_popup.show_all()
|
|
||||||
self.calendar_popup_visible = True
|
|
||||||
|
|
||||||
def update_calendar_display(self, service, events):
|
|
||||||
"""Update the calendar popup with events"""
|
|
||||||
self.calendar_popup.update_events_display(events)
|
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ import json
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, date
|
from datetime import date
|
||||||
|
|
||||||
# Add common binary paths to PATH for user binaries
|
# Add common binary paths to PATH for user binaries
|
||||||
os.environ['PATH'] = '/run/current-system/sw/bin:/home/' + os.environ.get('USER', 'user') + '/.nix-profile/bin:' + os.environ.get('PATH', '')
|
os.environ['PATH'] = '/run/current-system/sw/bin:/home/' + os.environ.get('USER', 'user') + '/.nix-profile/bin:' + os.environ.get('PATH', '')
|
||||||
from fabric.widgets.box import Box
|
|
||||||
from fabric.widgets.label import Label
|
|
||||||
from fabric.widgets.button import Button
|
|
||||||
from fabric.widgets.image import Image
|
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sims.config import CALENDAR
|
from sims.config import CALENDAR
|
||||||
|
|
||||||
@@ -203,203 +198,3 @@ class CalendarService:
|
|||||||
else:
|
else:
|
||||||
logger.info("[Calendar] Using khal subprocess")
|
logger.info("[Calendar] Using khal subprocess")
|
||||||
self.update_events_subprocess()
|
self.update_events_subprocess()
|
||||||
|
|
||||||
|
|
||||||
class CalendarPopup(Window):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
name="calendar-popup",
|
|
||||||
layer="top",
|
|
||||||
anchor="top right",
|
|
||||||
margin="10px 10px 0px 0px", # Just a few pixels under the bar
|
|
||||||
exclusivity="none",
|
|
||||||
visible=False,
|
|
||||||
all_visible=False,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Events container
|
|
||||||
self.events_box = Box(
|
|
||||||
name="events-box",
|
|
||||||
orientation="v",
|
|
||||||
spacing=6,
|
|
||||||
style="min-width: 450px; min-height: 200px;",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add a test label to make sure popup is working
|
|
||||||
test_label = Label("Calendar Events", name="calendar-title")
|
|
||||||
|
|
||||||
container = Box(
|
|
||||||
orientation="v", spacing=4, children=[test_label, self.events_box]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.children = container
|
|
||||||
|
|
||||||
# Set explicit size - much bigger
|
|
||||||
self.set_size_request(500, 400)
|
|
||||||
|
|
||||||
def update_events_display(self, events):
|
|
||||||
"""Update the events display"""
|
|
||||||
logger.info(f"[Calendar] Updating popup with {len(events)} events")
|
|
||||||
|
|
||||||
# Clear existing children first
|
|
||||||
self.events_box.children = []
|
|
||||||
|
|
||||||
if not events:
|
|
||||||
logger.info("[Calendar] No events, showing 'no events' message")
|
|
||||||
no_events_label = Label("No events today", name="no-events")
|
|
||||||
self.events_box.add(no_events_label)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check current time for time indicator placement
|
|
||||||
now = datetime.now()
|
|
||||||
current_time = now.strftime("%H:%M")
|
|
||||||
current_time_added = False
|
|
||||||
|
|
||||||
for i, event in enumerate(events):
|
|
||||||
logger.info(f"[Calendar] Processing event {i+1} for display")
|
|
||||||
title = event.get("title", "No title")
|
|
||||||
start_time = event.get("start", "").split()[1] if event.get("start") else ""
|
|
||||||
end_time = event.get("end", "").split()[1] if event.get("end") else ""
|
|
||||||
location = event.get("location", "")
|
|
||||||
|
|
||||||
# Check if we should add current time indicator before this event
|
|
||||||
if not current_time_added and start_time and start_time > current_time:
|
|
||||||
self.add_current_time_indicator(current_time)
|
|
||||||
current_time_added = True
|
|
||||||
|
|
||||||
# Format time display
|
|
||||||
time_str = ""
|
|
||||||
if start_time and end_time:
|
|
||||||
time_str = f"{start_time} - {end_time}"
|
|
||||||
elif start_time:
|
|
||||||
time_str = start_time
|
|
||||||
|
|
||||||
logger.info(f"[Calendar] Creating widget for: {title} ({time_str})")
|
|
||||||
|
|
||||||
# Create event item with horizontal layout - time on left, content on right
|
|
||||||
event_box = Box(
|
|
||||||
name="event-item",
|
|
||||||
orientation="h", # Horizontal layout
|
|
||||||
spacing=12,
|
|
||||||
style_classes=["event-item"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Left side: Time display (fixed width for alignment)
|
|
||||||
time_display = time_str if time_str else "All day"
|
|
||||||
time_label = Label(
|
|
||||||
time_display,
|
|
||||||
name="event-time",
|
|
||||||
style_classes=["event-time"],
|
|
||||||
style="min-width: 100px;" # Fixed width for consistent alignment
|
|
||||||
)
|
|
||||||
|
|
||||||
# Right side: Content (title and location)
|
|
||||||
content_box = Box(
|
|
||||||
name="event-content",
|
|
||||||
orientation="v",
|
|
||||||
spacing=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Title (no more status prefix)
|
|
||||||
title_label = Label(
|
|
||||||
title,
|
|
||||||
name="event-title",
|
|
||||||
style_classes=["event-title"],
|
|
||||||
)
|
|
||||||
content_box.add(title_label)
|
|
||||||
|
|
||||||
if location:
|
|
||||||
location_label = Label(
|
|
||||||
f"📍 {location}",
|
|
||||||
name="event-location",
|
|
||||||
style_classes=["event-location"],
|
|
||||||
)
|
|
||||||
content_box.add(location_label)
|
|
||||||
|
|
||||||
# Add time and content to the main event box
|
|
||||||
event_box.add(time_label)
|
|
||||||
event_box.add(content_box)
|
|
||||||
|
|
||||||
self.events_box.add(event_box)
|
|
||||||
logger.info(f"[Calendar] Added event widget to events_box")
|
|
||||||
|
|
||||||
# Add current time indicator at the end if not added yet
|
|
||||||
if not current_time_added:
|
|
||||||
self.add_current_time_indicator(current_time)
|
|
||||||
|
|
||||||
# Force refresh the popup display
|
|
||||||
self.events_box.show_all()
|
|
||||||
logger.info(f"[Calendar] Finished updating popup")
|
|
||||||
|
|
||||||
def add_current_time_indicator(self, current_time):
|
|
||||||
"""Add a current time indicator to the events list"""
|
|
||||||
time_indicator = Box(
|
|
||||||
name="current-time-indicator",
|
|
||||||
orientation="h",
|
|
||||||
spacing=8,
|
|
||||||
style_classes=["current-time-indicator"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Current time label
|
|
||||||
time_label = Label(
|
|
||||||
current_time,
|
|
||||||
name="current-time-label",
|
|
||||||
style_classes=["current-time-label"],
|
|
||||||
style="min-width: 100px; font-weight: bold;"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Line indicator
|
|
||||||
line_label = Label(
|
|
||||||
"━━━ NOW",
|
|
||||||
name="current-time-line",
|
|
||||||
style_classes=["current-time-line"],
|
|
||||||
)
|
|
||||||
|
|
||||||
time_indicator.add(time_label)
|
|
||||||
time_indicator.add(line_label)
|
|
||||||
|
|
||||||
self.events_box.add(time_indicator)
|
|
||||||
logger.info(f"[Calendar] Added current time indicator at {current_time}")
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarWidget(Button):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
name="calendar-widget",
|
|
||||||
child=Image(icon_name="x-office-calendar-symbolic", icon_size=16),
|
|
||||||
on_clicked=self.toggle_events,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.service = CalendarService()
|
|
||||||
self.service.connect("events-changed", self.update_events_display)
|
|
||||||
|
|
||||||
# Create popup window
|
|
||||||
self.popup = CalendarPopup()
|
|
||||||
self.popup_visible = False
|
|
||||||
logger.info("[Calendar] Calendar widget initialized with popup")
|
|
||||||
|
|
||||||
# Initial update
|
|
||||||
self.update_events_display(self.service, self.service.events)
|
|
||||||
|
|
||||||
def toggle_events(self, button=None):
|
|
||||||
"""Toggle the visibility of the events popup"""
|
|
||||||
logger.info(f"[Calendar] Button clicked, popup_visible: {self.popup_visible}")
|
|
||||||
|
|
||||||
if self.popup_visible:
|
|
||||||
logger.info("[Calendar] Hiding popup")
|
|
||||||
self.popup.set_visible(False)
|
|
||||||
self.popup_visible = False
|
|
||||||
else:
|
|
||||||
logger.info("[Calendar] Showing popup")
|
|
||||||
# Refresh events when opening
|
|
||||||
self.service.update_events()
|
|
||||||
self.popup.set_visible(True)
|
|
||||||
self.popup.show_all()
|
|
||||||
self.popup_visible = True
|
|
||||||
|
|
||||||
def update_events_display(self, service, events):
|
|
||||||
"""Update the events display in popup"""
|
|
||||||
self.popup.update_events_display(events)
|
|
||||||
|
|||||||
352
sims/modules/control_center.py
Normal file
352
sims/modules/control_center.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||||
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
|
from sims.modules.calendar import CalendarService
|
||||||
|
from sims.modules.vinyl import VinylButton
|
||||||
|
from sims.services.notification_history import NotificationHistoryService
|
||||||
|
from sims.widgets.notification_history_entry import NotificationHistoryEntryWidget
|
||||||
|
|
||||||
|
|
||||||
|
class ControlCenter(Window):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
history: NotificationHistoryService,
|
||||||
|
calendar_service: CalendarService | None = None,
|
||||||
|
vinyl_button: VinylButton | None = None,
|
||||||
|
monitor: int = 0,
|
||||||
|
width: int = 380,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name="control-center",
|
||||||
|
anchor="top right bottom",
|
||||||
|
monitor=monitor,
|
||||||
|
margin="0",
|
||||||
|
exclusivity="none",
|
||||||
|
keyboard_mode="on-demand",
|
||||||
|
visible=False,
|
||||||
|
)
|
||||||
|
self._history = history
|
||||||
|
self._calendar_service = calendar_service
|
||||||
|
self._vinyl_button = vinyl_button
|
||||||
|
self._width = width
|
||||||
|
self._visibility_listeners: list = []
|
||||||
|
|
||||||
|
close_button = Button(
|
||||||
|
name="control-center-close",
|
||||||
|
image=Image(icon_name="window-close-symbolic", icon_size=16),
|
||||||
|
on_clicked=lambda *_: self.hide(),
|
||||||
|
)
|
||||||
|
header = Box(
|
||||||
|
name="control-center-header",
|
||||||
|
orientation="h",
|
||||||
|
spacing=8,
|
||||||
|
)
|
||||||
|
header.pack_start(
|
||||||
|
Label(name="control-center-title", label="Control Center", h_align="start"),
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
header.pack_end(close_button, False, False, 0)
|
||||||
|
|
||||||
|
sections: list = []
|
||||||
|
|
||||||
|
if vinyl_button is not None:
|
||||||
|
sections.append(self._build_settings_section())
|
||||||
|
|
||||||
|
if calendar_service is not None:
|
||||||
|
sections.append(self._build_calendar_section())
|
||||||
|
|
||||||
|
sections.append(self._build_notifications_section())
|
||||||
|
|
||||||
|
sections_box = Box(
|
||||||
|
name="control-center-sections",
|
||||||
|
orientation="v",
|
||||||
|
spacing=12,
|
||||||
|
children=sections,
|
||||||
|
h_expand=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
scroll = ScrolledWindow(
|
||||||
|
name="control-center-scroll",
|
||||||
|
h_scrollbar_policy="never",
|
||||||
|
v_scrollbar_policy="automatic",
|
||||||
|
child=sections_box,
|
||||||
|
h_expand=True,
|
||||||
|
v_expand=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
body = Box(
|
||||||
|
name="control-center-body",
|
||||||
|
orientation="v",
|
||||||
|
spacing=8,
|
||||||
|
children=[header, scroll],
|
||||||
|
h_expand=True,
|
||||||
|
v_expand=True,
|
||||||
|
)
|
||||||
|
body.set_size_request(self._width, -1)
|
||||||
|
self.add(body)
|
||||||
|
self.connect("key-press-event", self._on_key_press)
|
||||||
|
|
||||||
|
self._history.connect("changed", lambda *_: self._refresh_notifications())
|
||||||
|
if self._calendar_service is not None:
|
||||||
|
self._calendar_service.connect(
|
||||||
|
"events-changed",
|
||||||
|
lambda _service, events: self._refresh_calendar(events),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._refresh_notifications()
|
||||||
|
if self._calendar_service is not None:
|
||||||
|
self._refresh_calendar(self._calendar_service.get_cached_events())
|
||||||
|
|
||||||
|
def _build_section(self, name: str, title: str | None) -> Box:
|
||||||
|
section = Box(
|
||||||
|
name=name,
|
||||||
|
orientation="v",
|
||||||
|
spacing=6,
|
||||||
|
h_expand=True,
|
||||||
|
)
|
||||||
|
if title is not None:
|
||||||
|
section.add(
|
||||||
|
Label(
|
||||||
|
name="control-center-section-title",
|
||||||
|
label=title,
|
||||||
|
h_align="start",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return section
|
||||||
|
|
||||||
|
def _build_settings_section(self) -> Box:
|
||||||
|
section = self._build_section("control-center-settings", "Settings")
|
||||||
|
|
||||||
|
row = Box(
|
||||||
|
name="control-center-settings-row",
|
||||||
|
orientation="h",
|
||||||
|
spacing=8,
|
||||||
|
)
|
||||||
|
row.pack_start(
|
||||||
|
Label(
|
||||||
|
name="control-center-settings-label",
|
||||||
|
label="Vinyl Passthrough",
|
||||||
|
h_align="start",
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
row.pack_end(self._vinyl_button, False, False, 0)
|
||||||
|
section.add(row)
|
||||||
|
return section
|
||||||
|
|
||||||
|
def _build_calendar_section(self) -> Box:
|
||||||
|
section = self._build_section("control-center-calendar", "Calendar")
|
||||||
|
self._calendar_events_box = Box(
|
||||||
|
name="control-center-events",
|
||||||
|
orientation="v",
|
||||||
|
spacing=4,
|
||||||
|
h_expand=True,
|
||||||
|
)
|
||||||
|
section.add(self._calendar_events_box)
|
||||||
|
return section
|
||||||
|
|
||||||
|
def _build_notifications_section(self) -> Box:
|
||||||
|
section = self._build_section("control-center-notifications", None)
|
||||||
|
|
||||||
|
clear_button = Button(
|
||||||
|
name="control-center-notifications-clear",
|
||||||
|
label="Clear all",
|
||||||
|
on_clicked=lambda *_: self._history.clear(),
|
||||||
|
)
|
||||||
|
header = Box(
|
||||||
|
name="control-center-notifications-header",
|
||||||
|
orientation="h",
|
||||||
|
spacing=8,
|
||||||
|
)
|
||||||
|
header.pack_start(
|
||||||
|
Label(
|
||||||
|
name="control-center-section-title",
|
||||||
|
label="Notifications",
|
||||||
|
h_align="start",
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
header.pack_end(clear_button, False, False, 0)
|
||||||
|
|
||||||
|
self._notifications_empty = Label(
|
||||||
|
name="control-center-notifications-empty",
|
||||||
|
label="No notifications",
|
||||||
|
h_align="start",
|
||||||
|
)
|
||||||
|
self._notifications_list = Box(
|
||||||
|
name="control-center-notifications-list",
|
||||||
|
orientation="v",
|
||||||
|
spacing=6,
|
||||||
|
h_expand=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
section.add(header)
|
||||||
|
section.add(self._notifications_list)
|
||||||
|
return section
|
||||||
|
|
||||||
|
def add_visibility_listener(self, callback) -> None:
|
||||||
|
self._visibility_listeners.append(callback)
|
||||||
|
|
||||||
|
def _notify_visibility(self, visible: bool) -> None:
|
||||||
|
for callback in self._visibility_listeners:
|
||||||
|
callback(visible)
|
||||||
|
|
||||||
|
def toggle(self) -> None:
|
||||||
|
if self.get_visible():
|
||||||
|
self.hide()
|
||||||
|
else:
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def show(self) -> None: # type: ignore[override]
|
||||||
|
self._history.mark_all_seen()
|
||||||
|
super().show()
|
||||||
|
self.show_all()
|
||||||
|
self._notify_visibility(True)
|
||||||
|
|
||||||
|
def hide(self) -> None: # type: ignore[override]
|
||||||
|
super().hide()
|
||||||
|
self._notify_visibility(False)
|
||||||
|
|
||||||
|
def _on_key_press(self, _widget, event):
|
||||||
|
if event.keyval == Gdk.KEY_Escape:
|
||||||
|
self.hide()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _refresh_notifications(self) -> None:
|
||||||
|
for child in self._notifications_list.get_children():
|
||||||
|
self._notifications_list.remove(child)
|
||||||
|
if child is not self._notifications_empty:
|
||||||
|
child.destroy()
|
||||||
|
entries = self._history.entries
|
||||||
|
if not entries:
|
||||||
|
self._notifications_list.add(self._notifications_empty)
|
||||||
|
self._notifications_empty.show_all()
|
||||||
|
return
|
||||||
|
for entry in entries:
|
||||||
|
self._notifications_list.add(
|
||||||
|
NotificationHistoryEntryWidget(
|
||||||
|
entry, on_dismiss=self._history.remove
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._notifications_list.show_all()
|
||||||
|
|
||||||
|
def _refresh_calendar(self, events) -> None:
|
||||||
|
for child in self._calendar_events_box.get_children():
|
||||||
|
self._calendar_events_box.remove(child)
|
||||||
|
child.destroy()
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
self._calendar_events_box.add(
|
||||||
|
Label(
|
||||||
|
name="control-center-no-events",
|
||||||
|
label="No events today",
|
||||||
|
h_align="start",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._calendar_events_box.show_all()
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M")
|
||||||
|
current_time_added = False
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
title = event.get("title", "No title")
|
||||||
|
start_raw = event.get("start", "")
|
||||||
|
end_raw = event.get("end", "")
|
||||||
|
start_time = start_raw.split()[1] if start_raw else ""
|
||||||
|
end_time = end_raw.split()[1] if end_raw else ""
|
||||||
|
location = event.get("location", "")
|
||||||
|
|
||||||
|
if not current_time_added and start_time and start_time > current_time:
|
||||||
|
self._calendar_events_box.add(self._build_now_indicator(current_time))
|
||||||
|
current_time_added = True
|
||||||
|
|
||||||
|
if start_time and end_time:
|
||||||
|
time_str = f"{start_time} - {end_time}"
|
||||||
|
elif start_time:
|
||||||
|
time_str = start_time
|
||||||
|
else:
|
||||||
|
time_str = "All day"
|
||||||
|
|
||||||
|
event_box = Box(
|
||||||
|
name="event-item",
|
||||||
|
orientation="h",
|
||||||
|
spacing=12,
|
||||||
|
style_classes=["event-item"],
|
||||||
|
)
|
||||||
|
event_box.add(
|
||||||
|
Label(
|
||||||
|
time_str,
|
||||||
|
name="event-time",
|
||||||
|
style_classes=["event-time"],
|
||||||
|
style="min-width: 90px;",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
content_box = Box(
|
||||||
|
name="event-content",
|
||||||
|
orientation="v",
|
||||||
|
spacing=2,
|
||||||
|
)
|
||||||
|
content_box.add(
|
||||||
|
Label(
|
||||||
|
title,
|
||||||
|
name="event-title",
|
||||||
|
style_classes=["event-title"],
|
||||||
|
h_align="start",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if location:
|
||||||
|
content_box.add(
|
||||||
|
Label(
|
||||||
|
f"📍 {location}",
|
||||||
|
name="event-location",
|
||||||
|
style_classes=["event-location"],
|
||||||
|
h_align="start",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
event_box.add(content_box)
|
||||||
|
self._calendar_events_box.add(event_box)
|
||||||
|
|
||||||
|
if not current_time_added:
|
||||||
|
self._calendar_events_box.add(self._build_now_indicator(current_time))
|
||||||
|
|
||||||
|
self._calendar_events_box.show_all()
|
||||||
|
|
||||||
|
def _build_now_indicator(self, current_time: str) -> Box:
|
||||||
|
indicator = Box(
|
||||||
|
name="current-time-indicator",
|
||||||
|
orientation="h",
|
||||||
|
spacing=8,
|
||||||
|
style_classes=["current-time-indicator"],
|
||||||
|
)
|
||||||
|
indicator.add(
|
||||||
|
Label(
|
||||||
|
current_time,
|
||||||
|
name="current-time-label",
|
||||||
|
style_classes=["current-time-label"],
|
||||||
|
style="min-width: 90px; font-weight: bold;",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
indicator.add(
|
||||||
|
Label(
|
||||||
|
"━━━ NOW",
|
||||||
|
name="current-time-line",
|
||||||
|
style_classes=["current-time-line"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return indicator
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
from fabric.widgets.box import Box
|
|
||||||
from fabric.widgets.button import Button
|
|
||||||
from fabric.widgets.image import Image
|
|
||||||
from fabric.widgets.label import Label
|
|
||||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
|
||||||
from gi.repository import Gdk
|
|
||||||
|
|
||||||
from sims.services.notification_history import NotificationHistoryService
|
|
||||||
from sims.widgets.notification_history_entry import NotificationHistoryEntryWidget
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationCenter(Window):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
history: NotificationHistoryService,
|
|
||||||
monitor: int = 0,
|
|
||||||
width: int = 380,
|
|
||||||
):
|
|
||||||
super().__init__(
|
|
||||||
name="notification-center",
|
|
||||||
anchor="top right bottom",
|
|
||||||
monitor=monitor,
|
|
||||||
margin="0",
|
|
||||||
exclusivity="none",
|
|
||||||
keyboard_mode="on-demand",
|
|
||||||
visible=False,
|
|
||||||
)
|
|
||||||
self._history = history
|
|
||||||
self._width = width
|
|
||||||
|
|
||||||
self._empty_label = Label(
|
|
||||||
name="notification-center-empty",
|
|
||||||
label="No notifications",
|
|
||||||
h_align="center",
|
|
||||||
v_align="center",
|
|
||||||
h_expand=True,
|
|
||||||
v_expand=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._list = Box(
|
|
||||||
name="notification-center-list",
|
|
||||||
spacing=6,
|
|
||||||
orientation="v",
|
|
||||||
h_expand=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_button = Button(
|
|
||||||
name="notification-center-clear",
|
|
||||||
label="Clear all",
|
|
||||||
on_clicked=lambda *_: self._history.clear(),
|
|
||||||
)
|
|
||||||
close_button = Button(
|
|
||||||
name="notification-center-close",
|
|
||||||
image=Image(icon_name="window-close-symbolic", icon_size=16),
|
|
||||||
on_clicked=lambda *_: self.hide(),
|
|
||||||
)
|
|
||||||
header = Box(
|
|
||||||
name="notification-center-header",
|
|
||||||
orientation="h",
|
|
||||||
spacing=8,
|
|
||||||
)
|
|
||||||
header.pack_start(
|
|
||||||
Label(name="notification-center-title", label="Notifications", h_align="start"),
|
|
||||||
True,
|
|
||||||
True,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
header.pack_end(close_button, False, False, 0)
|
|
||||||
header.pack_end(clear_button, False, False, 4)
|
|
||||||
|
|
||||||
scroll = ScrolledWindow(
|
|
||||||
name="notification-center-scroll",
|
|
||||||
h_scrollbar_policy="never",
|
|
||||||
v_scrollbar_policy="automatic",
|
|
||||||
child=self._list,
|
|
||||||
h_expand=True,
|
|
||||||
v_expand=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
body = Box(
|
|
||||||
name="notification-center-body",
|
|
||||||
orientation="v",
|
|
||||||
spacing=8,
|
|
||||||
children=[header, scroll],
|
|
||||||
h_expand=True,
|
|
||||||
v_expand=True,
|
|
||||||
)
|
|
||||||
body.set_size_request(self._width, -1)
|
|
||||||
self.add(body)
|
|
||||||
self.connect("key-press-event", self._on_key_press)
|
|
||||||
|
|
||||||
self._history.connect("changed", lambda *_: self._refresh())
|
|
||||||
self._refresh()
|
|
||||||
|
|
||||||
def toggle(self) -> None:
|
|
||||||
if self.get_visible():
|
|
||||||
self.hide()
|
|
||||||
else:
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def show(self) -> None: # type: ignore[override]
|
|
||||||
self._history.mark_all_seen()
|
|
||||||
super().show()
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def _on_key_press(self, _widget, event):
|
|
||||||
if event.keyval == Gdk.KEY_Escape:
|
|
||||||
self.hide()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _refresh(self) -> None:
|
|
||||||
for child in self._list.get_children():
|
|
||||||
self._list.remove(child)
|
|
||||||
if child is not self._empty_label:
|
|
||||||
child.destroy()
|
|
||||||
entries = self._history.entries
|
|
||||||
if not entries:
|
|
||||||
self._list.add(self._empty_label)
|
|
||||||
self._empty_label.show_all()
|
|
||||||
return
|
|
||||||
for entry in entries:
|
|
||||||
self._list.add(
|
|
||||||
NotificationHistoryEntryWidget(
|
|
||||||
entry, on_dismiss=self._history.remove
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._list.show_all()
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
from fabric.widgets.button import Button
|
|
||||||
from fabric.widgets.image import Image
|
|
||||||
from fabric.widgets.box import Box
|
|
||||||
from fabric.widgets.label import Label
|
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenuItem(Box):
|
|
||||||
"""Base class for quick menu items"""
|
|
||||||
def __init__(self, title, icon_name=None, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
orientation="h",
|
|
||||||
spacing=12,
|
|
||||||
name="quick-menu-item",
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
self.set_style("padding: 8px 12px; min-width: 280px;")
|
|
||||||
|
|
||||||
# Icon and title on the left
|
|
||||||
left_box = Box(orientation="h", spacing=8)
|
|
||||||
if icon_name:
|
|
||||||
icon = Image(icon_name=icon_name, icon_size=16)
|
|
||||||
left_box.add(icon)
|
|
||||||
|
|
||||||
self.title_label = Label(title)
|
|
||||||
self.title_label.set_style("font-size: 14px;")
|
|
||||||
left_box.add(self.title_label)
|
|
||||||
|
|
||||||
self.add(left_box)
|
|
||||||
|
|
||||||
# Derived classes can add controls to the right side
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenuToggle(QuickMenuItem):
|
|
||||||
"""A menu item with a toggle switch"""
|
|
||||||
def __init__(self, title, icon_name=None, active=False, on_toggle=None, **kwargs):
|
|
||||||
super().__init__(title, icon_name, **kwargs)
|
|
||||||
|
|
||||||
# Create a custom toggle using a button with state tracking
|
|
||||||
self._active = active
|
|
||||||
self._on_toggle = on_toggle
|
|
||||||
|
|
||||||
# Create toggle indicator box
|
|
||||||
self.toggle_box = Box(
|
|
||||||
orientation="h",
|
|
||||||
spacing=0
|
|
||||||
)
|
|
||||||
self.toggle_box.set_style("min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px;")
|
|
||||||
|
|
||||||
# Toggle indicator (circle)
|
|
||||||
self.toggle_indicator = Label("")
|
|
||||||
self.toggle_indicator.set_style("min-width: 20px; min-height: 20px; border-radius: 10px; background: white;")
|
|
||||||
|
|
||||||
self.toggle_box.add(self.toggle_indicator)
|
|
||||||
|
|
||||||
# Make it clickable
|
|
||||||
self.toggle_button = Button(
|
|
||||||
child=self.toggle_box,
|
|
||||||
on_clicked=self._on_click
|
|
||||||
)
|
|
||||||
self.toggle_button.set_style("background: transparent; border: none; padding: 0;")
|
|
||||||
|
|
||||||
# Add spacer to push toggle to the right
|
|
||||||
spacer = Label("", h_expand=True)
|
|
||||||
self.add(spacer)
|
|
||||||
self.add(self.toggle_button)
|
|
||||||
|
|
||||||
# Set initial state
|
|
||||||
self._update_appearance()
|
|
||||||
|
|
||||||
def _on_click(self, button):
|
|
||||||
self._active = not self._active
|
|
||||||
self._update_appearance()
|
|
||||||
if self._on_toggle:
|
|
||||||
self._on_toggle(self._active)
|
|
||||||
|
|
||||||
def _update_appearance(self):
|
|
||||||
if self._active:
|
|
||||||
self.toggle_box.set_style_classes(["toggle-active"])
|
|
||||||
self.toggle_box.set_style(
|
|
||||||
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
|
||||||
"transition: all 0.2s;"
|
|
||||||
)
|
|
||||||
self.toggle_indicator.set_style(
|
|
||||||
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
|
||||||
"background: white; margin-left: 20px; transition: all 0.2s;"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.toggle_box.set_style_classes(["toggle-inactive"])
|
|
||||||
self.toggle_box.set_style(
|
|
||||||
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
|
||||||
"transition: all 0.2s;"
|
|
||||||
)
|
|
||||||
self.toggle_indicator.set_style(
|
|
||||||
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
|
||||||
"background: white; margin-left: 0px; transition: all 0.2s;"
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_active(self, active):
|
|
||||||
self._active = active
|
|
||||||
self._update_appearance()
|
|
||||||
|
|
||||||
def get_active(self):
|
|
||||||
return self._active
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenuButton(QuickMenuItem):
|
|
||||||
"""A menu item that acts as a button"""
|
|
||||||
def __init__(self, title, icon_name=None, on_click=None, **kwargs):
|
|
||||||
super().__init__(title, icon_name, **kwargs)
|
|
||||||
|
|
||||||
if on_click:
|
|
||||||
# Make the entire item clickable
|
|
||||||
button_overlay = Button(
|
|
||||||
child=Box(), # Empty box as child
|
|
||||||
on_clicked=on_click
|
|
||||||
)
|
|
||||||
button_overlay.set_style("background: transparent; border: none; padding: 0; margin: 0;")
|
|
||||||
|
|
||||||
# Add arrow indicator on the right
|
|
||||||
arrow = Label("›")
|
|
||||||
arrow.set_style("font-size: 18px; opacity: 0.5;")
|
|
||||||
spacer = Label("", h_expand=True)
|
|
||||||
self.add(spacer)
|
|
||||||
self.add(arrow)
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenuSection(Box):
|
|
||||||
"""A section in the quick menu with optional title"""
|
|
||||||
def __init__(self, title=None, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
orientation="v",
|
|
||||||
spacing=4,
|
|
||||||
name="quick-menu-section",
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
if title:
|
|
||||||
title_label = Label(
|
|
||||||
title,
|
|
||||||
name="section-title"
|
|
||||||
)
|
|
||||||
title_label.set_style("font-size: 12px; opacity: 0.6; padding: 8px 12px 4px 12px; font-weight: bold;")
|
|
||||||
self.add(title_label)
|
|
||||||
|
|
||||||
self.items_box = Box(orientation="v", spacing=2)
|
|
||||||
self.add(self.items_box)
|
|
||||||
|
|
||||||
def add_item(self, item):
|
|
||||||
self.items_box.add(item)
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenu(Window):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
name="quick-menu",
|
|
||||||
layer="overlay", # Changed from 'top' to 'overlay' for better shadow support
|
|
||||||
anchor="top right",
|
|
||||||
margin="40px 10px 0px 0px",
|
|
||||||
exclusivity="none",
|
|
||||||
visible=False,
|
|
||||||
all_visible=False,
|
|
||||||
style_classes=["popup-window"],
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Main container
|
|
||||||
self.main_box = Box(
|
|
||||||
orientation="v",
|
|
||||||
spacing=8,
|
|
||||||
name="quick-menu-container"
|
|
||||||
)
|
|
||||||
# Remove redundant styling since it's handled in stylix.css
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Title
|
|
||||||
title_box = Box(
|
|
||||||
orientation="h",
|
|
||||||
spacing=8
|
|
||||||
)
|
|
||||||
title_box.set_style("padding: 12px;")
|
|
||||||
title = Label("Quick Menu")
|
|
||||||
title.set_style("font-size: 16px; font-weight: bold;")
|
|
||||||
title_box.add(title)
|
|
||||||
|
|
||||||
self.main_box.add(title_box)
|
|
||||||
# Add a simple divider line
|
|
||||||
divider = Label("")
|
|
||||||
divider.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 0px 12px;")
|
|
||||||
self.main_box.add(divider)
|
|
||||||
|
|
||||||
# Sections container
|
|
||||||
self.sections_container = Box(
|
|
||||||
orientation="v",
|
|
||||||
spacing=8
|
|
||||||
)
|
|
||||||
self.sections_container.set_style("padding: 8px 0px;")
|
|
||||||
self.main_box.add(self.sections_container)
|
|
||||||
|
|
||||||
self.children = self.main_box
|
|
||||||
self.set_size_request(360, -1)
|
|
||||||
|
|
||||||
# Store references to dynamic items
|
|
||||||
self.vinyl_toggle = None
|
|
||||||
self.sections = {}
|
|
||||||
|
|
||||||
def add_section(self, section_id, title=None):
|
|
||||||
"""Add a new section to the menu"""
|
|
||||||
section = QuickMenuSection(title=title)
|
|
||||||
self.sections[section_id] = section
|
|
||||||
self.sections_container.add(section)
|
|
||||||
|
|
||||||
# Add separator before section if not the first
|
|
||||||
if len(self.sections) > 1:
|
|
||||||
separator = Label("")
|
|
||||||
separator.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 4px 12px;")
|
|
||||||
self.sections_container.add(separator)
|
|
||||||
|
|
||||||
return section
|
|
||||||
|
|
||||||
def setup_audio_section(self, vinyl_service=None):
|
|
||||||
"""Setup the audio controls section"""
|
|
||||||
audio_section = self.add_section("audio", None) # No section title since it's the only section
|
|
||||||
|
|
||||||
# Vinyl passthrough toggle
|
|
||||||
if vinyl_service:
|
|
||||||
self.vinyl_toggle = QuickMenuToggle(
|
|
||||||
title="Vinyl Passthrough",
|
|
||||||
icon_name="folder-music-symbolic",
|
|
||||||
active=vinyl_service.active,
|
|
||||||
on_toggle=lambda active: self._on_vinyl_toggle(active, vinyl_service)
|
|
||||||
)
|
|
||||||
audio_section.add_item(self.vinyl_toggle)
|
|
||||||
|
|
||||||
# Store reference to vinyl service
|
|
||||||
self.vinyl_service = vinyl_service
|
|
||||||
|
|
||||||
def _on_vinyl_toggle(self, active, vinyl_service):
|
|
||||||
"""Handle vinyl toggle"""
|
|
||||||
logger.info(f"[QuickMenu] Vinyl toggled: {active}")
|
|
||||||
vinyl_service.active = active
|
|
||||||
|
|
||||||
def setup_system_section(self):
|
|
||||||
"""Setup system controls section"""
|
|
||||||
# Removed for now - can add system controls later
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_vinyl_state(self, active):
|
|
||||||
"""Update vinyl toggle state from external source"""
|
|
||||||
if self.vinyl_toggle:
|
|
||||||
self.vinyl_toggle.set_active(active)
|
|
||||||
|
|
||||||
|
|
||||||
class QuickMenuOpener(Button):
|
|
||||||
"""Button to open the quick menu"""
|
|
||||||
def __init__(self, icon_name="open-menu-symbolic", **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
name="quick-menu-button",
|
|
||||||
child=Image(icon_name=icon_name, icon_size=16),
|
|
||||||
on_clicked=self.toggle_menu,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
self.menu = QuickMenu()
|
|
||||||
self.menu_visible = False
|
|
||||||
|
|
||||||
def toggle_menu(self, button=None):
|
|
||||||
"""Toggle the quick menu visibility"""
|
|
||||||
if self.menu_visible:
|
|
||||||
logger.info("[QuickMenu] Hiding menu")
|
|
||||||
self.menu.set_visible(False)
|
|
||||||
self.menu_visible = False
|
|
||||||
else:
|
|
||||||
logger.info("[QuickMenu] Showing menu")
|
|
||||||
self.menu.set_visible(True)
|
|
||||||
self.menu.show_all()
|
|
||||||
self.menu_visible = True
|
|
||||||
|
|
||||||
def get_menu(self):
|
|
||||||
"""Get the menu instance for configuration"""
|
|
||||||
return self.menu
|
|
||||||
@@ -16,51 +16,13 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendar popup */
|
|
||||||
#calendar-popup {
|
|
||||||
background-color: var(--window-bg);
|
|
||||||
border: solid 2px var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
animation: slide-down 200ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-down {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
margin-top: -20px;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#calendar-title {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#events-box {
|
|
||||||
background-color: var(--window-bg);
|
|
||||||
border: none; /* Remove outline */
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#no-events {
|
|
||||||
color: var(--light-grey);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calendar event items */
|
/* Calendar event items */
|
||||||
.event-item {
|
.event-item {
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
padding: 8px 12px;
|
padding: 8px 10px;
|
||||||
margin: 4px 0px;
|
margin: 2px 0px;
|
||||||
background-color: var(--module-bg);
|
background-color: var(--light-bg);
|
||||||
border: none; /* Remove outline */
|
border: none;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,43 +43,100 @@
|
|||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notification center side rail */
|
/* Control center side rail */
|
||||||
|
|
||||||
#notification-center {
|
#control-center {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-body {
|
#control-center-body {
|
||||||
background-color: var(--mid-bg);
|
background-color: var(--background);
|
||||||
border-left: solid 1px var(--border-color);
|
border-left: solid 2px var(--border-color);
|
||||||
padding: 12px;
|
border-bottom: solid 2px var(--border-color);
|
||||||
|
border-bottom-left-radius: 28px;
|
||||||
|
padding: 50px 12px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-header {
|
#control-center-header {
|
||||||
padding: 0 0 8px 0;
|
padding: 4px 8px 12px 8px;
|
||||||
border-bottom: solid 1px var(--border-color);
|
border-bottom: solid 1px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-title {
|
#control-center-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-clear,
|
#control-center-close {
|
||||||
#notification-center-close {
|
padding: 4px 8px;
|
||||||
padding: 4px 10px;
|
border-radius: 12px;
|
||||||
border-radius: 0.5rem;
|
background-color: var(--module-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-close:hover {
|
||||||
background-color: var(--light-bg);
|
background-color: var(--light-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-clear:hover,
|
#control-center-sections {
|
||||||
#notification-center-close:hover {
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-settings,
|
||||||
|
#control-center-calendar,
|
||||||
|
#control-center-notifications {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--light-grey);
|
||||||
|
padding: 4px 4px 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-settings-row {
|
||||||
|
padding: 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-settings-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-events {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-no-events {
|
||||||
|
color: var(--light-grey);
|
||||||
|
padding: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-notifications-header {
|
||||||
|
padding: 0 4px 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-notifications-clear {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-notifications-clear:hover {
|
||||||
background-color: var(--dark-grey);
|
background-color: var(--dark-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-center-empty {
|
#control-center-notifications-list {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-center-notifications-empty {
|
||||||
color: var(--light-grey);
|
color: var(--light-grey);
|
||||||
padding: 24px;
|
padding: 8px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-history-entry {
|
#notification-history-entry {
|
||||||
@@ -120,25 +177,3 @@
|
|||||||
background-color: var(--dark-grey);
|
background-color: var(--dark-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bar bell */
|
|
||||||
|
|
||||||
#notification-bell {
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notification-bell:hover {
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#notification-bell-badge {
|
|
||||||
background-color: var(--red);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 14px;
|
|
||||||
min-height: 14px;
|
|
||||||
margin: -4px -4px 0 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
from typing import Callable
|
|
||||||
|
|
||||||
from fabric.widgets.button import Button
|
|
||||||
from fabric.widgets.image import Image
|
|
||||||
from fabric.widgets.label import Label
|
|
||||||
from fabric.widgets.overlay import Overlay
|
|
||||||
|
|
||||||
from sims.services.notification_history import NotificationHistoryService
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationBell(Button):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
history: NotificationHistoryService,
|
|
||||||
on_clicked: Callable[[], None],
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
self._badge = Label(
|
|
||||||
name="notification-bell-badge",
|
|
||||||
label="",
|
|
||||||
visible=False,
|
|
||||||
v_align="start",
|
|
||||||
h_align="end",
|
|
||||||
)
|
|
||||||
self._badge.set_no_show_all(True)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
name="notification-bell",
|
|
||||||
child=Overlay(
|
|
||||||
child=Image(
|
|
||||||
icon_name="preferences-system-notifications-symbolic",
|
|
||||||
icon_size=18,
|
|
||||||
),
|
|
||||||
overlays=[self._badge],
|
|
||||||
),
|
|
||||||
on_clicked=lambda *_: on_clicked(),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._history = history
|
|
||||||
self._history.connect("changed", lambda *_: self._refresh())
|
|
||||||
self._refresh()
|
|
||||||
|
|
||||||
def _refresh(self) -> None:
|
|
||||||
count = self._history.unseen_count
|
|
||||||
if count <= 0:
|
|
||||||
self._badge.set_visible(False)
|
|
||||||
return
|
|
||||||
self._badge.set_label(str(count) if count < 10 else "9+")
|
|
||||||
self._badge.set_visible(True)
|
|
||||||
Reference in New Issue
Block a user