diff --git a/sims/main.py b/sims/main.py index 1a9694c..5c1e72d 100644 --- a/sims/main.py +++ b/sims/main.py @@ -25,10 +25,12 @@ from .modules.launcher.clipboard import ClipboardMenu from .modules.launcher.power import PowerMenu from .modules.launcher.screenrec import ScreenrecMenu 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.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.notification_history import NotificationHistoryService from .services.screenrec import ScreenrecService @@ -57,7 +59,6 @@ if SCREENREC.get("enable", False): notifications_service: Notifications | None = None notification_history: NotificationHistoryService | None = None notification_toasts: NotificationToasts | None = None -notification_center: NotificationCenter | None = None if NOTIFICATIONS.get("enable", False): notifications_service = Notifications() notification_history = NotificationHistoryService( @@ -73,8 +74,18 @@ if NOTIFICATIONS.get("enable", False): width=NOTIFICATIONS.get("width", 360), 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, width=NOTIFICATIONS.get("center_width", 380), ) @@ -87,8 +98,8 @@ if screenrec_menu is not None: _app_windows.append(screenrec_menu) if notification_toasts is not None: _app_windows.append(notification_toasts) -if notification_center is not None: - _app_windows.append(notification_center) +if control_center is not None: + _app_windows.append(control_center) app = Application("sims", *_app_windows) @@ -148,9 +159,9 @@ def screenrec_stop(): @Application.action() -def toggle_notification_center(): - if notification_center is not None: - notification_center.toggle() +def toggle_control_center(): + if control_center is not None: + control_center.toggle() def _set_all_bars_rounded(rounded: bool): @@ -210,8 +221,7 @@ def spawn_bars(): tray=tray if i == 0 else None, monitor=i, screenrec_service=screenrec_service if i == 0 else None, - notification_history=notification_history if i == 0 else None, - notification_center=notification_center if i == 0 else None, + control_center=control_center if i == 0 else None, ) bar_windows.append(bar) if i == 0 and bar.notmuch: diff --git a/sims/modules/bar.py b/sims/modules/bar.py index e584dcf..1200609 100644 --- a/sims/modules/bar.py +++ b/sims/modules/bar.py @@ -1,4 +1,3 @@ -import psutil from fabric.widgets.box import Box from fabric.widgets.label import Label 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.centerbox import CenterBox 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.calendar import CalendarService, CalendarPopup -from sims.modules.notification_center import NotificationCenter +from sims.modules.control_center import ControlCenter from sims.modules.notmuch import NotmuchWidget 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.system_tray.widgets import SystemTray -from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow -from sims.services.fenster import get_i3_connection +from sims.widgets.fenster import FensterWorkspaces, FensterActiveWindow from sims.services.screenrec import ScreenrecService from sims.services.smart_corners import get_smart_corners_service +from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar 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): @@ -34,8 +28,7 @@ class StatusBar(Window): tray: SystemTray | None = None, monitor: int = 1, screenrec_service: ScreenrecService | None = None, - notification_history: NotificationHistoryService | None = None, - notification_center: NotificationCenter | None = None, + control_center: ControlCenter | None = None, ): super().__init__( name="sims", @@ -49,29 +42,23 @@ class StatusBar(Window): ) self.output = display self._corners_rounded = False + self._right_flat = False self.workspaces = FensterWorkspaces( output=display, name="workspaces", 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") self.date_time = Button( name="date-time-button", child=datetime_widget, - on_clicked=self.toggle_calendar, - style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;" + on_clicked=self._on_date_time_clicked, + 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.active_window = FensterActiveWindow( @@ -95,15 +82,6 @@ class StatusBar(Window): overlays=[self.cpu_progress_bar, self.progress_label], ) 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 if BATTERY["enable"]: @@ -117,13 +95,6 @@ class StatusBar(Window): if screenrec_service is not None: 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( name="widgets-container", spacing=4, @@ -146,11 +117,6 @@ class StatusBar(Window): if 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) center_children = [] @@ -183,23 +149,19 @@ class StatusBar(Window): ) self.children = self.inner - # Create system stats service with signal-based updates self.system_stats_service = SystemStatsService(update_interval=3000) self.system_stats_service.connect("stats-changed", self.update_progress_bars) - # Set the bar height self.set_size_request(-1, BAR_HEIGHT) smart_corners = get_smart_corners_service() smart_corners.connect("state-changed", self._on_smart_corners_changed) 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): - """Cleanup when bar is destroyed""" - if hasattr(self, 'calendar_service'): - self.calendar_service.stop_monitoring() + self.show_all() @property def corners_rounded(self) -> bool: @@ -219,30 +181,19 @@ class StatusBar(Window): return 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): - """Update progress bars when system stats change""" self.cpu_progress_bar.value = cpu_percent self.ram_progress_bar.value = memory_percent - def toggle_calendar(self, button=None): - """Toggle the calendar popup when datetime is clicked""" - from loguru import logger - 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) + def _on_date_time_clicked(self, _button=None): + if self.control_center is not None: + self.control_center.toggle() diff --git a/sims/modules/calendar.py b/sims/modules/calendar.py index 5cd06c8..bdaa84d 100644 --- a/sims/modules/calendar.py +++ b/sims/modules/calendar.py @@ -2,15 +2,10 @@ import json import os import subprocess import shutil -from datetime import datetime, date +from datetime import date # 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', '') -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 sims.config import CALENDAR @@ -203,203 +198,3 @@ class CalendarService: else: logger.info("[Calendar] Using khal 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) diff --git a/sims/modules/control_center.py b/sims/modules/control_center.py new file mode 100644 index 0000000..c20396a --- /dev/null +++ b/sims/modules/control_center.py @@ -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 diff --git a/sims/modules/notification_center.py b/sims/modules/notification_center.py deleted file mode 100644 index 224592f..0000000 --- a/sims/modules/notification_center.py +++ /dev/null @@ -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() diff --git a/sims/modules/quick_menu.py b/sims/modules/quick_menu.py deleted file mode 100644 index c323ba1..0000000 --- a/sims/modules/quick_menu.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/sims/styles/calendar.css b/sims/styles/calendar.css index 0c4d8b8..ff6a103 100644 --- a/sims/styles/calendar.css +++ b/sims/styles/calendar.css @@ -16,51 +16,13 @@ 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 */ .event-item { - border-radius: 6px; - padding: 8px 12px; - margin: 4px 0px; - background-color: var(--module-bg); - border: none; /* Remove outline */ + border-radius: 8px; + padding: 8px 10px; + margin: 2px 0px; + background-color: var(--light-bg); + border: none; transition: background-color 0.15s ease; } diff --git a/sims/styles/notifications.css b/sims/styles/notifications.css index 9dcc28a..b030ee5 100644 --- a/sims/styles/notifications.css +++ b/sims/styles/notifications.css @@ -43,43 +43,100 @@ color: var(--red); } -/* Notification center side rail */ +/* Control center side rail */ -#notification-center { +#control-center { background-color: transparent; } -#notification-center-body { - background-color: var(--mid-bg); - border-left: solid 1px var(--border-color); - padding: 12px; +#control-center-body { + background-color: var(--background); + border-left: solid 2px var(--border-color); + border-bottom: solid 2px var(--border-color); + border-bottom-left-radius: 28px; + padding: 50px 12px 12px 12px; } -#notification-center-header { - padding: 0 0 8px 0; +#control-center-header { + padding: 4px 8px 12px 8px; border-bottom: solid 1px var(--border-color); } -#notification-center-title { +#control-center-title { font-size: 18px; font-weight: bold; + color: var(--foreground); } -#notification-center-clear, -#notification-center-close { - padding: 4px 10px; - border-radius: 0.5rem; +#control-center-close { + padding: 4px 8px; + border-radius: 12px; + background-color: var(--module-bg); +} + +#control-center-close:hover { background-color: var(--light-bg); } -#notification-center-clear:hover, -#notification-center-close:hover { +#control-center-sections { + 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); } -#notification-center-empty { +#control-center-notifications-list { + padding: 0 4px; +} + +#control-center-notifications-empty { color: var(--light-grey); - padding: 24px; + padding: 8px 4px; } #notification-history-entry { @@ -120,25 +177,3 @@ 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; -} diff --git a/sims/widgets/notification_bell.py b/sims/widgets/notification_bell.py deleted file mode 100644 index 1b78fce..0000000 --- a/sims/widgets/notification_bell.py +++ /dev/null @@ -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)