From c6858fea9d273348c436b3dc3d592fe9ff7dd0d9 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 18:57:03 -0700 Subject: [PATCH 01/51] feat: load calendar events --- Bin/calendar-events.py | 191 +++++++++++++++++++++ Bin/check-calendar.py | 11 ++ Bin/list-calendars.py | 21 +++ Modules/Bar/Calendar/CalendarPanel.qml | 78 +++++++++ Services/CalendarService.qml | 224 +++++++++++++++++++++++++ 5 files changed, 525 insertions(+) create mode 100755 Bin/calendar-events.py create mode 100755 Bin/check-calendar.py create mode 100755 Bin/list-calendars.py create mode 100644 Services/CalendarService.qml diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py new file mode 100755 index 00000000..20ac44c3 --- /dev/null +++ b/Bin/calendar-events.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +import json +import re +import sqlite3 +import sys +from datetime import datetime +from pathlib import Path + +from gi.repository import EDataServer + +start_time = int(sys.argv[1]) +end_time = int(sys.argv[2]) + +all_events = [] + +def safe_get_time(ical_time_str): + """Parse iCalendar time string""" + try: + if not ical_time_str: + return None + + ical_time_str = ical_time_str.strip().replace('\r', '').replace('\n', '') + + # Check for TZID parameter (format: TZID=America/Los_Angeles:20240822T180000) + if 'TZID=' in ical_time_str: + # Split on the colon that comes after the TZID value + match = re.match(r'TZID=([^:]+):(.+)', ical_time_str) + if match: + ical_time_str = match.group(2) + elif ';' in ical_time_str and ':' in ical_time_str: + ical_time_str = ical_time_str.split(':', 1)[1] + + ical_time_str = ical_time_str.strip() + + if len(ical_time_str) == 8 and ical_time_str.isdigit(): + dt = datetime.strptime(ical_time_str, '%Y%m%d') + return int(dt.timestamp()) + + # DateTime (YYYYMMDDTHHMMSS or YYYYMMDDTHHMMSSZ) + is_utc = ical_time_str.endswith('Z') + ical_time_str = ical_time_str.rstrip('Z') + dt = datetime.strptime(ical_time_str, '%Y%m%dT%H%M%S') + + if not is_utc: + return int(dt.timestamp()) + + from datetime import timezone + dt = dt.replace(tzinfo=timezone.utc) + return int(dt.timestamp()) + except Exception: + return None + +def parse_ical_component(ical_string, calendar_name): + """Parse an iCalendar component""" + try: + lines = ical_string.split('\n') + event = {} + current_key = None + current_value = [] + + for line in lines: + line = line.replace('\r', '') + + if line.startswith(' ') and current_key: + current_value.append(line[1:]) + continue + + if current_key: + full_value = ''.join(current_value) + event[current_key] = full_value + current_value = [] + + if ':' in line: + key_part, value_part = line.split(':', 1) + + key = key_part.split(';')[0] + + current_key = key + current_value = [line] + + if current_key: + event[current_key] = ''.join(current_value) + + if 'DTSTART' not in event: + return None + + dtstart_line = event.get('DTSTART', '') + if ':' in dtstart_line: + dtstart_value = dtstart_line.split('DTSTART', 1)[1] + else: + dtstart_value = dtstart_line + + start_timestamp = safe_get_time(dtstart_value) + if not start_timestamp: + return None + + if start_timestamp < start_time or start_timestamp > end_time: + return None + + dtend_line = event.get('DTEND', '') + if dtend_line and ':' in dtend_line: + dtend_value = dtend_line.split('DTEND', 1)[1] + end_timestamp = safe_get_time(dtend_value) + else: + end_timestamp = None + + if not end_timestamp or end_timestamp == start_timestamp: + end_timestamp = start_timestamp + 3600 + + summary_line = event.get('SUMMARY', '(No title)') + if 'SUMMARY:' in summary_line: + summary = summary_line.split('SUMMARY:', 1)[1].strip() + else: + summary = summary_line.strip() or '(No title)' + + location_line = event.get('LOCATION', '') + if 'LOCATION:' in location_line: + location = location_line.split('LOCATION:', 1)[1].strip() + else: + location = location_line.strip() + + desc_line = event.get('DESCRIPTION', '') + if 'DESCRIPTION:' in desc_line: + description = desc_line.split('DESCRIPTION:', 1)[1].strip() + else: + description = desc_line.strip() + + return { + 'summary': summary, + 'start': start_timestamp, + 'end': end_timestamp, + 'location': location, + 'description': description, + 'calendar': calendar_name + } + except Exception: + return None + +registry = EDataServer.SourceRegistry.new_sync(None) +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) + +cache_base = Path.home() / ".cache/evolution/calendar" + +for source in sources: + if not source.get_enabled(): + continue + + calendar_name = source.get_display_name() + source_uid = source.get_uid() + + cache_file = cache_base / source_uid / "cache.db" + + if not cache_file.exists(): + cache_file = Path.home() / ".local/share/evolution/calendar" / source_uid / "calendar.ics" + if cache_file.exists(): + try: + with open(cache_file, 'r') as f: + content = f.read() + events = content.split('BEGIN:VEVENT') + for event_str in events[1:]: + event_str = 'BEGIN:VEVENT' + event_str.split('END:VEVENT')[0] + 'END:VEVENT' + event = parse_ical_component(event_str, calendar_name) + if event: + all_events.append(event) + except Exception: + pass + continue + + try: + conn = sqlite3.connect(str(cache_file)) + cursor = conn.cursor() + + cursor.execute("SELECT ECacheOBJ FROM ECacheObjects") + rows = cursor.fetchall() + + for row in rows: + ical_string = row[0] + if ical_string and 'BEGIN:VEVENT' in str(ical_string): + event = parse_ical_component(str(ical_string), calendar_name) + if event: + all_events.append(event) + + conn.close() + except Exception as e: + print(f"Error processing {calendar_name}: {e}", file=sys.stderr) + +all_events.sort(key=lambda x: x['start']) +print(json.dumps(all_events)) diff --git a/Bin/check-calendar.py b/Bin/check-calendar.py new file mode 100755 index 00000000..463d2e40 --- /dev/null +++ b/Bin/check-calendar.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') + +try: + from gi.repository import ECal, EDataServer + print("available") +except ImportError as e: + print(f"unavailable: {e}") diff --git a/Bin/list-calendars.py b/Bin/list-calendars.py new file mode 100755 index 00000000..12c67bd1 --- /dev/null +++ b/Bin/list-calendars.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +import json + +from gi.repository import EDataServer + +registry = EDataServer.SourceRegistry.new_sync(None) +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) + +calendars = [] +for source in sources: + if source.get_enabled(): + calendars.append({ + 'uid': source.get_uid(), + 'name': source.get_display_name(), + 'enabled': True + }) + +print(json.dumps(calendars)) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 277745b5..f88318cf 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -10,6 +10,8 @@ import qs.Widgets NPanel { id: root + property ShellScreen screen + preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 400 : 380 preferredHeight: 520 @@ -315,6 +317,14 @@ NPanel { grid.year = newDate.getFullYear() grid.month = newDate.getMonth() content.isCurrentMonth = content.checkIsCurrentMonth() + const now = new Date() + const monthStart = new Date(grid.year, grid.month, 1) + const monthEnd = new Date(grid.year, grid.month + 1, 0) + + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))) + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))) + + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30) } } @@ -324,6 +334,7 @@ NPanel { grid.month = Time.date.getMonth() grid.year = Time.date.getFullYear() content.isCurrentMonth = true + CalendarService.loadEvents() } } @@ -334,6 +345,14 @@ NPanel { grid.year = newDate.getFullYear() grid.month = newDate.getMonth() content.isCurrentMonth = content.checkIsCurrentMonth() + const now = new Date() + const monthStart = new Date(grid.year, grid.month, 1) + const monthEnd = new Date(grid.year, grid.month + 1, 0) + + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))) + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))) + + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30) } } } @@ -385,6 +404,35 @@ NPanel { Layout.fillHeight: true spacing: 0 + // Helper function to check if a date has events + function hasEventsOnDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return false + + const targetDate = new Date(year, month, day) + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetEnd = targetStart + 86400 // +24 hours + + return CalendarService.events.some(event => { + // Check if event starts or overlaps with this day + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) + } + + // Helper function to get events for a specific date + function getEventsForDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return [] + + const targetDate = new Date(year, month, day) + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetEnd = targetStart + 86400 // +24 hours + + return CalendarService.events.filter(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + }) + } + // Column of week numbers ColumnLayout { visible: Settings.data.location.showWeekNumberInCalendar @@ -466,6 +514,36 @@ NPanel { font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium } + // Event indicator dot + Rectangle { + visible: parent.parent.parent.parent.parent.hasEventsOnDate(model.year, model.month, model.day) + width: 4 * scaling + height: 4 * scaling + radius: 2 * scaling + color: model.today ? Color.mOnSecondary : Color.mPrimary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginXS * scaling + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + onEntered: { + const events = parent.parent.parent.parent.parent.getEventsForDate(model.year, model.month, model.day) + if (events.length > 0) { + const summaries = events.map(e => e.summary).join('\n') + TooltipService.show(Screen, parent, summaries) + TooltipService.updateText(summaries) + } + } + + onExited: { + TooltipService.hide() + } + } + Behavior on color { ColorAnimation { duration: Style.animationFast diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml new file mode 100644 index 00000000..83aa5a2d --- /dev/null +++ b/Services/CalendarService.qml @@ -0,0 +1,224 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + // Core state + property var events: ([]) + property bool loading: false + property bool available: false + property string lastError: "" + property var calendars: ([]) + + // Persistent cache + property string cacheFile: Settings.cacheDir + "calendar.json" + + // Python scripts + readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Bin/check-calendar.py' + readonly property string listCalendarsScript: Quickshell.shellDir + '/Bin/list-calendars.py' + readonly property string calendarEventsScript: Quickshell.shellDir + '/Bin/calendar-events.py' + + // Cache file handling + FileView { + id: cacheFileView + path: root.cacheFile + printErrors: false + + JsonAdapter { + id: cacheAdapter + property var cachedEvents: ([]) + property var cachedCalendars: ([]) + property string lastUpdate: "" + } + + onLoadFailed: { + cacheAdapter.cachedEvents = ([]) + cacheAdapter.cachedCalendars = ([]) + cacheAdapter.lastUpdate = "" + } + } + + Component.onCompleted: { + Logger.log("Calendar", "Service initialized") + checkAvailability() + } + + // Save cache with debounce + Timer { + id: saveDebounce + interval: 1000 + onTriggered: cacheFileView.writeAdapter() + } + + function saveCache() { + saveDebounce.restart() + } + + // Auto-refresh timer (every 5 minutes) + Timer { + id: refreshTimer + interval: 300000 + running: true + repeat: true + onTriggered: loadEvents() + } + + // Core functions + function checkAvailability() { + availabilityCheckProcess.running = true + } + + function loadCalendars() { + listCalendarsProcess.running = true + } + + function loadEvents(daysAhead = 31, daysBehind = 14) { + if (loading) + return + + loading = true + lastError = "" + + const now = new Date() + const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000)) + const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000)) + + loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000) + loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000) + loadEventsProcess.running = true + + Logger.log("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`) + } + + // Helper to format date/time + function formatDateTime(timestamp) { + const date = new Date(timestamp * 1000) + return Qt.formatDateTime(date, "yyyy-MM-dd hh:mm") + } + + // Process to check for evolution-data-server libraries + Process { + id: availabilityCheckProcess + running: false + command: ["python3", root.checkCalendarAvailableScript] + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim() + root.available = result === "available" + + if (root.available) { + Logger.log("Calendar", "EDS libraries available") + loadCalendars() + } else { + Logger.warn("Calendar", "EDS libraries not available: " + result) + root.lastError = "Evolution Data Server libraries not installed" + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Calendar", "Availability check error: " + text) + root.available = false + root.lastError = "Failed to check library availability" + } + } + } + } + + // Process to list available calendars + Process { + id: listCalendarsProcess + running: false + command: ["python3", root.listCalendarsScript] + + stdout: StdioCollector { + onStreamFinished: { + try { + const result = JSON.parse(text.trim()) + root.calendars = result + cacheAdapter.cachedCalendars = result + saveCache() + + Logger.log("Calendar", `Found ${result.length} calendar(s)`) + + // Auto-load events after discovering calendars + if (result.length > 0) { + loadEvents() + } + } catch (e) { + Logger.warn("Calendar", "Failed to parse calendars: " + e) + root.lastError = "Failed to parse calendar list" + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Calendar", "List calendars error: " + text) + root.lastError = text.trim() + } + } + } + } + + // Process to load events + Process { + id: loadEventsProcess + running: false + property int startTime: 0 + property int endTime: 0 + + command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()] + + stdout: StdioCollector { + onStreamFinished: { + root.loading = false + + try { + const result = JSON.parse(text.trim()) + root.events = result + cacheAdapter.cachedEvents = result + cacheAdapter.lastUpdate = new Date().toISOString() + saveCache() + + Logger.log("Calendar", `Loaded ${result.length} event(s)`) + } catch (e) { + Logger.warn("Calendar", "Failed to parse events: " + e) + root.lastError = "Failed to parse events" + + // Fall back to cached events if available + if (cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.log("Calendar", "Using cached events") + } + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.loading = false + + if (text.trim()) { + Logger.warn("Calendar", "Load events error: " + text) + root.lastError = text.trim() + + // Fall back to cached events if available + if (cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.log("Calendar", "Using cached events due to error") + } + } + } + } + } +} From 3a3c70c4e081be22833688c2276324a36d2c2e14 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:27:49 -0700 Subject: [PATCH 02/51] fix: use events from connection, not hacky db parsing --- Bin/calendar-events.py | 245 ++++++++++--------------- Modules/Bar/Calendar/CalendarPanel.qml | 20 +- 2 files changed, 110 insertions(+), 155 deletions(-) diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py index 20ac44c3..643a6184 100755 --- a/Bin/calendar-events.py +++ b/Bin/calendar-events.py @@ -2,190 +2,133 @@ import gi gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') import json -import re -import sqlite3 import sys -from datetime import datetime -from pathlib import Path +from datetime import datetime, timezone -from gi.repository import EDataServer +from gi.repository import ECal, EDataServer start_time = int(sys.argv[1]) end_time = int(sys.argv[2]) +print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr) + all_events = [] -def safe_get_time(ical_time_str): - """Parse iCalendar time string""" +def safe_get_time(ical_time): + """Safely get time from ICalTime object""" + if not ical_time: + return None + try: - if not ical_time_str: + year = ical_time.get_year() + month = ical_time.get_month() + day = ical_time.get_day() + + if year < 1970 or year > 2100 or month < 1 or month > 12 or day < 1 or day > 31: return None - ical_time_str = ical_time_str.strip().replace('\r', '').replace('\n', '') - - # Check for TZID parameter (format: TZID=America/Los_Angeles:20240822T180000) - if 'TZID=' in ical_time_str: - # Split on the colon that comes after the TZID value - match = re.match(r'TZID=([^:]+):(.+)', ical_time_str) - if match: - ical_time_str = match.group(2) - elif ';' in ical_time_str and ':' in ical_time_str: - ical_time_str = ical_time_str.split(':', 1)[1] - - ical_time_str = ical_time_str.strip() - - if len(ical_time_str) == 8 and ical_time_str.isdigit(): - dt = datetime.strptime(ical_time_str, '%Y%m%d') + if ical_time.is_date(): + dt = datetime(year, month, day, 0, 0, 0, tzinfo=timezone.utc) return int(dt.timestamp()) - # DateTime (YYYYMMDDTHHMMSS or YYYYMMDDTHHMMSSZ) - is_utc = ical_time_str.endswith('Z') - ical_time_str = ical_time_str.rstrip('Z') - dt = datetime.strptime(ical_time_str, '%Y%m%dT%H%M%S') + hour = ical_time.get_hour() + minute = ical_time.get_minute() + second = ical_time.get_second() - if not is_utc: - return int(dt.timestamp()) - - from datetime import timezone - dt = dt.replace(tzinfo=timezone.utc) + dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc) return int(dt.timestamp()) except Exception: return None -def parse_ical_component(ical_string, calendar_name): - """Parse an iCalendar component""" - try: - lines = ical_string.split('\n') - event = {} - current_key = None - current_value = [] - - for line in lines: - line = line.replace('\r', '') - - if line.startswith(' ') and current_key: - current_value.append(line[1:]) - continue - - if current_key: - full_value = ''.join(current_value) - event[current_key] = full_value - current_value = [] - - if ':' in line: - key_part, value_part = line.split(':', 1) - - key = key_part.split(';')[0] - - current_key = key - current_value = [line] - - if current_key: - event[current_key] = ''.join(current_value) - - if 'DTSTART' not in event: - return None - - dtstart_line = event.get('DTSTART', '') - if ':' in dtstart_line: - dtstart_value = dtstart_line.split('DTSTART', 1)[1] - else: - dtstart_value = dtstart_line - - start_timestamp = safe_get_time(dtstart_value) - if not start_timestamp: - return None - - if start_timestamp < start_time or start_timestamp > end_time: - return None - - dtend_line = event.get('DTEND', '') - if dtend_line and ':' in dtend_line: - dtend_value = dtend_line.split('DTEND', 1)[1] - end_timestamp = safe_get_time(dtend_value) - else: - end_timestamp = None - - if not end_timestamp or end_timestamp == start_timestamp: - end_timestamp = start_timestamp + 3600 - - summary_line = event.get('SUMMARY', '(No title)') - if 'SUMMARY:' in summary_line: - summary = summary_line.split('SUMMARY:', 1)[1].strip() - else: - summary = summary_line.strip() or '(No title)' - - location_line = event.get('LOCATION', '') - if 'LOCATION:' in location_line: - location = location_line.split('LOCATION:', 1)[1].strip() - else: - location = location_line.strip() - - desc_line = event.get('DESCRIPTION', '') - if 'DESCRIPTION:' in desc_line: - description = desc_line.split('DESCRIPTION:', 1)[1].strip() - else: - description = desc_line.strip() - - return { - 'summary': summary, - 'start': start_timestamp, - 'end': end_timestamp, - 'location': location, - 'description': description, - 'calendar': calendar_name - } - except Exception: - return None - +print("Getting registry...", file=sys.stderr) registry = EDataServer.SourceRegistry.new_sync(None) -sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) +print("Registry obtained", file=sys.stderr) -cache_base = Path.home() / ".cache/evolution/calendar" +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) +print(f"Found {len(sources)} calendar sources", file=sys.stderr) for source in sources: if not source.get_enabled(): + print(f"Skipping disabled calendar: {source.get_display_name()}", file=sys.stderr) continue calendar_name = source.get_display_name() - source_uid = source.get_uid() - - cache_file = cache_base / source_uid / "cache.db" - - if not cache_file.exists(): - cache_file = Path.home() / ".local/share/evolution/calendar" / source_uid / "calendar.ics" - if cache_file.exists(): - try: - with open(cache_file, 'r') as f: - content = f.read() - events = content.split('BEGIN:VEVENT') - for event_str in events[1:]: - event_str = 'BEGIN:VEVENT' + event_str.split('END:VEVENT')[0] + 'END:VEVENT' - event = parse_ical_component(event_str, calendar_name) - if event: - all_events.append(event) - except Exception: - pass - continue + print(f"\nProcessing calendar: {calendar_name}", file=sys.stderr) try: - conn = sqlite3.connect(str(cache_file)) - cursor = conn.cursor() + print(f" Connecting to {calendar_name}...", file=sys.stderr) + client = ECal.Client.connect_sync( + source, + ECal.ClientSourceType.EVENTS, + 30, + None + ) + print(f" Connected to {calendar_name}", file=sys.stderr) - cursor.execute("SELECT ECacheOBJ FROM ECacheObjects") - rows = cursor.fetchall() + start_dt = datetime.fromtimestamp(start_time, tz=timezone.utc) + end_dt = datetime.fromtimestamp(end_time, tz=timezone.utc) - for row in rows: - ical_string = row[0] - if ical_string and 'BEGIN:VEVENT' in str(ical_string): - event = parse_ical_component(str(ical_string), calendar_name) - if event: - all_events.append(event) + start_str = start_dt.strftime("%Y%m%dT%H%M%SZ") + end_str = end_dt.strftime("%Y%m%dT%H%M%SZ") + + query = f'(occur-in-time-range? (make-time "{start_str}") (make-time "{end_str}"))' + print(f" Query: {query}", file=sys.stderr) + + print(f" Getting object list for {calendar_name}...", file=sys.stderr) + success, ical_objects = client.get_object_list_sync(query, None) + print(f" Got object list for {calendar_name}: success={success}, count={len(ical_objects) if ical_objects else 0}", file=sys.stderr) + + if not success or not ical_objects: + print(f" No events found in {calendar_name}", file=sys.stderr) + continue + + print(f" Processing {len(ical_objects)} events from {calendar_name}...", file=sys.stderr) + for idx, ical_obj in enumerate(ical_objects): + try: + if hasattr(ical_obj, 'get_summary'): + comp = ical_obj + else: + comp = ECal.Component.new_from_string(ical_obj) + + if not comp: + continue + + summary = comp.get_summary() or "(No title)" + + start_timestamp = safe_get_time(comp.get_dtstart()) + if start_timestamp is None: + continue + + end_timestamp = safe_get_time(comp.get_dtend()) + if end_timestamp is None or end_timestamp == start_timestamp: + end_timestamp = start_timestamp + 3600 + + location = comp.get_location() or "" + description = comp.get_description() or "" + + all_events.append({ + 'summary': summary, + 'start': start_timestamp, + 'end': end_timestamp, + 'location': location, + 'description': description, + 'calendar': calendar_name + }) + + if (idx + 1) % 10 == 0: + print(f" Processed {idx + 1} events from {calendar_name}...", file=sys.stderr) + except Exception as e: + print(f" Error processing event {idx} in {calendar_name}: {e}", file=sys.stderr) + continue + + print(f" Finished processing {calendar_name}, found {len([e for e in all_events if e['calendar'] == calendar_name])} events", file=sys.stderr) - conn.close() except Exception as e: - print(f"Error processing {calendar_name}: {e}", file=sys.stderr) + print(f" Error for {calendar_name}: {e}", file=sys.stderr) +print(f"\nSorting {len(all_events)} total events...", file=sys.stderr) all_events.sort(key=lambda x: x['start']) +print("Done! Outputting JSON...", file=sys.stderr) print(json.dumps(all_events)) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index f88318cf..4b5e37e8 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -410,12 +410,24 @@ NPanel { return false const targetDate = new Date(year, month, day) - const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) const targetEnd = targetStart + 86400 // +24 hours + if (year === 2025 && month === 9 && day === 8) { + Logger.log(`Checking ${year}-${month + 1}-${day}:`) + Logger.log(` Target range: ${targetStart} to ${targetEnd}`) + Logger.log(` Target dates: ${new Date(targetStart * 1000)} to ${new Date(targetEnd * 1000)}`) + Logger.log(` Total events: ${CalendarService.events.length}`) + CalendarService.events.forEach(event => { + Logger.log(` Event: "${event.summary}" start=${event.start} (${new Date(event.start * 1000)}) end=${event.end} (${new Date(event.end * 1000)})`) + const matches = (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end < targetEnd) || (event.start <= targetStart && event.end >= targetEnd) + Logger.log(` Matches: ${matches}`) + }) + } + return CalendarService.events.some(event => { // Check if event starts or overlaps with this day - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end < targetEnd) || (event.start <= targetStart && event.end >= targetEnd) }) } @@ -425,11 +437,11 @@ NPanel { return [] const targetDate = new Date(year, month, day) - const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 + const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) const targetEnd = targetStart + 86400 // +24 hours return CalendarService.events.filter(event => { - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) + return (event.start >= targetStart && event.start < targetEnd) || +(event.end > targetStart && event.end < targetEnd) || +(event.start <= targetStart && event.end >= targetEnd) }) } From d19f6ee15f455d9ea2d306c57c70041180010d99 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:28:57 -0700 Subject: [PATCH 03/51] remove debug logging --- Modules/Bar/Calendar/CalendarPanel.qml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 4b5e37e8..6f9a91e5 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -413,18 +413,6 @@ NPanel { const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) const targetEnd = targetStart + 86400 // +24 hours - if (year === 2025 && month === 9 && day === 8) { - Logger.log(`Checking ${year}-${month + 1}-${day}:`) - Logger.log(` Target range: ${targetStart} to ${targetEnd}`) - Logger.log(` Target dates: ${new Date(targetStart * 1000)} to ${new Date(targetEnd * 1000)}`) - Logger.log(` Total events: ${CalendarService.events.length}`) - CalendarService.events.forEach(event => { - Logger.log(` Event: "${event.summary}" start=${event.start} (${new Date(event.start * 1000)}) end=${event.end} (${new Date(event.end * 1000)})`) - const matches = (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end < targetEnd) || (event.start <= targetStart && event.end >= targetEnd) - Logger.log(` Matches: ${matches}`) - }) - } - return CalendarService.events.some(event => { // Check if event starts or overlaps with this day return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end < targetEnd) || (event.start <= targetStart && event.end >= targetEnd) From 30a940e45b57578598aa29e1d3393c922a5a4817 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:33:59 -0700 Subject: [PATCH 04/51] fix: load events from cache before syncing --- Services/CalendarService.qml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index 83aa5a2d..a63eafd4 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -41,10 +41,15 @@ Singleton { cacheAdapter.cachedCalendars = ([]) cacheAdapter.lastUpdate = "" } + + onLoaded: { + loadFromCache() + } } Component.onCompleted: { Logger.log("Calendar", "Service initialized") + loadFromCache() checkAvailability() } @@ -59,6 +64,23 @@ Singleton { saveDebounce.restart() } + // Load events and calendars from cache + function loadFromCache() { + if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents + Logger.log("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`) + } + + if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) { + root.calendars = cacheAdapter.cachedCalendars + Logger.log("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`) + } + + if (cacheAdapter.lastUpdate) { + Logger.log("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`) + } + } + // Auto-refresh timer (every 5 minutes) Timer { id: refreshTimer @@ -150,7 +172,11 @@ Singleton { Logger.log("Calendar", `Found ${result.length} calendar(s)`) // Auto-load events after discovering calendars - if (result.length > 0) { + // Only load if we have calendars and no cached events + if (result.length > 0 && root.events.length === 0) { + loadEvents() + } else if (result.length > 0) { + // If we already have cached events, load in background loadEvents() } } catch (e) { From bf06e5a3bbf5bb32061b07f6b9b1c185c5e8079d Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:36:37 -0700 Subject: [PATCH 05/51] check if python3 is installed --- Services/CalendarService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index a63eafd4..8a519c95 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -127,7 +127,7 @@ Singleton { Process { id: availabilityCheckProcess running: false - command: ["python3", root.checkCalendarAvailableScript] + command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'python3 unavailable'"] stdout: StdioCollector { onStreamFinished: { From 799f4e0257fa9d4adcee444376275c1c98032562 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:47:49 -0700 Subject: [PATCH 06/51] open gnome-calendar on date click --- Modules/Bar/Calendar/CalendarPanel.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 6f9a91e5..1dbc473a 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -539,6 +539,11 @@ NPanel { } } + onClicked: { + const dateWithSlashes = `${model.month.toString().padStart(2, '0')}/${model.day.toString().padStart(2, '0')}/${model.year.toString().substring(2)}` + Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]) + } + onExited: { TooltipService.hide() } From d6f8eb0bed079286091b7cce7e00a0ac3be46c1c Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 19:55:44 -0700 Subject: [PATCH 07/51] fix: handle timezone properly for all-day events --- Bin/calendar-events.py | 5 +++-- Modules/Bar/Calendar/CalendarPanel.qml | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py index 643a6184..febfe2ed 100755 --- a/Bin/calendar-events.py +++ b/Bin/calendar-events.py @@ -5,6 +5,7 @@ gi.require_version('EDataServer', '1.2') gi.require_version('ECal', '2.0') import json import sys +import time from datetime import datetime, timezone from gi.repository import ECal, EDataServer @@ -30,8 +31,8 @@ def safe_get_time(ical_time): return None if ical_time.is_date(): - dt = datetime(year, month, day, 0, 0, 0, tzinfo=timezone.utc) - return int(dt.timestamp()) + local_struct = time.struct_time((year, month, day, 0, 0, 0, 0, 0, -1)) + return int(time.mktime(local_struct)) hour = ical_time.get_hour() minute = ical_time.get_minute() diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 1dbc473a..d9269a3a 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -410,12 +410,12 @@ NPanel { return false const targetDate = new Date(year, month, day) - const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000) + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000 const targetEnd = targetStart + 86400 // +24 hours return CalendarService.events.some(event => { // Check if event starts or overlaps with this day - return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end < targetEnd) || (event.start <= targetStart && event.end >= targetEnd) + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) }) } @@ -429,7 +429,7 @@ NPanel { const targetEnd = targetStart + 86400 // +24 hours return CalendarService.events.filter(event => { - return (event.start >= targetStart && event.start < targetEnd) || +(event.end > targetStart && event.end < targetEnd) || +(event.start <= targetStart && event.end >= targetEnd) + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd) }) } From 64f10ff1c4f3a2af7a7cc94436528e4718857339 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 20:28:55 -0700 Subject: [PATCH 08/51] one dot per event, different dot colors depending on event length --- Modules/Bar/Calendar/CalendarPanel.qml | 59 +++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index d9269a3a..0c33873c 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -433,6 +433,40 @@ NPanel { }) } + // Helper function to check if an event is all-day + function isAllDayEvent(event) { + const duration = event.end - event.start + const startDate = new Date(event.start * 1000) + const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0 + return duration === 86400 && isAtMidnight + } + + // Helper function to check if an event is multi-day + function isMultiDayEvent(event) { + if (isAllDayEvent(event)) { + return false + } + + const startDate = new Date(event.start * 1000) + const endDate = new Date(event.end * 1000) + + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()) + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()) + + return startDateOnly.getTime() !== endDateOnly.getTime() + } + + // Helper function to get color for a specific event + function getEventColor(event, isToday) { + if (isMultiDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mTertiary + } else if (isAllDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mSecondary + } else { + return isToday ? Color.mOnSecondary : Color.mPrimary + } + } + // Column of week numbers ColumnLayout { visible: Settings.data.location.showWeekNumberInCalendar @@ -514,16 +548,29 @@ NPanel { font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium } - // Event indicator dot - Rectangle { + // Event indicator dots + Row { visible: parent.parent.parent.parent.parent.hasEventsOnDate(model.year, model.month, model.day) - width: 4 * scaling - height: 4 * scaling - radius: 2 * scaling - color: model.today ? Color.mOnSecondary : Color.mPrimary + spacing: 2 * scaling anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Style.marginXS * scaling + + readonly property int currentYear: model.year + readonly property int currentMonth: model.month + readonly property int currentDay: model.day + readonly property bool isToday: model.today + + Repeater { + model: parent.parent.parent.parent.parent.parent.getEventsForDate(parent.currentYear, parent.currentMonth, parent.currentDay) + + Rectangle { + width: 4 * scaling + height: 4 * scaling + radius: 2 * scaling + color: parent.parent.parent.parent.parent.parent.getEventColor(modelData, model.today) + } + } } MouseArea { From 80d5c3be23a1bbb6f71a889be7672d39fe7ae3cd Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Wed, 8 Oct 2025 20:31:17 -0700 Subject: [PATCH 09/51] fix python not installed error msg --- Services/CalendarService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index 8a519c95..c7498ed0 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -127,7 +127,7 @@ Singleton { Process { id: availabilityCheckProcess running: false - command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'python3 unavailable'"] + command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"] stdout: StdioCollector { onStreamFinished: { From a64899d76aa2b10e0c8e923c4cc03654f0a61e2c Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Sun, 12 Oct 2025 10:39:47 -0700 Subject: [PATCH 10/51] revert accidental personal change --- Modules/Bar/Calendar/CalendarPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 20ca5899..b29e7d8a 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -459,7 +459,7 @@ NPanel { // Helper function to get color for a specific event function getEventColor(event, isToday) { if (isMultiDayEvent(event)) { - return isToday ? Color.mOnSecondary : "#c4a7e7" + return isToday ? Color.mOnSecondary : Color.mTertiary } else if (isAllDayEvent(event)) { return isToday ? Color.mOnSecondary : Color.mSecondary } else { From e6cc02c8b2a71412fe1fc237f198140603870908 Mon Sep 17 00:00:00 2001 From: herobrauni Date: Wed, 15 Oct 2025 15:13:58 +0000 Subject: [PATCH 11/51] added taskbar that is grouped by workspace --- Modules/Bar/Widgets/TaskbarGrouped.qml | 256 +++++++++++++++++++++++++ Services/BarWidgetRegistry.qml | 7 + 2 files changed, 263 insertions(+) create mode 100644 Modules/Bar/Widgets/TaskbarGrouped.qml diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml new file mode 100644 index 00000000..620e92e4 --- /dev/null +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -0,0 +1,256 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property bool density: Settings.data.bar.density + readonly property real itemSize: (density === "compact") ? Style.capsuleHeight * 0.9 : Style.capsuleHeight * 0.8 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false + property ListModel localWorkspaces: ListModel {} + + function getWindowsForWorkspace(workspaceId) { + var windowsInWs = [] + for (var i = 0; i < CompositorService.windows.count; i++) { + var window = CompositorService.windows.get(i) + if (window.workspaceId === workspaceId) { + windowsInWs.push(window) + } + } + return windowsInWs + } + + function refreshWorkspaces() { + localWorkspaces.clear() + if (screen !== null) { + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) { + continue + } + + var windowsInWs = getWindowsForWorkspace(ws.id) + var windowsModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root) + for (var j = 0; j < windowsInWs.length; j++) { + windowsModel.append(windowsInWs[j]) + } + localWorkspaces.append({ + "id": ws.id, + "name": ws.name, + "output": ws.output, + "isFocused": ws.isFocused, + "isOccupied": ws.isOccupied, + "isActive": ws.isActive, + "isUrgent": ws.isUrgent, + "windows": windowsModel + }) + } + } + } + } + + Component.onCompleted: { + refreshWorkspaces() + } + implicitWidth: isVerticalBar ? taskbarLayoutVertical.implicitWidth + Style.marginM * 2 : Math.round(taskbarLayoutHorizontal.implicitWidth + Style.marginM * 2) + implicitHeight: isVerticalBar ? Math.round(taskbarLayoutVertical.implicitHeight + Style.marginM * 2) : Style.barHeight + + Connections { + target: CompositorService + + function onWorkspacesChanged() { + refreshWorkspaces() + } + + function onWindowListChanged() { + refreshWorkspaces() + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + y: isVerticalBar ? 0 : (parent.height - height) / 2 + height: isVerticalBar ? parent.height : Style.capsuleHeight + radius: Style.radiusM + color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent + } + + Component { + id: workspaceRepeaterDelegate + + Rectangle { + id: container + + property var workspaceModel: model + property bool hasWindows: workspaceModel.windows.count > 0 + + radius: Style.radiusM + color: "transparent" + border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline + border.width: 1 + // Dynamic sizing + width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + Style.marginL + height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + Style.marginXS + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: !hasWindows + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + CompositorService.switchToWorkspace(workspaceModel) + } + } + + Flow { + id: iconsFlow + + anchors.centerIn: parent + spacing: 4 + flow: root.isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: workspaceModel.windows + + delegate: Item { + id: taskbarItem + + width: root.itemSize * 0.8 + height: root.itemSize * 0.8 + + IconImage { + id: appIcon + + width: parent.width + height: parent.height + source: ThemeIcons.iconForAppId(model.appId) + smooth: true + asynchronous: true + opacity: model.isFocused ? Style.opacityFull : 0.6 + layer.enabled: widgetSettings.colorizeIcons === true + + Rectangle { + anchors.bottomMargin: -2 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: 4 + height: 4 + color: model.isFocused ? Color.mPrimary : Color.transparent + radius: width * 0.5 + } + + layer.effect: ShaderEffect { + property color targetColor: Color.mOnSurface + property real colorizeMode: 0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function (mouse) { + if (!model) { + return + } + + if (mouse.button === Qt.LeftButton) { + try { + CompositorService.focusWindow(model) + } catch (error) { + Logger.error("TaskbarGrouped", "Failed to focus window: " + error) + } + } else if (mouse.button === Qt.RightButton) { + try { + CompositorService.closeWindow(model) + } catch (error) { + Logger.error("TaskbarGrouped", "Failed to close window: " + error) + } + } + } + onEntered: TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection()) + onExited: TooltipService.hide() + } + } + } + } + + // Animate size changes for a smooth look + Behavior on width { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + } + } + + Row { + id: taskbarLayoutHorizontal + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Style.marginM + spacing: Style.marginS + visible: !isVerticalBar + + Repeater { + model: localWorkspaces + delegate: workspaceRepeaterDelegate + } + } + + Column { + id: taskbarLayoutVertical + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Style.marginM + spacing: Style.marginS + visible: isVerticalBar + + Repeater { + model: localWorkspaces + delegate: workspaceRepeaterDelegate + } + } +} \ No newline at end of file diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 15263558..1ce6affc 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -30,6 +30,7 @@ Singleton { "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, + "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, "Volume": volumeComponent, "WiFi": wiFiComponent, @@ -121,6 +122,9 @@ Singleton { "hideMode": "hidden", "colorizeIcons": false }, + "TaskbarGrouped": { + "allowUserSettings": true + }, "Tray": { "allowUserSettings": true, "blacklist": [], @@ -213,6 +217,9 @@ Singleton { property Component taskbarComponent: Component { Taskbar {} } + property Component taskbarGroupedComponent: Component { + TaskbarGrouped {} + } function init() { Logger.log("BarWidgetRegistry", "Service started") From 4df7fedc532dc41b431ac183a739b9836d332ffb Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Fri, 17 Oct 2025 19:52:46 +0200 Subject: [PATCH 12/51] NPanel: make panels open in overlay layer --- Widgets/NPanel.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 72e4905e..4b80964d 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -157,6 +157,7 @@ Loader { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None Region { From 9e39867117fc59e4604b11c912ffadf62f85177e Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Sun, 19 Oct 2025 11:24:25 +0800 Subject: [PATCH 13/51] ActiveWindow: synchronize title width with widget width --- Modules/Bar/Widgets/ActiveWindow.qml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 2058bf1a..04e9a0ab 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -343,13 +343,6 @@ Item { easing.type: Easing.Linear } } - - Behavior on Layout.preferredWidth { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic - } - } } } From 70153217727fbd847b1d4c1c8b4d5b66acacaa17 Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Sun, 19 Oct 2025 11:28:42 +0800 Subject: [PATCH 14/51] ActiveWindow: reset scrolling when focused window changes --- Modules/Bar/Widgets/ActiveWindow.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 04e9a0ab..b951e958 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -307,6 +307,14 @@ Item { font.weight: Style.fontWeightMedium verticalAlignment: Text.AlignVCenter color: Color.mOnSurface + onTextChanged: { + if (root.scrollingMode === "always") { + titleContainer.isScrolling = false + titleContainer.isResetting = false + scrollContainer.scrollX = 0 + scrollStartTimer.restart() + } + } } // Second copy for seamless scrolling From 7f5fd5fa14ac2269db178a4f3bd65a97f5caf1aa Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Sun, 19 Oct 2025 12:09:39 +0800 Subject: [PATCH 15/51] ActiveWindow: add fade-in and fade-out transitions of widget --- Modules/Bar/Widgets/ActiveWindow.qml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index b951e958..47041393 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -45,12 +45,12 @@ Item { readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window" readonly property string fallbackIcon: "user-desktop" - implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.barHeight) : 0 - implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : dynamicWidth) : 0 + implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.capsuleHeight) : 0 + implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : (((!hasFocusedWindow) && (hideMode === "transparent" || hideMode === "hidden")) ? 0 : dynamicWidth)) : 0 // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty - visible: hideMode !== "hidden" || hasFocusedWindow - opacity: hideMode !== "transparent" || hasFocusedWindow ? 1.0 : 0 + visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0 + opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: Style.animationNormal @@ -58,6 +58,20 @@ Item { } } + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + function calculatedVerticalDimension() { return Math.round((Style.baseWidgetSize - 5) * scaling) } @@ -157,7 +171,7 @@ Item { visible: root.visible anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: isVerticalBar ? root.width : dynamicWidth + width: isVerticalBar ? root.width : (((!hasFocusedWindow) && (hideMode === "transparent" || hideMode === "hidden")) ? 0 : dynamicWidth) height: isVerticalBar ? width : Style.capsuleHeight radius: isVerticalBar ? width / 2 : Style.radiusM color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent From 5e562bb9a92cdc120da006d736724122ccff5cad Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Sun, 19 Oct 2025 13:14:02 +0800 Subject: [PATCH 16/51] ActiveWindow: fix widget width still remains on maxWidth when visiblity is Always Visible --- Modules/Bar/Widgets/ActiveWindow.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 47041393..a3c76f29 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -46,7 +46,7 @@ Item { readonly property string fallbackIcon: "user-desktop" implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.capsuleHeight) : 0 - implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : (((!hasFocusedWindow) && (hideMode === "transparent" || hideMode === "hidden")) ? 0 : dynamicWidth)) : 0 + implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)) : 0 // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0 @@ -107,7 +107,7 @@ Item { } // Otherwise, adapt to content if (!hasFocusedWindow) { - return maxWidth + return Math.min(calculateContentWidth(), maxWidth) } // Use content width but don't exceed user-set maximum width return Math.min(calculateContentWidth(), maxWidth) @@ -171,7 +171,7 @@ Item { visible: root.visible anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: isVerticalBar ? root.width : (((!hasFocusedWindow) && (hideMode === "transparent" || hideMode === "hidden")) ? 0 : dynamicWidth) + width: isVerticalBar ? root.width : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth) height: isVerticalBar ? width : Style.capsuleHeight radius: isVerticalBar ? width / 2 : Style.radiusM color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent From c56d1430e5daca27984524b5cf08725cb0786ff9 Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Sun, 19 Oct 2025 13:33:45 +0800 Subject: [PATCH 17/51] ActiveWindow: fix inconsistency of animation on vertical bar --- Modules/Bar/Widgets/ActiveWindow.qml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index a3c76f29..ae639ffb 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -45,8 +45,8 @@ Item { readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window" readonly property string fallbackIcon: "user-desktop" - implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.capsuleHeight) : 0 - implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)) : 0 + implicitHeight: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight) : 0 + implicitWidth: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)) : 0 // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0 @@ -169,10 +169,9 @@ Item { Rectangle { id: windowActiveRect visible: root.visible - anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: isVerticalBar ? root.width : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth) - height: isVerticalBar ? width : Style.capsuleHeight + width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth) + height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight radius: isVerticalBar ? width / 2 : Style.radiusM color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent From 4e134dfee19b6701cc70220706cbd24881ac7923 Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Sun, 19 Oct 2025 12:38:36 +0200 Subject: [PATCH 18/51] WiFi: Display SSID of connected WiFi network in bar pill. --- Modules/Bar/Widgets/WiFi.qml | 103 ++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index d2b996b5..efa80d8d 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -1,46 +1,83 @@ import QtQuick -import QtQuick.Layouts -import QtQuick.Controls import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -import qs.Widgets +import qs.Modules.Bar.Extras -NIconButton { +Item { id: root property ShellScreen screen - density: Settings.data.bar.density - baseSize: Style.capsuleHeight - applyUiScale: false - colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.transparent - tooltipText: I18n.tr("tooltips.manage-wifi") - tooltipDirection: BarService.getTooltipDirection() - icon: { - try { - if (NetworkService.ethernetConnected) { - return "ethernet" + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] } - let connected = false - let signalStrength = 0 - for (const net in NetworkService.networks) { - if (NetworkService.networks[net].connected) { - connected = true - signalStrength = NetworkService.networks[net].signal - break - } - } - return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" - } catch (error) { - Logger.e("Wi-Fi", "Error getting icon:", error) - return "signal_wifi_bad" } + return {} + } + + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + + implicitWidth: pill.width + implicitHeight: pill.height + + BarPill { + id: pill + + density: Settings.data.bar.density + rightOpen: BarService.getPillDirection(root) + icon: { + try { + if (NetworkService.ethernetConnected) { + return "ethernet" + } + let connected = false + let signalStrength = 0 + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + connected = true + signalStrength = NetworkService.networks[net].signal + break + } + } + return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" + } catch (error) { + Logger.e("Wi-Fi", "Error getting icon:", error) + return "signal_wifi_bad" + } + } + text: { + try { + if (NetworkService.ethernetConnected) { + return "ethernet" + } + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + return net + } + } + } catch (error) { + Logger.e("Wi-Fi", "Error getting ssid:", error) + } + return "unknown" + } + autoHide: false + forceOpen: root.displayMode === "alwaysShow" + forceClose: root.displayMode === "alwaysHide" + disableOpen: NetworkService.ethernetConnected + onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + tooltipText: I18n.tr("tooltips.manage-wifi") } - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) } From 742684e80308bdab8f2971bdf42fd301b1c81a6d Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Sun, 19 Oct 2025 12:44:35 +0200 Subject: [PATCH 19/51] WiFi: Add display settings for WiFi bar pill. --- .../Settings/Bar/BarWidgetSettingsDialog.qml | 1 + .../Bar/WidgetSettings/WiFiSettings.qml | 40 +++++++++++++++++++ Services/BarWidgetRegistry.qml | 4 ++ 3 files changed, 45 insertions(+) create mode 100644 Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml index 4b35b7fd..bdab9808 100644 --- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -133,6 +133,7 @@ Popup { "Spacer": "WidgetSettings/SpacerSettings.qml", "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", "Volume": "WidgetSettings/VolumeSettings.qml", + "WiFi": "WidgetSettings/WiFiSettings.qml", "Workspace": "WidgetSettings/WorkspaceSettings.qml", "Taskbar": "WidgetSettings/TaskbarSettings.qml", "Tray": "WidgetSettings/TraySettings.qml" diff --git a/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml new file mode 100644 index 00000000..d6548476 --- /dev/null +++ b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.displayMode = valueDisplayMode + return settings + } + + NComboBox { + label: I18n.tr("bar.widget-settings.battery.display-mode.label") + description: I18n.tr("bar.widget-settings.battery.display-mode.description") + minimumWidth: 134 + model: [{ + "key": "onhover", + "name": I18n.tr("options.display-mode.on-hover") + }, { + "key": "alwaysShow", + "name": I18n.tr("options.display-mode.always-show") + }, { + "key": "alwaysHide", + "name": I18n.tr("options.display-mode.always-hide") + }] + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key + } +} diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index a5823db6..6a0fcfe0 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -129,6 +129,10 @@ Singleton { "blacklist": [], "colorizeIcons": false }, + "WiFi": { + "allowUserSettings": true, + "displayMode": "onhover" + }, "Workspace": { "allowUserSettings": true, "labelMode": "index", From 56dc0d70dec1019d3d2a47870937a9c3f39d128a Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:17:22 +0200 Subject: [PATCH 20/51] NPanel: extract PanelWindow to separate reusable component --- Widgets/NPanel.qml | 349 +------------------------------------- Widgets/NPanelWindow.qml | 355 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+), 347 deletions(-) create mode 100644 Widgets/NPanelWindow.qml diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 4b80964d..70931010 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -126,353 +126,8 @@ Loader { // ----------------------------------------- sourceComponent: Component { // PanelWindow has its own screen property inherited of QsWindow - PanelWindow { - id: panelWindow - - readonly property string barPosition: Settings.data.bar.position - readonly property bool isVertical: barPosition === "left" || barPosition === "right" - readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) - readonly property real verticalBarWidth: Style.barHeight - - Component.onCompleted: { - Logger.d("NPanel", "Opened", root.objectName, "on", screen.name) - dimmingOpacity = Style.opacityHeavy - } - - Connections { - target: panelWindow - function onScreenChanged() { - root.screen = screen - - // If called from IPC always reposition if screen is updated - if (buttonName) { - setPosition() - } - Logger.d("NPanel", "OnScreenChanged", root.screen.name) - } - } - - visible: true - color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent - - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "noctalia-panel" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - Region { - id: maskRegion - } - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - } - } - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - - // Close any panel with Esc without requiring focus - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: root.close() - context: Qt.WindowShortcut - } - - // Clicking outside of the rectangle to close - MouseArea { - anchors.fill: parent - enabled: root.backgroundClickEnabled - onClicked: root.close() - } - - // The actual panel's content - Rectangle { - id: panelBackground - color: panelBackgroundColor - radius: Style.radiusL - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS) - // Dragging support - property bool draggable: root.draggable - property bool isDragged: false - property real manualX: 0 - property real manualY: 0 - width: { - var w - if (preferredWidthRatio !== undefined) { - w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth)) - } else { - w = preferredWidth - } - // Clamp width so it is never bigger than the screen - return Math.min(w, screen?.width - Style.marginL * 2) - } - height: { - var h - if (preferredHeightRatio !== undefined) { - h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight)) - } else { - h = preferredHeight - } - - // Clamp width so it is never bigger than the screen - return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) - } - - scale: root.scaleValue - x: isDragged ? manualX : calculatedX - y: isDragged ? manualY : calculatedY - - // --------------------------------------------- - // Does not account for corners are they are negligible and helps keep the code clean. - // --------------------------------------------- - property real marginTop: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "top": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginBottom: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "bottom": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginLeft: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "left": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginRight: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "right": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) - default: - return Style.marginS - } - } - - // --------------------------------------------- - property int calculatedX: { - // Priority to fixed anchoring - if (panelAnchorHorizontalCenter) { - // Center horizontally but respect bar margins - var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) - var minX = marginLeft - var maxX = panelWindow.width - panelBackground.width - marginRight - return Math.round(Math.max(minX, Math.min(centerX, maxX))) - } else if (panelAnchorLeft) { - return marginLeft - } else if (panelAnchorRight) { - return Math.round(panelWindow.width - panelBackground.width - marginRight) - } - - // No fixed anchoring - if (isVertical) { - // Vertical bar - if (barPosition === "right") { - // To the left of the right bar - return Math.round(panelWindow.width - panelBackground.width - marginRight) - } else { - // To the right of the left bar - return marginLeft - } - } else { - // Horizontal bar - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) - // Keep panel within screen bounds - var maxX = panelWindow.width - panelBackground.width - marginRight - var minX = marginLeft - return Math.round(Math.max(minX, Math.min(targetX, maxX))) - } else { - // Fallback to center horizontally - return Math.round((panelWindow.width - panelBackground.width) / 2) - } - } - } - - // --------------------------------------------- - property int calculatedY: { - // Priority to fixed anchoring - if (panelAnchorVerticalCenter) { - // Center vertically but respect bar margins - var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) - var minY = marginTop - var maxY = panelWindow.height - panelBackground.height - marginBottom - return Math.round(Math.max(minY, Math.min(centerY, maxY))) - } else if (panelAnchorTop) { - return marginTop - } else if (panelAnchorBottom) { - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } - - // No fixed anchoring - if (isVertical) { - // Vertical bar - if (useButtonPosition) { - // Position panel relative to button - var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) - // Keep panel within screen bounds - var maxY = panelWindow.height - panelBackground.height - marginBottom - var minY = marginTop - return Math.round(Math.max(minY, Math.min(targetY, maxY))) - } else { - // Fallback to center vertically - return Math.round((panelWindow.height - panelBackground.height) / 2) - } - } else { - // Horizontal bar - if (barPosition === "bottom") { - // Above the bottom bar - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } else { - // Below the top bar - return marginTop - } - } - } - - // Animate in when component is completed - Component.onCompleted: { - root.scaleValue = 1.0 - } - - // Reset drag position when panel closes - Connections { - target: root - function onClosed() { - panelBackground.isDragged = false - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - Loader { - id: panelContentLoader - anchors.fill: parent - sourceComponent: root.panelContent - } - - // Handle drag move on the whole panel area - DragHandler { - id: dragHandler - target: null - enabled: panelBackground.draggable - property real dragStartX: 0 - property real dragStartY: 0 - onActiveChanged: { - if (active) { - // Capture current position into manual coordinates BEFORE toggling isDragged - panelBackground.manualX = panelBackground.x - panelBackground.manualY = panelBackground.y - dragStartX = panelBackground.x - dragStartY = panelBackground.y - panelBackground.isDragged = true - if (root.enableBackgroundClick) - root.disableBackgroundClick() - } else { - // Keep isDragged true so we continue using the manual x/y after release - if (root.enableBackgroundClick) - root.enableBackgroundClick() - } - } - onTranslationChanged: { - // Proposed new coordinates from fixed drag origin - var nx = dragStartX + translation.x - var ny = dragStartY + translation.y - - // Calculate gaps so we never overlap the bar on any side - var baseGap = Style.marginS - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 - - var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) - var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) - var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) - var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) - - // Clamp within screen bounds accounting for insets - var maxX = panelWindow.width - panelBackground.width - insetRight - var minX = insetLeft - var maxY = panelWindow.height - panelBackground.height - insetBottom - var minY = insetTop - - panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) - panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) - } - } - - // Drag indicator border - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(2, Style.borderL) - radius: parent.radius - visible: panelBackground.isDragged && dragHandler.active - opacity: 0.8 - z: 3000 - - // Subtle glow effect - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS) - radius: parent.radius - opacity: 0.3 - } - } - } + NPanelWindow { + loggerPrefix: "NPanel" } } } diff --git a/Widgets/NPanelWindow.qml b/Widgets/NPanelWindow.qml new file mode 100644 index 00000000..ae9129f6 --- /dev/null +++ b/Widgets/NPanelWindow.qml @@ -0,0 +1,355 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services + +PanelWindow { + id: panelWindow + + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) + readonly property real verticalBarWidth: Style.barHeight + + property string loggerPrefix + + Component.onCompleted: { + Logger.d(loggerPrefix, "Opened", root.objectName, "on", screen.name) + dimmingOpacity = Style.opacityHeavy + } + + Connections { + target: panelWindow + function onScreenChanged() { + root.screen = screen + + // If called from IPC always reposition if screen is updated + if (buttonName) { + setPosition() + } + Logger.d(loggerPrefix, "OnScreenChanged", root.screen.name) + } + } + + visible: true + color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-panel" + WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + Region { + id: maskRegion + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + + // Close any panel with Esc without requiring focus + Shortcut { + sequences: ["Escape"] + enabled: root.active + onActivated: root.close() + context: Qt.WindowShortcut + } + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + enabled: root.backgroundClickEnabled + onClicked: root.close() + } + + // The actual panel's content + Rectangle { + id: panelBackground + color: panelBackgroundColor + radius: Style.radiusL + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + // Dragging support + property bool draggable: root.draggable + property bool isDragged: false + property real manualX: 0 + property real manualY: 0 + width: { + var w + if (preferredWidthRatio !== undefined) { + w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth)) + } else { + w = preferredWidth + } + // Clamp width so it is never bigger than the screen + return Math.min(w, screen?.width - Style.marginL * 2) + } + height: { + var h + if (preferredHeightRatio !== undefined) { + h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight)) + } else { + h = preferredHeight + } + + // Clamp width so it is never bigger than the screen + return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) + } + + scale: root.scaleValue + x: isDragged ? manualX : calculatedX + y: isDragged ? manualY : calculatedY + + // --------------------------------------------- + // Does not account for corners are they are negligible and helps keep the code clean. + // --------------------------------------------- + property real marginTop: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "top": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginBottom: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "bottom": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginLeft: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "left": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginRight: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "right": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + default: + return Style.marginS + } + } + + // --------------------------------------------- + property int calculatedX: { + // Priority to fixed anchoring + if (panelAnchorHorizontalCenter) { + // Center horizontally but respect bar margins + var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) + var minX = marginLeft + var maxX = panelWindow.width - panelBackground.width - marginRight + return Math.round(Math.max(minX, Math.min(centerX, maxX))) + } else if (panelAnchorLeft) { + return marginLeft + } else if (panelAnchorRight) { + return Math.round(panelWindow.width - panelBackground.width - marginRight) + } + + // No fixed anchoring + if (isVertical) { + // Vertical bar + if (barPosition === "right") { + // To the left of the right bar + return Math.round(panelWindow.width - panelBackground.width - marginRight) + } else { + // To the right of the left bar + return marginLeft + } + } else { + // Horizontal bar + if (root.useButtonPosition) { + // Position panel relative to button + var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) + // Keep panel within screen bounds + var maxX = panelWindow.width - panelBackground.width - marginRight + var minX = marginLeft + return Math.round(Math.max(minX, Math.min(targetX, maxX))) + } else { + // Fallback to center horizontally + return Math.round((panelWindow.width - panelBackground.width) / 2) + } + } + } + + // --------------------------------------------- + property int calculatedY: { + // Priority to fixed anchoring + if (panelAnchorVerticalCenter) { + // Center vertically but respect bar margins + var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) + var minY = marginTop + var maxY = panelWindow.height - panelBackground.height - marginBottom + return Math.round(Math.max(minY, Math.min(centerY, maxY))) + } else if (panelAnchorTop) { + return marginTop + } else if (panelAnchorBottom) { + return Math.round(panelWindow.height - panelBackground.height - marginBottom) + } + + // No fixed anchoring + if (isVertical) { + // Vertical bar + if (useButtonPosition) { + // Position panel relative to button + var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) + // Keep panel within screen bounds + var maxY = panelWindow.height - panelBackground.height - marginBottom + var minY = marginTop + return Math.round(Math.max(minY, Math.min(targetY, maxY))) + } else { + // Fallback to center vertically + return Math.round((panelWindow.height - panelBackground.height) / 2) + } + } else { + // Horizontal bar + if (barPosition === "bottom") { + // Above the bottom bar + return Math.round(panelWindow.height - panelBackground.height - marginBottom) + } else { + // Below the top bar + return marginTop + } + } + } + + // Animate in when component is completed + Component.onCompleted: { + root.scaleValue = 1.0 + } + + // Reset drag position when panel closes + Connections { + target: root + function onClosed() { + panelBackground.isDragged = false + } + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Loader { + id: panelContentLoader + anchors.fill: parent + sourceComponent: root.panelContent + } + + // Handle drag move on the whole panel area + DragHandler { + id: dragHandler + target: null + enabled: panelBackground.draggable + property real dragStartX: 0 + property real dragStartY: 0 + onActiveChanged: { + if (active) { + // Capture current position into manual coordinates BEFORE toggling isDragged + panelBackground.manualX = panelBackground.x + panelBackground.manualY = panelBackground.y + dragStartX = panelBackground.x + dragStartY = panelBackground.y + panelBackground.isDragged = true + if (root.enableBackgroundClick) + root.disableBackgroundClick() + } else { + // Keep isDragged true so we continue using the manual x/y after release + if (root.enableBackgroundClick) + root.enableBackgroundClick() + } + } + onTranslationChanged: { + // Proposed new coordinates from fixed drag origin + var nx = dragStartX + translation.x + var ny = dragStartY + translation.y + + // Calculate gaps so we never overlap the bar on any side + var baseGap = Style.marginS + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 + + var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) + var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) + var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) + var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) + + // Clamp within screen bounds accounting for insets + var maxX = panelWindow.width - panelBackground.width - insetRight + var minX = insetLeft + var maxY = panelWindow.height - panelBackground.height - insetBottom + var minY = insetTop + + panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) + panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) + } + } + + // Drag indicator border + Rectangle { + anchors.fill: parent + anchors.margins: 0 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(2, Style.borderL) + radius: parent.radius + visible: panelBackground.isDragged && dragHandler.active + opacity: 0.8 + z: 3000 + + // Subtle glow effect + Rectangle { + anchors.fill: parent + anchors.margins: 0 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS) + radius: parent.radius + opacity: 0.3 + } + } + } +} From 17ed483285ca509c4f5c08ba9a920f8a06143f3d Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:17:49 +0200 Subject: [PATCH 21/51] NPanelOverlay: implement wrapper for NPanel that opens in overlay layer --- Widgets/NPanelOverlay.qml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Widgets/NPanelOverlay.qml diff --git a/Widgets/NPanelOverlay.qml b/Widgets/NPanelOverlay.qml new file mode 100644 index 00000000..9debdbcb --- /dev/null +++ b/Widgets/NPanelOverlay.qml @@ -0,0 +1,15 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services + +NPanel { + sourceComponent: Component { + // PanelWindow has its own screen property inherited of QsWindow + NPanelWindow { + loggerPrefix: "NPanelOverlay" + WlrLayershell.layer: WlrLayer.Overlay + } + } +} From 8a79c298e2342c24176d79612a1bdc058839c4e9 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:46:15 +0200 Subject: [PATCH 22/51] ControlCenterPanel: change NPanel to NPanelOverlay --- Modules/ControlCenter/ControlCenterPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index fcc13cb8..a60ffbce 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanel { +NPanelOverlay { id: root panelKeyboardFocus: true From ab2e172f9c652a0fef853295fbaef5974bf4a2b8 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:46:21 +0200 Subject: [PATCH 23/51] Launcher: change NPanel to NPanelOverlay --- Modules/Launcher/Launcher.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index f8ee5db1..443a70ef 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanel { +NPanelOverlay { id: root // Panel configuration From ad1fe55772d05618cee6d32cbbd5d4d550a4d199 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:46:34 +0200 Subject: [PATCH 24/51] NotificationHistoryPanel: change NPanel to NPanelOverlay --- Modules/Notification/NotificationHistoryPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 8bf5583f..24950d27 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -9,7 +9,7 @@ import qs.Services import qs.Widgets // Notification History panel -NPanel { +NPanelOverlay { id: root preferredWidth: 380 From 1adf86d5937273f4354876571c6a8eb5e489e9c3 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:46:44 +0200 Subject: [PATCH 25/51] SessionMenu: change NPanel to NPanelOverlay --- Modules/SessionMenu/SessionMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml index 83afaf5f..e5f536dc 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -10,7 +10,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanel { +NPanelOverlay { id: root preferredWidth: 320 * Style.uiScaleRatio From a1bde12f9a68744a92c2490f97854d40b6b041f0 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Sun, 19 Oct 2025 13:46:52 +0200 Subject: [PATCH 26/51] SettingsPanel: change NPanel to NPanelOverlay --- Modules/Settings/SettingsPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index e5b64566..7726b1d2 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -8,7 +8,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanel { +NPanelOverlay { id: root preferredWidth: 820 * Style.uiScaleRatio From 51b6455dd00e269468dec0915b1ccdbfff105af4 Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Mon, 20 Oct 2025 14:04:55 +0200 Subject: [PATCH 27/51] WiFi: Hide pill text when no WiFi or ethernet is connected. --- Modules/Bar/Widgets/WiFi.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index efa80d8d..abfbdcdf 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -60,21 +60,22 @@ Item { text: { try { if (NetworkService.ethernetConnected) { - return "ethernet" + return "" } for (const net in NetworkService.networks) { if (NetworkService.networks[net].connected) { return net } } + return "" } catch (error) { Logger.e("Wi-Fi", "Error getting ssid:", error) + return "error" } - return "unknown" } autoHide: false forceOpen: root.displayMode === "alwaysShow" - forceClose: root.displayMode === "alwaysHide" + forceClose: root.displayMode === "alwaysHide" || !pill.text disableOpen: NetworkService.ethernetConnected onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) From 4ee82ecbc3ca8245ca9be99f7f6e455ee238055d Mon Sep 17 00:00:00 2001 From: Absurd <158203519+4fd485@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:19:43 +0200 Subject: [PATCH 28/51] Slightly more Translatio --- Assets/Translations/de.json | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 01311b44..396c0e6d 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -243,13 +243,13 @@ "widgets": { "section": { "label": "Widget-Positionierung", - "description": "Widgets per Drag & Drop neu anordnen. Abzeichen zeigen die Verwendung an: [L]inks, [M]itte, [R]echts." + "description": "Widgets per Drag & Drop neu anordnen. Symoble zeigen die Verwendung an: [L]inks, [M]itte, [R]echts." } }, "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "Statusleiste auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "Leiste auf bestimmten Monitoren anzeigen. Standardmäßig auf allen." } }, "tray": { @@ -319,7 +319,7 @@ }, "clipboard-history": { "label": "Zwischenablage-Verlauf aktivieren", - "description": "Zugriff auf zuvor kopierte Elemente über den Starter." + "description": "Zugriff auf zuvor kopierte Elemente über den Launcher." }, "sort-by-usage": { "label": "Nach Häufigkeit sortieren", @@ -343,8 +343,8 @@ "description": "Erscheinungsbild und Verhalten von Benachrichtigungen konfigurieren." }, "do-not-disturb": { - "label": "Nicht stören", - "description": "Alle Benachrichtigungs-Popups deaktivieren, wenn aktiviert." + "label": "Bitte Nicht stören", + "description": "Alle Benachrichtigungs-Popups deaktivieren." }, "enable-osd": { "label": "Bildschirmanzeige aktivieren", @@ -384,35 +384,35 @@ "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standardmäßig werden sie auf allen Monitoren angezeigt" } } }, "osd": { - "title": "Bildschirmanzeige", + "title": "On-Screen Display", "description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.", "section": { "general": { "label": "Allgemein", - "description": "Sichtbarkeit und Verhalten der OSD konfigurieren." + "description": "Sichtbarkeit und Verhalten vom On-Screen Display konfigurieren." } }, "enabled": { - "label": "Bildschirmanzeige aktivieren", + "label": "On-Screen Display aktivieren", "description": "Lautstärke- und Helligkeitsänderungen in Echtzeit anzeigen." }, "always-on-top": { "label": "Immer im Vordergrund", - "description": "Bildschirmanzeige über Vollbildfenstern und anderen Ebenen anzeigen." + "description": "On-Screen Display über Vollbildfenstern und anderen Ebenen anzeigen." }, "location": { "label": "Position", - "description": "Wo Bildschirmanzeigen erscheinen." + "description": "Wo On-Screen Displays erscheinen." }, "duration": { "section": { "label": "Automatisches Ausblenden", - "description": "Wie lange die OSD sichtbar bleibt, bevor sie automatisch ausgeblendet wird." + "description": "Wie lange das On-Screen Display sichtbar bleibt, bevor es automatisch ausgeblendet wird." }, "auto-hide": { "label": "Ausblenden nach", @@ -422,7 +422,7 @@ "monitors": { "section": { "label": "Monitor-Anzeige", - "description": "OSD auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." + "description": "On-Screen Display auf bestimmten Monitoren anzeigen. Standardmäßig auf allen angezeigt." } } }, @@ -471,7 +471,7 @@ "description": "Dauer der Übergangsanimationen in Sekunden." }, "edge-smoothness": { - "label": "Übergangskante weichzeichnen", + "label": "Übergangseffect weichzeichnen", "description": "Wendet einen weichen, gefiederten Effekt auf die Kante von Übergängen an." } }, @@ -551,15 +551,15 @@ "description": "Terminal-Emulator-Theming.", "kitty": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert kitty Terminal" + "description-missing": "Erfordert {app} Terminal" }, "ghostty": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert ghostty Terminal" + "description-missing": "Erfordert {app} Terminal" }, "foot": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert foot Terminal" + "description-missing": "Erfordert {app} Terminal" } }, "programs": { @@ -567,11 +567,11 @@ "description": "Anwendungsspezifisches Theming.", "fuzzel": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert fuzzel Starter" + "description-missing": "Erfordert die Installation von {app}" }, "vicinae": { "description": "Schreibt {filepath} und lädt neu", - "description-missing": "Erfordert {app} Starter" + "description-missing": "Erfordert die Installation von {app}" }, "discord": { "description": "Schreibt {filepath} für {client}", @@ -579,7 +579,7 @@ }, "pywalfox": { "description": "Schreibt {filepath} und führt pywalfox update aus", - "description-missing": "Erfordert pywalfox Paket" + "description-missing": "Erfordert die Installation von {app} " } }, "misc": { @@ -605,7 +605,7 @@ }, "search": { "label": "Nach einem Standort suchen", - "description": "z.B. Berlin, Deutschland", + "description": "z.B. Dortmund, Deutschland", "placeholder": "Standortnamen eingeben" } }, @@ -634,7 +634,7 @@ }, "week-numbers": { "label": "Wochennummern anzeigen", - "description": "Zeigt die Woche des Jahres (z.B. Woche 38) im Kalender an." + "description": "Zeigt die Kalender Wochen an (z.B. Woche 38)" } } }, @@ -727,7 +727,7 @@ "description_plural": "Ein Dankeschön an unsere {count} großartigen Mitwirkenden!" } }, - "support": "Unterstützen Sie uns" + "support": "Unterstütz uns" }, "hooks": { "title": "Hooks", @@ -836,7 +836,7 @@ "mainly-clear": "Überwiegend klar", "partly-cloudy": "Teilweise bewölkt", "overcast": "Bedeckt", - "fog": "Nebel", + "fog": "Nebelig", "drizzle": "Nieselregen", "snow": "Schnee", "rain-showers": "Regenschauer", @@ -852,7 +852,7 @@ "select-file": "Datei auswählen", "cancel": "Abbrechen", "search-placeholder": "Dateien und Ordner suchen...", - "select-current": "Aktuelle auswählen", + "select-current": "Aktuelles Objekt auswählen", "title": "Dateiauswahl" }, "datetime-tokens": { From 8652fdb731f4379116dc793f62da2016c4ce7cb4 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 12:22:30 -0400 Subject: [PATCH 29/51] added missing translation --- Assets/Translations/es.json | 7 +++++++ Assets/Translations/fr.json | 7 +++++++ Assets/Translations/pt.json | 7 +++++++ Assets/Translations/zh-CN.json | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 68d2010c..aa00ce0a 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -513,6 +513,13 @@ "switch": { "label": "Modo oscuro", "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche." + }, + "mode": { + "description": "Permite el cambio automático entre el modo claro y el modo oscuro.", + "label": "Programación del modo oscuro", + "location": "Ubicación", + "manual": "Manual", + "off": "Apagado" } }, "predefined": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index b20b2308..d15b0c42 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -513,6 +513,13 @@ "switch": { "label": "Mode sombre", "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit." + }, + "mode": { + "description": "Active la commutation automatique entre le mode clair et le mode sombre.", + "label": "Programmation du mode sombre", + "location": "Emplacement", + "manual": "Manuel", + "off": "Éteint" } }, "predefined": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index d78a4ec6..b72251e7 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -475,6 +475,13 @@ "switch": { "label": "Modo escuro", "description": "Muda para um tema mais escuro para facilitar a visualização à noite." + }, + "mode": { + "description": "Ativa a mudança automática entre o modo claro e o modo escuro.", + "label": "Agendamento do modo escuro", + "location": "Localização", + "manual": "Manual", + "off": "Desligado" } }, "predefined": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 0159a41a..d9891679 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -513,6 +513,13 @@ "switch": { "label": "深色模式", "description": "切换到更暗的主题,便于夜间观看。" + }, + "mode": { + "description": "启用自动切换浅色和深色模式。", + "label": "深色模式计划", + "location": "位置", + "manual": "手册", + "off": "关" } }, "predefined": { From 621b37cd1f481f5f5fcf7622ade566050a0f8468 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 12:22:38 -0400 Subject: [PATCH 30/51] autofmt --- Commons/I18n.qml | 1 - Modules/Settings/Tabs/ColorSchemeTab.qml | 34 ++-- Modules/Settings/Tabs/GeneralTab.qml | 28 +-- Services/AppThemeService.qml | 4 +- Services/ProgramCheckerService.qml | 4 +- Widgets/NFilePicker.qml | 212 +++++++++++------------ Widgets/NTextInput.qml | 78 ++++----- 7 files changed, 181 insertions(+), 180 deletions(-) diff --git a/Commons/I18n.qml b/Commons/I18n.qml index 4ec5e86f..59eeb922 100644 --- a/Commons/I18n.qml +++ b/Commons/I18n.qml @@ -8,7 +8,6 @@ import qs.Commons Singleton { id: root - property bool isLoaded: false property string langCode: "" property string systemDetectedLangCode: "" diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 314cc07d..81146436 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -665,24 +665,22 @@ ColumnLayout { } } NCheckbox { - label: "Vicinae" - description: ProgramCheckerService.vicinaeAvailable - ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", { - "filepath": "~/.local/share/vicinae/themes/matugen.toml" - }) - : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", { - "app": "vicinae" - }) - checked: Settings.data.templates.vicinae - enabled: ProgramCheckerService.vicinaeAvailable - opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6 - onToggled: checked => { - if (ProgramCheckerService.vicinaeAvailable) { - Settings.data.templates.vicinae = checked - AppThemeService.generate() - } - } -} + label: "Vicinae" + description: ProgramCheckerService.vicinaeAvailable ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", { + "filepath": "~/.local/share/vicinae/themes/matugen.toml" + }) : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", { + "app": "vicinae" + }) + checked: Settings.data.templates.vicinae + enabled: ProgramCheckerService.vicinaeAvailable + opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.vicinaeAvailable) { + Settings.data.templates.vicinae = checked + AppThemeService.generate() + } + } + } } // Miscellaneous diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index 40b5276d..4f084445 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -204,20 +204,24 @@ ColumnLayout { Layout.fillWidth: true label: I18n.tr("settings.general.language.select.label") description: I18n.tr("settings.general.language.select.description") - model: [ - { "key": "", "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" } - ].concat(I18n.availableLanguages.map(function(langCode) { - return { "key": langCode, "name": langCode } - })) + model: [{ + "key": "", + "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" + }].concat(I18n.availableLanguages.map(function (langCode) { + return { + "key": langCode, + "name": langCode + } + })) currentKey: Settings.data.general.language onSelected: key => { - Settings.data.general.language = key - if (key === "") { - I18n.detectLanguage() // Re-detect system language if "Automatic" is selected - } else { - I18n.setLanguage(key) // Set specific language - } - } + Settings.data.general.language = key + if (key === "") { + I18n.detectLanguage() // Re-detect system language if "Automatic" is selected + } else { + I18n.setLanguage(key) // Set specific language + } + } } } diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml index ac5eb72a..c54662ce 100644 --- a/Services/AppThemeService.qml +++ b/Services/AppThemeService.qml @@ -70,8 +70,8 @@ Singleton { "vicinae": { "input": "vicinae.toml", "outputs": [{ - "path": "~/.local/share/vicinae/themes/matugen.toml" - }], + "path": "~/.local/share/vicinae/themes/matugen.toml" + }], "postProcess": () => `cp -n ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${colorsApplyScript} vicinae\n` } }) diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml index ca06dc0c..153b35c9 100644 --- a/Services/ProgramCheckerService.qml +++ b/Services/ProgramCheckerService.qml @@ -97,8 +97,8 @@ Singleton { "kittyAvailable": ["which", "kitty"], "ghosttyAvailable": ["which", "ghostty"], "footAvailable": ["which", "foot"], - "fuzzelAvailable": ["which", "fuzzel"], - "vicinaeAvailable": ["which", "vicinae"], + "fuzzelAvailable": ["which", "fuzzel"], + "vicinaeAvailable": ["which", "vicinae"], "app2unitAvailable": ["which", "app2unit"], "gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"], "wlsunsetAvailable": ["which", "wlsunset"] diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index b9bd7863..4b82e77d 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -39,8 +39,8 @@ Popup { function openFilePicker() { if (!root.currentPath) root.currentPath = root.initialPath - shouldResetSelection = true - open() + shouldResetSelection = true + open() } function getFileIcon(fileName) { @@ -92,18 +92,18 @@ Popup { function formatFileSize(bytes) { if (bytes === 0) return "0 B" - const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] + const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] } function confirmSelection() { if (filePickerPanel.currentSelection.length === 0) return - root.selectedPaths = filePickerPanel.currentSelection - root.accepted(filePickerPanel.currentSelection) - root.close() + root.selectedPaths = filePickerPanel.currentSelection + root.accepted(filePickerPanel.currentSelection) + root.close() } function updateFilteredModel() { @@ -126,14 +126,14 @@ Popup { if (root.selectionMode === "folders" && !fileIsDir) continue - if (searchText === "" || fileName.toLowerCase().includes(searchText)) { - filteredModel.append({ - "fileName": fileName, - "filePath": filePath, - "fileIsDir": fileIsDir, - "fileSize": fileSize - }) - } + if (searchText === "" || fileName.toLowerCase().includes(searchText)) { + filteredModel.append({ + "fileName": fileName, + "filePath": filePath, + "fileIsDir": fileIsDir, + "fileSize": fileSize + }) + } } } @@ -165,19 +165,19 @@ Popup { focus: true Keys.onPressed: event => { - if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { - filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar - if (filePickerPanel.showSearchBar) - Qt.callLater(() => searchInput.forceActiveFocus()) - event.accepted = true - } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { - filePickerPanel.showSearchBar = false - filePickerPanel.searchText = "" - filePickerPanel.filterText = "" - root.updateFilteredModel() - event.accepted = true - } - } + if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar + if (filePickerPanel.showSearchBar) + Qt.callLater(() => searchInput.forceActiveFocus()) + event.accepted = true + } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { + filePickerPanel.showSearchBar = false + filePickerPanel.searchText = "" + filePickerPanel.filterText = "" + root.updateFilteredModel() + event.accepted = true + } + } ColumnLayout { anchors.fill: parent @@ -473,11 +473,11 @@ Popup { bottomMargin: Style.marginS ScrollBar.vertical: scrollBarComponent.createObject(gridView, { - "parent": gridView, - "x": gridView.mirrored ? 0 : gridView.width - width, - "y": 0, - "height": gridView.height - }) + "parent": gridView, + "x": gridView.mirrored ? 0 : gridView.width - width, + "y": 0, + "height": gridView.height + }) delegate: Rectangle { id: gridItem @@ -533,8 +533,8 @@ Popup { property bool isImage: { if (model.fileIsDir) return false - const ext = model.fileName.split('.').pop().toLowerCase() - return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) + const ext = model.fileName.split('.').pop().toLowerCase() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) } Image { @@ -574,10 +574,10 @@ Popup { color: { if (isSelected) return Color.mSecondary - else if (mouseArea.containsMouse) - return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary - else - return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant + else if (mouseArea.containsMouse) + return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary + else + return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant } anchors.centerIn: parent visible: !iconContainer.isImage || thumbnail.status !== Image.Ready @@ -608,10 +608,10 @@ Popup { color: { if (isSelected) return Color.mSecondary - else if (mouseArea.containsMouse) - return Color.mOnTertiary - else - return Color.mOnSurfaceVariant + else if (mouseArea.containsMouse) + return Color.mOnTertiary + else + return Color.mOnSurfaceVariant } pointSize: Style.fontSizeS font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular @@ -630,37 +630,37 @@ Popup { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // In folder mode, single click selects the folder - if (root.selectionMode === "folders") { - filePickerPanel.currentSelection = [model.filePath] - } - // In file mode, single click on folder does nothing (must double-click to enter) - } else { - // Single click on file selects it (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.currentSelection = [model.filePath] + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + } + } + } + } onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // Double-click on folder always navigates into it - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } else { - // Double-click on file selects and confirms (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + root.confirmSelection() + } + } + } + } } } } @@ -680,9 +680,9 @@ Popup { color: { if (filePickerPanel.currentSelection.includes(model.filePath)) return Color.mSecondary - if (mouseArea.containsMouse) - return Color.mTertiary - return Color.transparent + if (mouseArea.containsMouse) + return Color.mTertiary + return Color.transparent } radius: Style.radiusS Behavior on color { @@ -728,37 +728,37 @@ Popup { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // In folder mode, single click selects the folder - if (root.selectionMode === "folders") { - filePickerPanel.currentSelection = [model.filePath] - } - // In file mode, single click on folder does nothing (must double-click to enter) - } else { - // Single click on file selects it (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.currentSelection = [model.filePath] + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + } + } + } + } onDoubleClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - if (model.fileIsDir) { - // Double-click on folder always navigates into it - folderModel.folder = "file://" + model.filePath - root.currentPath = model.filePath - } else { - // Double-click on file selects and confirms (only in file mode) - if (root.selectionMode === "files") { - filePickerPanel.currentSelection = [model.filePath] - root.confirmSelection() - } - } - } - } + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath] + root.confirmSelection() + } + } + } + } } } } @@ -816,7 +816,7 @@ Popup { Component.onCompleted: { if (!root.currentPath) root.currentPath = root.initialPath - folderModel.folder = "file://" + root.currentPath + folderModel.folder = "file://" + root.currentPath } } } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 6043a51d..f11f9ca4 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -75,31 +75,31 @@ ColumnLayout { propagateComposedEvents: false onPressed: mouse => { - mouse.accepted = true - // Focus the input and position cursor - input.forceActiveFocus() - var inputPos = mapToItem(inputContainer, mouse.x, mouse.y) - if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) { - var textPos = inputPos.x - Style.marginM - if (textPos >= 0 && textPos <= input.width) { - input.cursorPosition = input.positionAt(textPos, input.height / 2) - } - } - } + mouse.accepted = true + // Focus the input and position cursor + input.forceActiveFocus() + var inputPos = mapToItem(inputContainer, mouse.x, mouse.y) + if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) { + var textPos = inputPos.x - Style.marginM + if (textPos >= 0 && textPos <= input.width) { + input.cursorPosition = input.positionAt(textPos, input.height / 2) + } + } + } onReleased: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onDoubleClicked: mouse => { - mouse.accepted = true - input.selectAll() - } + mouse.accepted = true + input.selectAll() + } onPositionChanged: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onWheel: wheel => { - wheel.accepted = true - } + wheel.accepted = true + } } // Container for the actual text field @@ -167,32 +167,32 @@ ColumnLayout { property int selectionStart: 0 onPressed: mouse => { - mouse.accepted = true - input.forceActiveFocus() - var pos = input.positionAt(mouse.x, mouse.y) - input.cursorPosition = pos - selectionStart = pos - } + mouse.accepted = true + input.forceActiveFocus() + var pos = input.positionAt(mouse.x, mouse.y) + input.cursorPosition = pos + selectionStart = pos + } onPositionChanged: mouse => { - if (mouse.buttons & Qt.LeftButton) { - mouse.accepted = true - var pos = input.positionAt(mouse.x, mouse.y) - input.select(selectionStart, pos) - } - } + if (mouse.buttons & Qt.LeftButton) { + mouse.accepted = true + var pos = input.positionAt(mouse.x, mouse.y) + input.select(selectionStart, pos) + } + } onDoubleClicked: mouse => { - mouse.accepted = true - input.selectAll() - } + mouse.accepted = true + input.selectAll() + } onReleased: mouse => { - mouse.accepted = true - } + mouse.accepted = true + } onWheel: wheel => { - wheel.accepted = true - } + wheel.accepted = true + } } } NIconButton { From 73267d1d37b60c963fc4f938acab1eef8a655fe7 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 13:33:46 -0400 Subject: [PATCH 31/51] Settings + SetupWizard - Added a Lock screen settings tabs - Added button in settings/general tab to re-run the setup wizard - Fixed missing translations - Fixed bug when matugen not installed in setup wizard - Added enabled property for NToggle --- Assets/Translations/de.json | 28 ++++++++--------- Assets/Translations/en.json | 26 +++++++-------- Assets/Translations/es.json | 28 ++++++++--------- Assets/Translations/fr.json | 28 ++++++++--------- Assets/Translations/pt.json | 28 ++++++++--------- Assets/Translations/zh-CN.json | 28 ++++++++--------- Commons/TablerIcons.qml | 1 + Modules/Settings/SettingsPanel.qml | 11 +++++++ Modules/Settings/Tabs/ColorSchemeTab.qml | 2 +- Modules/Settings/Tabs/GeneralTab.qml | 25 ++++----------- Modules/Settings/Tabs/LockScreenTab.qml | 31 ++++++++++++++++++ Modules/Settings/Tabs/UserInterfaceTab.qml | 7 ----- Modules/SetupWizard/SetupAppearanceStep.qml | 35 ++------------------- Widgets/NToggle.qml | 8 +++++ 14 files changed, 138 insertions(+), 148 deletions(-) create mode 100644 Modules/Settings/Tabs/LockScreenTab.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 396c0e6d..6907ed91 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -32,16 +32,6 @@ "reset": "Eckenradius des Bildschirms zurücksetzen" } }, - "lockscreen": { - "section": { - "label": "Sperrbildschirm", - "description": "Hier kannst du deinen Sperrbildschirm konfigurieren." - }, - "lock-on-suspend": { - "label": "Beim Standby sperren", - "description": "Bildschirm automatisch sperren, wenn das System in den Standby-Modus wechselt." - } - }, "fonts": { "section": { "label": "Schriftarten", @@ -79,7 +69,8 @@ "description": "Wählen die in der Anwendungsoberfläche verwendete Sprache.", "auto-detect": "Automatisch" } - } + }, + "launch-setup-wizard": "Starte den Setup-Assistenten" }, "audio": { "title": "Audio", @@ -802,10 +793,6 @@ "label": "Eckenradius", "reset": "Rahmenradius zurücksetzen" }, - "compact-lockscreen": { - "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.", - "label": "Kompakter Sperrbildschirm" - }, "dim-desktop": { "description": "Den Desktop abdunkeln, wenn Bedienfelder oder Menüs geöffnet sind.", "label": "Dim Desktop" @@ -824,6 +811,17 @@ "description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.", "label": "Tooltips anzeigen" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.", + "label": "Kompakter Sperrbildschirm" + }, + "lock-on-suspend": { + "description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.", + "label": "Sperren beim Ruhezustand" + }, + "title": "Sperrbildschirm" } }, "general": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 88acd5c9..f8c8286f 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -2,6 +2,7 @@ "settings": { "general": { "title": "General", + "launch-setup-wizard": "Launch the setup wizard", "profile": { "section": { "label": "Profile", @@ -32,16 +33,6 @@ "reset": "Reset screen corners radius" } }, - "lockscreen": { - "section": { - "label": "Lock screen", - "description": "Configure lock screen behavior." - }, - "lock-on-suspend": { - "label": "Lock on suspend", - "description": "Automatically lock the screen when suspending the system." - } - }, "fonts": { "reset-scaling": "Reset scaling", "section": { @@ -806,10 +797,6 @@ "label": "Dim desktop", "description": "Dim the desktop when panels or menus are open." }, - "compact-lockscreen": { - "label": "Compact lock screen", - "description": "Show only the login input and system controls, hiding weather and media widgets." - }, "border-radius": { "label": "Border radius", "description": "Controls the corner roundness of windows, buttons, and other elements.", @@ -824,6 +811,17 @@ "label": "Disable UI Animations", "description": "Disable all animations for a faster, more responsive experience." } + }, + "lock-screen": { + "title": "Lock screen", + "compact-lockscreen": { + "label": "Compact lock screen", + "description": "Show only the login input and system controls, hiding weather and media widgets." + }, + "lock-on-suspend": { + "label": "Lock on suspend", + "description": "Automatically lock the screen when suspending the system." + } } }, "widgets": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index aa00ce0a..c6311f6d 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -32,16 +32,6 @@ "reset": "Restablecer el radio de las esquinas de la pantalla" } }, - "lockscreen": { - "section": { - "label": "Pantalla de bloqueo", - "description": "Configura el comportamiento de la pantalla de bloqueo." - }, - "lock-on-suspend": { - "label": "Bloquear al suspender", - "description": "Bloquear automáticamente la pantalla al suspender el sistema." - } - }, "fonts": { "section": { "label": "Fuentes", @@ -79,7 +69,8 @@ "description": "Selecciona el idioma utilizado en la interfaz de la aplicación.", "auto-detect": "Automático" } - } + }, + "launch-setup-wizard": "Inicie el asistente de configuración" }, "audio": { "title": "Audio", @@ -802,10 +793,6 @@ "label": "Radio de borde", "reset": "Restablecer el radio del borde" }, - "compact-lockscreen": { - "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.", - "label": "Pantalla de bloqueo compacta" - }, "dim-desktop": { "description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", "label": "Dim escritorio" @@ -824,6 +811,17 @@ "description": "Activar o desactivar los avisos emergentes en toda la interfaz.", "label": "Mostrar sugerencias" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.", + "label": "Pantalla de bloqueo compacta" + }, + "lock-on-suspend": { + "description": "Bloquear la pantalla automáticamente al suspender el sistema.", + "label": "Bloquear al suspender" + }, + "title": "Pantalla de bloqueo" } }, "widgets": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index d15b0c42..c50cf02e 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -32,16 +32,6 @@ "reset": "Réinitialiser le rayon des coins de l'écran" } }, - "lockscreen": { - "section": { - "label": "Écran de verrouillage", - "description": "Configurer le comportement de l'écran de verrouillage." - }, - "lock-on-suspend": { - "label": "Verrouiller lors de la suspension", - "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système." - } - }, "fonts": { "section": { "label": "Polices", @@ -79,7 +69,8 @@ "description": "Sélectionnez la langue utilisée dans l'interface de l'application.", "auto-detect": "Automatique" } - } + }, + "launch-setup-wizard": "Lancer l'assistant d'installation" }, "audio": { "title": "Audio", @@ -802,10 +793,6 @@ "label": "Rayon de bordure", "reset": "Réinitialiser le rayon de la bordure" }, - "compact-lockscreen": { - "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.", - "label": "Écran de verrouillage compact" - }, "dim-desktop": { "description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", "label": "Dim bureau" @@ -824,6 +811,17 @@ "description": "Activer ou désactiver les info-bulles dans toute l'interface.", "label": "Afficher les infobulles" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.", + "label": "Écran de verrouillage compact" + }, + "lock-on-suspend": { + "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.", + "label": "Verrouiller à la suspension" + }, + "title": "Écran de verrouillage" } }, "widgets": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index b72251e7..ccc2cfa1 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -32,16 +32,6 @@ "reset": "Redefinir raio dos cantos da tela" } }, - "lockscreen": { - "section": { - "label": "Tela de bloqueio", - "description": "Configure o comportamento da tela de bloqueio." - }, - "lock-on-suspend": { - "label": "Bloquear ao suspender", - "description": "Bloquear automaticamente a tela ao suspender o sistema." - } - }, "fonts": { "section": { "label": "Fontes", @@ -79,7 +69,8 @@ "description": "Selecione o idioma usado na interface da aplicação.", "auto-detect": "Automático" } - } + }, + "launch-setup-wizard": "Iniciar o assistente de configuração" }, "audio": { "title": "Áudio", @@ -802,10 +793,6 @@ "label": "Raio da borda", "reset": "Redefinir raio da borda" }, - "compact-lockscreen": { - "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando widgets de clima e mídia.", - "label": "Tela de bloqueio compacta" - }, "dim-desktop": { "description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", "label": "Dim área de trabalho" @@ -824,6 +811,17 @@ "description": "Ativar ou desativar dicas de ferramentas em toda a interface.", "label": "Mostrar dicas de ferramenta" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando os widgets de clima e mídia.", + "label": "Tela de bloqueio compacta" + }, + "lock-on-suspend": { + "description": "Bloquear a tela automaticamente ao suspender o sistema.", + "label": "Bloquear ao suspender" + }, + "title": "Tela de bloqueio" } }, "widgets": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d9891679..e46e952e 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -32,16 +32,6 @@ "reset": "重置屏幕圆角半径" } }, - "lockscreen": { - "section": { - "label": "锁屏", - "description": "配置锁屏行为。" - }, - "lock-on-suspend": { - "label": "挂起时锁定", - "description": "在系统挂起时自动锁定屏幕。" - } - }, "fonts": { "reset-scaling": "恢复默认缩放", "section": { @@ -79,7 +69,8 @@ "description": "选择应用程序界面中使用的语言。", "auto-detect": "自动检测" } - } + }, + "launch-setup-wizard": "启动安装向导" }, "audio": { "title": "音频", @@ -802,10 +793,6 @@ "label": "边框半径", "reset": "重置边框半径" }, - "compact-lockscreen": { - "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。", - "label": "紧凑型锁屏" - }, "dim-desktop": { "description": "当面板或菜单打开时,桌面变暗。", "label": "昏暗的桌面" @@ -824,6 +811,17 @@ "description": "启用或禁用整个界面的工具提示。", "label": "显示工具提示" } + }, + "lock-screen": { + "compact-lockscreen": { + "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。", + "label": "紧凑型锁屏" + }, + "lock-on-suspend": { + "description": "系统挂起时自动锁定屏幕。", + "label": "挂起时锁定" + }, + "title": "锁屏" } }, "widgets": { diff --git a/Commons/TablerIcons.qml b/Commons/TablerIcons.qml index a840d72f..59b226d3 100644 --- a/Commons/TablerIcons.qml +++ b/Commons/TablerIcons.qml @@ -119,6 +119,7 @@ Singleton { "settings-notifications": "bell", "settings-osd": "picture-in-picture", "settings-about": "info-square-rounded", + "settings-lock-screen": "lock", "bluetooth": "bluetooth", "bt-device-generic": "bluetooth", "bt-device-headphones": "headphones", diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index e5b64566..47ebc40a 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -26,6 +26,7 @@ NPanel { Audio, Bar, ColorScheme, + LockScreen, ControlCenter, OSD, Display, @@ -118,6 +119,11 @@ NPanel { id: userInterfaceTab UserInterfaceTab {} } + Component { + id: lockScreenTab + LockScreenTab {} + } + // Order *DOES* matter function updateTabsModel() { let newTabs = [{ @@ -150,6 +156,11 @@ NPanel { "label": "settings.launcher.title", "icon": "settings-launcher", "source": launcherTab + }, { + "id": SettingsPanel.Tab.LockScreen, + "label": "settings.lock-screen.title", + "icon": "settings-lock-screen", + "source": lockScreenTab }, { "id": SettingsPanel.Tab.Audio, "label": "settings.audio.title", diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 81146436..bbd02a8e 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -159,7 +159,6 @@ ColumnLayout { label: I18n.tr("settings.color-scheme.dark-mode.switch.label") description: I18n.tr("settings.color-scheme.dark-mode.switch.description") checked: Settings.data.colorSchemes.darkMode - enabled: true onToggled: checked => { Settings.data.colorSchemes.darkMode = checked root.cacheVersion++ // Force UI update for dark/light variants @@ -241,6 +240,7 @@ ColumnLayout { NToggle { label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label") description: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description") + enabled: ProgramCheckerService.matugenAvailable checked: Settings.data.colorSchemes.useWallpaperColors onToggled: checked => { if (checked) { diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index 4f084445..d750f3c7 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -230,26 +230,13 @@ ColumnLayout { Layout.topMargin: Style.marginXL Layout.bottomMargin: Style.marginXL } - ColumnLayout { - spacing: Style.marginL - Layout.fillWidth: true - NHeader { - label: I18n.tr("settings.general.lockscreen.section.label") - description: I18n.tr("settings.general.lockscreen.section.description") + NButton { + visible: !DistroService.isNixOS + text: I18n.tr("settings.general.launch-setup-wizard") + onClicked: { + setupWizardLoader.active = false + setupWizardLoader.active = true } - - NToggle { - label: I18n.tr("settings.general.lockscreen.lock-on-suspend.label") - description: I18n.tr("settings.general.lockscreen.lock-on-suspend.description") - checked: Settings.data.general.lockOnSuspend - onToggled: Settings.data.general.lockOnSuspend = checked - } - } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL - Layout.bottomMargin: Style.marginXL } } diff --git a/Modules/Settings/Tabs/LockScreenTab.qml b/Modules/Settings/Tabs/LockScreenTab.qml new file mode 100644 index 00000000..2b9d0ea7 --- /dev/null +++ b/Modules/Settings/Tabs/LockScreenTab.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + NToggle { + label: I18n.tr("settings.lock-screen.lock-on-suspend.label") + description: I18n.tr("settings.lock-screen.lock-on-suspend.description") + checked: Settings.data.general.lockOnSuspend + onToggled: Settings.data.general.lockOnSuspend = checked + } + + NToggle { + label: I18n.tr("settings.lock-screen.compact-lockscreen.label") + description: I18n.tr("settings.lock-screen.compact-lockscreen.description") + checked: Settings.data.general.compactLockScreen + onToggled: checked => Settings.data.general.compactLockScreen = checked + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL + Layout.bottomMargin: Style.marginXL + } +} diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index 13557c29..cffb7489 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -33,13 +33,6 @@ ColumnLayout { onToggled: checked => Settings.data.ui.tooltipsEnabled = checked } - NToggle { - label: I18n.tr("settings.user-interface.compact-lockscreen.label") - description: I18n.tr("settings.user-interface.compact-lockscreen.description") - checked: Settings.data.general.compactLockScreen - onToggled: checked => Settings.data.general.compactLockScreen = checked - } - NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml index e247e070..a59b2ad3 100644 --- a/Modules/SetupWizard/SetupAppearanceStep.qml +++ b/Modules/SetupWizard/SetupAppearanceStep.qml @@ -124,14 +124,14 @@ ColumnLayout { spacing: 2 NText { - text: I18n.tr("settings.color-scheme.color-source.dark-mode.label") + text: I18n.tr("settings.color-scheme.dark-mode.switch.label") pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mOnSurface } NText { - text: I18n.tr("settings.color-scheme.color-source.dark-mode.description") + text: I18n.tr("settings.color-scheme.dark-mode.switch.description") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap @@ -167,7 +167,7 @@ ColumnLayout { color: Color.mSurface NIcon { - icon: "color-picker" + icon: ProgramCheckerService.matugenAvailable ? "color-picker" : "alert-triangle" pointSize: Style.fontSizeL color: Color.mPrimary anchors.centerIn: parent @@ -196,7 +196,6 @@ ColumnLayout { NToggle { enabled: ProgramCheckerService.matugenAvailable - opacity: ProgramCheckerService.matugenAvailable ? 1.0 : 0.6 checked: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable onToggled: checked => { if (!ProgramCheckerService.matugenAvailable) @@ -214,34 +213,6 @@ ColumnLayout { } } - // Matugen not available notice - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - visible: !ProgramCheckerService.matugenAvailable - - Rectangle { - width: 28 - height: 28 - radius: Style.radiusM - color: Color.mSurface - NIcon { - icon: "alert-triangle" - pointSize: Style.fontSizeL - color: Color.mPrimary - anchors.centerIn: parent - } - } - NText { - text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description") - // Reuse description; availability is visually indicated - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - // Matugen scheme type (visible when wallpaper colors enabled and matugen available) ColumnLayout { Layout.fillWidth: true diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 18259ed6..45ad8bc3 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -9,6 +9,7 @@ RowLayout { property string label: "" property string description: "" + property bool enabled: true property bool checked: false property bool hovering: false property int baseSize: Math.round(Style.baseWidgetSize * 0.8 * Style.uiScaleRatio) @@ -18,6 +19,7 @@ RowLayout { signal exited Layout.fillWidth: true + opacity: enabled ? 1.0 : 0.6 NLabel { label: root.label @@ -71,14 +73,20 @@ RowLayout { cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: { + if (!enabled) + return hovering = true root.entered() } onExited: { + if (!enabled) + return hovering = false root.exited() } onClicked: { + if (!enabled) + return root.toggled(!root.checked) } } From 1dc740092eee681d6b13b66a043d78a5694399fe Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Mon, 20 Oct 2025 20:18:10 +0200 Subject: [PATCH 32/51] Bluetooth: Display name of connected Bluetooth device in bar pill. --- Modules/Bar/Widgets/Bluetooth.qml | 68 +++++++++++++++++++++++-------- Services/BluetoothService.qml | 6 +++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index e6f50f0f..e58d5e43 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -1,27 +1,61 @@ import QtQuick -import QtQuick.Layouts -import QtQuick.Controls import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -import qs.Widgets +import qs.Modules.Bar.Extras -NIconButton { +Item { id: root property ShellScreen screen - baseSize: Style.capsuleHeight - applyUiScale: false - density: Settings.data.bar.density - colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.transparent - tooltipText: I18n.tr("tooltips.bluetooth-devices") - tooltipDirection: BarService.getTooltipDirection() - icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + + implicitWidth: pill.width + implicitHeight: pill.height + + BarPill { + id: pill + + density: Settings.data.bar.density + rightOpen: BarService.getPillDirection(root) + icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + text: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) { + const firstDevice = BluetoothService.connectedDevices[0] + return firstDevice.name || firstDevice.deviceName + } + return "" + } + suffix: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) { + return ` + ${BluetoothService.connectedDevices.length - 1}` + } + return "" + } + autoHide: false + forceOpen: root.displayMode === "alwaysShow" + forceClose: root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + tooltipText: I18n.tr("tooltips.bluetooth-devices") + } } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index d6c56e79..f6a42743 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -21,6 +21,12 @@ Singleton { return dev && (dev.paired || dev.trusted) }) } + readonly property var connectedDevices: { + if (!adapter || !adapter.devices) { + return [] + } + return adapter.devices.values.filter(dev => dev && dev.connected) + } readonly property var allDevicesWithBattery: { if (!adapter || !adapter.devices) { From d85acc7f07a314b8f481cb774ba417f0cdd67e8e Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Mon, 20 Oct 2025 20:18:32 +0200 Subject: [PATCH 33/51] Bluetooth: Add display settings for Bluetooth bar pill. --- .../Settings/Bar/BarWidgetSettingsDialog.qml | 1 + .../Bar/WidgetSettings/BluetoothSettings.qml | 40 +++++++++++++++++++ Services/BarWidgetRegistry.qml | 4 ++ 3 files changed, 45 insertions(+) create mode 100644 Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml index 4b35b7fd..307dcf8f 100644 --- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -122,6 +122,7 @@ Popup { const widgetSettingsMap = { "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", "Battery": "WidgetSettings/BatterySettings.qml", + "Bluetooth": "WidgetSettings/BluetoothSettings.qml", "Brightness": "WidgetSettings/BrightnessSettings.qml", "Clock": "WidgetSettings/ClockSettings.qml", "ControlCenter": "WidgetSettings/ControlCenterSettings.qml", diff --git a/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml new file mode 100644 index 00000000..d6548476 --- /dev/null +++ b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.displayMode = valueDisplayMode + return settings + } + + NComboBox { + label: I18n.tr("bar.widget-settings.battery.display-mode.label") + description: I18n.tr("bar.widget-settings.battery.display-mode.description") + minimumWidth: 134 + model: [{ + "key": "onhover", + "name": I18n.tr("options.display-mode.on-hover") + }, { + "key": "alwaysShow", + "name": I18n.tr("options.display-mode.always-show") + }, { + "key": "alwaysHide", + "name": I18n.tr("options.display-mode.always-hide") + }] + currentKey: root.valueDisplayMode + onSelected: key => root.valueDisplayMode = key + } +} diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index a5823db6..2c61e788 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -53,6 +53,10 @@ Singleton { "displayMode": "onhover", "warningThreshold": 30 }, + "Bluetooth": { + "allowUserSettings": true, + "displayMode": "onhover" + }, "Brightness": { "allowUserSettings": true, "displayMode": "onhover" From 903a04e09630d51bf97f6ab8a05a37c826f9580e Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Mon, 20 Oct 2025 20:19:49 +0200 Subject: [PATCH 34/51] WiFi: Avoid using `BarPill.disableOpen` property. --- Modules/Bar/Widgets/WiFi.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index abfbdcdf..5a6a6901 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -76,7 +76,6 @@ Item { autoHide: false forceOpen: root.displayMode === "alwaysShow" forceClose: root.displayMode === "alwaysHide" || !pill.text - disableOpen: NetworkService.ethernetConnected onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) tooltipText: I18n.tr("tooltips.manage-wifi") From eba6c7ec2743a5ea5ec99b871f5df70b647ee6ba Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Mon, 20 Oct 2025 21:44:19 +0200 Subject: [PATCH 35/51] NPanel: revert NPanelWindow change --- Modules/ControlCenter/ControlCenterPanel.qml | 2 +- Modules/Launcher/Launcher.qml | 2 +- .../Notification/NotificationHistoryPanel.qml | 2 +- Modules/SessionMenu/SessionMenu.qml | 2 +- Modules/Settings/SettingsPanel.qml | 2 +- Widgets/NPanel.qml | 349 ++++++++++++++++- Widgets/NPanelOverlay.qml | 15 - Widgets/NPanelWindow.qml | 355 ------------------ 8 files changed, 352 insertions(+), 377 deletions(-) delete mode 100644 Widgets/NPanelOverlay.qml delete mode 100644 Widgets/NPanelWindow.qml diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index a60ffbce..fcc13cb8 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanelOverlay { +NPanel { id: root panelKeyboardFocus: true diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 443a70ef..f8ee5db1 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanelOverlay { +NPanel { id: root // Panel configuration diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 24950d27..8bf5583f 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -9,7 +9,7 @@ import qs.Services import qs.Widgets // Notification History panel -NPanelOverlay { +NPanel { id: root preferredWidth: 380 diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml index e5f536dc..83afaf5f 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -10,7 +10,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanelOverlay { +NPanel { id: root preferredWidth: 320 * Style.uiScaleRatio diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 7726b1d2..e5b64566 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -8,7 +8,7 @@ import qs.Commons import qs.Services import qs.Widgets -NPanelOverlay { +NPanel { id: root preferredWidth: 820 * Style.uiScaleRatio diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 70931010..4b80964d 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -126,8 +126,353 @@ Loader { // ----------------------------------------- sourceComponent: Component { // PanelWindow has its own screen property inherited of QsWindow - NPanelWindow { - loggerPrefix: "NPanel" + PanelWindow { + id: panelWindow + + readonly property string barPosition: Settings.data.bar.position + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) + readonly property real verticalBarWidth: Style.barHeight + + Component.onCompleted: { + Logger.d("NPanel", "Opened", root.objectName, "on", screen.name) + dimmingOpacity = Style.opacityHeavy + } + + Connections { + target: panelWindow + function onScreenChanged() { + root.screen = screen + + // If called from IPC always reposition if screen is updated + if (buttonName) { + setPosition() + } + Logger.d("NPanel", "OnScreenChanged", root.screen.name) + } + } + + visible: true + color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-panel" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + Region { + id: maskRegion + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + + // Close any panel with Esc without requiring focus + Shortcut { + sequences: ["Escape"] + enabled: root.active + onActivated: root.close() + context: Qt.WindowShortcut + } + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + enabled: root.backgroundClickEnabled + onClicked: root.close() + } + + // The actual panel's content + Rectangle { + id: panelBackground + color: panelBackgroundColor + radius: Style.radiusL + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + // Dragging support + property bool draggable: root.draggable + property bool isDragged: false + property real manualX: 0 + property real manualY: 0 + width: { + var w + if (preferredWidthRatio !== undefined) { + w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth)) + } else { + w = preferredWidth + } + // Clamp width so it is never bigger than the screen + return Math.min(w, screen?.width - Style.marginL * 2) + } + height: { + var h + if (preferredHeightRatio !== undefined) { + h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight)) + } else { + h = preferredHeight + } + + // Clamp width so it is never bigger than the screen + return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) + } + + scale: root.scaleValue + x: isDragged ? manualX : calculatedX + y: isDragged ? manualY : calculatedY + + // --------------------------------------------- + // Does not account for corners are they are negligible and helps keep the code clean. + // --------------------------------------------- + property real marginTop: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "top": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginBottom: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "bottom": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginLeft: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "left": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + default: + return Style.marginS + } + } + + property real marginRight: { + if (!barIsVisible) { + return 0 + } + switch (barPosition) { + case "right": + return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + default: + return Style.marginS + } + } + + // --------------------------------------------- + property int calculatedX: { + // Priority to fixed anchoring + if (panelAnchorHorizontalCenter) { + // Center horizontally but respect bar margins + var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) + var minX = marginLeft + var maxX = panelWindow.width - panelBackground.width - marginRight + return Math.round(Math.max(minX, Math.min(centerX, maxX))) + } else if (panelAnchorLeft) { + return marginLeft + } else if (panelAnchorRight) { + return Math.round(panelWindow.width - panelBackground.width - marginRight) + } + + // No fixed anchoring + if (isVertical) { + // Vertical bar + if (barPosition === "right") { + // To the left of the right bar + return Math.round(panelWindow.width - panelBackground.width - marginRight) + } else { + // To the right of the left bar + return marginLeft + } + } else { + // Horizontal bar + if (root.useButtonPosition) { + // Position panel relative to button + var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) + // Keep panel within screen bounds + var maxX = panelWindow.width - panelBackground.width - marginRight + var minX = marginLeft + return Math.round(Math.max(minX, Math.min(targetX, maxX))) + } else { + // Fallback to center horizontally + return Math.round((panelWindow.width - panelBackground.width) / 2) + } + } + } + + // --------------------------------------------- + property int calculatedY: { + // Priority to fixed anchoring + if (panelAnchorVerticalCenter) { + // Center vertically but respect bar margins + var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) + var minY = marginTop + var maxY = panelWindow.height - panelBackground.height - marginBottom + return Math.round(Math.max(minY, Math.min(centerY, maxY))) + } else if (panelAnchorTop) { + return marginTop + } else if (panelAnchorBottom) { + return Math.round(panelWindow.height - panelBackground.height - marginBottom) + } + + // No fixed anchoring + if (isVertical) { + // Vertical bar + if (useButtonPosition) { + // Position panel relative to button + var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) + // Keep panel within screen bounds + var maxY = panelWindow.height - panelBackground.height - marginBottom + var minY = marginTop + return Math.round(Math.max(minY, Math.min(targetY, maxY))) + } else { + // Fallback to center vertically + return Math.round((panelWindow.height - panelBackground.height) / 2) + } + } else { + // Horizontal bar + if (barPosition === "bottom") { + // Above the bottom bar + return Math.round(panelWindow.height - panelBackground.height - marginBottom) + } else { + // Below the top bar + return marginTop + } + } + } + + // Animate in when component is completed + Component.onCompleted: { + root.scaleValue = 1.0 + } + + // Reset drag position when panel closes + Connections { + target: root + function onClosed() { + panelBackground.isDragged = false + } + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Loader { + id: panelContentLoader + anchors.fill: parent + sourceComponent: root.panelContent + } + + // Handle drag move on the whole panel area + DragHandler { + id: dragHandler + target: null + enabled: panelBackground.draggable + property real dragStartX: 0 + property real dragStartY: 0 + onActiveChanged: { + if (active) { + // Capture current position into manual coordinates BEFORE toggling isDragged + panelBackground.manualX = panelBackground.x + panelBackground.manualY = panelBackground.y + dragStartX = panelBackground.x + dragStartY = panelBackground.y + panelBackground.isDragged = true + if (root.enableBackgroundClick) + root.disableBackgroundClick() + } else { + // Keep isDragged true so we continue using the manual x/y after release + if (root.enableBackgroundClick) + root.enableBackgroundClick() + } + } + onTranslationChanged: { + // Proposed new coordinates from fixed drag origin + var nx = dragStartX + translation.x + var ny = dragStartY + translation.y + + // Calculate gaps so we never overlap the bar on any side + var baseGap = Style.marginS + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 + + var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) + var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) + var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) + var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) + + // Clamp within screen bounds accounting for insets + var maxX = panelWindow.width - panelBackground.width - insetRight + var minX = insetLeft + var maxY = panelWindow.height - panelBackground.height - insetBottom + var minY = insetTop + + panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) + panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) + } + } + + // Drag indicator border + Rectangle { + anchors.fill: parent + anchors.margins: 0 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(2, Style.borderL) + radius: parent.radius + visible: panelBackground.isDragged && dragHandler.active + opacity: 0.8 + z: 3000 + + // Subtle glow effect + Rectangle { + anchors.fill: parent + anchors.margins: 0 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS) + radius: parent.radius + opacity: 0.3 + } + } + } } } } diff --git a/Widgets/NPanelOverlay.qml b/Widgets/NPanelOverlay.qml deleted file mode 100644 index 9debdbcb..00000000 --- a/Widgets/NPanelOverlay.qml +++ /dev/null @@ -1,15 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services - -NPanel { - sourceComponent: Component { - // PanelWindow has its own screen property inherited of QsWindow - NPanelWindow { - loggerPrefix: "NPanelOverlay" - WlrLayershell.layer: WlrLayer.Overlay - } - } -} diff --git a/Widgets/NPanelWindow.qml b/Widgets/NPanelWindow.qml deleted file mode 100644 index ae9129f6..00000000 --- a/Widgets/NPanelWindow.qml +++ /dev/null @@ -1,355 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services - -PanelWindow { - id: panelWindow - - readonly property string barPosition: Settings.data.bar.position - readonly property bool isVertical: barPosition === "left" || barPosition === "right" - readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) - readonly property real verticalBarWidth: Style.barHeight - - property string loggerPrefix - - Component.onCompleted: { - Logger.d(loggerPrefix, "Opened", root.objectName, "on", screen.name) - dimmingOpacity = Style.opacityHeavy - } - - Connections { - target: panelWindow - function onScreenChanged() { - root.screen = screen - - // If called from IPC always reposition if screen is updated - if (buttonName) { - setPosition() - } - Logger.d(loggerPrefix, "OnScreenChanged", root.screen.name) - } - } - - visible: true - color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent - - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "noctalia-panel" - WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - Region { - id: maskRegion - } - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - } - } - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - - // Close any panel with Esc without requiring focus - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: root.close() - context: Qt.WindowShortcut - } - - // Clicking outside of the rectangle to close - MouseArea { - anchors.fill: parent - enabled: root.backgroundClickEnabled - onClicked: root.close() - } - - // The actual panel's content - Rectangle { - id: panelBackground - color: panelBackgroundColor - radius: Style.radiusL - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS) - // Dragging support - property bool draggable: root.draggable - property bool isDragged: false - property real manualX: 0 - property real manualY: 0 - width: { - var w - if (preferredWidthRatio !== undefined) { - w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth)) - } else { - w = preferredWidth - } - // Clamp width so it is never bigger than the screen - return Math.min(w, screen?.width - Style.marginL * 2) - } - height: { - var h - if (preferredHeightRatio !== undefined) { - h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight)) - } else { - h = preferredHeight - } - - // Clamp width so it is never bigger than the screen - return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) - } - - scale: root.scaleValue - x: isDragged ? manualX : calculatedX - y: isDragged ? manualY : calculatedY - - // --------------------------------------------- - // Does not account for corners are they are negligible and helps keep the code clean. - // --------------------------------------------- - property real marginTop: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "top": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginBottom: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "bottom": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginLeft: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "left": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) - default: - return Style.marginS - } - } - - property real marginRight: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "right": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) - default: - return Style.marginS - } - } - - // --------------------------------------------- - property int calculatedX: { - // Priority to fixed anchoring - if (panelAnchorHorizontalCenter) { - // Center horizontally but respect bar margins - var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) - var minX = marginLeft - var maxX = panelWindow.width - panelBackground.width - marginRight - return Math.round(Math.max(minX, Math.min(centerX, maxX))) - } else if (panelAnchorLeft) { - return marginLeft - } else if (panelAnchorRight) { - return Math.round(panelWindow.width - panelBackground.width - marginRight) - } - - // No fixed anchoring - if (isVertical) { - // Vertical bar - if (barPosition === "right") { - // To the left of the right bar - return Math.round(panelWindow.width - panelBackground.width - marginRight) - } else { - // To the right of the left bar - return marginLeft - } - } else { - // Horizontal bar - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) - // Keep panel within screen bounds - var maxX = panelWindow.width - panelBackground.width - marginRight - var minX = marginLeft - return Math.round(Math.max(minX, Math.min(targetX, maxX))) - } else { - // Fallback to center horizontally - return Math.round((panelWindow.width - panelBackground.width) / 2) - } - } - } - - // --------------------------------------------- - property int calculatedY: { - // Priority to fixed anchoring - if (panelAnchorVerticalCenter) { - // Center vertically but respect bar margins - var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) - var minY = marginTop - var maxY = panelWindow.height - panelBackground.height - marginBottom - return Math.round(Math.max(minY, Math.min(centerY, maxY))) - } else if (panelAnchorTop) { - return marginTop - } else if (panelAnchorBottom) { - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } - - // No fixed anchoring - if (isVertical) { - // Vertical bar - if (useButtonPosition) { - // Position panel relative to button - var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) - // Keep panel within screen bounds - var maxY = panelWindow.height - panelBackground.height - marginBottom - var minY = marginTop - return Math.round(Math.max(minY, Math.min(targetY, maxY))) - } else { - // Fallback to center vertically - return Math.round((panelWindow.height - panelBackground.height) / 2) - } - } else { - // Horizontal bar - if (barPosition === "bottom") { - // Above the bottom bar - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } else { - // Below the top bar - return marginTop - } - } - } - - // Animate in when component is completed - Component.onCompleted: { - root.scaleValue = 1.0 - } - - // Reset drag position when panel closes - Connections { - target: root - function onClosed() { - panelBackground.isDragged = false - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - Loader { - id: panelContentLoader - anchors.fill: parent - sourceComponent: root.panelContent - } - - // Handle drag move on the whole panel area - DragHandler { - id: dragHandler - target: null - enabled: panelBackground.draggable - property real dragStartX: 0 - property real dragStartY: 0 - onActiveChanged: { - if (active) { - // Capture current position into manual coordinates BEFORE toggling isDragged - panelBackground.manualX = panelBackground.x - panelBackground.manualY = panelBackground.y - dragStartX = panelBackground.x - dragStartY = panelBackground.y - panelBackground.isDragged = true - if (root.enableBackgroundClick) - root.disableBackgroundClick() - } else { - // Keep isDragged true so we continue using the manual x/y after release - if (root.enableBackgroundClick) - root.enableBackgroundClick() - } - } - onTranslationChanged: { - // Proposed new coordinates from fixed drag origin - var nx = dragStartX + translation.x - var ny = dragStartY + translation.y - - // Calculate gaps so we never overlap the bar on any side - var baseGap = Style.marginS - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 - - var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) - var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) - var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) - var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) - - // Clamp within screen bounds accounting for insets - var maxX = panelWindow.width - panelBackground.width - insetRight - var minX = insetLeft - var maxY = panelWindow.height - panelBackground.height - insetBottom - var minY = insetTop - - panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) - panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) - } - } - - // Drag indicator border - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(2, Style.borderL) - radius: parent.radius - visible: panelBackground.isDragged && dragHandler.active - opacity: 0.8 - z: 3000 - - // Subtle glow effect - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS) - radius: parent.radius - opacity: 0.3 - } - } - } -} From 47bb77f1039acfd75dbb2c449f2b90c2cda107ec Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Mon, 20 Oct 2025 22:05:13 +0200 Subject: [PATCH 36/51] NPanel: add persistent useOverlay property --- Assets/settings-default.json | 3 ++- Commons/Settings.qml | 1 + Widgets/NPanel.qml | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Assets/settings-default.json b/Assets/settings-default.json index ce680d75..2c1ca7bb 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -207,7 +207,8 @@ "fontFixed": "DejaVu Sans Mono", "fontDefaultScale": 1, "fontFixedScale": 1, - "tooltipsEnabled": true + "tooltipsEnabled": true, + "panelsOverlayLayer": true }, "brightness": { "brightnessStep": 5 diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 1f925537..62695685 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -343,6 +343,7 @@ Singleton { property real fontDefaultScale: 1.0 property real fontFixedScale: 1.0 property bool tooltipsEnabled: true + property bool panelsOverlayLayer: true } // brightness diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 4b80964d..527f31c6 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -9,6 +9,8 @@ Loader { property ShellScreen screen + property bool useOverlay: Settings.data.ui.panelsOverlayLayer + property Component panelContent: null property real preferredWidth: 700 property real preferredHeight: 900 @@ -157,7 +159,7 @@ Loader { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" - WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.layer: useOverlay ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None Region { From b6c95b8ed5a786e044d0c34bd0335c840e33b9a0 Mon Sep 17 00:00:00 2001 From: Damian D'Souza Date: Mon, 20 Oct 2025 22:10:58 +0200 Subject: [PATCH 37/51] Settings: add panels overlay setting toggle un UI tab --- Assets/Translations/de.json | 4 ++++ Assets/Translations/en.json | 6 +++++- Assets/Translations/es.json | 4 ++++ Assets/Translations/fr.json | 4 ++++ Assets/Translations/pt.json | 4 ++++ Assets/Translations/zh-CN.json | 4 ++++ Modules/Settings/Tabs/UserInterfaceTab.qml | 7 +++++++ 7 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index aed4c10c..26bd0515 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -754,6 +754,10 @@ "description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.", "label": "UI-Animationen deaktivieren" }, + "panels-overlay": { + "label": "Panels in Overlay-Ebene öffnen", + "description": "Panels werden über Vollbildfenstern angezeigt" + }, "animation-speed": { "description": "Globale Animationsgeschwindigkeit anpassen.", "label": "Animationsgeschwindigkeit", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index f18e6ac9..65e46e63 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -785,6 +785,10 @@ "animation-disable": { "label": "Disable UI Animations", "description": "Disable all animations for a faster, more responsive experience." + }, + "panels-overlay": { + "label": "Open panels in overlay layer", + "description": "Panels will appear above fullscreen windows" } } }, @@ -1187,7 +1191,7 @@ "scan-again": "Scan again" } }, - + "tooltips": { "refresh": "Refresh", "close": "Close", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index a00881fe..8a663fd1 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -754,6 +754,10 @@ "description": "Desactiva todas las animaciones para una experiencia más rápida y con mayor capacidad de respuesta.", "label": "Desactivar animaciones de la interfaz de usuario" }, + "panels-overlay": { + "label": "Abrir paneles en capa superpuesta", + "description": "Los paneles aparecerán sobre las ventanas en pantalla completa" + }, "animation-speed": { "description": "Ajustar la velocidad global de la animación.", "label": "Velocidad de animación", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index ec9af8ac..190573f5 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -754,6 +754,10 @@ "description": "Désactiver toutes les animations pour une expérience plus rapide et plus réactive.", "label": "Désactiver les animations de l'interface utilisateur" }, + "panels-overlay": { + "label": "Ouvrir les panneaux en couche superposée", + "description": "Les panneaux apparaîtront au-dessus des fenêtres en plein écran" + }, "animation-speed": { "description": "Ajuster la vitesse globale de l'animation.", "label": "Vitesse d'animation", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 02979bf1..69f96962 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -754,6 +754,10 @@ "description": "Desative todas as animações para uma experiência mais rápida e responsiva.", "label": "Desativar animações da interface do usuário" }, + "panels-overlay": { + "label": "Abrir painéis na camada de sobreposição", + "description": "Os painéis aparecerão sobre janelas em tela cheia" + }, "animation-speed": { "description": "Ajustar a velocidade global da animação.", "label": "Velocidade da animação", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index a251a5d4..c6c52c06 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -754,6 +754,10 @@ "description": "禁用所有动画以获得更快、更流畅的体验。", "label": "禁用 UI 动画" }, + "panels-overlay": { + "label": "在覆盖层中打开面板", + "description": "面板将显示在全屏窗口上方" + }, "animation-speed": { "description": "调整全局动画速度。", "label": "动画速度", diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index 13557c29..ec9a289d 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -40,6 +40,13 @@ ColumnLayout { onToggled: checked => Settings.data.general.compactLockScreen = checked } + NToggle { + label: I18n.tr("settings.user-interface.panels-overlay.label") + description: I18n.tr("settings.user-interface.panels-overlay.description") + checked: Settings.data.ui.panelsOverlayLayer + onToggled: checked => Settings.data.ui.panelsOverlayLayer = checked + } + NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL From eba1ace2f027f360f7a2ec3e84ea5a2901e598c9 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 19:04:38 -0400 Subject: [PATCH 38/51] Calendar: fix warnings and error due to old PR --- Modules/Bar/Calendar/CalendarPanel.qml | 10 ++++---- Services/CalendarService.qml | 32 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 42ced01a..888552bb 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -562,10 +562,10 @@ NPanel { // Event indicator dots Row { visible: parent.parent.parent.parent.parent.hasEventsOnDate(model.year, model.month, model.day) - spacing: 2 * scaling + spacing: 2 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Style.marginXS * scaling + anchors.bottomMargin: Style.marginXS readonly property int currentYear: model.year readonly property int currentMonth: model.month @@ -576,9 +576,9 @@ NPanel { model: parent.parent.parent.parent.parent.parent.getEventsForDate(parent.currentYear, parent.currentMonth, parent.currentDay) Rectangle { - width: 4 * scaling - height: 4 * scaling - radius: 2 * scaling + width: 4 + height: width + radius: width / 2 color: parent.parent.parent.parent.parent.parent.getEventColor(modelData, model.today) } } diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index c7498ed0..7e3fa221 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -48,7 +48,7 @@ Singleton { } Component.onCompleted: { - Logger.log("Calendar", "Service initialized") + Logger.i("Calendar", "Service initialized") loadFromCache() checkAvailability() } @@ -68,16 +68,16 @@ Singleton { function loadFromCache() { if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) { root.events = cacheAdapter.cachedEvents - Logger.log("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`) + Logger.i("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`) } if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) { root.calendars = cacheAdapter.cachedCalendars - Logger.log("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`) + Logger.i("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`) } if (cacheAdapter.lastUpdate) { - Logger.log("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`) + Logger.i("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`) } } @@ -114,7 +114,7 @@ Singleton { loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000) loadEventsProcess.running = true - Logger.log("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`) + Logger.i("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`) } // Helper to format date/time @@ -135,10 +135,10 @@ Singleton { root.available = result === "available" if (root.available) { - Logger.log("Calendar", "EDS libraries available") + Logger.i("Calendar", "EDS libraries available") loadCalendars() } else { - Logger.warn("Calendar", "EDS libraries not available: " + result) + Logger.w("Calendar", "EDS libraries not available: " + result) root.lastError = "Evolution Data Server libraries not installed" } } @@ -147,7 +147,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.warn("Calendar", "Availability check error: " + text) + Logger.w("Calendar", "Availability check error: " + text) root.available = false root.lastError = "Failed to check library availability" } @@ -169,7 +169,7 @@ Singleton { cacheAdapter.cachedCalendars = result saveCache() - Logger.log("Calendar", `Found ${result.length} calendar(s)`) + Logger.i("Calendar", `Found ${result.length} calendar(s)`) // Auto-load events after discovering calendars // Only load if we have calendars and no cached events @@ -180,7 +180,7 @@ Singleton { loadEvents() } } catch (e) { - Logger.warn("Calendar", "Failed to parse calendars: " + e) + Logger.w("Calendar", "Failed to parse calendars: " + e) root.lastError = "Failed to parse calendar list" } } @@ -189,7 +189,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.warn("Calendar", "List calendars error: " + text) + Logger.w("Calendar", "List calendars error: " + text) root.lastError = text.trim() } } @@ -216,15 +216,15 @@ Singleton { cacheAdapter.lastUpdate = new Date().toISOString() saveCache() - Logger.log("Calendar", `Loaded ${result.length} event(s)`) + Logger.i("Calendar", `Loaded ${result.length} event(s)`) } catch (e) { - Logger.warn("Calendar", "Failed to parse events: " + e) + Logger.w("Calendar", "Failed to parse events: " + e) root.lastError = "Failed to parse events" // Fall back to cached events if available if (cacheAdapter.cachedEvents.length > 0) { root.events = cacheAdapter.cachedEvents - Logger.log("Calendar", "Using cached events") + Logger.i("Calendar", "Using cached events") } } } @@ -235,13 +235,13 @@ Singleton { root.loading = false if (text.trim()) { - Logger.warn("Calendar", "Load events error: " + text) + Logger.w("Calendar", "Load events error: " + text) root.lastError = text.trim() // Fall back to cached events if available if (cacheAdapter.cachedEvents.length > 0) { root.events = cacheAdapter.cachedEvents - Logger.log("Calendar", "Using cached events due to error") + Logger.i("Calendar", "Using cached events due to error") } } } From 096aa3899fddfd56ae629fa213587311dd449795 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 19:20:40 -0400 Subject: [PATCH 39/51] CalendarService: little less spam --- Services/CalendarService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index 7e3fa221..5b2c0285 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -147,7 +147,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.w("Calendar", "Availability check error: " + text) + //Logger.w("Calendar", "Availability check error: " + text) root.available = false root.lastError = "Failed to check library availability" } From 690a89b58e86178f0ad7f08c92e20629b2347205 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 19:38:27 -0400 Subject: [PATCH 40/51] Calendar: switching some warnings to debug so it does not scare people of. --- Services/CalendarService.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml index 5b2c0285..b152d177 100644 --- a/Services/CalendarService.qml +++ b/Services/CalendarService.qml @@ -147,7 +147,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - //Logger.w("Calendar", "Availability check error: " + text) + Logger.d("Calendar", "Availability check error: " + text) root.available = false root.lastError = "Failed to check library availability" } @@ -180,7 +180,7 @@ Singleton { loadEvents() } } catch (e) { - Logger.w("Calendar", "Failed to parse calendars: " + e) + Logger.d("Calendar", "Failed to parse calendars: " + e) root.lastError = "Failed to parse calendar list" } } @@ -189,7 +189,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - Logger.w("Calendar", "List calendars error: " + text) + Logger.d("Calendar", "List calendars error: " + text) root.lastError = text.trim() } } @@ -218,7 +218,7 @@ Singleton { Logger.i("Calendar", `Loaded ${result.length} event(s)`) } catch (e) { - Logger.w("Calendar", "Failed to parse events: " + e) + Logger.d("Calendar", "Failed to parse events: " + e) root.lastError = "Failed to parse events" // Fall back to cached events if available @@ -235,7 +235,7 @@ Singleton { root.loading = false if (text.trim()) { - Logger.w("Calendar", "Load events error: " + text) + Logger.d("Calendar", "Load events error: " + text) root.lastError = text.trim() // Fall back to cached events if available From ae8a309c54105bb215e6da011d78528d3cd74c57 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 20:14:24 -0400 Subject: [PATCH 41/51] TaskbarGrouped: BugFix + simplifications - fixed workspace switching, when workspace is empty. - moved some logic to the compositor service - removed unecessary Qt.createQmlObject --- Modules/Bar/Widgets/TaskbarGrouped.qml | 32 ++++---------------------- Services/CompositorService.qml | 11 +++++++++ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index 620e92e4..ffa29311 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -35,17 +35,6 @@ Item { readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false property ListModel localWorkspaces: ListModel {} - function getWindowsForWorkspace(workspaceId) { - var windowsInWs = [] - for (var i = 0; i < CompositorService.windows.count; i++) { - var window = CompositorService.windows.get(i) - if (window.workspaceId === workspaceId) { - windowsInWs.push(window) - } - } - return windowsInWs - } - function refreshWorkspaces() { localWorkspaces.clear() if (screen !== null) { @@ -56,21 +45,10 @@ Item { continue } - var windowsInWs = getWindowsForWorkspace(ws.id) - var windowsModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root) - for (var j = 0; j < windowsInWs.length; j++) { - windowsModel.append(windowsInWs[j]) - } - localWorkspaces.append({ - "id": ws.id, - "name": ws.name, - "output": ws.output, - "isFocused": ws.isFocused, - "isOccupied": ws.isOccupied, - "isActive": ws.isActive, - "isUrgent": ws.isUrgent, - "windows": windowsModel - }) + // Copy all properties from ws and add windows + var workspaceData = Object.assign({}, ws) + workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id) + localWorkspaces.append(workspaceData) } } } @@ -253,4 +231,4 @@ Item { delegate: workspaceRepeaterDelegate } } -} \ No newline at end of file +} diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 307dc9b7..cc752fbd 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -256,6 +256,17 @@ Singleton { return "" } + function getWindowsForWorkspace(workspaceId) { + var windowsInWs = [] + for (var i = 0; i < windows.count; i++) { + var window = windows.get(i) + if (window.workspaceId === workspaceId) { + windowsInWs.push(window) + } + } + return windowsInWs + } + // Generic workspace switching function switchToWorkspace(workspace) { if (backend && backend.switchToWorkspace) { From 97d59127d0313efe62117b2c0b09185f670bf0ff Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 20:22:25 -0400 Subject: [PATCH 42/51] TaskbarGrouped: better look when using capsule. --- Modules/Bar/Widgets/TaskbarGrouped.qml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index ffa29311..bc3899fc 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -72,15 +72,6 @@ Item { } } - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - y: isVerticalBar ? 0 : (parent.height - height) / 2 - height: isVerticalBar ? parent.height : Style.capsuleHeight - radius: Style.radiusM - color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent - } - Component { id: workspaceRepeaterDelegate @@ -90,13 +81,12 @@ Item { property var workspaceModel: model property bool hasWindows: workspaceModel.windows.count > 0 - radius: Style.radiusM - color: "transparent" + radius: Style.radiusS border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline border.width: 1 - // Dynamic sizing width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + Style.marginL height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + Style.marginXS + color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent MouseArea { anchors.fill: parent From 6604373524b016b99a67a8b8a1b2f417289a8e5c Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 21:40:11 -0400 Subject: [PATCH 43/51] TaskbarGrouped: Cleanup, optimizations, smarter layout --- Modules/Bar/Widgets/TaskbarGrouped.qml | 137 ++++++++++++------------- 1 file changed, 68 insertions(+), 69 deletions(-) diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index bc3899fc..616032c8 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -20,7 +20,7 @@ Item { property int sectionWidgetsCount: 0 readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" - readonly property bool density: Settings.data.bar.density + readonly property string density: Settings.data.bar.density readonly property real itemSize: (density === "compact") ? Style.capsuleHeight * 0.9 : Style.capsuleHeight * 0.8 property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { @@ -37,28 +37,35 @@ Item { function refreshWorkspaces() { localWorkspaces.clear() - if (screen !== null) { - for (var i = 0; i < CompositorService.workspaces.count; i++) { - const ws = CompositorService.workspaces.get(i) - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) { - continue - } + if (!screen) + return - // Copy all properties from ws and add windows - var workspaceData = Object.assign({}, ws) - workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id) - localWorkspaces.append(workspaceData) - } - } + const screenName = screen.name.toLowerCase() + + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + + if (ws.output.toLowerCase() !== screenName) + continue + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) + continue + + // Copy all properties from ws and add windows + var workspaceData = Object.assign({}, ws) + workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id) + + localWorkspaces.append(workspaceData) } } Component.onCompleted: { refreshWorkspaces() } - implicitWidth: isVerticalBar ? taskbarLayoutVertical.implicitWidth + Style.marginM * 2 : Math.round(taskbarLayoutHorizontal.implicitWidth + Style.marginM * 2) - implicitHeight: isVerticalBar ? Math.round(taskbarLayoutVertical.implicitHeight + Style.marginM * 2) : Style.barHeight + + onScreenChanged: refreshWorkspaces() + + implicitWidth: isVerticalBar ? taskbarGrid.implicitWidth + Style.marginM * 2 : Math.round(taskbarGrid.implicitWidth + Style.marginM * 2) + implicitHeight: isVerticalBar ? Math.round(taskbarGrid.implicitHeight + Style.marginM * 2) : Style.barHeight Connections { target: CompositorService @@ -78,14 +85,15 @@ Item { Rectangle { id: container + required property var model property var workspaceModel: model property bool hasWindows: workspaceModel.windows.count > 0 radius: Style.radiusS border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline border.width: 1 - width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + Style.marginL - height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + Style.marginXS + width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginXS : Style.marginL) + height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginL : Style.marginXS) color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent MouseArea { @@ -111,9 +119,21 @@ Item { delegate: Item { id: taskbarItem + property bool itemHovered: false + width: root.itemSize * 0.8 height: root.itemSize * 0.8 + // Smooth scale animation on hover + scale: itemHovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + IconImage { id: appIcon @@ -125,12 +145,20 @@ Item { opacity: model.isFocused ? Style.opacityFull : 0.6 layer.enabled: widgetSettings.colorizeIcons === true + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + Rectangle { + id: focusIndicator anchors.bottomMargin: -2 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - width: 4 - height: 4 + width: model.isFocused ? 4 : 0 + height: model.isFocused ? 4 : 0 color: model.isFocused ? Color.mPrimary : Color.transparent radius: width * 0.5 } @@ -138,7 +166,6 @@ Item { layer.effect: ShaderEffect { property color targetColor: Color.mOnSurface property real colorizeMode: 0 - fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") } } @@ -155,66 +182,38 @@ Item { } if (mouse.button === Qt.LeftButton) { - try { - CompositorService.focusWindow(model) - } catch (error) { - Logger.error("TaskbarGrouped", "Failed to focus window: " + error) - } + CompositorService.focusWindow(model) } else if (mouse.button === Qt.RightButton) { - try { - CompositorService.closeWindow(model) - } catch (error) { - Logger.error("TaskbarGrouped", "Failed to close window: " + error) - } + CompositorService.closeWindow(model) } } - onEntered: TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection()) - onExited: TooltipService.hide() + onEntered: { + taskbarItem.itemHovered = true + TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection()) + } + onExited: { + taskbarItem.itemHovered = false + TooltipService.hide() + } } } } } - - // Animate size changes for a smooth look - Behavior on width { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - - Behavior on height { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } } } - Row { - id: taskbarLayoutHorizontal + Flow { + id: taskbarGrid + + anchors.verticalCenter: isVerticalBar ? undefined : parent.verticalCenter + anchors.left: isVerticalBar ? undefined : parent.left + anchors.leftMargin: isVerticalBar ? 0 : Style.marginM + anchors.horizontalCenter: isVerticalBar ? parent.horizontalCenter : undefined + anchors.top: isVerticalBar ? parent.top : undefined + anchors.topMargin: isVerticalBar ? Style.marginM : 0 - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Style.marginM spacing: Style.marginS - visible: !isVerticalBar - - Repeater { - model: localWorkspaces - delegate: workspaceRepeaterDelegate - } - } - - Column { - id: taskbarLayoutVertical - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Style.marginM - spacing: Style.marginS - visible: isVerticalBar + flow: isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight Repeater { model: localWorkspaces From 95faefa8eb49be829bb2e7b6b518ff248e65e500 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 20 Oct 2025 21:56:59 -0400 Subject: [PATCH 44/51] Pill: cleanup - removed duality between disableOpen and forceClose - renamed rightOpen to oppositeDirection - wifi+bt: for vertical bar, use tooltip rather than the classic pill opening as text will most likely never fit. --- Modules/Bar/Extras/BarPill.qml | 9 +++------ Modules/Bar/Extras/BarPillHorizontal.qml | 23 +++++++++++------------ Modules/Bar/Extras/BarPillVertical.qml | 11 +++++------ Modules/Bar/Widgets/Battery.qml | 5 ++--- Modules/Bar/Widgets/Bluetooth.qml | 13 +++++++++---- Modules/Bar/Widgets/Brightness.qml | 2 +- Modules/Bar/Widgets/CustomButton.qml | 5 ++--- Modules/Bar/Widgets/KeyboardLayout.qml | 2 +- Modules/Bar/Widgets/Microphone.qml | 2 +- Modules/Bar/Widgets/Volume.qml | 2 +- Modules/Bar/Widgets/WiFi.qml | 13 +++++++++---- 11 files changed, 45 insertions(+), 42 deletions(-) diff --git a/Modules/Bar/Extras/BarPill.qml b/Modules/Bar/Extras/BarPill.qml index ba1601d3..aef33339 100644 --- a/Modules/Bar/Extras/BarPill.qml +++ b/Modules/Bar/Extras/BarPill.qml @@ -16,8 +16,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false readonly property string barPosition: Settings.data.bar.position @@ -51,8 +50,7 @@ Item { autoHide: root.autoHide forceOpen: root.forceOpen forceClose: root.forceClose - disableOpen: root.disableOpen - rightOpen: root.rightOpen + oppositeDirection: root.oppositeDirection hovered: root.hovered density: root.density onShown: root.shown() @@ -76,8 +74,7 @@ Item { autoHide: root.autoHide forceOpen: root.forceOpen forceClose: root.forceClose - disableOpen: root.disableOpen - rightOpen: root.rightOpen + oppositeDirection: root.oppositeDirection hovered: root.hovered density: root.density onShown: root.shown() diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml index 5a732263..e5e5ccda 100644 --- a/Modules/Bar/Extras/BarPillHorizontal.qml +++ b/Modules/Bar/Extras/BarPillHorizontal.qml @@ -18,8 +18,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false // Effective shown state (true if hovered/animated open or forced) @@ -79,18 +78,18 @@ Item { width: revealed ? pillMaxWidth : 1 height: pillHeight - x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right - (iconCircle.x + iconCircle.width / 2) - width // Opens left + x: oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : // Opens right + (iconCircle.x + iconCircle.width / 2) - width // Opens left opacity: revealed ? Style.opacityFull : Style.opacityNone color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent readonly property int halfPillHeight: Math.round(pillHeight * 0.5) - topLeftRadius: rightOpen ? 0 : halfPillHeight - bottomLeftRadius: rightOpen ? 0 : halfPillHeight - topRightRadius: rightOpen ? halfPillHeight : 0 - bottomRightRadius: rightOpen ? halfPillHeight : 0 + topLeftRadius: oppositeDirection ? 0 : halfPillHeight + bottomLeftRadius: oppositeDirection ? 0 : halfPillHeight + topRightRadius: oppositeDirection ? halfPillHeight : 0 + bottomRightRadius: oppositeDirection ? halfPillHeight : 0 anchors.verticalCenter: parent.verticalCenter NText { @@ -99,10 +98,10 @@ Item { x: { // Better text horizontal centering var centerX = (parent.width - width) / 2 - var offset = rightOpen ? Style.marginXS : -Style.marginXS + var offset = oppositeDirection ? Style.marginXS : -Style.marginXS if (forceOpen) { // If its force open, the icon disc background is the same color as the bg pill move text slightly - offset += rightOpen ? -Style.marginXXS : Style.marginXXS + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS } return centerX + offset } @@ -139,7 +138,7 @@ Item { color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent anchors.verticalCenter: parent.verticalCenter - x: rightOpen ? 0 : (parent.width - width) + x: oppositeDirection ? 0 : (parent.width - width) Behavior on color { ColorAnimation { @@ -245,7 +244,7 @@ Item { hovered = true root.entered() TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong) - if (disableOpen || forceClose) { + if (forceClose) { return } if (!forceOpen) { diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml index 485d9732..fe94a534 100644 --- a/Modules/Bar/Extras/BarPillVertical.qml +++ b/Modules/Bar/Extras/BarPillVertical.qml @@ -16,8 +16,7 @@ Item { property bool autoHide: false property bool forceOpen: false property bool forceClose: false - property bool disableOpen: false - property bool rightOpen: false + property bool oppositeDirection: false property bool hovered: false // Bar position detection for pill direction @@ -25,8 +24,8 @@ Item { readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" // Determine pill direction based on section position - readonly property bool openDownward: rightOpen - readonly property bool openUpward: !rightOpen + readonly property bool openDownward: oppositeDirection + readonly property bool openUpward: !oppositeDirection // Effective shown state (true if animated open or forced, but not if force closed) readonly property bool revealed: !forceClose && (forceOpen || showPill) @@ -114,7 +113,7 @@ Item { var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75) if (forceOpen) { // If its force open, the icon disc background is the same color as the bg pill move text slightly - offset += rightOpen ? -Style.marginXXS : Style.marginXXS + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS } return offset } @@ -284,7 +283,7 @@ Item { hovered = true root.entered() TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong) - if (disableOpen || forceClose) { + if (forceClose) { return } if (!forceOpen) { diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index d29c817e..e19e564f 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -87,14 +87,13 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady) text: (isReady || testMode) ? Math.round(percent) : "-" suffix: "%" autoHide: false forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" - forceClose: displayMode === "alwaysHide" - disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) + forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) onClicked: PanelService.getPanel("batteryPanel")?.toggle(this) tooltipText: { let lines = [] diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index e58d5e43..1c3fd2b0 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -36,7 +36,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" text: { if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) { @@ -52,10 +52,15 @@ Item { return "" } autoHide: false - forceOpen: root.displayMode === "alwaysShow" - forceClose: root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - tooltipText: I18n.tr("tooltips.bluetooth-devices") + tooltipText: { + if (pill.text !== "") { + return pill.text + } + return I18n.tr("tooltips.bluetooth-devices") + } } } diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 5c02e490..60efc609 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -77,7 +77,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want text: { diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 4015c4f4..652d391a 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -45,14 +45,13 @@ Item { BarPill { id: pill - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: customIcon text: _dynamicText density: Settings.data.bar.density autoHide: false forceOpen: _dynamicText !== "" - forceClose: false - disableOpen: true + forceClose: true tooltipText: { if (!hasExec) { return "Custom button, configure in settings." diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 6d597227..41ffd027 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -43,7 +43,7 @@ Item { anchors.verticalCenter: parent.verticalCenter density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: "keyboard" autoHide: false // Important to be false so we can hover as long as we want text: currentLayout.toUpperCase() diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 86338210..6fe3d709 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -89,7 +89,7 @@ Item { BarPill { id: pill - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() density: Settings.data.bar.density autoHide: false // Important to be false so we can hover as long as we want diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 10018771..edf75b9d 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -75,7 +75,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: getIcon() autoHide: false // Important to be false so we can hover as long as we want text: Math.round(AudioService.volume * 100) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 5a6a6901..25de98ae 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -36,7 +36,7 @@ Item { id: pill density: Settings.data.bar.density - rightOpen: BarService.getPillDirection(root) + oppositeDirection: BarService.getPillDirection(root) icon: { try { if (NetworkService.ethernetConnected) { @@ -74,10 +74,15 @@ Item { } } autoHide: false - forceOpen: root.displayMode === "alwaysShow" - forceClose: root.displayMode === "alwaysHide" || !pill.text + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - tooltipText: I18n.tr("tooltips.manage-wifi") + tooltipText: { + if (pill.text !== "") { + return pill.text + } + return I18n.tr("tooltips.manage-wifi") + } } } From e39e9e0d39caeb1ee5db7fcbf8dc7a558da42a5b Mon Sep 17 00:00:00 2001 From: Aiser <2912778691@qq.com> Date: Tue, 21 Oct 2025 18:27:23 +0800 Subject: [PATCH 45/51] Matugen: Add post_hook for ghostty and kitty --- Services/MatugenTemplates.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml index 19066213..a34ecd8f 100644 --- a/Services/MatugenTemplates.qml +++ b/Services/MatugenTemplates.qml @@ -97,11 +97,13 @@ Singleton { }, { "name": "ghostty", "path": "Terminal/ghostty", - "output": "~/.config/ghostty/themes/noctalia" + "output": "~/.config/ghostty/themes/noctalia", + "post_hook": "pkill -SIGUSR2 ghostty" }, { "name": "kitty", "path": "Terminal/kitty.conf", - "output": "~/.config/kitty/themes/noctalia.conf" + "output": "~/.config/kitty/themes/noctalia.conf", + "post_hook": "kitten themes --reload-in=all noctalia" }] terminals.forEach(function (terminal) { From 743aff74ef14e8d2b8bce148084b91439764cd7d Mon Sep 17 00:00:00 2001 From: Aiser <2912778691@qq.com> Date: Tue, 21 Oct 2025 19:27:12 +0800 Subject: [PATCH 46/51] Matugen: Add post_hook for ghostty --- Services/MatugenTemplates.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml index a34ecd8f..8740aaf8 100644 --- a/Services/MatugenTemplates.qml +++ b/Services/MatugenTemplates.qml @@ -102,8 +102,7 @@ Singleton { }, { "name": "kitty", "path": "Terminal/kitty.conf", - "output": "~/.config/kitty/themes/noctalia.conf", - "post_hook": "kitten themes --reload-in=all noctalia" + "output": "~/.config/kitty/themes/noctalia.conf" }] terminals.forEach(function (terminal) { From 49fd63502e07cb6994402206ddcf1e67a2de4ec9 Mon Sep 17 00:00:00 2001 From: Sighthesia Date: Tue, 21 Oct 2025 20:12:39 +0800 Subject: [PATCH 47/51] ActiveWindow: small improvement for fade-in --- Modules/Bar/Widgets/MediaMini.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 9831626e..617b73d3 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -69,7 +69,7 @@ Item { Behavior on opacity { NumberAnimation { duration: Style.animationNormal - easing.type: Easing.OutCubic + easing.type: Easing.InOutCubic } } From b5691d932f30630f19c0814548e103c9d0b0bcc3 Mon Sep 17 00:00:00 2001 From: lysec Date: Tue, 21 Oct 2025 14:34:27 +0200 Subject: [PATCH 48/51] Notification: respect animation setting (fixes #538) --- Modules/Notification/Notification.qml | 38 +++++++++++++------ .../Notification/NotificationHistoryPanel.qml | 2 + 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 00b93297..1ccacd21 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -161,6 +161,7 @@ Variants { // Animate when notifications are added/removed Behavior on implicitHeight { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 2.0 damping: 0.4 @@ -196,6 +197,7 @@ Variants { anchors.right: parent.right height: 2 color: Color.transparent + visible: !Settings.data.general.animationDisabled // Pre-calculate available width for the progress bar readonly property real availableWidth: parent.width - (2 * parent.radius) @@ -222,7 +224,7 @@ Variants { // Smooth progress animation Behavior on width { - enabled: !card.isRemoving // Disable during removal animation + enabled: !card.isRemoving && !Settings.data.general.animationDisabled // Disable during removal animation or when animations disabled NumberAnimation { duration: 100 // Quick but smooth easing.type: Easing.Linear @@ -230,7 +232,7 @@ Variants { } Behavior on x { - enabled: !card.isRemoving + enabled: !card.isRemoving && !Settings.data.general.animationDisabled NumberAnimation { duration: 100 easing.type: Easing.Linear @@ -312,14 +314,21 @@ Variants { // Animate in when the item is created Component.onCompleted: { - // Start from slide position - slideOffset = slideInOffset - scaleValue = 0.8 - opacityValue = 0.0 + if (Settings.data.general.animationDisabled) { + // No animation - set to final state immediately + slideOffset = 0 + scaleValue = 1.0 + opacityValue = 1.0 + } else { + // Start from slide position + slideOffset = slideInOffset + scaleValue = 0.8 + opacityValue = 0.0 - // Delay animation based on index for staggered effect - delayTimer.interval = animationDelay - delayTimer.start() + // Delay animation based on index for staggered effect + delayTimer.interval = animationDelay + delayTimer.start() + } } // Timer for staggered animation start @@ -341,9 +350,11 @@ Variants { return // Prevent multiple animations isRemoving = true - slideOffset = slideOutOffset - scaleValue = 0.8 - opacityValue = 0.0 + if (!Settings.data.general.animationDisabled) { + slideOffset = slideOutOffset + scaleValue = 0.8 + opacityValue = 0.0 + } } // Timer for delayed removal after animation @@ -365,6 +376,7 @@ Variants { // Animation behaviors with spring physics Behavior on scale { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 3 damping: 0.4 @@ -374,6 +386,7 @@ Variants { } Behavior on opacity { + enabled: !Settings.data.general.animationDisabled NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutCubic @@ -381,6 +394,7 @@ Variants { } Behavior on y { + enabled: !Settings.data.general.animationDisabled SpringAnimation { spring: 2.5 damping: 0.3 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 8bf5583f..76a8037b 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -148,6 +148,7 @@ NPanel { border.width: Math.max(1, Style.borderS) Behavior on height { + enabled: !Settings.data.general.animationDisabled NumberAnimation { duration: Style.animationNormal easing.type: Easing.InOutQuad @@ -156,6 +157,7 @@ NPanel { // Smooth color transition on hover Behavior on color { + enabled: !Settings.data.general.animationDisabled ColorAnimation { duration: Style.animationFast } From 96cb0a51992ad0f72fb8788f7b83024c7df28ad4 Mon Sep 17 00:00:00 2001 From: lysec Date: Tue, 21 Oct 2025 14:44:22 +0200 Subject: [PATCH 49/51] IPC: lockScreen toggle is deprecated, use lockScreen lock --- Modules/LockScreen/LockScreen.qml | 67 ++++++++++++++++++++++++++++++- Services/IPCService.qml | 19 ++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 09a91ff8..2b8add42 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -17,11 +17,18 @@ Loader { id: lockScreen active: false + // Track if triggered via deprecated IPC call + property bool triggeredViaDeprecatedCall: false + Timer { id: unloadAfterUnlockTimer interval: 250 repeat: false - onTriggered: lockScreen.active = false + onTriggered: { + lockScreen.active = false + // Reset the deprecation flag when unlocking + lockScreen.triggeredViaDeprecatedCall = false + } } function scheduleUnloadAfterUnlock() { @@ -424,6 +431,64 @@ Loader { } } + // Deprecation warning (shown above error notification) + Rectangle { + width: Math.min(650, parent.width - 40) + implicitHeight: deprecationContent.implicitHeight + 24 + height: implicitHeight + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 320 : 400) * Style.uiScaleRatio + radius: Style.radiusL + color: Qt.alpha(Color.mTertiary, 0.95) + border.color: Color.mTertiary + border.width: 2 + visible: lockScreen.triggeredViaDeprecatedCall + opacity: visible ? 1.0 : 0.0 + + ColumnLayout { + id: deprecationContent + anchors.fill: parent + anchors.margins: 12 + spacing: 6 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + + NIcon { + icon: "alert-triangle" + pointSize: Style.fontSizeL + color: Color.mOnTertiary + } + + NText { + text: "Deprecated IPC Call" + color: Color.mOnTertiary + pointSize: Style.fontSizeL + font.weight: Font.Bold + } + } + + NText { + text: "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead." + color: Color.mOnTertiary + pointSize: Style.fontSizeM + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + } + + Behavior on opacity { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + // Error notification Rectangle { width: 450 diff --git a/Services/IPCService.qml b/Services/IPCService.qml index 3d1d3924..12c7c204 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -77,9 +77,26 @@ Item { IpcHandler { target: "lockScreen" - function toggle() { + + // New preferred method - lock the screen + function lock() { // Only lock if not already locked (prevents the red screen issue) // Note: No unlock via IPC for security reasons + if (!lockScreen.active) { + lockScreen.triggeredViaDeprecatedCall = false + lockScreen.active = true + } + } + + // Deprecated: Use 'lockScreen lock' instead + function toggle() { + // Mark as triggered via deprecated call - warning will show in lock screen + lockScreen.triggeredViaDeprecatedCall = true + + // Log deprecation warning for users checking logs + Logger.w("IPC", "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead.") + + // Still functional for backward compatibility if (!lockScreen.active) { lockScreen.active = true } From 4aa32dbdb3a952b37fdb24efe7f3aa279399bf92 Mon Sep 17 00:00:00 2001 From: lysec Date: Tue, 21 Oct 2025 14:50:27 +0200 Subject: [PATCH 50/51] Notification: move lastSeenTs to cache/noctalia/notifications-state.json --- Assets/settings-default.json | 21 ++++---- Commons/Settings.qml | 1 - Modules/Bar/Widgets/NotificationHistory.qml | 6 +-- .../Notification/NotificationHistoryPanel.qml | 2 +- Services/NotificationService.qml | 52 ++++++++++++++++++- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 904e3961..b98b0aff 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -179,17 +179,16 @@ "network": { "wifiEnabled": true }, - "notifications": { - "doNotDisturb": false, - "monitors": [], - "location": "top_right", - "alwaysOnTop": false, - "lastSeenTs": 0, - "respectExpireTimeout": false, - "lowUrgencyDuration": 3, - "normalUrgencyDuration": 8, - "criticalUrgencyDuration": 15 - }, + "notifications": { + "doNotDisturb": false, + "monitors": [], + "location": "top_right", + "alwaysOnTop": false, + "respectExpireTimeout": false, + "lowUrgencyDuration": 3, + "normalUrgencyDuration": 8, + "criticalUrgencyDuration": 15 + }, "osd": { "enabled": true, "location": "top_right", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 08005398..ab00de7e 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -313,7 +313,6 @@ Singleton { property list monitors: [] property string location: "top_right" property bool alwaysOnTop: false - property real lastSeenTs: 0 property bool respectExpireTimeout: false property int lowUrgencyDuration: 3 property int normalUrgencyDuration: 8 diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 6d6652fe..ccceeaa3 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -31,12 +31,8 @@ NIconButton { readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero - function lastSeenTs() { - return Settings.data.notifications?.lastSeenTs || 0 - } - function computeUnreadCount() { - var since = lastSeenTs() + var since = NotificationService.lastSeenTs var count = 0 var model = NotificationService.historyList for (var i = 0; i < model.count; i++) { diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 76a8037b..b4121199 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -17,7 +17,7 @@ NPanel { panelKeyboardFocus: true onOpened: function () { - Settings.data.notifications.lastSeenTs = Time.timestamp * 1000 + NotificationService.updateLastSeenTs() } panelContent: Rectangle { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 4bb5042d..8a55af35 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -16,6 +16,10 @@ Singleton { property int maxVisible: 5 property int maxHistory: 100 property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + property string stateFile: Settings.cacheDir + "notifications-state.json" + + // State + property real lastSeenTs: 0 // Models property ListModel activeList: ListModel {} @@ -264,7 +268,7 @@ Singleton { saveHistory() } - // Persistence + // Persistence - History FileView { id: historyFileView path: historyFile @@ -281,6 +285,23 @@ Singleton { } } + // Persistence - State (lastSeenTs, etc.) + FileView { + id: stateFileView + path: stateFile + printErrors: false + onLoaded: loadState() + onLoadFailed: error => { + if (error === 2) + writeAdapter() + } + + JsonAdapter { + id: stateAdapter + property real lastSeenTs: 0 + } + } + Timer { id: saveTimer interval: 200 @@ -337,6 +358,35 @@ Singleton { } } + function loadState() { + try { + root.lastSeenTs = stateAdapter.lastSeenTs || 0 + + // Migration: if state file is empty but settings has lastSeenTs, migrate it + if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) { + root.lastSeenTs = Settings.data.notifications.lastSeenTs + saveState() + Logger.i("Notifications", "Migrated lastSeenTs from settings to state file") + } + } catch (e) { + Logger.e("Notifications", "Load state failed:", e) + } + } + + function saveState() { + try { + stateAdapter.lastSeenTs = root.lastSeenTs + stateFileView.writeAdapter() + } catch (e) { + Logger.e("Notifications", "Save state failed:", e) + } + } + + function updateLastSeenTs() { + root.lastSeenTs = Time.timestamp * 1000 + saveState() + } + function getAppName(name) { if (!name || name.trim() === "") return "Unknown" From 7fcf54a9d3d28358c9fdb02863780985036162e8 Mon Sep 17 00:00:00 2001 From: lysec Date: Tue, 21 Oct 2025 15:34:42 +0200 Subject: [PATCH 51/51] OSD: add always on top setting Notification: add always on top setting --- Assets/settings-default.json | 16 ++++++++-------- Commons/Settings.qml | 4 ++-- Modules/Notification/Notification.qml | 2 +- Modules/OSD/OSD.qml | 2 +- Modules/Settings/Tabs/NotificationsTab.qml | 4 ++-- Modules/Settings/Tabs/OsdTab.qml | 4 ++-- Modules/Toast/ToastScreen.qml | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Assets/settings-default.json b/Assets/settings-default.json index b98b0aff..1bf17a16 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -183,19 +183,19 @@ "doNotDisturb": false, "monitors": [], "location": "top_right", - "alwaysOnTop": false, + "overlayLayer": true, "respectExpireTimeout": false, "lowUrgencyDuration": 3, "normalUrgencyDuration": 8, "criticalUrgencyDuration": 15 }, - "osd": { - "enabled": true, - "location": "top_right", - "monitors": [], - "autoHideMs": 2000, - "alwaysOnTop": false - }, + "osd": { + "enabled": true, + "location": "top_right", + "monitors": [], + "autoHideMs": 2000, + "overlayLayer": true + }, "audio": { "volumeStep": 5, "volumeOverdrive": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index ab00de7e..9e7a92d8 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -312,7 +312,7 @@ Singleton { property bool doNotDisturb: false property list monitors: [] property string location: "top_right" - property bool alwaysOnTop: false + property bool overlayLayer: true property bool respectExpireTimeout: false property int lowUrgencyDuration: 3 property int normalUrgencyDuration: 8 @@ -325,7 +325,7 @@ Singleton { property string location: "top_right" property list monitors: [] property int autoHideMs: 2000 - property bool alwaysOnTop: false + property bool overlayLayer: true } // audio diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 1ccacd21..e6ea0702 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -45,7 +45,7 @@ Variants { screen: modelData WlrLayershell.namespace: "noctalia-notifications" - WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top color: Color.transparent diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 5c98c45d..c460b672 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -190,7 +190,7 @@ Variants { color: Color.transparent WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top exclusionMode: PanelWindow.ExclusionMode.Ignore Rectangle { diff --git a/Modules/Settings/Tabs/NotificationsTab.qml b/Modules/Settings/Tabs/NotificationsTab.qml index beae8cdc..316b620d 100644 --- a/Modules/Settings/Tabs/NotificationsTab.qml +++ b/Modules/Settings/Tabs/NotificationsTab.qml @@ -68,8 +68,8 @@ ColumnLayout { NToggle { label: I18n.tr("settings.notifications.settings.always-on-top.label") description: I18n.tr("settings.notifications.settings.always-on-top.description") - checked: Settings.data.notifications.alwaysOnTop - onToggled: checked => Settings.data.notifications.alwaysOnTop = checked + checked: Settings.data.notifications.overlayLayer + onToggled: checked => Settings.data.notifications.overlayLayer = checked } // OSD settings moved to the dedicated OSD tab diff --git a/Modules/Settings/Tabs/OsdTab.qml b/Modules/Settings/Tabs/OsdTab.qml index edb16fd3..365b2396 100644 --- a/Modules/Settings/Tabs/OsdTab.qml +++ b/Modules/Settings/Tabs/OsdTab.qml @@ -86,8 +86,8 @@ ColumnLayout { NToggle { label: I18n.tr("settings.osd.always-on-top.label") description: I18n.tr("settings.osd.always-on-top.description") - checked: Settings.data.osd.alwaysOnTop - onToggled: checked => Settings.data.osd.alwaysOnTop = checked + checked: Settings.data.osd.overlayLayer + onToggled: checked => Settings.data.osd.overlayLayer = checked } NLabel { diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml index badb00b6..fafd9e04 100644 --- a/Modules/Toast/ToastScreen.qml +++ b/Modules/Toast/ToastScreen.qml @@ -197,7 +197,7 @@ Item { color: Color.transparent - WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: PanelWindow.ExclusionMode.Ignore