Merge branch 'main' into media_manager_toggle_artist_first

This commit is contained in:
Lemmy
2025-11-16 20:55:59 -05:00
committed by GitHub
241 changed files with 10102 additions and 10806 deletions
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Zeige nur die Workspace-Bezeichnung für Workspaces an, die offene Fenster haben.",
"label": "Zeige Label nur bei Belegung"
},
"show-workspace-numbers": {
"description": "Arbeitsbereichsbezeichnung in der oberen linken Ecke von Aufgabengruppen anzeigen.",
"label": "Arbeitsbereichsbezeichnung anzeigen"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Zustand: {percent}%",
"idle": "Leerlauf.",
"no-battery-detected": "Keine Batterie erkannt.",
"panel": {
"balanced": "Ausgeglichen ({percent}%)",
"disabled": "Batteriemanager deaktiviert",
"full": "Volle Kapazität ({percent}%)",
"lifespan": "Verlängerte Lebensdauer ({percent}%)",
"title": "Ladeschwelle"
},
"plugged-in": "Angeschlossen.",
"time-left": "Verbleibende Zeit: {time}.",
"time-until-full": "Zeit bis vollständig geladen: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Index",
"index+name": "Index und Name",
"name": "Name",
"none": "Keine"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Wenn aktiviert, können sich Panels auf jedem Bildschirm öffnen. Wenn deaktiviert, öffnen sich Panels nur auf Bildschirmen mit einer Leiste, was den Speicherverbrauch reduzieren kann.",
"label": "Paneele auf Bildschirmen ohne Leiste zulassen"
},
"animation-disable": {
"description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.",
"label": "UI-Animationen deaktivieren"
@@ -1870,19 +1864,6 @@
"low": "Niedriger Batteriestand",
"low-desc": "Batterie ist bei {percent}%. Bitte schließen Sie das Ladegerät an."
},
"battery-manager": {
"initial-setup": "Ersteinrichtung erforderlich",
"install-failed": "Installation fehlgeschlagen",
"install-missing": "Erforderliche Dateien fehlen",
"install-success": "Erfolgreich installiert",
"install-unsupported": "System wird nicht unterstützt",
"set-failed": "Fehler beim Setzen der Batterieschwelle",
"set-success-desc": "Batterieschwelle auf {percent}% gesetzt",
"title": "Batterieschwelle",
"uninstall-failed": "Deinstallation fehlgeschlagen",
"uninstall-setup": "Deinstallation, Authentifizierung erforderlich",
"uninstall-success": "Erfolgreich deinstalliert"
},
"bluetooth": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
+6 -25
View File
@@ -318,13 +318,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Only show workspace label for workspaces that have open windows.",
"label": "Show label only when occupied"
},
"show-workspace-numbers": {
"description": "Display workspace label in the top-left corner of task groups.",
"label": "Show workspace label"
}
},
"tray": {
@@ -367,13 +363,6 @@
"health": "Health: {percent}%",
"idle": "Idle.",
"no-battery-detected": "No battery detected.",
"panel": {
"balanced": "Balanced ({percent}%)",
"disabled": "Battery manager disabled",
"full": "Full capacity ({percent}%)",
"lifespan": "Extended lifespan ({percent}%)",
"title": "Charge threshold"
},
"plugged-in": "Plugged in.",
"time-left": "Time left: {time}.",
"time-until-full": "Time until full: {time}."
@@ -568,6 +557,7 @@
},
"workspace-labels": {
"index": "Index",
"index+name": "Index and Name",
"name": "Name",
"none": "None"
}
@@ -1667,6 +1657,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "When enabled, panels can open on any screen. When disabled, panels will only open on screens that have a bar, which can reduce memory usage.",
"label": "Allow panels on screens without a bar"
},
"animation-disable": {
"description": "Disable all animations for a faster, more responsive experience.",
"label": "Disable UI Animations"
@@ -1874,19 +1868,6 @@
"low": "Low battery",
"low-desc": "Battery is at {percent}%. Please connect the charger."
},
"battery-manager": {
"initial-setup": "Initial setup required",
"install-failed": "Installation failed",
"install-missing": "Required files are missing",
"install-success": "Installed successfully",
"install-unsupported": "System is not supported",
"set-failed": "Failed to set battery threshold",
"set-success-desc": "Battery threshold set to {percent}%",
"title": "Battery threshold",
"uninstall-failed": "Uninstallation failed",
"uninstall-setup": "Uninstalling, authentication required",
"uninstall-success": "Uninstalled successfully"
},
"bluetooth": {
"disabled": "Disabled",
"enabled": "Enabled"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Mostrar solo la etiqueta del espacio de trabajo para los espacios de trabajo que tienen ventanas abiertas.",
"label": "Mostrar etiqueta solo cuando esté ocupado."
},
"show-workspace-numbers": {
"description": "Mostrar la etiqueta del espacio de trabajo en la esquina superior izquierda de los grupos de tareas.",
"label": "Mostrar etiqueta del espacio de trabajo"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Salud: {percent}%",
"idle": "Inactivo.",
"no-battery-detected": "No se detectó ninguna batería.",
"panel": {
"balanced": "Equilibrado ({percent}%)",
"disabled": "Administrador de batería deshabilitado",
"full": "Capacidad total ({percent}%)",
"lifespan": "Vida útil prolongada ({percent}%)",
"title": "Umbral de carga"
},
"plugged-in": "Conectado.",
"time-left": "Tiempo restante: {time}.",
"time-until-full": "Tiempo hasta carga completa: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Índice",
"index+name": "Índice y Nombre",
"name": "Nombre",
"none": "Ninguno"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Cuando está activado, los paneles se pueden abrir en cualquier pantalla. Cuando está desactivado, los paneles solo se abrirán en las pantallas que tengan una barra, lo que puede reducir el uso de memoria.",
"label": "Permitir paneles en pantallas sin barra."
},
"animation-disable": {
"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"
@@ -1870,19 +1864,6 @@
"low": "Batería baja",
"low-desc": "La batería está al {percent}%. Por favor, conecta el cargador."
},
"battery-manager": {
"initial-setup": "Configuración inicial requerida",
"install-failed": "Error en la instalación",
"install-missing": "Faltan archivos requeridos",
"install-success": "Instalado correctamente",
"install-unsupported": "El sistema no es compatible",
"set-failed": "No se pudo establecer el umbral de batería",
"set-success-desc": "Umbral de batería establecido en {percent}%",
"title": "Umbral de batería",
"uninstall-failed": "Error en la desinstalación",
"uninstall-setup": "Desinstalando, se requiere autenticación",
"uninstall-success": "Desinstalado correctamente"
},
"bluetooth": {
"disabled": "Desactivado",
"enabled": "Activado"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Afficher uniquement l'étiquette de l'espace de travail pour les espaces de travail qui ont des fenêtres ouvertes.",
"label": "Afficher l'étiquette uniquement lorsque c'est occupé."
},
"show-workspace-numbers": {
"description": "Afficher l'étiquette de l'espace de travail dans le coin supérieur gauche des groupes de tâches.",
"label": "Afficher l'étiquette de l'espace de travail"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "État : {percent}%",
"idle": "Inactif.",
"no-battery-detected": "Aucune batterie détectée.",
"panel": {
"balanced": "Équilibré ({percent}%)",
"disabled": "Gestionnaire de batterie désactivé",
"full": "Capacité totale ({percent}%)",
"lifespan": "Durée de vie prolongée ({percent}%)",
"title": "Seuil de charge"
},
"plugged-in": "Branché.",
"time-left": "Temps restant : {time}.",
"time-until-full": "Temps jusqu'à charge complète : {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Index",
"index+name": "Index et Nom",
"name": "Nom",
"none": "Aucun"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Quand cette option est activée, les panneaux peuvent s'ouvrir sur n'importe quel écran. Quand elle est désactivée, les panneaux s'ouvriront uniquement sur les écrans qui ont une barre, ce qui peut réduire l'utilisation de la mémoire.",
"label": "Autoriser les panneaux sur les écrans sans barre."
},
"animation-disable": {
"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"
@@ -1870,19 +1864,6 @@
"low": "Batterie faible",
"low-desc": "La batterie est à {percent}%. Veuillez brancher le chargeur."
},
"battery-manager": {
"initial-setup": "Configuration initiale requise",
"install-failed": "Échec de l'installation",
"install-missing": "Fichiers requis manquants",
"install-success": "Installation réussie",
"install-unsupported": "Système non pris en charge",
"set-failed": "Échec de la définition du seuil de batterie",
"set-success-desc": "Seuil de batterie défini à {percent}%",
"title": "Seuil de batterie",
"uninstall-failed": "Échec de la désinstallation",
"uninstall-setup": "Désinstallation, authentification requise",
"uninstall-success": "Désinstallation réussie"
},
"bluetooth": {
"disabled": "Désactivé",
"enabled": "Activé"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Toon alleen de werkomgevinglabels voor werkomgevingen met open vensters.",
"label": "Label alleen tonen wanneer bezet"
},
"show-workspace-numbers": {
"description": "Toon het label van de werkruimte in de linkerbovenhoek van taakgroepen.",
"label": "Werkruimtelabel weergeven"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Gezondheid: {percent}%",
"idle": "In rust.",
"no-battery-detected": "Geen batterij gedetecteerd.",
"panel": {
"balanced": "Gebalanceerd ({percent}%)",
"disabled": "Batterijbeheer uitgeschakeld",
"full": "Volledige capaciteit ({percent}%)",
"lifespan": "Verlengde levensduur ({percent}%)",
"title": "Batterijdrempel"
},
"plugged-in": "Op netstroom.",
"time-left": "Resterende tijd: {time}.",
"time-until-full": "Tijd tot vol: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Index",
"index+name": "Index en Naam",
"name": "Naam",
"none": "Geen"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Indien ingeschakeld, kunnen panelen op elk scherm worden geopend. Indien uitgeschakeld, worden panelen alleen geopend op schermen met een balk, wat het geheugengebruik kan verminderen.",
"label": "Sta panelen toe op schermen zonder balk."
},
"animation-disable": {
"description": "Schakel alle animaties uit voor een snellere, responsievere ervaring.",
"label": "UI-animaties uitschakelen"
@@ -1870,19 +1864,6 @@
"low": "Batterij bijna leeg",
"low-desc": "De batterij is op {percent}%. Sluit de oplader aan."
},
"battery-manager": {
"initial-setup": "Initiële configuratie vereist",
"install-failed": "Installatie mislukt",
"install-missing": "Vereiste bestanden ontbreken",
"install-success": "Succesvol geïnstalleerd",
"install-unsupported": "Systeem wordt niet ondersteund",
"set-failed": "Instellen van batterijdrempel mislukt",
"set-success-desc": "Batterijdrempel ingesteld op {percent}%",
"title": "Batterijdrempel",
"uninstall-failed": "Deïnstallatie mislukt",
"uninstall-setup": "Verwijderen, authenticatie vereist",
"uninstall-success": "Succesvol verwijderd"
},
"bluetooth": {
"disabled": "Uitgeschakeld",
"enabled": "Ingeschakeld"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Mostrar apenas o rótulo da área de trabalho para áreas de trabalho que têm janelas abertas.",
"label": "Mostrar rótulo somente quando ocupado."
},
"show-workspace-numbers": {
"description": "Exibir o rótulo do espaço de trabalho no canto superior esquerdo dos grupos de tarefas.",
"label": "Mostrar rótulo do espaço de trabalho"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Saúde: {percent}%",
"idle": "Ocioso.",
"no-battery-detected": "Nenhuma bateria detectada.",
"panel": {
"balanced": "Balanceado ({percent}%)",
"disabled": "Gerenciador de bateria desativado",
"full": "Capacidade máxima ({percent}%)",
"lifespan": "Vida útil prolongada ({percent}%)",
"title": "Limite de carga"
},
"plugged-in": "Conectado.",
"time-left": "Tempo restante: {time}.",
"time-until-full": "Tempo até carga completa: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Índice",
"index+name": "Índice e Nome",
"name": "Nome",
"none": "Nenhum"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Quando ativado, os painéis podem abrir em qualquer tela. Quando desativado, os painéis só abrirão em telas que tenham uma barra, o que pode reduzir o uso de memória.",
"label": "Permitir painéis em telas sem uma barra."
},
"animation-disable": {
"description": "Desative todas as animações para uma experiência mais rápida e responsiva.",
"label": "Desativar animações da interface do usuário"
@@ -1870,19 +1864,6 @@
"low": "Bateria Fraca",
"low-desc": "A bateria está em {percent}%. Por favor, conecte o carregador."
},
"battery-manager": {
"initial-setup": "Configuração inicial necessária",
"install-failed": "Falha na instalação",
"install-missing": "Arquivos necessários ausentes",
"install-success": "Instalado com sucesso",
"install-unsupported": "Sistema não suportado",
"set-failed": "Falha ao definir o limite da bateria",
"set-success-desc": "Limite da bateria definido para {percent}%",
"title": "Limite da bateria",
"uninstall-failed": "Falha na desinstalação",
"uninstall-setup": "Desinstalando, autenticação necessária",
"uninstall-success": "Desinstalado com sucesso"
},
"bluetooth": {
"disabled": "Desativado",
"enabled": "Ativado"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Отображать метку рабочего пространства только для тех рабочих пространств, в которых есть открытые окна.",
"label": "Показывать метку только когда занято"
},
"show-workspace-numbers": {
"description": "Отображать метку рабочего пространства в верхнем левом углу групп задач.",
"label": "Показать метку рабочей области"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Здоровье: {percent}%",
"idle": "Простой.",
"no-battery-detected": "Батарея не обнаружена.",
"panel": {
"balanced": "Сбалансированный ({percent}%)",
"disabled": "Диспетчер батареи отключен",
"full": "Полная емкость ({percent}%)",
"lifespan": "Увеличенный срок службы ({percent}%)",
"title": "Порог зарядки"
},
"plugged-in": "Подключено.",
"time-left": "Осталось времени: {time}.",
"time-until-full": "Время до полной зарядки: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Индекс",
"index+name": "Индекс и название",
"name": "Имя",
"none": "Нет"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Когда включено, панели могут открываться на любом экране. Когда выключено, панели будут открываться только на экранах с панелью, что может снизить использование памяти.",
"label": "Разрешить панели на экранах без панели задач."
},
"animation-disable": {
"description": "Отключить все анимации для более быстрого и отзывчивого взаимодействия.",
"label": "Отключить анимации интерфейса"
@@ -1870,19 +1864,6 @@
"low": "Низкий заряд батареи",
"low-desc": "Заряд батареи {percent}%. Пожалуйста, подключите зарядное устройство."
},
"battery-manager": {
"initial-setup": "Требуется начальная настройка",
"install-failed": "Сбой установки",
"install-missing": "Отсутствуют необходимые файлы",
"install-success": "Успешно установлено",
"install-unsupported": "Система не поддерживается",
"set-failed": "Не удалось установить порог зарядки батареи",
"set-success-desc": "Порог зарядки батареи установлен на {percent}%",
"title": "Порог зарядки батареи",
"uninstall-failed": "Сбой удаления",
"uninstall-setup": "Удаление, требуется аутентификация",
"uninstall-success": "Успешно удалено"
},
"bluetooth": {
"disabled": "Отключен",
"enabled": "Включен"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Sadece açık pencereleri olan çalışma alanları için çalışma alanı etiketini göster.",
"label": "Sadece dolu olduğunda etiketi göster."
},
"show-workspace-numbers": {
"description": "Görev gruplarının sol üst köşesinde çalışma alanı etiketini görüntüle.",
"label": "Çalışma alanı etiketini göster"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Sağlık: {percent}%",
"idle": "Boşta.",
"no-battery-detected": "Pil tespit edilmedi.",
"panel": {
"balanced": "Dengeli ({percent}%)",
"disabled": "Pil yöneticisi devre dışı",
"full": "Tam kapasite ({percent}%)",
"lifespan": "Uzatılmış ömür ({percent}%)",
"title": "Şarj eşiği"
},
"plugged-in": "Prize takılı.",
"time-left": "Kalan süre: {time}.",
"time-until-full": "Dolma süresi: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "İndeks",
"index+name": "Dizin ve Ad",
"name": "İsim",
"none": "Hiçbiri"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Etkinleştirildiğinde, paneller herhangi bir ekranda açılabilir. Devre dışı bırakıldığında, paneller yalnızca bir çubuğu olan ekranlarda açılır, bu da bellek kullanımını azaltabilir.",
"label": "Çubuksuz ekranlardaki panellere izin ver."
},
"animation-disable": {
"description": "Daha hızlı ve daha duyarlı bir deneyim için tüm animasyonları devre dışı bırakın.",
"label": "UI Animasyonlarını Devre Dışı Bırak"
@@ -1870,19 +1864,6 @@
"low": "Düşük batarya",
"low-desc": "Batarya % {percent}. Lütfen şarj bağlantısını yapın."
},
"battery-manager": {
"initial-setup": "Başlangıç kurulumu gerekli",
"install-failed": "Kurulum başarısız oldu",
"install-missing": "Gerekli dosyalar eksik",
"install-success": "Başarıyla kuruldu",
"install-unsupported": "Sistem desteklenmiyor",
"set-failed": "Batarya eşiği ayarlanamadı",
"set-success-desc": "Batarya eşiği % {percent} olarak ayarlandı",
"title": "Batarya eşiği",
"uninstall-failed": "Kaldırma başarısız oldu",
"uninstall-setup": "Kaldırılıyor, kimlik doğrulama gerekli",
"uninstall-success": "Başarıyla kaldırıldı"
},
"bluetooth": {
"disabled": "Devre dışı",
"enabled": "Etkin"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "Показувати мітку робочого простору лише для робочих просторів, які мають відкриті вікна.",
"label": "Показувати мітку лише коли зайнято"
},
"show-workspace-numbers": {
"description": "Відображати мітку робочої області у верхньому лівому куті груп завдань.",
"label": "Показати мітку робочої області"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "Здоров'я: {percent}%",
"idle": "Бездіяльність.",
"no-battery-detected": "Батарею не виявлено.",
"panel": {
"balanced": "Збалансований ({percent}%)",
"disabled": "Менеджер батареї вимкнено",
"full": "Повна ємність ({percent}%)",
"lifespan": "Подовжений термін служби ({percent}%)",
"title": "Поріг зарядки"
},
"plugged-in": "Підключено.",
"time-left": "Залишилось часу: {time}.",
"time-until-full": "Час до повного заряду: {time}."
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "Індекс",
"index+name": "Індекс та назва",
"name": "Назва",
"none": "Немає"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "Якщо ввімкнено, панелі можуть відкриватися на будь-якому екрані. Якщо вимкнено, панелі відкриватимуться лише на екранах, де є панель завдань, що може зменшити використання пам'яті.",
"label": "Дозволити панелі на екранах без панелі завдань."
},
"animation-disable": {
"description": "Вимкнути всі анімації для швидшого, більш відгукового досвіду.",
"label": "Вимкнути анімації інтерфейсу"
@@ -1870,19 +1864,6 @@
"low": "Низький заряд батареї",
"low-desc": "Батарея на {percent}%. Будь ласка, підключіть зарядний пристрій."
},
"battery-manager": {
"initial-setup": "Потрібне початкове налаштування",
"install-failed": "Помилка встановлення",
"install-missing": "Необхідні файли відсутні",
"install-success": "Встановлено успішно",
"install-unsupported": "Система не підтримується",
"set-failed": "Не вдалося встановити поріг батареї",
"set-success-desc": "Поріг батареї встановлено на {percent}%",
"title": "Поріг батареї",
"uninstall-failed": "Помилка видалення",
"uninstall-setup": "Видалення, потрібна автентифікація",
"uninstall-success": "Видалено успішно"
},
"bluetooth": {
"disabled": "Вимкнено",
"enabled": "Увімкнено"
+6 -25
View File
@@ -314,13 +314,9 @@
}
},
"taskbar-grouped": {
"show-numbers-only-when-occupied": {
"show-labels-only-when-occupied": {
"description": "只显示有打开窗口的工作区标签。",
"label": "仅在被占用时显示标签"
},
"show-workspace-numbers": {
"description": "在任务组左上角显示工作区标签。",
"label": "显示工作区标签"
}
},
"tray": {
@@ -363,13 +359,6 @@
"health": "健康状况:{percent}%",
"idle": "空闲。",
"no-battery-detected": "未检测到电池。",
"panel": {
"balanced": "平衡 ({percent}%)",
"disabled": "电池管理器已禁用",
"full": "完全容量 ({percent}%)",
"lifespan": "延长寿命 ({percent}%)",
"title": "充电阈值"
},
"plugged-in": "已接通电源。",
"time-left": "剩余时间:{time}。",
"time-until-full": "充满所需时间:{time}。"
@@ -564,6 +553,7 @@
},
"workspace-labels": {
"index": "索引",
"index+name": "索引和名称",
"name": "名称",
"none": "无"
}
@@ -1663,6 +1653,10 @@
}
},
"user-interface": {
"allow-panels-without-bar": {
"description": "启用后,面板可以在任何屏幕上打开。禁用后,面板将仅在有栏的屏幕上打开,这可以减少内存使用。",
"label": "允许在没有栏的屏幕上显示面板"
},
"animation-disable": {
"description": "禁用所有动画以获得更快、更流畅的体验。",
"label": "禁用 UI 动画"
@@ -1870,19 +1864,6 @@
"low": "电量低",
"low-desc": "电量为 {percent}%。请连接充电器。"
},
"battery-manager": {
"initial-setup": "需要初始设置",
"install-failed": "安装失败",
"install-missing": "缺少必要文件",
"install-success": "安装成功",
"install-unsupported": "系统不受支持",
"set-failed": "设置电池阈值失败",
"set-success-desc": "电池阈值已设置为 {percent}%",
"title": "电池阈值",
"uninstall-failed": "卸载失败",
"uninstall-setup": "正在卸载,需要身份验证",
"uninstall-success": "卸载成功"
},
"bluetooth": {
"disabled": "已禁用",
"enabled": "已启用"
+2 -1
View File
@@ -73,7 +73,8 @@
"shadowDirection": "bottom_right",
"shadowOffsetX": 2,
"shadowOffsetY": 3,
"language": ""
"language": "",
"allowPanelsOnScreenWithoutBar": true
},
"ui": {
"fontDefault": "Roboto",
-5
View File
@@ -1,5 +0,0 @@
# Battery charge control paths
# Add one path per line
/sys/class/power_supply/BAT0/charge_control_end_threshold
/sys/class/power_supply/BAT1/charge_control_end_threshold
/sys/class/power_supply/BAT0/charge_stop_threshold
@@ -1,178 +0,0 @@
#!/usr/bin/env bash
set -e
SUCCESS=0
FAILURE=1
MISSING_FILES=2
UNSUPPORTED=3
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
print_error() {
echo -e "$1" >&2
}
print_info() {
echo -e "$1"
}
if [ "$EUID" -ne 0 ]; then
print_error "This script must be run with root privileges"
exit $FAILURE
fi
print_info "Installing Battery Manager..."
echo
if [ -n "$PKEXEC_UID" ]; then
ACTUAL_USER=$(getent passwd "$PKEXEC_UID" | cut -d: -f1)
else
ACTUAL_USER="$SUDO_USER"
fi
if [ -z "$ACTUAL_USER" ]; then
print_error "Could not determine the actual user"
exit $FAILURE
fi
print_info "Installing for user: $ACTUAL_USER"
echo
print_info "Checking required files..."
MISSING_FILES_LIST=()
if [ ! -f "$SCRIPT_DIR/battery-paths.conf" ]; then
MISSING_FILES_LIST+=("battery-paths.conf")
fi
if [ ! -f "$SCRIPT_DIR/templates/battery-manager.sh" ]; then
MISSING_FILES_LIST+=("battery-manager.sh")
fi
if [ ! -f "$SCRIPT_DIR/templates/battery-manager.policy" ]; then
MISSING_FILES_LIST+=("battery-manager.policy")
fi
if [ ! -f "$SCRIPT_DIR/templates/battery-manager.rules" ]; then
MISSING_FILES_LIST+=("battery-manager.rules")
fi
if [ ${#MISSING_FILES_LIST[@]} -gt 0 ]; then
print_error "Missing required files in $SCRIPT_DIR:"
for file in "${MISSING_FILES_LIST[@]}"; do
print_error " - $file"
done
exit $MISSING_FILES
fi
print_info "All required files found"
print_info "Checking battery paths..."
BATTERY_PATHS=($(grep -v '^#' "$SCRIPT_DIR/battery-paths.conf" | grep -v '^$'))
EXISTING_PATHS=()
for path in "${BATTERY_PATHS[@]}"; do
if [ -f "$path" ]; then
EXISTING_PATHS+=("$path")
fi
done
if [ ${#EXISTING_PATHS[@]} -eq 0 ]; then
print_error "None of the battery control files exist. Please check your hardware compatibility."
exit $UNSUPPORTED
fi
print_info "Found ${#EXISTING_PATHS[@]} compatible battery control file(s)"
print_info "Installing battery manager script..."
BATTERY_MANAGER_SCRIPT="/usr/bin/battery-manager-$ACTUAL_USER"
SHEBANG=$(head -n 1 "$SCRIPT_DIR/templates/battery-manager.sh")
echo "$SHEBANG" > "$BATTERY_MANAGER_SCRIPT"
echo "" >> "$BATTERY_MANAGER_SCRIPT"
echo "BATTERY_PATHS=(" >> "$BATTERY_MANAGER_SCRIPT"
for path in "${EXISTING_PATHS[@]}"; do
echo " \"$path\"" >> "$BATTERY_MANAGER_SCRIPT"
done
echo ")" >> "$BATTERY_MANAGER_SCRIPT"
echo "" >> "$BATTERY_MANAGER_SCRIPT"
tail -n +2 "$SCRIPT_DIR/templates/battery-manager.sh" >> "$BATTERY_MANAGER_SCRIPT"
chmod +x "$BATTERY_MANAGER_SCRIPT"
print_info "Battery manager script created from $SCRIPT_DIR/templates/battery-manager.sh with compatible paths"
print_info "Script installed at $BATTERY_MANAGER_SCRIPT"
print_info "Creating log file..."
touch /var/log/battery-manager.log
chmod 644 /var/log/battery-manager.log
print_info "Log file created at /var/log/battery-manager.log"
print_info "Creating polkit policy..."
POLICY_FILE="/usr/share/polkit-1/actions/com.local.battery-manager.$ACTUAL_USER.policy"
sed -e "s/ACTUAL_USER_PLACEHOLDER/$ACTUAL_USER/g" \
"$SCRIPT_DIR/templates/battery-manager.policy" > "$POLICY_FILE"
print_info "Polkit policy copied from $SCRIPT_DIR/templates/battery-manager.policy"
print_info "Polkit policy created at $POLICY_FILE"
print_info "Creating polkit rule..."
RULES_FILE="/etc/polkit-1/rules.d/50-battery-manager-$ACTUAL_USER.rules"
sed "s/ACTUAL_USER_PLACEHOLDER/$ACTUAL_USER/g" \
"$SCRIPT_DIR/templates/battery-manager.rules" > "$RULES_FILE"
print_info "Polkit rule copied from $SCRIPT_DIR/templates/battery-manager.rules"
print_info "Polkit rule created for user: $ACTUAL_USER at $RULES_FILE"
print_info "Restarting polkit..."
if systemctl restart polkit 2>/dev/null; then
print_info "Polkit restarted"
else
print_info "Could not restart polkit automatically, you may need to reboot"
fi
print_info "Creating uninstall script..."
UNINSTALL_SCRIPT="$SCRIPT_DIR/uninstall-battery-manager.sh"
if [ -f "$SCRIPT_DIR/templates/uninstall-template" ]; then
SHEBANG=$(head -n 1 "$SCRIPT_DIR/templates/uninstall-template")
else
SHEBANG="#!/usr/bin/env bash"
fi
echo "$SHEBANG" > "$UNINSTALL_SCRIPT"
echo "" >> "$UNINSTALL_SCRIPT"
cat >> "$UNINSTALL_SCRIPT" << EOF
SCRIPT_PATH="$BATTERY_MANAGER_SCRIPT"
POLICY_PATH="$POLICY_FILE"
RULE_PATH="$RULES_FILE"
LOG_PATH="/var/log/battery-manager.log"
EOF
if [ -f "$SCRIPT_DIR/templates/uninstall-template" ]; then
tail -n +2 "$SCRIPT_DIR/templates/uninstall-template" >> "$UNINSTALL_SCRIPT"
fi
chmod 744 "$UNINSTALL_SCRIPT"
chown root:root "$UNINSTALL_SCRIPT"
print_info "Uninstall script created at $UNINSTALL_SCRIPT"
echo
print_info "Installation complete!"
echo
print_info "Log file: /var/log/battery-manager.log"
print_info "User-specific script: $BATTERY_MANAGER_SCRIPT"
print_info "User-specific policy: $POLICY_FILE"
print_info "User-specific rules: $RULES_FILE"
print_info "User-specific uninstall script: $UNINSTALL_SCRIPT"
@@ -1,86 +0,0 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUPPRESS_NOTIFICATIONS=false
print_error() {
echo -e "$1" >&2
}
print_info() {
echo -e "$1"
}
send_notification() {
local urgency="$1"
local title="$2"
local message="$3"
if [ "$SUPPRESS_NOTIFICATIONS" = false ] && command -v notify-send >/dev/null 2>&1; then
notify-send -u "$urgency" "$title" "$message"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-q|--quiet)
SUPPRESS_NOTIFICATIONS=true
shift
;;
-*)
print_error "Unknown option: $1"
echo "Usage: $0 [OPTIONS] <number>" >&2
echo "Options:" >&2
echo " -q, --quiet Suppress notifications" >&2
exit 1
;;
*)
BATTERY_LEVEL="$1"
shift
;;
esac
done
if [ -z "$BATTERY_LEVEL" ]; then
print_error "Battery level not specified"
echo "Usage: $0 [OPTIONS] <number>" >&2
echo "Options:" >&2
echo " -q, --quiet Suppress notifications" >&2
exit 1
fi
if ! [[ "$BATTERY_LEVEL" =~ ^[0-9]+$ ]] || [ "$BATTERY_LEVEL" -gt 100 ] || [ "$BATTERY_LEVEL" -lt 0 ]; then
print_error "Battery level must be a number between 0-100"
echo "Usage: $0 [OPTIONS] <number>" >&2
echo "Options:" >&2
echo " -q, --quiet Suppress notifications" >&2
exit 1
fi
CURRENT_USER="$USER"
if [ -z "$CURRENT_USER" ]; then
CURRENT_USER="$(whoami)"
fi
BATTERY_MANAGER_PATH="/usr/bin/battery-manager-$CURRENT_USER"
SUCCESS=0
MISSING_FILES=2
if [ ! -f "$BATTERY_MANAGER_PATH" ]; then
print_error "Battery manager components missing for user $CURRENT_USER!"
exit $MISSING_FILES
fi
print_info "Setting battery charging threshold to $BATTERY_LEVEL% for user $CURRENT_USER..."
if pkexec "$BATTERY_MANAGER_PATH" "$BATTERY_LEVEL"; then
print_info "Battery charging threshold set to $BATTERY_LEVEL%"
send_notification "normal" "Battery Threshold Updated" \
"Battery charging threshold has been set to $BATTERY_LEVEL%"
else
print_error "Failed to set battery charging threshold"
send_notification "critical" "Battery Threshold Failed" \
"Failed to set battery charging threshold to $BATTERY_LEVEL%"
exit 1
fi
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="com.local.battery-manager.ACTUAL_USER_PLACEHOLDER">
<description>Manage battery settings for ACTUAL_USER_PLACEHOLDER</description>
<message>Authentication is required to manage battery settings</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/battery-manager-ACTUAL_USER_PLACEHOLDER</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>
@@ -1,14 +0,0 @@
polkit.addRule(function(action, subject) {
if (action.id == "com.local.battery-manager.ACTUAL_USER_PLACEHOLDER" &&
subject.user == "ACTUAL_USER_PLACEHOLDER") {
// Check if the parent process is quickshell or set-battery-threshold
var pid = subject.pid;
var ppid = polkit.spawn(["ps", "-o", "ppid=", "-p", pid.toString()]).trim();
var parentCmd = polkit.spawn(["ps", "-o", "comm=", "-p", ppid]).trim();
if (parentCmd.indexOf("quickshell") !== -1 || parentCmd.indexOf("set-battery-treshold") !== -1) {
return polkit.Result.YES;
}
}
});
@@ -1,53 +0,0 @@
#!/usr/bin/env bash
LOG_FILE="/var/log/battery-manager.log"
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
if [ -z "$1" ]; then
echo "Error: No battery level provided" >&2
log_message "ERROR: No battery level provided"
exit 1
fi
BATTERY_LEVEL="$1"
if ! [[ "$BATTERY_LEVEL" =~ ^[0-9]+$ ]] || [ "$BATTERY_LEVEL" -gt 100 ] || [ "$BATTERY_LEVEL" -lt 0 ]; then
echo "Error: Invalid battery level. Must be 0-100" >&2
log_message "ERROR: Invalid battery level: $BATTERY_LEVEL"
exit 1
fi
SUCCESS_COUNT=0
FAIL_COUNT=0
for path in "${BATTERY_PATHS[@]}"; do
[[ -z "$path" || "$path" =~ ^# ]] && continue
if [ -f "$path" ] && [ -w "$path" ]; then
if echo "$BATTERY_LEVEL" > "$path" 2>/dev/null; then
echo "Updated: $path"
log_message "SUCCESS: Updated $path to $BATTERY_LEVEL"
((SUCCESS_COUNT++))
else
echo "Failed to write: $path" >&2
log_message "ERROR: Failed to write to $path"
((FAIL_COUNT++))
fi
else
echo "Skipped (not found/writable): $path"
log_message "INFO: Skipped $path (not found or not writable)"
fi
done
log_message "SUMMARY: Updated $SUCCESS_COUNT file(s), failed $FAIL_COUNT, battery level: $BATTERY_LEVEL"
if [ "$SUCCESS_COUNT" -eq 0 ]; then
echo "Error: No battery files were updated" >&2
exit 1
fi
echo "Successfully updated $SUCCESS_COUNT battery file(s)"
exit 0
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root"
exit 1
fi
echo "Uninstalling battery manager..."
if [ -f "$SCRIPT_PATH" ]; then
rm -f "$SCRIPT_PATH"
echo "Removed script from $SCRIPT_PATH"
fi
if [ -f "$POLICY_PATH" ]; then
rm -f "$POLICY_PATH"
echo "Removed policy file from $POLICY_PATH"
fi
if [ -f "$RULE_PATH" ]; then
rm -f "$RULE_PATH"
echo "Removed udev rule from $RULE_PATH"
fi
if [ -f "$LOG_PATH" ]; then
rm -f "$LOG_PATH"
echo "Removed log file from $LOG_PATH"
fi
echo "Uninstallation completed successfully"
+12 -22
View File
@@ -2,38 +2,28 @@
set -euo pipefail
# QML Formatter Script
# Uses: https://github.com/jesperhh/qmlfmt
# Install: AUR package "qmlfmt-git" (requires qt6-5compat)
if command -v qmlfmt &>/dev/null; then
echo "Using 'qmlfmt' for formatting."
format_file() { qmlfmt -e -b 360 -t 2 -i 2 -w "$1" || { echo "Failed: $1" >&2; return 1; }; }
# Find qmlformat binary
if command -v qmlformat &>/dev/null; then
QMLFORMAT="qmlformat"
elif [ -x "/usr/lib/qt6/bin/qmlformat" ]; then
QMLFORMAT="/usr/lib/qt6/bin/qmlformat"
else
echo "No 'qmlfmt' found in PATH." >&2
echo "No 'qmlformat' found in PATH or /usr/lib/qt6/bin." >&2
exit 1
fi
echo "Using 'qmlformat' for formatting: $QMLFORMAT"
export QMLFORMAT
format_file() { "$QMLFORMAT" -w 2 -W 360 -S --semicolon-rule always -i "$1" || { echo "Failed: $1" >&2; return 1; }; }
export -f format_file
# Find all .qml files
mapfile -t all_files < <(find "${1:-.}" -name "*.qml" -type f)
[ ${#all_files[@]} -eq 0 ] && { echo "No QML files found"; exit 0; }
echo "Scanning ${#all_files[@]} files for array destructuring..."
safe_files=()
for file in "${all_files[@]}"; do
# Checks for a comma inside brackets followed by an equals sign aka "array destructuring"
# as this ES6 syntax is not supported by qmlfmt and will result in breakage.
if grep -qE '\[.*,.*\]\s*=' "$file"; then
echo "-> Skipping (Array destructuring detected): $file" >&2
else
safe_files+=("$file")
fi
done
[ ${#safe_files[@]} -eq 0 ] && { echo "No safe files to format after filtering."; exit 0; }
echo "Formatting ${#safe_files[@]} files..."
printf '%s\0' "${safe_files[@]}" | \
echo "Formatting ${#all_files[@]} files..."
printf '%s\0' "${all_files[@]}" | \
xargs -0 -P "${QMLFMT_JOBS:-$(nproc)}" -I {} bash -c 'format_file "$@"' _ {} \
&& echo "Done" || { echo "Errors occurred" >&2; exit 1; }
+49 -79
View File
@@ -5,111 +5,80 @@ import Quickshell
import Quickshell.Io
import qs.Commons
/*
Noctalia is not strictly a Material Design project, it supports both some predefined
color schemes and dynamic color generation from the wallpaper (using Matugen).
Noctalia is not strictly a Material Design project, it supports both some predefined
color schemes and dynamic color generation from the wallpaper (using Matugen).
We ultimately decided to use a restricted set of colors that follows the
Material Design 3 naming convention.
We ultimately decided to use a restricted set of colors that follows the
Material Design 3 naming convention.
NOTE: All color names are prefixed with 'm' (e.g., mPrimary) to prevent QML from
misinterpreting them as signals (e.g., the 'onPrimary' property name).
NOTE: All color names are prefixed with 'm' (e.g., mPrimary) to prevent QML from
misinterpreting them as signals (e.g., the 'onPrimary' property name).
*/
Singleton {
id: root
// --- Key Colors: These are the main accent colors that define your app's style
property color mPrimary: customColors.mPrimary
property color mOnPrimary: customColors.mOnPrimary
property color mSecondary: customColors.mSecondary
property color mOnSecondary: customColors.mOnSecondary
property color mTertiary: customColors.mTertiary
property color mOnTertiary: customColors.mOnTertiary
readonly property color mPrimary: customColorsData.mPrimary
readonly property color mOnPrimary: customColorsData.mOnPrimary
readonly property color mSecondary: customColorsData.mSecondary
readonly property color mOnSecondary: customColorsData.mOnSecondary
readonly property color mTertiary: customColorsData.mTertiary
readonly property color mOnTertiary: customColorsData.mOnTertiary
// --- Utility Colors: These colors serve specific, universal purposes like indicating errors
property color mError: customColors.mError
property color mOnError: customColors.mOnError
readonly property color mError: customColorsData.mError
readonly property color mOnError: customColorsData.mOnError
// --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy
property color mSurface: customColors.mSurface
property color mOnSurface: customColors.mOnSurface
readonly property color mSurface: customColorsData.mSurface
readonly property color mOnSurface: customColorsData.mOnSurface
property color mSurfaceVariant: customColors.mSurfaceVariant
property color mOnSurfaceVariant: customColors.mOnSurfaceVariant
readonly property color mSurfaceVariant: customColorsData.mSurfaceVariant
readonly property color mOnSurfaceVariant: customColorsData.mOnSurfaceVariant
property color mOutline: customColors.mOutline
property color mShadow: customColors.mShadow
readonly property color mOutline: customColorsData.mOutline
readonly property color mShadow: customColorsData.mShadow
property color mHover: customColors.mHover
property color mOnHover: customColors.mOnHover
readonly property color mHover: customColorsData.mHover
readonly property color mOnHover: customColorsData.mOnHover
// --- Absolute Colors
property color transparent: "transparent"
property color black: "#000000"
property color white: "#ffffff"
readonly property color transparent: "transparent"
readonly property color black: "#000000"
readonly property color white: "#ffffff"
// --------------------------------
// Default colors: RosePine
QtObject {
id: defaultColors
property color mPrimary: "#c7a1d8"
property color mOnPrimary: "#1a151f"
readonly property color mPrimary: "#c7a1d8"
readonly property color mOnPrimary: "#1a151f"
property color mSecondary: "#a984c4"
property color mOnSecondary: "#f3edf7"
readonly property color mSecondary: "#a984c4"
readonly property color mOnSecondary: "#f3edf7"
property color mTertiary: "#e0b7c9"
property color mOnTertiary: "#20161f"
readonly property color mTertiary: "#e0b7c9"
readonly property color mOnTertiary: "#20161f"
property color mError: "#e9899d"
property color mOnError: "#1e1418"
readonly property color mError: "#e9899d"
readonly property color mOnError: "#1e1418"
property color mSurface: "#1c1822"
property color mOnSurface: "#e9e4f0"
readonly property color mSurface: "#1c1822"
readonly property color mOnSurface: "#e9e4f0"
property color mSurfaceVariant: "#262130"
property color mOnSurfaceVariant: "#a79ab0"
readonly property color mSurfaceVariant: "#262130"
readonly property color mOnSurfaceVariant: "#a79ab0"
property color mOutline: "#342c42"
property color mShadow: "#120f18"
readonly property color mOutline: "#342c42"
readonly property color mShadow: "#120f18"
property color mHover: "#e0b7c9"
property color mOnHover: "#20161f"
readonly property color mHover: "#e0b7c9"
readonly property color mOnHover: "#20161f"
}
// ----------------------------------------------------------------
// Custom colors loaded from colors.json
// These can be generated by matugen or simply come from a well know color scheme (Dracula, Gruvbox, Nord, ...)
QtObject {
id: customColors
property color mPrimary: customColorsData.mPrimary
property color mOnPrimary: customColorsData.mOnPrimary
property color mSecondary: customColorsData.mSecondary
property color mOnSecondary: customColorsData.mOnSecondary
property color mTertiary: customColorsData.mTertiary
property color mOnTertiary: customColorsData.mOnTertiary
property color mError: customColorsData.mError
property color mOnError: customColorsData.mOnError
property color mSurface: customColorsData.mSurface
property color mOnSurface: customColorsData.mOnSurface
property color mSurfaceVariant: customColorsData.mSurfaceVariant
property color mOnSurfaceVariant: customColorsData.mOnSurfaceVariant
property color mOutline: customColorsData.mOutline
property color mShadow: customColorsData.mShadow
property color mHover: customColorsData.mHover
property color mOnHover: customColorsData.mOnHover
}
// FileView to load custom colors data from colors.json
FileView {
id: customColorsFile
@@ -117,24 +86,25 @@ Singleton {
printErrors: false
watchChanges: true
onFileChanged: {
Logger.i("Color", "Reloading colors from disk")
reload()
Logger.i("Color", "Reloading colors from disk");
reload();
}
onAdapterUpdated: {
Logger.i("Color", "Writing colors to disk")
writeAdapter()
Logger.i("Color", "Writing colors to disk");
writeAdapter();
}
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
reload();
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// Error code 2 = ENOENT (No such file or directory)
if (error === 2 || error.toString().includes("No such file")) {
// File doesn't exist, create it with default values
writeAdapter()
writeAdapter();
}
}
JsonAdapter {
+126 -127
View File
@@ -33,13 +33,13 @@ Singleton {
onExited: function (exitCode, exitStatus) {
if (exitCode === 0) {
var output = stdoutCollector.text || ""
parseDirectoryListing(output)
var output = stdoutCollector.text || "";
parseDirectoryListing(output);
} else {
Logger.e("I18n", `Failed to scan translation directory`)
Logger.e("I18n", `Failed to scan translation directory`);
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
availableLanguages = ["en"];
detectLanguage();
}
}
}
@@ -51,21 +51,21 @@ Singleton {
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text())
root.translations = data
Logger.i("I18n", `Loaded translations for "${root.langCode}"`)
Logger.d("I18n", `Available root keys: ${Object.keys(data).join(", ")}`)
var data = JSON.parse(text());
root.translations = data;
Logger.i("I18n", `Loaded translations for "${root.langCode}"`);
Logger.d("I18n", `Available root keys: ${Object.keys(data).join(", ")}`);
root.isLoaded = true
root.translationsLoaded()
root.isLoaded = true;
root.translationsLoaded();
} catch (e) {
Logger.e("I18n", `Failed to parse translation file: ${e}`)
setLanguage("en")
Logger.e("I18n", `Failed to parse translation file: ${e}`);
setLanguage("en");
}
}
onLoadFailed: function (error) {
setLanguage("en")
Logger.e("I18n", `Failed to load translation file: ${error}`)
setLanguage("en");
Logger.e("I18n", `Failed to load translation file: ${error}`);
}
}
@@ -76,162 +76,161 @@ Singleton {
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text())
root.fallbackTranslations = data
Logger.d("I18n", `Loaded english fallback translations`)
var data = JSON.parse(text());
root.fallbackTranslations = data;
Logger.d("I18n", `Loaded english fallback translations`);
} catch (e) {
Logger.e("I18n", `Failed to parse fallback translation file: ${e}`)
Logger.e("I18n", `Failed to parse fallback translation file: ${e}`);
}
}
onLoadFailed: function (error) {
Logger.e("I18n", `Failed to load fallback translation file: ${error}`)
Logger.e("I18n", `Failed to load fallback translation file: ${error}`);
}
}
Component.onCompleted: {
Logger.i("I18n", "Service started")
scanAvailableLanguages()
Logger.i("I18n", "Service started");
scanAvailableLanguages();
}
// -------------------------------------------
function scanAvailableLanguages() {
Logger.d("I18n", "Scanning for available translation files...")
directoryScanner.running = true
Logger.d("I18n", "Scanning for available translation files...");
directoryScanner.running = true;
}
// -------------------------------------------
function parseDirectoryListing(output) {
var languages = []
var languages = [];
try {
if (!output || output.trim() === "") {
Logger.w("I18n", "Empty directory listing output")
availableLanguages = ["en"]
detectLanguage()
return
Logger.w("I18n", "Empty directory listing output");
availableLanguages = ["en"];
detectLanguage();
return;
}
const entries = output.trim().split('\n')
const entries = output.trim().split('\n');
for (var i = 0; i < entries.length; i++) {
const entry = entries[i].trim()
const entry = entries[i].trim();
if (entry && entry.endsWith('.json')) {
// Extract language code from filename (e.g., "en.json" -> "en")
const langCode = entry.substring(0, entry.lastIndexOf('.json'))
const langCode = entry.substring(0, entry.lastIndexOf('.json'));
if (langCode.length >= 2 && langCode.length <= 5) {
// Basic validation for language codes
languages.push(langCode)
languages.push(langCode);
}
}
}
// Sort languages alphabetically, but ensure "en" comes first if available
languages.sort()
const enIndex = languages.indexOf("en")
languages.sort();
const enIndex = languages.indexOf("en");
if (enIndex > 0) {
languages.splice(enIndex, 1)
languages.unshift("en")
languages.splice(enIndex, 1);
languages.unshift("en");
}
if (languages.length === 0) {
Logger.w("I18n", "No translation files found, using fallback")
languages = ["en"] // Fallback
Logger.w("I18n", "No translation files found, using fallback");
languages = ["en"]; // Fallback
}
availableLanguages = languages
Logger.d("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
availableLanguages = languages;
Logger.d("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`);
// Detect language after scanning
detectLanguage()
detectLanguage();
} catch (e) {
Logger.e("I18n", `Failed to parse directory listing: ${e}`)
Logger.e("I18n", `Failed to parse directory listing: ${e}`);
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
availableLanguages = ["en"];
detectLanguage();
}
}
// -------------------------------------------
function detectLanguage() {
Logger.d("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
Logger.d("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`);
if (availableLanguages.length === 0) {
Logger.w("I18n", "No available languages found")
return
Logger.w("I18n", "No available languages found");
return;
}
var detectedLang = ""
var detectedFullLocale = ""
var detectedLang = "";
var detectedFullLocale = "";
// First, determine the system's preferred language
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
const fullUserLang = Qt.locale().uiLanguages[i]
const fullUserLang = Qt.locale().uiLanguages[i];
if (availableLanguages.includes(fullUserLang)) {
detectedLang = fullUserLang
detectedFullLocale = fullUserLang
break
detectedLang = fullUserLang;
detectedFullLocale = fullUserLang;
break;
}
const shortUserLang = fullUserLang.substring(0, 2)
const shortUserLang = fullUserLang.substring(0, 2);
if (availableLanguages.includes(shortUserLang)) {
detectedLang = shortUserLang
detectedFullLocale = fullUserLang
break
detectedLang = shortUserLang;
detectedFullLocale = fullUserLang;
break;
}
}
// If no system language is found among available languages, fallback
if (detectedLang === "") {
detectedLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
detectedFullLocale = detectedLang
detectedLang = availableLanguages.includes("en") ? "en" : availableLanguages[0];
detectedFullLocale = detectedLang;
}
root.systemDetectedLangCode = detectedLang
root.fullLocaleCode = detectedFullLocale
Logger.d("I18n", `System detected language: "${root.systemDetectedLangCode}" (full locale: "${root.fullLocaleCode}")`)
root.systemDetectedLangCode = detectedLang;
root.fullLocaleCode = detectedFullLocale;
Logger.d("I18n", `System detected language: "${root.systemDetectedLangCode}" (full locale: "${root.fullLocaleCode}")`);
// Now, apply the language: user-defined, then system-detected
if (Settings.data.general.language !== "" && availableLanguages.includes(Settings.data.general.language)) {
Logger.d("I18n", `User-defined language found: "${Settings.data.general.language}"`)
setLanguage(Settings.data.general.language)
Logger.d("I18n", `User-defined language found: "${Settings.data.general.language}"`);
setLanguage(Settings.data.general.language);
} else {
Logger.d("I18n", `No user-defined language, using system detected: "${root.systemDetectedLangCode}"`)
setLanguage(root.systemDetectedLangCode, root.fullLocaleCode)
Logger.d("I18n", `No user-defined language, using system detected: "${root.systemDetectedLangCode}"`);
setLanguage(root.systemDetectedLangCode, root.fullLocaleCode);
}
}
// -------------------------------------------
function setLanguage(newLangCode, fullLocale) {
if (typeof fullLocale === "undefined") {
fullLocale = newLangCode
fullLocale = newLangCode;
}
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
langCode = newLangCode
fullLocaleCode = fullLocale
locale = Qt.locale(fullLocale)
Logger.i("I18n", `Language set to "${langCode}" with locale "${fullLocale}"`)
languageChanged(langCode)
loadTranslations()
langCode = newLangCode;
fullLocaleCode = fullLocale;
locale = Qt.locale(fullLocale);
Logger.i("I18n", `Language set to "${langCode}" with locale "${fullLocale}"`);
languageChanged(langCode);
loadTranslations();
} else if (!availableLanguages.includes(newLangCode)) {
Logger.w("I18n", `Language "${newLangCode}" is not available`)
Logger.w("I18n", `Language "${newLangCode}" is not available`);
}
}
// -------------------------------------------
function loadTranslations() {
if (langCode === "")
return
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`
fileView.path = filePath
isLoaded = false
Logger.d("I18n", `Loading translations: ${langCode}`)
return;
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`;
fileView.path = filePath;
isLoaded = false;
Logger.d("I18n", `Loading translations: ${langCode}`);
// Only load fallback translations if we are not using english and english is available
if (langCode !== "en" && availableLanguages.includes("en")) {
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`;
}
}
@@ -239,133 +238,133 @@ Singleton {
// Check if a translation exists
function hasTranslation(key) {
if (!isLoaded)
return false
return false;
const keys = key.split(".")
var value = translations
const keys = key.split(".");
var value = translations;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
value = value[keys[i]];
} else {
return false
return false;
}
}
return typeof value === "string"
return typeof value === "string";
}
// -------------------------------------------
// Get all translation keys (useful for debugging)
function getAllKeys(obj, prefix) {
if (typeof obj === "undefined")
obj = translations
obj = translations;
if (typeof prefix === "undefined")
prefix = ""
prefix = "";
var keys = []
var keys = [];
for (var key in (obj || {})) {
const value = obj[key]
const fullKey = prefix ? `${prefix}.${key}` : key
const value = obj[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
keys = keys.concat(getAllKeys(value, fullKey))
keys = keys.concat(getAllKeys(value, fullKey));
} else if (typeof value === "string") {
keys.push(fullKey)
keys.push(fullKey);
}
}
return keys
return keys;
}
// -------------------------------------------
// Reload translations (useful for development)
function reload() {
Logger.d("I18n", "Reloading translations")
loadTranslations()
Logger.d("I18n", "Reloading translations");
loadTranslations();
}
// -------------------------------------------
// Main translation function
function tr(key, interpolations) {
if (typeof interpolations === "undefined")
interpolations = {}
interpolations = {};
if (!isLoaded) {
//Logger.d("I18n", "Translations not loaded yet")
return key
return key;
}
// Navigate nested keys (e.g., "menu.file.open")
const keys = key.split(".")
const keys = key.split(".");
// Look-up translation in the active language
var value = translations
var notFound = false
var value = translations;
var notFound = false;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
value = value[keys[i]];
} else {
Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`)
Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`)
notFound = true
break
Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`);
Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`);
notFound = true;
break;
}
}
// Fallback to english if not found
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
value = fallbackTranslations
value = fallbackTranslations;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
value = value[keys[i]];
} else {
// Indicate this key does not even exists in the english fallback
return `## ${key} ##`
return `## ${key} ##`;
}
}
// Make untranslated string easy to spot
value = `<i>${value}</i>`
value = `<i>${value}</i>`;
} else if (notFound) {
// No fallback available
return `## ${key} ##`
return `## ${key} ##`;
}
if (typeof value !== "string") {
Logger.d("I18n", `Translation key "${key}" is not a string`)
return key
Logger.d("I18n", `Translation key "${key}" is not a string`);
return key;
}
// Handle interpolations (e.g., "Hello {name}!")
var result = value
var result = value;
for (var placeholder in interpolations) {
const regex = new RegExp(`\\{${placeholder}\\}`, 'g')
result = result.replace(regex, interpolations[placeholder])
const regex = new RegExp(`\\{${placeholder}\\}`, 'g');
result = result.replace(regex, interpolations[placeholder]);
}
return result
return result;
}
// -------------------------------------------
// Plural translation function
function trp(key, count, defaultSingular, defaultPlural, interpolations) {
if (typeof defaultSingular === "undefined")
defaultSingular = ""
defaultSingular = "";
if (typeof defaultPlural === "undefined")
defaultPlural = ""
defaultPlural = "";
if (typeof interpolations === "undefined")
interpolations = {}
interpolations = {};
const pluralKey = count === 1 ? key : `${key}_plural`
const defaultValue = count === 1 ? defaultSingular : defaultPlural
const pluralKey = count === 1 ? key : `${key}_plural`;
const defaultValue = count === 1 ? defaultSingular : defaultPlural;
// Merge interpolations with count (QML doesn't support spread operator)
var finalInterpolations = {
"count": count
}
};
for (var prop in interpolations) {
finalInterpolations[prop] = interpolations[prop]
finalInterpolations[prop] = interpolations[prop];
}
return tr(pluralKey, finalInterpolations)
return tr(pluralKey, finalInterpolations);
}
}
+17 -17
View File
@@ -26,15 +26,15 @@ Singleton {
signal fontReloaded
Component.onCompleted: {
Logger.i("Icons", "Service started")
loadFontWithCacheBusting()
Logger.i("Icons", "Service started");
loadFontWithCacheBusting();
}
Connections {
target: Quickshell
function onReloadCompleted() {
Logger.d("Icons", "Quickshell reload completed - forcing font reload")
reloadFont()
Logger.d("Icons", "Quickshell reload completed - forcing font reload");
reloadFont();
}
}
@@ -42,20 +42,20 @@ Singleton {
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
iconName = aliases[iconName]
iconName = aliases[iconName];
}
// Find the appropriate codepoint
return icons[iconName]
return icons[iconName];
}
function loadFontWithCacheBusting() {
Logger.d("Icons", "Loading font with cache busting")
Logger.d("Icons", "Loading font with cache busting");
// Destroy old loader first
if (currentFontLoader) {
currentFontLoader.destroy()
currentFontLoader = null
currentFontLoader.destroy();
currentFontLoader = null;
}
// Create new loader with cache-busting URL
@@ -64,22 +64,22 @@ Singleton {
FontLoader {
source: "${cacheBustingPath}"
}
`, root, "dynamicFontLoader_" + fontVersion)
`, root, "dynamicFontLoader_" + fontVersion);
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function () {
if (currentFontLoader.status === FontLoader.Ready) {
Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
fontReloaded()
Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")");
fontReloaded();
} else if (currentFontLoader.status === FontLoader.Error) {
Logger.e("Icons", "Font failed to load (version " + fontVersion + ")")
Logger.e("Icons", "Font failed to load (version " + fontVersion + ")");
}
})
});
}
function reloadFont() {
Logger.d("Icons", "Forcing font reload...")
fontVersion++
loadFontWithCacheBusting()
Logger.d("Icons", "Forcing font reload...");
fontVersion++;
loadFontWithCacheBusting();
}
}
+79 -79
View File
@@ -310,8 +310,8 @@ Singleton {
"align-left": "\u{ea09}",
"align-left-2": "\u{ff00}",
"align-right": "\u{ea0a}",
"alpha"//"align-right-2": "\u{feff}",
: "\u{f543}",
//"align-right-2": "\u{feff}",
"alpha": "\u{f543}",
"alphabet-arabic": "\u{ff2f}",
"alphabet-bangla": "\u{ff2e}",
"alphabet-cyrillic": "\u{f1df}",
@@ -3143,8 +3143,8 @@ Singleton {
"friends": "\u{eab0}",
"friends-off": "\u{f136}",
"frustum": "\u{fa9f}",
"frustum-plus"//"frustum-off": "\u{fa9d}",
: "\u{fa9e}",
//"frustum-off": "\u{fa9d}",
"frustum-plus": "\u{fa9e}",
"function": "\u{f225}",
"function-filled": "\u{fc2b}",
"function-off": "\u{f3f0}",
@@ -3403,13 +3403,13 @@ Singleton {
"hexagon-letter-x": "\u{f479}",
"hexagon-letter-x-filled": "\u{fe30}",
"hexagon-letter-y": "\u{f47a}",
"hexagon-letter-z"//"hexagon-letter-y-filled": "\u{fe2f}",
: "\u{f47b}",
"hexagon-minus"//"hexagon-letter-z-filled": "\u{fe2e}",
: "\u{fc8f}",
//"hexagon-letter-y-filled": "\u{fe2f}",
"hexagon-letter-z": "\u{f47b}",
//"hexagon-letter-z-filled": "\u{fe2e}",
"hexagon-minus": "\u{fc8f}",
"hexagon-minus-2": "\u{fc8e}",
"hexagon-number-0"//"hexagon-minus-filled": "\u{fe2d}",
: "\u{f459}",
//"hexagon-minus-filled": "\u{fe2d}",
"hexagon-number-0": "\u{f459}",
"hexagon-number-0-filled": "\u{f74c}",
"hexagon-number-1": "\u{f45a}",
"hexagon-number-1-filled": "\u{f74d}",
@@ -3432,8 +3432,8 @@ Singleton {
"hexagon-off": "\u{ee9c}",
"hexagon-plus": "\u{fc45}",
"hexagon-plus-2": "\u{fc90}",
"hexagonal-prism"//"hexagon-plus-filled": "\u{fe2c}",
: "\u{faa5}",
//"hexagon-plus-filled": "\u{fe2c}",
"hexagonal-prism": "\u{faa5}",
"hexagonal-prism-off": "\u{faa3}",
"hexagonal-prism-plus": "\u{faa4}",
"hexagonal-pyramid": "\u{faa8}",
@@ -3463,8 +3463,8 @@ Singleton {
"home-eco": "\u{f351}",
"home-edit": "\u{f352}",
"home-exclamation": "\u{f33c}",
"home-hand"//"home-filled": "\u{fe2b}",
: "\u{f504}",
//"home-filled": "\u{fe2b}",
"home-hand": "\u{f504}",
"home-heart": "\u{f353}",
"home-infinity": "\u{f505}",
"home-link": "\u{f354}",
@@ -3582,8 +3582,8 @@ Singleton {
"ironing-2-filled": "\u{1006e}",
"ironing-3": "\u{f2f6}",
"ironing-3-filled": "\u{1006d}",
"ironing-off"//"ironing-filled": "\u{fe2a}",
: "\u{f2f7}",
//"ironing-filled": "\u{fe2a}",
"ironing-off": "\u{f2f7}",
"ironing-steam": "\u{f2f9}",
"ironing-steam-filled": "\u{1006c}",
"ironing-steam-off": "\u{f2f8}",
@@ -3593,8 +3593,8 @@ Singleton {
"italic": "\u{eb93}",
"jacket": "\u{f661}",
"jetpack": "\u{f581}",
"jewish-star"//"jetpack-filled": "\u{fe29}",
: "\u{f3ff}",
//"jetpack-filled": "\u{fe29}",
"jewish-star": "\u{f3ff}",
"jewish-star-filled": "\u{f67e}",
"join-bevel": "\u{ff4c}",
"join-round": "\u{ff4b}",
@@ -3608,8 +3608,8 @@ Singleton {
"kering": "\u{efb8}",
"kerning": "\u{efb8}",
"key": "\u{eac7}",
"key-off"//"key-filled": "\u{fe28}",
: "\u{f14b}",
//"key-filled": "\u{fe28}",
"key-off": "\u{f14b}",
"keyboard": "\u{ebd6}",
"keyboard-filled": "\u{100a2}",
"keyboard-hide": "\u{ec7e}",
@@ -3665,20 +3665,20 @@ Singleton {
"layers-union": "\u{eacb}",
"layout": "\u{eadb}",
"layout-2": "\u{eacc}",
"layout-align-left"//"layout-2-filled": "\u{fe27}",
// "layout-align-bottom": "\u{eacd}",
//"layout-2-filled": "\u{fe27}",
//"layout-align-bottom": "\u{eacd}",
//"layout-align-bottom-filled": "\u{fe26}",
// "layout-align-center": "\u{eace}",
//"layout-align-center": "\u{eace}",
//"layout-align-center-filled": "\u{fe25}",
: "\u{eacf}",
"layout-align-middle"// "layout-align-left-filled": "\u{fe24}",
: "\u{ead0}",
"layout-align-right"//"layout-align-middle-filled": "\u{fe23}",
: "\u{ead1}",
"layout-align-top"//"layout-align-right-filled": "\u{fe22}",
: "\u{ead2}",
"layout-board"//"layout-align-top-filled": "\u{fe21}",
: "\u{ef95}",
"layout-align-left": "\u{eacf}",
//"layout-align-left-filled": "\u{fe24}",
"layout-align-middle": "\u{ead0}",
//"layout-align-middle-filled": "\u{fe23}",
"layout-align-right": "\u{ead1}",
//"layout-align-right-filled": "\u{fe22}",
"layout-align-top": "\u{ead2}",
//"layout-align-top-filled": "\u{fe21}",
"layout-board": "\u{ef95}",
"layout-board-filled": "\u{10182}",
"layout-board-split": "\u{ef94}",
"layout-board-split-filled": "\u{10183}",
@@ -3690,8 +3690,8 @@ Singleton {
"layout-bottombar-filled": "\u{fc37}",
"layout-bottombar-inactive": "\u{fd45}",
"layout-cards": "\u{ec13}",
"layout-collage"// "layout-cards-filled": "\u{fe20}",
: "\u{f389}",
//"layout-cards-filled": "\u{fe20}",
"layout-collage": "\u{f389}",
"layout-columns": "\u{ead4}",
"layout-dashboard": "\u{f02c}",
"layout-dashboard-filled": "\u{fe1f}",
@@ -4172,14 +4172,14 @@ Singleton {
"microphone": "\u{eaf0}",
"microphone-2": "\u{ef2c}",
"microphone-2-off": "\u{f40d}",
"microphone-off"//"microphone-filled": "\u{fe0f}",
: "\u{ed16}",
//"microphone-filled": "\u{fe0f}",
"microphone-off": "\u{ed16}",
"microscope": "\u{ef64}",
"microscope-filled": "\u{10166}",
"microscope-off": "\u{f40e}",
"microwave": "\u{f248}",
"microwave-off"//"microwave-filled": "\u{fe0e}",
: "\u{f264}",
//"microwave-filled": "\u{fe0e}",
"microwave-off": "\u{f264}",
"military-award": "\u{f079}",
"military-rank": "\u{efcf}",
"military-rank-filled": "\u{ff5e}",
@@ -4413,18 +4413,18 @@ Singleton {
"number-4-small": "\u{fcf9}",
"number-40-small": "\u{fffa}",
"number-41-small": "\u{fff9}",
"number-5"//"number-42-small": "\u{fff8}",
// "number-43-small": "\u{fff7}",
// "number-44-small": "\u{fff6}",
// "number-45-small": "\u{fff5}",
// "number-46-small": "\u{fff4}",
// "number-47-small": "\u{fff3}",
// "number-48-small": "\u{fff2}",
// "number-49-small": "\u{fff1}",
: "\u{edf5}",
//"number-42-small": "\u{fff8}",
//"number-43-small": "\u{fff7}",
//"number-44-small": "\u{fff6}",
//"number-45-small": "\u{fff5}",
//"number-46-small": "\u{fff4}",
//"number-47-small": "\u{fff3}",
//"number-48-small": "\u{fff2}",
//"number-49-small": "\u{fff1}",
"number-5": "\u{edf5}",
"number-5-small": "\u{fcfa}",
"number-51-small"// "number-50-small": "\u{fff0}",
: "\u{ffef}",
//"number-50-small": "\u{fff0}",
"number-51-small": "\u{ffef}",
"number-52-small": "\u{ffee}",
"number-53-small": "\u{ffed}",
"number-54-small": "\u{ffec}",
@@ -4864,11 +4864,11 @@ Singleton {
"quote": "\u{efbe}",
"quote-filled": "\u{1009c}",
"quote-off": "\u{f188}",
"radar"//"quotes": "\u{fb1e}",
: "\u{f017}",
//"quotes": "\u{fb1e}",
"radar": "\u{f017}",
"radar-2": "\u{f016}",
"radar-off"//"radar-filled": "\u{fe0d}",
: "\u{f41f}",
//"radar-filled": "\u{fe0d}",
"radar-off": "\u{f41f}",
"radio": "\u{ef2d}",
"radio-off": "\u{f420}",
"radioactive": "\u{ecc0}",
@@ -4928,12 +4928,12 @@ Singleton {
"regex-off": "\u{f421}",
"registered": "\u{eb14}",
"relation-many-to-many": "\u{ed7f}",
"relation-one-to-many"//"relation-many-to-many-filled": "\u{fe0c}",
: "\u{ed80}",
"relation-one-to-one"//"relation-one-to-many-filled": "\u{fe0b}",
: "\u{ed81}",
"reload"//"relation-one-to-one-filled": "\u{fe0a}",
: "\u{f3ae}",
//"relation-many-to-many-filled": "\u{fe0c}",
"relation-one-to-many": "\u{ed80}",
//"relation-one-to-many-filled": "\u{fe0b}",
"relation-one-to-one": "\u{ed81}",
//"relation-one-to-one-filled": "\u{fe0a}",
"reload": "\u{f3ae}",
"reorder": "\u{fc15}",
"repeat": "\u{eb72}",
"repeat-off": "\u{f18e}",
@@ -5085,8 +5085,8 @@ Singleton {
"search": "\u{eb1c}",
"search-off": "\u{f19c}",
"section": "\u{eed5}",
"section-sign"//"section-filled": "\u{fe09}",
: "\u{f019}",
//"section-filled": "\u{fe09}",
"section-sign": "\u{f019}",
"seeding": "\u{ed51}",
"seeding-filled": "\u{10006}",
"seeding-off": "\u{f19d}",
@@ -5294,8 +5294,8 @@ Singleton {
"sort-z-a": "\u{f550}",
"sos": "\u{f24a}",
"soup": "\u{ef2e}",
"soup-off"//"soup-filled": "\u{fe08}",
: "\u{f42d}",
//"soup-filled": "\u{fe08}",
"soup-off": "\u{f42d}",
"source-code": "\u{f4a2}",
"space": "\u{ec0c}",
"space-off": "\u{f1aa}",
@@ -5388,22 +5388,22 @@ Singleton {
"square-half": "\u{effb}",
"square-key": "\u{f638}",
"square-letter-a": "\u{f47c}",
"square-letter-b"//"square-letter-a-filled": "\u{fe07}",
: "\u{f47d}",
"square-letter-c"//"square-letter-b-filled": "\u{fe06}",
: "\u{f47e}",
"square-letter-d"//"square-letter-c-filled": "\u{fe05}",
: "\u{f47f}",
"square-letter-e"//"square-letter-d-filled": "\u{fe04}",
: "\u{f480}",
"square-letter-f"//"square-letter-e-filled": "\u{fe03}",
: "\u{f481}",
"square-letter-g"//"square-letter-f-filled": "\u{fe02}",
: "\u{f482}",
"square-letter-h"//"square-letter-g-filled": "\u{fe01}",
: "\u{f483}",
"square-letter-i"//"square-letter-h-filled": "\u{fe00}",
: "\u{f484}",
//"square-letter-a-filled": "\u{fe07}",
"square-letter-b": "\u{f47d}",
//"square-letter-b-filled": "\u{fe06}",
"square-letter-c": "\u{f47e}",
//"square-letter-c-filled": "\u{fe05}",
"square-letter-d": "\u{f47f}",
//"square-letter-d-filled": "\u{fe04}",
"square-letter-e": "\u{f480}",
//"square-letter-e-filled": "\u{fe03}",
"square-letter-f": "\u{f481}",
//"square-letter-f-filled": "\u{fe02}",
"square-letter-g": "\u{f482}",
//"square-letter-g-filled": "\u{fe01}",
"square-letter-h": "\u{f483}",
//"square-letter-h-filled": "\u{fe00}",
"square-letter-i": "\u{f484}",
"square-letter-i-filled": "\u{fdff}",
"square-letter-j": "\u{f485}",
"square-letter-j-filled": "\u{fdfe}",
+23 -23
View File
@@ -7,63 +7,63 @@ Singleton {
id: root
function _formatMessage(...args) {
var t = Time.getFormattedTimestamp()
var t = Time.getFormattedTimestamp();
if (args.length > 1) {
const maxLength = 14
var module = args.shift().substring(0, maxLength).padStart(maxLength, " ")
return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ")
const maxLength = 14;
var module = args.shift().substring(0, maxLength).padStart(maxLength, " ");
return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ");
} else {
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ")
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ");
}
}
function _getStackTrace() {
try {
throw new Error("Stack trace")
throw new Error("Stack trace");
} catch (e) {
return e.stack
return e.stack;
}
}
// Debug log (only when Settings.isDebug is true)
function d(...args) {
if (Settings && Settings.isDebug) {
var msg = _formatMessage(...args)
console.debug(msg)
if (Settings?.isDebug) {
var msg = _formatMessage(...args);
console.debug(msg);
}
}
// Info log (always visible)
function i(...args) {
var msg = _formatMessage(...args)
console.info(msg)
var msg = _formatMessage(...args);
console.info(msg);
}
// Warning log (always visible)
function w(...args) {
var msg = _formatMessage(...args)
console.warn(msg)
var msg = _formatMessage(...args);
console.warn(msg);
}
// Error log (always visible)
function e(...args) {
var msg = _formatMessage(...args)
console.error(msg)
var msg = _formatMessage(...args);
console.error(msg);
}
function callStack() {
var stack = _getStackTrace()
Logger.i("Debug", "--------------------------")
Logger.i("Debug", "Current call stack")
var stack = _getStackTrace();
Logger.i("Debug", "--------------------------");
Logger.i("Debug", "Current call stack");
// Split the stack into lines and log each one
var stackLines = stack.split('\n')
var stackLines = stack.split('\n');
for (var i = 0; i < stackLines.length; i++) {
var line = stackLines[i].trim() // Remove leading/trailing whitespace
var line = stackLines[i].trim(); // Remove leading/trailing whitespace
if (line.length > 0) {
// Only log non-empty lines
Logger.i("Debug", `- ${line}`)
Logger.i("Debug", `- ${line}`);
}
}
Logger.i("Debug", "--------------------------")
Logger.i("Debug", "--------------------------");
}
}
+189 -150
View File
@@ -3,9 +3,9 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "../Helpers/QtObj2JS.js" as QtObj2JS
import qs.Commons
import qs.Services.UI
import "../Helpers/QtObj2JS.js" as QtObj2JS
Singleton {
id: root
@@ -42,30 +42,30 @@ Singleton {
// Ensure directories exist before FileView tries to read files
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", configDir]);
Quickshell.execDetached(["mkdir", "-p", cacheDir]);
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesWallpapers])
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesWallpapers]);
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications]);
// Mark directories as created and trigger file loading
directoriesCreated = true
directoriesCreated = true;
// This should only be activated once when the settings structure has changed
// Then it should be commented out again, regular users don't need to generate
// default settings on every start
if (isDebug) {
generateDefaultSettings()
generateDefaultSettings();
}
// Patch-in the local default, resolved to user's home
adapter.general.avatarImage = defaultAvatar
adapter.screenRecorder.directory = defaultVideosDirectory
adapter.wallpaper.directory = defaultWallpapersDirectory
adapter.wallpaper.defaultWallpaper = Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
adapter.general.avatarImage = defaultAvatar;
adapter.screenRecorder.directory = defaultVideosDirectory;
adapter.wallpaper.directory = defaultWallpapersDirectory;
adapter.wallpaper.defaultWallpaper = Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png";
// Set the adapter to the settingsFileView to trigger the real settings load
settingsFileView.adapter = adapter
settingsFileView.adapter = adapter;
}
// Don't write settings to disk immediately
@@ -75,7 +75,7 @@ Singleton {
running: false
interval: 1000
onTriggered: {
root.saveImmediate()
root.saveImmediate();
}
}
@@ -90,31 +90,31 @@ Singleton {
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
reload();
}
}
onLoaded: function () {
if (!isLoaded) {
Logger.i("Settings", "Settings loaded")
Logger.i("Settings", "Settings loaded");
upgradeSettingsData()
validateMonitorConfigurations()
isLoaded = true
upgradeSettingsData();
validateMonitorConfigurations();
isLoaded = true;
// Emit the signal
root.settingsLoaded()
root.settingsLoaded();
// Finally, update our local settings version
adapter.settingsVersion = settingsVersion
adapter.settingsVersion = settingsVersion;
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values
writeAdapter()
writeAdapter();
// Also write to fallback if set
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
settingsFallbackFileView.writeAdapter()
settingsFallbackFileView.writeAdapter();
}
}
}
@@ -156,33 +156,48 @@ Singleton {
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
property list<var> left: [{
property list<var> left: [
{
"id": "ControlCenter"
}, {
},
{
"id": "SystemMonitor"
}, {
},
{
"id": "ActiveWindow"
}, {
},
{
"id": "MediaMini"
}]
property list<var> center: [{
}
]
property list<var> center: [
{
"id": "Workspace"
}]
property list<var> right: [{
}
]
property list<var> right: [
{
"id": "ScreenRecorder"
}, {
},
{
"id": "Tray"
}, {
},
{
"id": "NotificationHistory"
}, {
},
{
"id": "Battery"
}, {
},
{
"id": "Volume"
}, {
},
{
"id": "Brightness"
}, {
},
{
"id": "Clock"
}]
}
]
}
}
@@ -204,6 +219,7 @@ Singleton {
property int shadowOffsetX: 2
property int shadowOffsetY: 3
property string language: ""
property bool allowPanelsOnScreenWithoutBar: true
}
// ui
@@ -294,41 +310,57 @@ Singleton {
property string position: "close_to_bar_button"
property JsonObject shortcuts
shortcuts: JsonObject {
property list<var> left: [{
property list<var> left: [
{
"id": "WiFi"
}, {
},
{
"id": "Bluetooth"
}, {
},
{
"id": "ScreenRecorder"
}, {
},
{
"id": "WallpaperSelector"
}]
property list<var> right: [{
}
]
property list<var> right: [
{
"id": "Notifications"
}, {
},
{
"id": "PowerProfile"
}, {
},
{
"id": "KeepAwake"
}, {
},
{
"id": "NightLight"
}]
}
]
}
property list<var> cards: [{
property list<var> cards: [
{
"id": "profile-card",
"enabled": true
}, {
},
{
"id": "shortcuts-card",
"enabled": true
}, {
},
{
"id": "audio-card",
"enabled": true
}, {
},
{
"id": "weather-card",
"enabled": true
}, {
},
{
"id": "media-sysmon-card",
"enabled": true
}]
}
]
}
// system monitor
@@ -371,25 +403,32 @@ Singleton {
property int countdownDuration: 10000
property string position: "center"
property bool showHeader: true
property list<var> powerOptions: [{
property list<var> powerOptions: [
{
"action": "lock",
"enabled": true
}, {
},
{
"action": "suspend",
"enabled": true
}, {
},
{
"action": "hibernate",
"enabled": true
}, {
},
{
"action": "reboot",
"enabled": true
}, {
},
{
"action": "logout",
"enabled": true
}, {
},
{
"action": "shutdown",
"enabled": true
}]
}
]
}
// notifications
@@ -493,81 +532,81 @@ Singleton {
// Function to preprocess paths by expanding "~" to user's home directory
function preprocessPath(path) {
if (typeof path !== "string" || path === "") {
return path
return path;
}
// Expand "~" to user's home directory
if (path.startsWith("~/")) {
return Quickshell.env("HOME") + path.substring(1)
return Quickshell.env("HOME") + path.substring(1);
} else if (path === "~") {
return Quickshell.env("HOME")
return Quickshell.env("HOME");
}
return path
return path;
}
// -----------------------------------------------------
// Public function to trigger immediate settings saving
function saveImmediate() {
settingsFileView.writeAdapter()
settingsFileView.writeAdapter();
// Write to fallback location if set
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
settingsFallbackFileView.writeAdapter()
settingsFallbackFileView.writeAdapter();
}
root.settingsSaved() // Emit signal after saving
root.settingsSaved(); // Emit signal after saving
}
// -----------------------------------------------------
// Generate default settings at the root of the repo
function generateDefaultSettings() {
try {
Logger.d("Settings", "Generating settings-default.json")
Logger.d("Settings", "Generating settings-default.json");
// Prepare a clean JSON
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter)
var jsonData = JSON.stringify(plainAdapter, null, 2)
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter);
var jsonData = JSON.stringify(plainAdapter, null, 2);
var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json"
var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json";
// Encode transfer it has base64 to avoid any escaping issue
var base64Data = Qt.btoa(jsonData)
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`])
var base64Data = Qt.btoa(jsonData);
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`]);
} catch (error) {
Logger.e("Settings", "Failed to generate default settings file: " + error)
Logger.e("Settings", "Failed to generate default settings file: " + error);
}
}
// -----------------------------------------------------
// Function to validate monitor configurations
function validateMonitorConfigurations() {
var availableScreenNames = []
var availableScreenNames = [];
for (var i = 0; i < Quickshell.screens.length; i++) {
availableScreenNames.push(Quickshell.screens[i].name)
availableScreenNames.push(Quickshell.screens[i].name);
}
Logger.d("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
Logger.d("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
Logger.d("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]");
Logger.d("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]");
// Check bar monitors
if (adapter.bar.monitors.length > 0) {
var hasValidBarMonitor = false
var hasValidBarMonitor = false;
for (var j = 0; j < adapter.bar.monitors.length; j++) {
if (availableScreenNames.includes(adapter.bar.monitors[j])) {
hasValidBarMonitor = true
break
hasValidBarMonitor = true;
break;
}
}
if (!hasValidBarMonitor) {
Logger.w("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
Logger.w("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens");
adapter.bar.monitors = [];
} else
//Logger.i("Settings", "Found valid bar monitors, keeping configuration")
}
} else {
{}
} else
//Logger.i("Settings", "Bar monitor list is empty, will show on all available screens")
}
{}
}
// -----------------------------------------------------
@@ -576,50 +615,50 @@ Singleton {
function upgradeSettingsData() {
// Wait for BarWidgetRegistry to be ready
if (!BarWidgetRegistry.widgets || Object.keys(BarWidgetRegistry.widgets).length === 0) {
Logger.w("Settings", "BarWidgetRegistry not ready, deferring upgrade")
Qt.callLater(upgradeSettingsData)
return
Logger.w("Settings", "BarWidgetRegistry not ready, deferring upgrade");
Qt.callLater(upgradeSettingsData);
return;
}
const sections = ["left", "center", "right"]
const sections = ["left", "center", "right"];
// -----------------
// 1st. convert old widget id to new id
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const sectionName = sections[s];
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
var widget = adapter.bar.widgets[sectionName][i];
switch (widget.id) {
case "DarkModeToggle":
widget.id = "DarkMode"
break
widget.id = "DarkMode";
break;
case "PowerToggle":
widget.id = "SessionMenu"
break
widget.id = "SessionMenu";
break;
case "ScreenRecorderIndicator":
widget.id = "ScreenRecorder"
break
widget.id = "ScreenRecorder";
break;
case "SidePanelToggle":
widget.id = "ControlCenter"
break
widget.id = "ControlCenter";
break;
}
}
}
// -----------------
// 2nd. remove any non existing widget type
var removedWidget = false
var removedWidget = false;
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
const sectionName = sections[s];
const widgets = adapter.bar.widgets[sectionName];
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
var widget = widgets[i];
if (!BarWidgetRegistry.hasWidget(widget.id)) {
Logger.w(`Settings`, `Deleted invalid widget ${widget.id}`)
widgets.splice(i, 1)
removedWidget = true
Logger.w(`Settings`, `Deleted invalid widget ${widget.id}`);
widgets.splice(i, 1);
removedWidget = true;
}
}
}
@@ -627,18 +666,18 @@ Singleton {
// -----------------
// 3nd. upgrade widget settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const sectionName = sections[s];
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
var widget = adapter.bar.widgets[sectionName][i];
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
const reg = BarWidgetRegistry.widgetMetadata[widget.id];
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
continue;
}
if (upgradeWidget(widget)) {
Logger.d("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
Logger.d("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget));
}
}
}
@@ -647,14 +686,14 @@ Singleton {
// 4th. safety check
// if a widget was deleted, ensure we still have a control center
if (removedWidget) {
var gotControlCenter = false
var gotControlCenter = false;
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const sectionName = sections[s];
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
var widget = adapter.bar.widgets[sectionName][i];
if (widget.id === "ControlCenter") {
gotControlCenter = true
break
gotControlCenter = true;
break;
}
}
}
@@ -663,8 +702,8 @@ Singleton {
//const obj = JSON.parse('{"id": "ControlCenter"}');
adapter.bar.widgets["right"].push(({
"id": "ControlCenter"
}))
Logger.w("Settings", "Added a ControlCenter widget to the right section")
}));
Logger.w("Settings", "Added a ControlCenter widget to the right section");
}
}
@@ -674,31 +713,31 @@ Singleton {
if (adapter.settingsVersion < 21) {
// Read raw JSON file to access properties not in adapter schema
try {
var rawJson = settingsFileView.text()
var rawJson = settingsFileView.text();
if (rawJson) {
var parsed = JSON.parse(rawJson)
var anyDiscordEnabled = false
var parsed = JSON.parse(rawJson);
var anyDiscordEnabled = false;
// Check if any Discord client was enabled
const discordClients = ["discord_vesktop", "discord_webcord", "discord_armcord", "discord_equibop", "discord_lightcord", "discord_dorion", "discord_vencord"]
const discordClients = ["discord_vesktop", "discord_webcord", "discord_armcord", "discord_equibop", "discord_lightcord", "discord_dorion", "discord_vencord"];
if (parsed.templates) {
for (var i = 0; i < discordClients.length; i++) {
if (parsed.templates[discordClients[i]]) {
anyDiscordEnabled = true
break
anyDiscordEnabled = true;
break;
}
}
}
// Set unified discord property
adapter.templates.discord = anyDiscordEnabled
adapter.templates.discord = anyDiscordEnabled;
Logger.i("Settings", "Migrated Discord templates to unified 'discord' property (enabled:", anyDiscordEnabled + ")")
Logger.i("Settings", "Migrated Discord templates to unified 'discord' property (enabled:", anyDiscordEnabled + ")");
}
} catch (error) {
Logger.w("Settings", "Failed to read raw JSON for Discord migration:", error)
Logger.w("Settings", "Failed to read raw JSON for Discord migration:", error);
}
}
@@ -708,20 +747,20 @@ Singleton {
if (adapter.settingsVersion < 22) {
// Read raw JSON file to access properties not in adapter schema
try {
var rawJson = settingsFileView.text()
var rawJson = settingsFileView.text();
if (rawJson) {
var parsed = JSON.parse(rawJson)
var parsed = JSON.parse(rawJson);
if (parsed.appLauncher && parsed.appLauncher.backgroundOpacity !== undefined) {
var oldOpacity = parsed.appLauncher.backgroundOpacity
var oldOpacity = parsed.appLauncher.backgroundOpacity;
if (adapter.ui) {
adapter.ui.panelBackgroundOpacity = oldOpacity
Logger.i("Settings", "Migrated appLauncher.backgroundOpacity to ui.panelBackgroundOpacity (value:", oldOpacity + ")")
adapter.ui.panelBackgroundOpacity = oldOpacity;
Logger.i("Settings", "Migrated appLauncher.backgroundOpacity to ui.panelBackgroundOpacity (value:", oldOpacity + ")");
}
}
}
} catch (error) {
Logger.w("Settings", "Failed to read raw JSON for migration:", error)
Logger.w("Settings", "Failed to read raw JSON for migration:", error);
}
}
@@ -732,23 +771,23 @@ Singleton {
if (adapter.settingsVersion < 23) {
// Read raw JSON file to access dimDesktop property
try {
var rawJson = settingsFileView.text()
var rawJson = settingsFileView.text();
if (rawJson) {
var parsed = JSON.parse(rawJson)
var parsed = JSON.parse(rawJson);
if (parsed.general && parsed.general.dimDesktop === true) {
// Check if dimmerOpacity exists in raw JSON (not adapter default)
var dimmerOpacityInJson = parsed.general.dimmerOpacity
var dimmerOpacityInJson = parsed.general.dimmerOpacity;
// If dimmerOpacity wasn't explicitly set in JSON or was 0, set it to 0.8 (80% dimming)
if (dimmerOpacityInJson === undefined || dimmerOpacityInJson === 0) {
adapter.general.dimmerOpacity = 0.8
Logger.i("Settings", "Migrated dimDesktop=true: set dimmerOpacity to 0.8 (80% dimming)")
adapter.general.dimmerOpacity = 0.8;
Logger.i("Settings", "Migrated dimDesktop=true: set dimmerOpacity to 0.8 (80% dimming)");
}
}
}
} catch (error) {
Logger.w("Settings", "Failed to read raw JSON for dimDesktop migration:", error)
Logger.w("Settings", "Failed to read raw JSON for dimDesktop migration:", error);
}
}
}
@@ -756,35 +795,35 @@ Singleton {
// -----------------------------------------------------
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
const widgetBefore = JSON.stringify(widget);
// Get all existing custom settings keys
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id]);
// Delete deprecated user settings from the wiget
for (const k of Object.keys(widget)) {
if (k === "id" || k === "allowUserSettings") {
continue
continue;
}
if (!keys.includes(k)) {
delete widget[k]
delete widget[k];
}
}
// Inject missing default setting (metaData) from BarWidgetRegistry
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
const k = keys[i];
if (k === "id" || k === "allowUserSettings") {
continue
continue;
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k];
}
}
// Compare settings, to detect if something has been upgraded
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
const widgetAfter = JSON.stringify(widget);
return (widgetAfter !== widgetBefore);
}
}
+59 -66
View File
@@ -7,108 +7,101 @@ import qs.Services.Power
Singleton {
id: root
/*
Preset sizes for font, radii, ?
*/
// Font size
property real fontSizeXXS: 8
property real fontSizeXS: 9
property real fontSizeS: 10
property real fontSizeM: 11
property real fontSizeL: 13
property real fontSizeXL: 16
property real fontSizeXXL: 18
property real fontSizeXXXL: 24
readonly property real fontSizeXXS: 8
readonly property real fontSizeXS: 9
readonly property real fontSizeS: 10
readonly property real fontSizeM: 11
readonly property real fontSizeL: 13
readonly property real fontSizeXL: 16
readonly property real fontSizeXXL: 18
readonly property real fontSizeXXXL: 24
// Font weight
property int fontWeightRegular: 400
property int fontWeightMedium: 500
property int fontWeightSemiBold: 600
property int fontWeightBold: 700
readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500
readonly property int fontWeightSemiBold: 600
readonly property int fontWeightBold: 700
// Radii
property int radiusXXS: Math.round(4 * Settings.data.general.radiusRatio)
property int radiusXS: Math.round(8 * Settings.data.general.radiusRatio)
property int radiusS: Math.round(12 * Settings.data.general.radiusRatio)
property int radiusM: Math.round(16 * Settings.data.general.radiusRatio)
property int radiusL: Math.round(20 * Settings.data.general.radiusRatio)
property int screenRadius: Math.round(20 * Settings.data.general.screenRadiusRatio)
readonly property int radiusXXS: Math.round(4 * Settings.data.general.radiusRatio)
readonly property int radiusXS: Math.round(8 * Settings.data.general.radiusRatio)
readonly property int radiusS: Math.round(12 * Settings.data.general.radiusRatio)
readonly property int radiusM: Math.round(16 * Settings.data.general.radiusRatio)
readonly property int radiusL: Math.round(20 * Settings.data.general.radiusRatio)
readonly property int screenRadius: Math.round(20 * Settings.data.general.screenRadiusRatio)
// Border
property int borderS: Math.max(1, Math.round(1 * uiScaleRatio))
property int borderM: Math.max(1, Math.round(2 * uiScaleRatio))
property int borderL: Math.max(1, Math.round(3 * uiScaleRatio))
readonly property int borderS: Math.max(1, Math.round(1 * uiScaleRatio))
readonly property int borderM: Math.max(1, Math.round(2 * uiScaleRatio))
readonly property int borderL: Math.max(1, Math.round(3 * uiScaleRatio))
// Margins (for margins and spacing)
property int marginXXS: Math.round(2 * uiScaleRatio)
property int marginXS: Math.round(4 * uiScaleRatio)
property int marginS: Math.round(6 * uiScaleRatio)
property int marginM: Math.round(9 * uiScaleRatio)
property int marginL: Math.round(13 * uiScaleRatio)
property int marginXL: Math.round(18 * uiScaleRatio)
readonly property int marginXXS: Math.round(2 * uiScaleRatio)
readonly property int marginXS: Math.round(4 * uiScaleRatio)
readonly property int marginS: Math.round(6 * uiScaleRatio)
readonly property int marginM: Math.round(9 * uiScaleRatio)
readonly property int marginL: Math.round(13 * uiScaleRatio)
readonly property int marginXL: Math.round(18 * uiScaleRatio)
// Opacity
property real opacityNone: 0.0
property real opacityLight: 0.25
property real opacityMedium: 0.5
property real opacityHeavy: 0.75
property real opacityAlmost: 0.95
property real opacityFull: 1.0
readonly property real opacityNone: 0.0
readonly property real opacityLight: 0.25
readonly property real opacityMedium: 0.5
readonly property real opacityHeavy: 0.75
readonly property real opacityAlmost: 0.95
readonly property real opacityFull: 1.0
// Shadows
property real shadowOpacity: 0.85
property real shadowBlur: 1.0
property int shadowBlurMax: 22
property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX
property real shadowVerticalOffset: Settings.data.general.shadowOffsetY
readonly property real shadowOpacity: 0.85
readonly property real shadowBlur: 1.0
readonly property int shadowBlurMax: 22
readonly property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX
readonly property real shadowVerticalOffset: Settings.data.general.shadowOffsetY
// Animation duration (ms)
property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
readonly property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
readonly property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
readonly property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
readonly property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
readonly property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
// Delays
property int tooltipDelay: 300
property int tooltipDelayLong: 1200
property int pillDelay: 500
readonly property int tooltipDelay: 300
readonly property int tooltipDelayLong: 1200
readonly property int pillDelay: 500
// Widgets base size
property real baseWidgetSize: 33
property real sliderWidth: 200
readonly property real baseWidgetSize: 33
readonly property real sliderWidth: 200
property real uiScaleRatio: Settings.data.general.scaleRatio
readonly property real uiScaleRatio: Settings.data.general.scaleRatio
// Bar Dimensions
property real barHeight: {
readonly property real barHeight: {
switch (Settings.data.bar.density) {
case "mini":
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 22 : 20
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 22 : 20;
case "compact":
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25;
case "comfortable":
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37;
default:
case "default":
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31;
}
}
property real capsuleHeight: {
readonly property real capsuleHeight: {
switch (Settings.data.bar.density) {
case "mini":
return Math.round(barHeight * 1.0)
return Math.round(barHeight * 1.0);
case "compact":
return Math.round(barHeight * 0.85)
return Math.round(barHeight * 0.85);
case "comfortable":
return Math.round(barHeight * 0.73)
return Math.round(barHeight * 0.73);
default:
case "default":
return Math.round(barHeight * 0.82)
return Math.round(barHeight * 0.82);
}
}
}
+16 -16
View File
@@ -7,46 +7,46 @@ Singleton {
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
const fallback = fallbackName || "application-x-executable";
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
const p = Quickshell.iconPath(iconName, fallback);
if (p && p !== "")
return p
return p;
}
} catch (e) {
} catch (e)
// ignore and fall back
}
{}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
} catch (e2) {
return ""
return "";
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
const fallback = fallbackName || "application-x-executable";
if (!appId)
return iconFromName(fallback, fallback)
return iconFromName(fallback, fallback);
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
return iconFromName(fallback, fallback);
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
const name = entry && entry.icon ? entry.icon : "";
return iconFromName(name || fallback, fallback);
} catch (e) {
return iconFromName(fallback, fallback)
return iconFromName(fallback, fallback);
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
} catch (e) {
return ""
return "";
}
}
}
+38 -38
View File
@@ -1,7 +1,7 @@
pragma Singleton
import QtQuick
import Quickshell
import QtQuick
import qs.Commons
Singleton {
@@ -12,7 +12,7 @@ Singleton {
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(root.now / 1000)
return Math.floor(root.now / 1000);
}
Timer {
@@ -22,89 +22,89 @@ Singleton {
running: true
triggeredOnStart: false
onTriggered: {
var newTime = new Date()
root.now = newTime
var newTime = new Date();
root.now = newTime;
// Adjust next interval to sync with the start of the next second
var msIntoSecond = newTime.getMilliseconds()
var msIntoSecond = newTime.getMilliseconds();
if (msIntoSecond > 100) {
// If we're more than 100ms into the second, adjust for next time
updateTimer.interval = 1000 - msIntoSecond + 10 // +10ms buffer
updateTimer.restart()
updateTimer.interval = 1000 - msIntoSecond + 10; // +10ms buffer
updateTimer.restart();
} else {
updateTimer.interval = 1000
updateTimer.interval = 1000;
}
}
}
Component.onCompleted: {
// Start by syncing to the next second boundary
var now = new Date()
var msUntilNextSecond = 1000 - now.getMilliseconds()
updateTimer.interval = msUntilNextSecond + 10 // +10ms buffer
updateTimer.restart()
var now = new Date();
var msUntilNextSecond = 1000 - now.getMilliseconds();
updateTimer.interval = msUntilNextSecond + 10; // +10ms buffer
updateTimer.restart();
}
// Formats a Date object into a YYYYMMDD-HHMMSS string.
function getFormattedTimestamp(date) {
if (!date) {
date = new Date()
date = new Date();
}
const year = date.getFullYear()
const year = date.getFullYear();
// getMonth() is zero-based, so we add 1
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
// Format an easy to read approximate duration ex: 4h 32m
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
return '0s';
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
totalSeconds = Math.floor(totalSeconds);
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = []
const parts = [];
if (days)
parts.push(`${days}d`)
parts.push(`${days}d`);
if (hours)
parts.push(`${hours}h`)
parts.push(`${hours}h`);
if (minutes)
parts.push(`${minutes}m`)
parts.push(`${minutes}m`);
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`)
parts.push(`${seconds}s`);
}
return parts.join(' ')
return parts.join(' ');
}
// Format a date into
function formatRelativeTime(date) {
if (!date)
return ""
const diff = Date.now() - date.getTime()
return "";
const diff = Date.now() - date.getTime();
if (diff < 60000)
return "now"
return "now";
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`
return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`
return `${Math.floor(diff / 86400000)}d ago`
return `${Math.floor(diff / 3600000)}h ago`;
return `${Math.floor(diff / 86400000)}d ago`;
}
}
+120 -120
View File
@@ -48,17 +48,17 @@ Variants {
Component.onCompleted: setWallpaperInitial()
Component.onDestruction: {
transitionAnimation.stop()
debounceTimer.stop()
shaderLoader.active = false
currentWallpaper.source = ""
nextWallpaper.source = ""
transitionAnimation.stop();
debounceTimer.stop();
shaderLoader.active = false;
currentWallpaper.source = "";
nextWallpaper.source = "";
}
Connections {
target: Settings.data.wallpaper
function onFillModeChanged() {
fillMode = WallpaperService.getFillModeUniform()
fillMode = WallpaperService.getFillModeUniform();
}
}
@@ -69,8 +69,8 @@ Variants {
if (screenName === modelData.name) {
// Update wallpaper display
// Set wallpaper immediately on startup
futureWallpaper = path
debounceTimer.restart()
futureWallpaper = path;
debounceTimer.restart();
}
}
}
@@ -80,9 +80,9 @@ Variants {
function onDisplayScalesChanged() {
// Recalculate image sizes without interrupting startup transition
if (isStartupTransition) {
return
return;
}
recalculateImageSizes()
recalculateImageSizes();
}
}
@@ -105,7 +105,7 @@ Variants {
running: false
repeat: false
onTriggered: {
changeWallpaper()
changeWallpaper();
}
}
@@ -123,18 +123,18 @@ Variants {
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
Logger.w("Current wallpaper failed to load:", source)
Logger.w("Current wallpaper failed to load:", source);
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
dimensionsCalculated = true;
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight);
if (optimalSize !== false) {
sourceSize = optimalSize
sourceSize = optimalSize;
}
}
}
onSourceChanged: {
dimensionsCalculated = false
sourceSize = undefined
dimensionsCalculated = false;
sourceSize = undefined;
}
}
@@ -152,18 +152,18 @@ Variants {
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
Logger.w("Next wallpaper failed to load:", source)
Logger.w("Next wallpaper failed to load:", source);
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
dimensionsCalculated = true;
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight);
if (optimalSize !== false) {
sourceSize = optimalSize
sourceSize = optimalSize;
}
}
}
onSourceChanged: {
dimensionsCalculated = false
sourceSize = undefined
dimensionsCalculated = false;
sourceSize = undefined;
}
}
@@ -176,15 +176,15 @@ Variants {
sourceComponent: {
switch (transitionType) {
case "wipe":
return wipeShaderComponent
return wipeShaderComponent;
case "disc":
return discShaderComponent
return discShaderComponent;
case "stripes":
return stripesShaderComponent
return stripesShaderComponent;
case "fade":
case "none":
default:
return fadeShaderComponent
return fadeShaderComponent;
}
}
}
@@ -307,64 +307,64 @@ Variants {
easing.type: Easing.InOutCubic
onFinished: {
// Assign new image to current BEFORE clearing to prevent flicker
const tempSource = nextWallpaper.source
currentWallpaper.source = tempSource
transitionProgress = 0.0
const tempSource = nextWallpaper.source;
currentWallpaper.source = tempSource;
transitionProgress = 0.0;
// Now clear nextWallpaper after currentWallpaper has the new source
// Force complete cleanup to free texture memory (~18-25MB per monitor)
Qt.callLater(() => {
nextWallpaper.source = ""
nextWallpaper.sourceSize = undefined
nextWallpaper.source = "";
nextWallpaper.sourceSize = undefined;
Qt.callLater(() => {
currentWallpaper.asynchronous = true
})
})
currentWallpaper.asynchronous = true;
});
});
}
}
// ------------------------------------------------------
function calculateOptimalWallpaperSize(wpWidth, wpHeight) {
const compositorScale = CompositorService.getDisplayScale(modelData.name)
const screenWidth = modelData.width * compositorScale
const screenHeight = modelData.height * compositorScale
const compositorScale = CompositorService.getDisplayScale(modelData.name);
const screenWidth = modelData.width * compositorScale;
const screenHeight = modelData.height * compositorScale;
if (wpWidth <= screenWidth || wpHeight <= screenHeight || wpWidth <= 0 || wpHeight <= 0) {
// Do not resize if wallpaper is smaller than one of the screen dimension
return
return;
}
const imageAspectRatio = wpWidth / wpHeight
var dim = Qt.size(0, 0)
const imageAspectRatio = wpWidth / wpHeight;
var dim = Qt.size(0, 0);
if (screenWidth >= screenHeight) {
const w = Math.min(screenWidth, wpWidth)
dim = Qt.size(Math.round(w), Math.round(w / imageAspectRatio))
const w = Math.min(screenWidth, wpWidth);
dim = Qt.size(Math.round(w), Math.round(w / imageAspectRatio));
} else {
const h = Math.min(screenHeight, wpHeight)
dim = Qt.size(Math.round(h * imageAspectRatio), Math.round(h))
const h = Math.min(screenHeight, wpHeight);
dim = Qt.size(Math.round(h * imageAspectRatio), Math.round(h));
}
Logger.d("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height)
return dim
Logger.d("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height);
return dim;
}
// ------------------------------------------------------
function recalculateImageSizes() {
// Re-evaluate and apply optimal sourceSize for both images when ready
if (currentWallpaper.status === Image.Ready) {
const optimal = calculateOptimalWallpaperSize(currentWallpaper.implicitWidth, currentWallpaper.implicitHeight)
const optimal = calculateOptimalWallpaperSize(currentWallpaper.implicitWidth, currentWallpaper.implicitHeight);
if (optimal !== undefined && optimal !== false) {
currentWallpaper.sourceSize = optimal
currentWallpaper.sourceSize = optimal;
} else {
currentWallpaper.sourceSize = undefined
currentWallpaper.sourceSize = undefined;
}
}
if (nextWallpaper.status === Image.Ready) {
const optimal2 = calculateOptimalWallpaperSize(nextWallpaper.implicitWidth, nextWallpaper.implicitHeight)
const optimal2 = calculateOptimalWallpaperSize(nextWallpaper.implicitWidth, nextWallpaper.implicitHeight);
if (optimal2 !== undefined && optimal2 !== false) {
nextWallpaper.sourceSize = optimal2
nextWallpaper.sourceSize = optimal2;
} else {
nextWallpaper.sourceSize = undefined
nextWallpaper.sourceSize = undefined;
}
}
}
@@ -373,104 +373,104 @@ Variants {
function setWallpaperInitial() {
// On startup, defer assigning wallpaper until the service cache is ready, retries every tick
if (!WallpaperService || !WallpaperService.isInitialized) {
Qt.callLater(setWallpaperInitial)
return
Qt.callLater(setWallpaperInitial);
return;
}
const wallpaperPath = WallpaperService.getWallpaper(modelData.name)
const wallpaperPath = WallpaperService.getWallpaper(modelData.name);
futureWallpaper = wallpaperPath
performStartupTransition()
futureWallpaper = wallpaperPath;
performStartupTransition();
}
// ------------------------------------------------------
function setWallpaperImmediate(source) {
transitionAnimation.stop()
transitionProgress = 0.0
transitionAnimation.stop();
transitionProgress = 0.0;
// Clear nextWallpaper completely to free texture memory
nextWallpaper.source = ""
nextWallpaper.sourceSize = undefined
nextWallpaper.source = "";
nextWallpaper.sourceSize = undefined;
currentWallpaper.source = ""
currentWallpaper.source = "";
Qt.callLater(() => {
currentWallpaper.source = source
})
currentWallpaper.source = source;
});
}
// ------------------------------------------------------
function setWallpaperWithTransition(source) {
if (source === currentWallpaper.source) {
return
return;
}
if (transitioning) {
// We are interrupting a transition - handle cleanup properly
transitionAnimation.stop()
transitionProgress = 0
transitionAnimation.stop();
transitionProgress = 0;
// Assign nextWallpaper to currentWallpaper BEFORE clearing to prevent flicker
const newCurrentSource = nextWallpaper.source
currentWallpaper.source = newCurrentSource
const newCurrentSource = nextWallpaper.source;
currentWallpaper.source = newCurrentSource;
// Now clear nextWallpaper after current has the new source
Qt.callLater(() => {
nextWallpaper.source = ""
nextWallpaper.source = "";
// Now set the next wallpaper after a brief delay
Qt.callLater(() => {
nextWallpaper.source = source
currentWallpaper.asynchronous = false
transitionAnimation.start()
})
})
return
nextWallpaper.source = source;
currentWallpaper.asynchronous = false;
transitionAnimation.start();
});
});
return;
}
nextWallpaper.source = source
currentWallpaper.asynchronous = false
transitionAnimation.start()
nextWallpaper.source = source;
currentWallpaper.asynchronous = false;
transitionAnimation.start();
}
// ------------------------------------------------------
// Main method that actually trigger the wallpaper change
function changeWallpaper() {
// Get the transitionType from the settings
transitionType = Settings.data.wallpaper.transitionType
transitionType = Settings.data.wallpaper.transitionType;
if (transitionType == "random") {
var index = Math.floor(Math.random() * allTransitions.length)
transitionType = allTransitions[index]
var index = Math.floor(Math.random() * allTransitions.length);
transitionType = allTransitions[index];
}
// Ensure the transition type really exists
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
transitionType = "fade"
transitionType = "fade";
}
//Logger.i("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
switch (transitionType) {
case "none":
setWallpaperImmediate(futureWallpaper)
break
setWallpaperImmediate(futureWallpaper);
break;
case "wipe":
wipeDirection = Math.random() * 4
setWallpaperWithTransition(futureWallpaper)
break
wipeDirection = Math.random() * 4;
setWallpaperWithTransition(futureWallpaper);
break;
case "disc":
discCenterX = Math.random()
discCenterY = Math.random()
setWallpaperWithTransition(futureWallpaper)
break
discCenterX = Math.random();
discCenterY = Math.random();
setWallpaperWithTransition(futureWallpaper);
break;
case "stripes":
stripesCount = Math.round(Math.random() * 20 + 4)
stripesAngle = Math.random() * 360
setWallpaperWithTransition(futureWallpaper)
break
stripesCount = Math.round(Math.random() * 20 + 4);
stripesAngle = Math.random() * 360;
setWallpaperWithTransition(futureWallpaper);
break;
default:
setWallpaperWithTransition(futureWallpaper)
break
setWallpaperWithTransition(futureWallpaper);
break;
}
}
@@ -478,46 +478,46 @@ Variants {
// Dedicated function for startup animation
function performStartupTransition() {
// Get the transitionType from the settings
transitionType = Settings.data.wallpaper.transitionType
transitionType = Settings.data.wallpaper.transitionType;
if (transitionType == "random") {
var index = Math.floor(Math.random() * allTransitions.length)
transitionType = allTransitions[index]
var index = Math.floor(Math.random() * allTransitions.length);
transitionType = allTransitions[index];
}
// Ensure the transition type really exists
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
transitionType = "fade"
transitionType = "fade";
}
// Apply transitionType so the shader loader picks the correct shader
this.transitionType = transitionType
this.transitionType = transitionType;
switch (transitionType) {
case "none":
setWallpaperImmediate(futureWallpaper)
break
setWallpaperImmediate(futureWallpaper);
break;
case "wipe":
wipeDirection = Math.random() * 4
setWallpaperWithTransition(futureWallpaper)
break
wipeDirection = Math.random() * 4;
setWallpaperWithTransition(futureWallpaper);
break;
case "disc":
// Force center origin for elegant startup animation
discCenterX = 0.5
discCenterY = 0.5
setWallpaperWithTransition(futureWallpaper)
break
discCenterX = 0.5;
discCenterY = 0.5;
setWallpaperWithTransition(futureWallpaper);
break;
case "stripes":
stripesCount = Math.round(Math.random() * 20 + 4)
stripesAngle = Math.random() * 360
setWallpaperWithTransition(futureWallpaper)
break
stripesCount = Math.round(Math.random() * 20 + 4);
stripesAngle = Math.random() * 360;
setWallpaperWithTransition(futureWallpaper);
break;
default:
setWallpaperWithTransition(futureWallpaper)
break
setWallpaperWithTransition(futureWallpaper);
break;
}
// Mark startup transition complete
isStartupTransition = false
isStartupTransition = false;
}
}
}
+11 -11
View File
@@ -1,6 +1,6 @@
import QtQuick
import Quickshell
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.Compositor
@@ -20,16 +20,16 @@ Loader {
Component.onCompleted: {
if (modelData) {
Logger.d("Overview", "Loading overview for Niri on", modelData.name)
Logger.d("Overview", "Loading overview for Niri on", modelData.name);
}
setWallpaperInitial()
setWallpaperInitial();
}
Component.onDestruction: {
// Clean up resources to prevent memory leak when overviewEnabled is toggled off
timerDisableFx.stop()
bgImage.layer.enabled = false
bgImage.source = ""
timerDisableFx.stop();
bgImage.layer.enabled = false;
bgImage.source = "";
}
// External state management
@@ -37,19 +37,19 @@ Loader {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === modelData.name) {
wallpaper = path
wallpaper = path;
}
}
}
function setWallpaperInitial() {
if (!WallpaperService || !WallpaperService.isInitialized) {
Qt.callLater(setWallpaperInitial)
return
Qt.callLater(setWallpaperInitial);
return;
}
const wallpaperPath = WallpaperService.getWallpaper(modelData.name)
const wallpaperPath = WallpaperService.getWallpaper(modelData.name);
if (wallpaperPath && wallpaperPath !== wallpaper) {
wallpaper = wallpaperPath
wallpaper = wallpaperPath;
}
}
+34 -34
View File
@@ -2,13 +2,13 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.UPower
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Notification
import qs.Services.UI
import qs.Widgets
import qs.Modules.Notification
import qs.Modules.Bar.Extras
// Bar Component
Item {
@@ -34,9 +34,9 @@ Item {
// Register bar when screen becomes available
onScreenChanged: {
if (screen && screen.name) {
Logger.d("Bar", "Bar screen set to:", screen.name)
Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating)
BarService.registerBar(screen.name)
Logger.d("Bar", "Bar screen set to:", screen.name);
Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating);
BarService.registerBar(screen.name);
}
}
@@ -46,12 +46,12 @@ Item {
anchors.fill: parent
active: {
if (root.screen === null || root.screen === undefined) {
return false
return false;
}
var monitors = Settings.data.bar.monitors || []
var result = monitors.length === 0 || monitors.includes(root.screen.name)
return result
var monitors = Settings.data.bar.monitors || [];
var result = monitors.length === 0 || monitors.includes(root.screen.name);
return result;
}
sourceComponent: Item {
@@ -75,73 +75,73 @@ Item {
readonly property int topLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
return 0;
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
return -1;
// Left bar: top-left against screen edge = no radius
if (barPosition === "left")
return -1
return -1;
// Bottom/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2 // horizontal invert for vertical bars, vertical invert for horizontal
return barIsVertical ? 1 : 2; // horizontal invert for vertical bars, vertical invert for horizontal
}
// No outerCorners = square
return -1
return -1;
}
readonly property int topRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
return 0;
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
return -1;
// Right bar: top-right against screen edge = no radius
if (barPosition === "right")
return -1
return -1;
// Bottom/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
// No outerCorners = square
return -1
return -1;
}
readonly property int bottomLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
return 0;
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
return -1;
// Left bar: bottom-left against screen edge = no radius
if (barPosition === "left")
return -1
return -1;
// Top/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
// No outerCorners = square
return -1
return -1;
}
readonly property int bottomRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
return 0;
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
return -1;
// Right bar: bottom-right against screen edge = no radius
if (barPosition === "right")
return -1
return -1;
// Top/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
// No outerCorners = square
return -1
return -1;
}
MouseArea {
@@ -151,14 +151,14 @@ Item {
preventStealing: true
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen)
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel?.toggle(null, "ControlCenter")
controlCenterPanel?.toggle(null, "ControlCenter");
} else {
controlCenterPanel?.toggle()
controlCenterPanel?.toggle();
}
mouse.accepted = true
mouse.accepted = true;
}
}
}
+15 -16
View File
@@ -3,13 +3,12 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
/**
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
*
* This is a minimal window that works with the compositor to reserve space,
* while the actual bar UI is rendered in NFullScreenWindow.
*/
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
*
* This is a minimal window that works with the compositor to reserve space,
* while the actual bar UI is rendered in NFullScreenWindow.
*/
PanelWindow {
id: root
@@ -46,11 +45,11 @@ PanelWindow {
// Vertical bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For left bar, reserve left margin; for right bar, reserve right margin
return Style.barHeight + barMarginH
return Style.barHeight + barMarginH;
}
return Style.barHeight
return Style.barHeight;
}
return 0 // Auto-width when left/right anchors are true
return 0; // Auto-width when left/right anchors are true
}
implicitHeight: {
@@ -58,17 +57,17 @@ PanelWindow {
// Horizontal bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For top bar, reserve top margin; for bottom bar, reserve bottom margin
return Style.barHeight + barMarginV
return Style.barHeight + barMarginV;
}
return Style.barHeight
return Style.barHeight;
}
return 0 // Auto-height when top/bottom anchors are true
return 0; // Auto-height when top/bottom anchors are true
}
Component.onCompleted: {
Logger.d("BarExclusionZone", "Created for screen:", screen?.name)
Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating)
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right)
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight)
Logger.d("BarExclusionZone", "Created for screen:", screen?.name);
Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating);
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right);
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight);
}
}
+3 -3
View File
@@ -92,19 +92,19 @@ Item {
function show() {
if (pillLoader.item && pillLoader.item.show) {
pillLoader.item.show()
pillLoader.item.show();
}
}
function hide() {
if (pillLoader.item && pillLoader.item.hide) {
pillLoader.item.hide()
pillLoader.item.hide();
}
}
function showDelayed() {
if (pillLoader.item && pillLoader.item.showDelayed) {
pillLoader.item.showDelayed()
pillLoader.item.showDelayed();
}
}
}
+45 -45
View File
@@ -45,18 +45,18 @@ Item {
readonly property real iconSize: {
switch (root.density) {
case "compact":
return Math.max(1, Math.round(pillHeight * 0.65))
return Math.max(1, Math.round(pillHeight * 0.65));
default:
return Math.max(1, Math.round(pillHeight * 0.48))
return Math.max(1, Math.round(pillHeight * 0.48));
}
}
readonly property real textSize: {
switch (root.density) {
case "compact":
return Math.max(1, Math.round(pillHeight * 0.45))
return Math.max(1, Math.round(pillHeight * 0.45));
default:
return Math.max(1, Math.round(pillHeight * 0.33))
return Math.max(1, Math.round(pillHeight * 0.33));
}
}
@@ -67,7 +67,7 @@ Item {
target: root
function onTooltipTextChanged() {
if (hovered) {
TooltipService.updateText(root.tooltipText)
TooltipService.updateText(root.tooltipText);
}
}
}
@@ -97,13 +97,13 @@ Item {
anchors.verticalCenter: parent.verticalCenter
x: {
// Better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = oppositeDirection ? Style.marginXS : -Style.marginXS
var centerX = (parent.width - width) / 2;
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 += oppositeDirection ? -Style.marginXXS : Style.marginXXS
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
}
return centerX + offset
return centerX + offset;
}
text: root.text + root.suffix
family: Settings.data.ui.fontFixed
@@ -179,11 +179,11 @@ Item {
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
showPill = true;
}
onStopped: {
delayedHideAnim.start()
root.shown()
delayedHideAnim.start();
root.shown();
}
}
@@ -195,7 +195,7 @@ Item {
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
hideAnim.start();
}
}
}
@@ -220,9 +220,9 @@ Item {
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
showPill = false;
shouldAnimateHide = false;
root.hidden();
}
}
@@ -231,7 +231,7 @@ Item {
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
showAnim.start();
}
}
}
@@ -241,31 +241,31 @@ Item {
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
hovered = true;
root.entered();
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong);
if (forceClose) {
return
return;
}
if (!forceOpen) {
showDelayed()
showDelayed();
}
}
onExited: {
hovered = false
root.exited()
hovered = false;
root.exited();
if (!forceOpen && !forceClose) {
hide()
hide();
}
TooltipService.hide()
TooltipService.hide();
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
root.clicked();
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
root.rightClicked();
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
root.middleClicked();
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
@@ -273,43 +273,43 @@ Item {
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
shouldAnimateHide = autoHide;
showAnim.start();
} else {
hideAnim.stop()
delayedHideAnim.restart()
hideAnim.stop();
delayedHideAnim.restart();
}
}
function hide() {
if (forceOpen) {
return
return;
}
if (showPill) {
hideAnim.start()
hideAnim.start();
}
showTimer.stop()
showTimer.stop();
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
shouldAnimateHide = autoHide;
showTimer.start();
} else {
hideAnim.stop()
delayedHideAnim.restart()
hideAnim.stop();
delayedHideAnim.restart();
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
showAnim.stop();
hideAnim.stop();
delayedHideAnim.stop();
showPill = true;
} else {
hide()
hide();
}
}
}
+44 -44
View File
@@ -55,18 +55,18 @@ Item {
readonly property real iconSize: {
switch (root.density) {
case "compact":
return Math.max(1, Math.round(pillHeight * 0.65))
return Math.max(1, Math.round(pillHeight * 0.65));
default:
return Math.max(1, Math.round(pillHeight * 0.48))
return Math.max(1, Math.round(pillHeight * 0.48));
}
}
readonly property real textSize: {
switch (root.density) {
case "compact":
return Math.max(1, Math.round(pillHeight * 0.38))
return Math.max(1, Math.round(pillHeight * 0.38));
default:
return Math.max(1, Math.round(pillHeight * 0.33))
return Math.max(1, Math.round(pillHeight * 0.33));
}
}
@@ -78,7 +78,7 @@ Item {
target: root
function onTooltipTextChanged() {
if (hovered) {
TooltipService.updateText(root.tooltipText)
TooltipService.updateText(root.tooltipText);
}
}
}
@@ -123,11 +123,11 @@ Item {
visible: revealed
function getVerticalCenterOffset() {
var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75)
var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75);
if (forceOpen) {
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
}
return offset
return offset;
}
}
Behavior on width {
@@ -212,11 +212,11 @@ Item {
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
showPill = true;
}
onStopped: {
delayedHideAnim.start()
root.shown()
delayedHideAnim.start();
root.shown();
}
}
@@ -228,7 +228,7 @@ Item {
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
hideAnim.start();
}
}
}
@@ -261,9 +261,9 @@ Item {
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
showPill = false;
shouldAnimateHide = false;
root.hidden();
}
}
@@ -272,7 +272,7 @@ Item {
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
showAnim.start();
}
}
}
@@ -282,31 +282,31 @@ Item {
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
hovered = true;
root.entered();
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong);
if (forceClose) {
return
return;
}
if (!forceOpen) {
showDelayed()
showDelayed();
}
}
onExited: {
hovered = false
root.exited()
hovered = false;
root.exited();
if (!forceOpen && !forceClose) {
hide()
hide();
}
TooltipService.hide()
TooltipService.hide();
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
root.clicked();
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
root.rightClicked();
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
root.middleClicked();
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
@@ -314,43 +314,43 @@ Item {
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
shouldAnimateHide = autoHide;
showAnim.start();
} else {
hideAnim.stop()
delayedHideAnim.restart()
hideAnim.stop();
delayedHideAnim.restart();
}
}
function hide() {
if (forceOpen) {
return
return;
}
if (showPill) {
hideAnim.start()
hideAnim.start();
}
showTimer.stop()
showTimer.stop();
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
shouldAnimateHide = autoHide;
showTimer.start();
} else {
hideAnim.stop()
delayedHideAnim.restart()
hideAnim.stop();
delayedHideAnim.restart();
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
showAnim.stop();
hideAnim.stop();
delayedHideAnim.stop();
showPill = true;
} else {
hide()
hide();
}
}
}
+12 -13
View File
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import qs.Services.UI
import qs.Commons
import qs.Services.UI
Item {
id: root
@@ -25,7 +25,7 @@ Item {
visible: loader.item ? ((loader.item.opacity > 0.0) || (loader.item.hasOwnProperty("hideMode") && loader.item.hideMode === "transparent")) : false
function getImplicitSize(item, prop) {
return (item && item.visible) ? Math.round(item[prop]) : 0
return (item && item.visible) ? Math.round(item[prop]) : 0;
}
Loader {
@@ -36,42 +36,41 @@ Item {
onLoaded: {
if (!item)
return
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name)
return;
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name);
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
item[prop] = widgetProps[prop];
}
}
// Set screen property
if (item.hasOwnProperty("screen")) {
item.screen = widgetScreen
item.screen = widgetScreen;
}
// Set scaling property
if (item.hasOwnProperty("scaling")) {
item.scaling = Qt.binding(function () {
return root.scaling
})
return root.scaling;
});
}
// Register this widget instance with BarService
BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item)
BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item);
// Call custom onLoaded if it exists
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
item.onLoaded();
}
}
Component.onDestruction: {
// Unregister when destroyed
if (widgetScreen && section) {
BarService.unregisterWidget(widgetScreen.name, section, widgetId, sectionIndex)
BarService.unregisterWidget(widgetScreen.name, section, widgetId, sectionIndex);
}
}
}
@@ -79,7 +78,7 @@ Item {
// Error handling
Component.onCompleted: {
if (!BarWidgetRegistry.hasWidget(widgetId)) {
Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId)
Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId);
}
}
}
+100 -100
View File
@@ -25,20 +25,20 @@ PopupWindow {
// Compute if current tray item is pinned
readonly property bool isPinned: {
if (!trayItem || widgetSection === "" || widgetIndex < 0)
return false
var widgets = Settings.data.bar.widgets[widgetSection]
return false;
var widgets = Settings.data.bar.widgets[widgetSection];
if (!widgets || widgetIndex >= widgets.length)
return false
var widgetSettings = widgets[widgetIndex]
return false;
var widgetSettings = widgets[widgetIndex];
if (!widgetSettings || widgetSettings.id !== "Tray")
return false
var pinnedList = widgetSettings.pinned || []
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""
return false;
var pinnedList = widgetSettings.pinned || [];
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
for (var i = 0; i < pinnedList.length; i++) {
if (pinnedList[i] === itemName)
return true
return true;
}
return false
return false;
}
readonly property int menuWidth: 220
@@ -53,47 +53,47 @@ PopupWindow {
anchor.rect.x: anchorX
anchor.rect.y: {
if (isSubMenu) {
const offsetY = Settings.data.bar.position === "bottom" ? -10 : 10
return anchorY + offsetY
const offsetY = Settings.data.bar.position === "bottom" ? -10 : 10;
return anchorY + offsetY;
}
return anchorY + Settings.data.bar.position === "bottom" ? -implicitHeight : Style.barHeight
return anchorY + Settings.data.bar.position === "bottom" ? -implicitHeight : Style.barHeight;
}
function showAt(item, x, y) {
if (!item) {
Logger.w("TrayMenu", "anchorItem is undefined, won't show menu.")
return
Logger.w("TrayMenu", "anchorItem is undefined, won't show menu.");
return;
}
if (!opener.children || opener.children.values.length === 0) {
//Logger.w("TrayMenu", "Menu not ready, delaying show")
Qt.callLater(() => showAt(item, x, y))
return
Qt.callLater(() => showAt(item, x, y));
return;
}
anchorItem = item
anchorX = x
anchorY = y
anchorItem = item;
anchorX = x;
anchorY = y;
visible = true
forceActiveFocus()
visible = true;
forceActiveFocus();
// Force update after showing.
Qt.callLater(() => {
root.anchor.updateAnchor()
})
root.anchor.updateAnchor();
});
}
function hideMenu() {
visible = false
visible = false;
// Clean up all submenus recursively
for (var i = 0; i < columnLayout.children.length; i++) {
const child = columnLayout.children[i]
const child = columnLayout.children[i];
if (child?.subMenu) {
child.subMenu.hideMenu()
child.subMenu.destroy()
child.subMenu = null
child.subMenu.hideMenu();
child.subMenu.destroy();
child.subMenu = null;
}
}
}
@@ -166,11 +166,11 @@ PopupWindow {
Layout.preferredWidth: parent.width
Layout.preferredHeight: {
if (modelData?.isSeparator) {
return 8
return 8;
} else {
// Calculate based on text content
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2)
return Math.max(28, textHeight + (Style.marginS * 2))
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2);
return Math.max(28, textHeight + (Style.marginS * 2));
}
}
@@ -237,31 +237,31 @@ PopupWindow {
// Click on items with children toggles submenu
if (entry.subMenu) {
// Close existing submenu
entry.subMenu.hideMenu()
entry.subMenu.destroy()
entry.subMenu = null
entry.subMenu.hideMenu();
entry.subMenu.destroy();
entry.subMenu = null;
} else {
// Close any other open submenus first
for (var i = 0; i < columnLayout.children.length; i++) {
const sibling = columnLayout.children[i]
const sibling = columnLayout.children[i];
if (sibling !== entry && sibling.subMenu) {
sibling.subMenu.hideMenu()
sibling.subMenu.destroy()
sibling.subMenu = null
sibling.subMenu.hideMenu();
sibling.subMenu.destroy();
sibling.subMenu = null;
}
}
// Determine submenu opening direction
let openLeft = false
const barPosition = Settings.data.bar.position
const globalPos = entry.mapToItem(null, 0, 0)
let openLeft = false;
const barPosition = Settings.data.bar.position;
const globalPos = entry.mapToItem(null, 0, 0);
if (barPosition === "right") {
openLeft = true
openLeft = true;
} else if (barPosition === "left") {
openLeft = false
openLeft = false;
} else {
openLeft = (root.widgetSection === "right")
openLeft = (root.widgetSection === "right");
}
// Open new submenu
@@ -269,30 +269,30 @@ PopupWindow {
"menu": modelData,
"isSubMenu": true,
"screen": root.screen
})
});
if (entry.subMenu) {
const overlap = 60
entry.subMenu.anchorItem = entry
entry.subMenu.anchorX = openLeft ? -overlap : overlap
entry.subMenu.anchorY = 0
entry.subMenu.visible = true
const overlap = 60;
entry.subMenu.anchorItem = entry;
entry.subMenu.anchorX = openLeft ? -overlap : overlap;
entry.subMenu.anchorY = 0;
entry.subMenu.visible = true;
// Force anchor update with new position
Qt.callLater(() => {
entry.subMenu.anchor.updateAnchor()
})
entry.subMenu.anchor.updateAnchor();
});
}
}
} else {
// Click on regular items triggers them
modelData.triggered()
root.hideMenu()
modelData.triggered();
root.hideMenu();
// Close the drawer if it's open
if (root.screen) {
const panel = PanelService.getPanel("trayDrawerPanel", root.screen)
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
if (panel && panel.visible) {
panel.close()
panel.close();
}
}
}
@@ -303,8 +303,8 @@ PopupWindow {
Component.onDestruction: {
if (subMenu) {
subMenu.destroy()
subMenu = null
subMenu.destroy();
subMenu = null;
}
}
}
@@ -351,9 +351,9 @@ PopupWindow {
onClicked: {
if (root.isPinned) {
root.removeFromPinned()
root.removeFromPinned();
} else {
root.addToPinned()
root.addToPinned();
}
}
}
@@ -363,72 +363,72 @@ PopupWindow {
function addToPinned() {
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
Logger.w("TrayMenu", "Cannot pin: missing tray item or widget info")
return
Logger.w("TrayMenu", "Cannot pin: missing tray item or widget info");
return;
}
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
if (!itemName) {
Logger.w("TrayMenu", "Cannot pin: tray item has no name")
return
Logger.w("TrayMenu", "Cannot pin: tray item has no name");
return;
}
var widgets = Settings.data.bar.widgets[widgetSection]
var widgets = Settings.data.bar.widgets[widgetSection];
if (!widgets || widgetIndex >= widgets.length) {
Logger.w("TrayMenu", "Cannot pin: invalid widget index")
return
Logger.w("TrayMenu", "Cannot pin: invalid widget index");
return;
}
var widgetSettings = widgets[widgetIndex]
var widgetSettings = widgets[widgetIndex];
if (!widgetSettings || widgetSettings.id !== "Tray") {
Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget")
return
Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget");
return;
}
var pinnedList = widgetSettings.pinned || []
var newPinned = pinnedList.slice()
newPinned.push(itemName)
var newSettings = Object.assign({}, widgetSettings)
newSettings.pinned = newPinned
widgets[widgetIndex] = newSettings
Settings.data.bar.widgets[widgetSection] = widgets
Settings.saveImmediate()
var pinnedList = widgetSettings.pinned || [];
var newPinned = pinnedList.slice();
newPinned.push(itemName);
var newSettings = Object.assign({}, widgetSettings);
newSettings.pinned = newPinned;
widgets[widgetIndex] = newSettings;
Settings.data.bar.widgets[widgetSection] = widgets;
Settings.saveImmediate();
// Close drawer when pinning (drawer needs to resize)
if (screen) {
const panel = PanelService.getPanel("trayDrawerPanel", screen)
const panel = PanelService.getPanel("trayDrawerPanel", screen);
if (panel)
panel.close()
panel.close();
}
}
function removeFromPinned() {
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
Logger.w("TrayMenu", "Cannot unpin: missing tray item or widget info")
return
Logger.w("TrayMenu", "Cannot unpin: missing tray item or widget info");
return;
}
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
if (!itemName) {
Logger.w("TrayMenu", "Cannot unpin: tray item has no name")
return
Logger.w("TrayMenu", "Cannot unpin: tray item has no name");
return;
}
var widgets = Settings.data.bar.widgets[widgetSection]
var widgets = Settings.data.bar.widgets[widgetSection];
if (!widgets || widgetIndex >= widgets.length) {
Logger.w("TrayMenu", "Cannot unpin: invalid widget index")
return
Logger.w("TrayMenu", "Cannot unpin: invalid widget index");
return;
}
var widgetSettings = widgets[widgetIndex]
var widgetSettings = widgets[widgetIndex];
if (!widgetSettings || widgetSettings.id !== "Tray") {
Logger.w("TrayMenu", "Cannot unpin: widget is not a Tray widget")
return
Logger.w("TrayMenu", "Cannot unpin: widget is not a Tray widget");
return;
}
var pinnedList = widgetSettings.pinned || []
var newPinned = []
var pinnedList = widgetSettings.pinned || [];
var newPinned = [];
for (var i = 0; i < pinnedList.length; i++) {
if (pinnedList[i] !== itemName) {
newPinned.push(pinnedList[i])
newPinned.push(pinnedList[i]);
}
}
var newSettings = Object.assign({}, widgetSettings)
newSettings.pinned = newPinned
widgets[widgetIndex] = newSettings
Settings.data.bar.widgets[widgetSection] = widgets
Settings.saveImmediate()
var newSettings = Object.assign({}, widgetSettings);
newSettings.pinned = newPinned;
widgets[widgetIndex] = newSettings;
Settings.data.bar.widgets[widgetSection] = widgets;
Settings.saveImmediate();
}
}
+62 -62
View File
@@ -23,12 +23,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
// Widget settings - matching MediaMini pattern
@@ -73,60 +73,60 @@ Item {
}
function calculatedVerticalDimension() {
return Math.round((Style.baseWidgetSize - 5) * scaling)
return Math.round((Style.baseWidgetSize - 5) * scaling);
}
function calculateContentWidth() {
// Calculate the actual content width based on visible elements
var contentWidth = 0
var margins = Style.marginS * scaling * 2 // Left and right margins
var contentWidth = 0;
var margins = Style.marginS * scaling * 2; // Left and right margins
// Icon width (if visible)
if (showIcon) {
contentWidth += 18 * scaling
contentWidth += Style.marginS * scaling // Spacing after icon
contentWidth += 18 * scaling;
contentWidth += Style.marginS * scaling; // Spacing after icon
}
// Text width (use the measured width)
contentWidth += fullTitleMetrics.contentWidth
contentWidth += fullTitleMetrics.contentWidth;
// Additional small margin for text
contentWidth += Style.marginXXS * 2
contentWidth += Style.marginXXS * 2;
// Add container margins
contentWidth += margins
contentWidth += margins;
return Math.ceil(contentWidth)
return Math.ceil(contentWidth);
}
// Dynamic width: adapt to content but respect maximum width setting
readonly property real dynamicWidth: {
// If using fixed width mode, always use maxWidth
if (useFixedWidth) {
return maxWidth
return maxWidth;
}
// Otherwise, adapt to content
if (!hasFocusedWindow) {
return Math.min(calculateContentWidth(), maxWidth)
return Math.min(calculateContentWidth(), maxWidth);
}
// Use content width but don't exceed user-set maximum width
return Math.min(calculateContentWidth(), maxWidth)
return Math.min(calculateContentWidth(), maxWidth);
}
function getAppIcon() {
try {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow()
const focusedWindow = CompositorService.getFocusedWindow();
if (focusedWindow && focusedWindow.appId) {
try {
const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
const idValue = focusedWindow.appId;
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
if (iconResult && iconResult !== "") {
return iconResult
return iconResult;
}
} catch (iconError) {
Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError)
Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError);
}
}
@@ -134,25 +134,25 @@ Item {
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
const activeToplevel = ToplevelManager.activeToplevel;
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase())
const idValue2 = activeToplevel.appId;
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2);
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase());
if (iconResult2 && iconResult2 !== "") {
return iconResult2
return iconResult2;
}
}
} catch (fallbackError) {
Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError);
}
}
}
return ThemeIcons.iconFromName(fallbackIcon)
return ThemeIcons.iconFromName(fallbackIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in getAppIcon:", e)
return ThemeIcons.iconFromName(fallbackIcon)
Logger.w("ActiveWindow", "Error in getAppIcon:", e);
return ThemeIcons.iconFromName(fallbackIcon);
}
}
@@ -228,10 +228,10 @@ Item {
id: titleContainer
Layout.preferredWidth: {
// Calculate available width based on other elements
var iconWidth = (showIcon && windowIcon.visible ? (18 + Style.marginS) : 0)
var totalMargins = Style.marginXXS * 2
var availableWidth = mainContainer.width - iconWidth - totalMargins
return Math.max(20, availableWidth)
var iconWidth = (showIcon && windowIcon.visible ? (18 + Style.marginS) : 0);
var totalMargins = Style.marginXXS * 2;
var availableWidth = mainContainer.width - iconWidth - totalMargins;
return Math.max(20, availableWidth);
}
Layout.maximumWidth: Layout.preferredWidth
Layout.alignment: Qt.AlignVCenter
@@ -252,8 +252,8 @@ Item {
repeat: false
onTriggered: {
if (scrollingMode === "always" && titleContainer.needsScrolling) {
titleContainer.isScrolling = true
titleContainer.isResetting = false
titleContainer.isScrolling = true;
titleContainer.isResetting = false;
}
}
}
@@ -261,29 +261,29 @@ Item {
// Update scrolling state based on mode
property var updateScrollingState: function () {
if (scrollingMode === "never") {
isScrolling = false
isResetting = false
isScrolling = false;
isResetting = false;
} else if (scrollingMode === "always") {
if (needsScrolling) {
if (mouseArea.containsMouse) {
isScrolling = false
isResetting = true
isScrolling = false;
isResetting = true;
} else {
scrollStartTimer.restart()
scrollStartTimer.restart();
}
} else {
scrollStartTimer.stop()
isScrolling = false
isResetting = false
scrollStartTimer.stop();
isScrolling = false;
isResetting = false;
}
} else if (scrollingMode === "hover") {
if (mouseArea.containsMouse && needsScrolling) {
isScrolling = true
isResetting = false
isScrolling = true;
isResetting = false;
} else {
isScrolling = false
isScrolling = false;
if (needsScrolling) {
isResetting = true
isResetting = true;
}
}
}
@@ -296,7 +296,7 @@ Item {
Connections {
target: mouseArea
function onContainsMouseChanged() {
titleContainer.updateScrollingState()
titleContainer.updateScrollingState();
}
}
@@ -322,10 +322,10 @@ Item {
color: Color.mOnSurface
onTextChanged: {
if (root.scrollingMode === "always") {
titleContainer.isScrolling = false
titleContainer.isResetting = false
scrollContainer.scrollX = 0
scrollStartTimer.restart()
titleContainer.isScrolling = false;
titleContainer.isResetting = false;
scrollContainer.scrollX = 0;
scrollStartTimer.restart();
}
}
}
@@ -349,7 +349,7 @@ Item {
duration: 300
easing.type: Easing.OutQuad
onFinished: {
titleContainer.isResetting = false
titleContainer.isResetting = false;
}
}
@@ -412,11 +412,11 @@ Item {
acceptedButtons: Qt.LeftButton
onEntered: {
if ((windowTitle !== "") && isVerticalBar || (scrollingMode === "never")) {
TooltipService.show(Screen, root, windowTitle, BarService.getTooltipDirection())
TooltipService.show(Screen, root, windowTitle, BarService.getTooltipDirection());
}
}
onExited: {
TooltipService.hide()
TooltipService.hide();
}
}
}
@@ -426,18 +426,18 @@ Item {
target: CompositorService
function onActiveWindowChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
windowIcon.source = Qt.binding(getAppIcon);
windowIconVertical.source = Qt.binding(getAppIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e)
Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e);
}
}
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
windowIcon.source = Qt.binding(getAppIcon);
windowIconVertical.source = Qt.binding(getAppIcon);
} catch (e) {
Logger.w("ActiveWindow", "Error in onWindowListChanged:", e)
Logger.w("ActiveWindow", "Error in onWindowListChanged:", e);
}
}
}
+18 -18
View File
@@ -1,8 +1,8 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
@@ -22,12 +22,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
@@ -38,16 +38,16 @@ Item {
readonly property color fillColor: {
switch (colorName) {
case "primary":
return Color.mPrimary
return Color.mPrimary;
case "secondary":
return Color.mSecondary
return Color.mSecondary;
case "tertiary":
return Color.mTertiary
return Color.mTertiary;
case "error":
return Color.mError
return Color.mError;
case "onSurface":
default:
return Color.mOnSurface
return Color.mOnSurface;
}
}
@@ -92,13 +92,13 @@ Item {
sourceComponent: {
switch (currentVisualizerType) {
case "linear":
return linearComponent
return linearComponent;
case "mirrored":
return mirroredComponent
return mirroredComponent;
case "wave":
return waveComponent
return waveComponent;
default:
return null
return null;
}
}
}
@@ -112,13 +112,13 @@ Item {
acceptedButtons: Qt.LeftButton
onClicked: mouse => {
const types = ["linear", "mirrored", "wave"]
const currentIndex = types.indexOf(currentVisualizerType)
const nextIndex = (currentIndex + 1) % types.length
const newType = types[nextIndex]
const types = ["linear", "mirrored", "wave"];
const currentIndex = types.indexOf(currentVisualizerType);
const nextIndex = (currentIndex + 1) % types.length;
const newType = types[nextIndex];
// Update settings directly, maybe this should be a widget setting...
Settings.data.audio.visualizerType = newType
Settings.data.audio.visualizerType = newType;
}
}
+30 -31
View File
@@ -1,12 +1,12 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Services.Hardware
import qs.Widgets
import qs.Modules.Bar.Extras
import qs.Services.Hardware
import qs.Services.UI
import qs.Widgets
Item {
id: root
@@ -22,12 +22,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -53,13 +53,13 @@ Item {
function maybeNotify(percent, charging) {
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
root.hasNotifiedLowBattery = true;
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
"percent": Math.round(percent)
}))
}));
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
root.hasNotifiedLowBattery = false;
}
}
@@ -67,20 +67,20 @@ Item {
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
var currentPercent = UPower.displayDevice.percentage * 100
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
root.maybeNotify(currentPercent, isCharging)
var currentPercent = UPower.displayDevice.percentage * 100;
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging;
root.maybeNotify(currentPercent, isCharging);
}
function onStateChanged() {
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging;
// Reset notification flag when charging starts
if (isCharging) {
root.hasNotifiedLowBattery = false
root.hasNotifiedLowBattery = false;
}
// Also re-evaluate maybeNotify, as state might have changed
var currentPercent = UPower.displayDevice.percentage * 100
root.maybeNotify(currentPercent, isCharging)
var currentPercent = UPower.displayDevice.percentage * 100;
root.maybeNotify(currentPercent, isCharging);
}
}
@@ -95,51 +95,50 @@ Item {
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery)
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
tooltipText: {
let lines = []
let lines = [];
if (testMode) {
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
return lines.join("\n")
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`);
return lines.join("\n");
}
if (!isReady || !battery.isLaptopBattery) {
return I18n.tr("battery.no-battery-detected")
return I18n.tr("battery.no-battery-detected");
}
if (battery.timeToEmpty > 0) {
lines.push(I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
}))
}));
}
if (battery.timeToFull > 0) {
lines.push(I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
}))
}));
}
if (battery.changeRate !== undefined) {
const rate = battery.changeRate
const rate = battery.changeRate;
if (rate > 0) {
lines.push(charging ? I18n.tr("battery.charging-rate", {
"rate": rate.toFixed(2)
}) : I18n.tr("battery.discharging-rate", {
"rate": rate.toFixed(2)
}))
}));
} else if (rate < 0) {
lines.push(I18n.tr("battery.discharging-rate", {
"rate": Math.abs(rate).toFixed(2)
}))
}));
} else {
// Rate is 0 - check if plugged in (charging state) or idle
lines.push(charging ? I18n.tr("battery.plugged-in") : I18n.tr("battery.idle"))
lines.push(charging ? I18n.tr("battery.plugged-in") : I18n.tr("battery.idle"));
}
} else {
lines.push(charging ? I18n.tr("battery.charging") : I18n.tr("battery.discharging"))
lines.push(charging ? I18n.tr("battery.charging") : I18n.tr("battery.discharging"));
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push(I18n.tr("battery.health", {
"percent": Math.round(battery.healthPercentage)
}))
}));
}
return lines.join("\n")
return lines.join("\n");
}
}
}
+10 -10
View File
@@ -19,12 +19,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -41,16 +41,16 @@ Item {
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
text: {
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) {
const firstDevice = BluetoothService.connectedDevices[0]
return firstDevice.name || firstDevice.deviceName
const firstDevice = BluetoothService.connectedDevices[0];
return firstDevice.name || firstDevice.deviceName;
}
return ""
return "";
}
suffix: {
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) {
return ` + ${BluetoothService.connectedDevices.length - 1}`
return ` + ${BluetoothService.connectedDevices.length - 1}`;
}
return ""
return "";
}
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
@@ -59,9 +59,9 @@ Item {
onRightClicked: BluetoothService.setBluetoothEnabled(!BluetoothService.enabled)
tooltipText: {
if (pill.text !== "") {
return pill.text
return pill.text;
}
return I18n.tr("tooltips.bluetooth-devices")
return I18n.tr("tooltips.bluetooth-devices");
}
}
}
+26 -26
View File
@@ -21,12 +21,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -40,13 +40,13 @@ Item {
visible: getMonitor() !== null
function getMonitor() {
return BrightnessService.getMonitorForScreen(screen) || null
return BrightnessService.getMonitorForScreen(screen) || null;
}
function getIcon() {
var monitor = getMonitor()
var brightness = monitor ? monitor.brightness : 0
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
var monitor = getMonitor();
var brightness = monitor ? monitor.brightness : 0;
return brightness <= 0.5 ? "brightness-low" : "brightness-high";
}
// Connection used to open the pill when brightness changes
@@ -57,12 +57,12 @@ Item {
// Ignore if this is the first time we receive an update.
// Most likely service just kicked off.
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
return
firstBrightnessReceived = true;
return;
}
pill.show()
hideTimerAfterChange.restart()
pill.show();
hideTimerAfterChange.restart();
}
}
@@ -82,42 +82,42 @@ Item {
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {
var monitor = getMonitor()
return monitor ? Math.round(monitor.brightness * 100) : ""
var monitor = getMonitor();
return monitor ? Math.round(monitor.brightness * 100) : "";
}
suffix: text.length > 0 ? "%" : "-"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: {
var monitor = getMonitor()
var monitor = getMonitor();
if (!monitor)
return ""
return "";
return I18n.tr("tooltips.brightness-at", {
"brightness": Math.round(monitor.brightness * 100)
})
});
}
onWheel: function (angle) {
var monitor = getMonitor()
var monitor = getMonitor();
if (!monitor)
return
return;
if (angle > 0) {
monitor.increaseBrightness()
monitor.increaseBrightness();
} else if (angle < 0) {
monitor.decreaseBrightness()
monitor.decreaseBrightness();
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
}
}
}
+9 -9
View File
@@ -20,12 +20,12 @@ Rectangle {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string barPosition: Settings.data.bar.position
@@ -70,9 +70,9 @@ Rectangle {
Binding on pointSize {
value: {
if (repeater.model.length == 1) {
return Style.fontSizeS * scaling
return Style.fontSizeS * scaling;
} else {
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling;
}
}
}
@@ -122,15 +122,15 @@ Rectangle {
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel", screen)?.active) {
TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection());
}
}
onExited: {
TooltipService.hide()
TooltipService.hide();
}
onClicked: {
TooltipService.hide()
PanelService.getPanel("calendarPanel", screen)?.toggle(this)
TooltipService.hide();
PanelService.getPanel("calendarPanel", screen)?.toggle(this);
}
}
}
+14 -14
View File
@@ -1,11 +1,11 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import QtQuick.Effects
import qs.Commons
import qs.Widgets
import qs.Services.UI
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
@@ -21,12 +21,12 @@ NIconButton {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
@@ -34,8 +34,8 @@ NIconButton {
readonly property string customIconPath: widgetSettings.customIconPath || ""
readonly property bool colorizeDistroLogo: {
if (widgetSettings.colorizeDistroLogo !== undefined)
return widgetSettings.colorizeDistroLogo
return widgetMetadata.colorizeDistroLogo !== undefined ? widgetMetadata.colorizeDistroLogo : false
return widgetSettings.colorizeDistroLogo;
return widgetMetadata.colorizeDistroLogo !== undefined ? widgetMetadata.colorizeDistroLogo : false;
}
// If we have a custom path or distro logo, don't use the theme icon.
@@ -51,12 +51,12 @@ NIconButton {
colorBorder: Color.transparent
colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent
onClicked: {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen)
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Willopen the panel next to the bar button.
controlCenterPanel?.toggle(this)
controlCenterPanel?.toggle(this);
} else {
controlCenterPanel?.toggle()
controlCenterPanel?.toggle();
}
}
onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle()
@@ -69,10 +69,10 @@ NIconButton {
height: width
source: {
if (customIconPath !== "")
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath;
if (useDistroLogo)
return HostService.osLogo
return ""
return HostService.osLogo;
return "";
}
visible: source !== ""
smooth: true
+62 -62
View File
@@ -3,10 +3,10 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.UI
import qs.Widgets
import qs.Modules.Panels.Settings
import qs.Modules.Bar.Extras
Item {
id: root
@@ -22,12 +22,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -63,31 +63,31 @@ Item {
autoHide: false
forceOpen: _dynamicText !== ""
tooltipText: {
var tooltipLines = []
var tooltipLines = [];
if (hasExec) {
if (leftClickExec !== "") {
tooltipLines.push(`Left click: ${leftClickExec}.`)
tooltipLines.push(`Left click: ${leftClickExec}.`);
}
if (rightClickExec !== "") {
tooltipLines.push(`Right click: ${rightClickExec}.`)
tooltipLines.push(`Right click: ${rightClickExec}.`);
}
if (middleClickExec !== "") {
tooltipLines.push(`Middle click: ${middleClickExec}.`)
tooltipLines.push(`Middle click: ${middleClickExec}.`);
}
}
if (_dynamicTooltip !== "") {
if (tooltipLines.length > 0) {
tooltipLines.push("")
tooltipLines.push("");
}
tooltipLines.push(_dynamicTooltip)
tooltipLines.push(_dynamicTooltip);
}
if (tooltipLines.length === 0) {
return "Custom button, configure in settings."
return "Custom button, configure in settings.";
} else {
return tooltipLines.join("\n")
return tooltipLines.join("\n");
}
}
@@ -135,121 +135,121 @@ Item {
stderr: StdioCollector {}
onExited: (exitCode, exitStatus) => {
if (textStream) {
Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`)
return
Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`);
return;
}
}
}
function parseDynamicContent(content) {
var contentStr = String(content || "").trim()
var contentStr = String(content || "").trim();
if (parseJson) {
var lineToParse = contentStr
var lineToParse = contentStr;
if (!textStream && contentStr.includes('\n')) {
const lines = contentStr.split('\n').filter(line => line.trim() !== '')
const lines = contentStr.split('\n').filter(line => line.trim() !== '');
if (lines.length > 0) {
lineToParse = lines[lines.length - 1]
lineToParse = lines[lines.length - 1];
}
}
try {
const parsed = JSON.parse(lineToParse)
const text = parsed.text || ""
const icon = parsed.icon || ""
const tooltip = parsed.tooltip || ""
const parsed = JSON.parse(lineToParse);
const text = parsed.text || "";
const icon = parsed.icon || "";
const tooltip = parsed.tooltip || "";
if (checkCollapse(text)) {
_dynamicText = ""
_dynamicIcon = ""
_dynamicTooltip = ""
return
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
return;
}
_dynamicText = text
_dynamicIcon = icon
_dynamicTooltip = tooltip
return
_dynamicText = text;
_dynamicIcon = icon;
_dynamicTooltip = tooltip;
return;
} catch (e) {
Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`)
Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`);
}
}
if (checkCollapse(contentStr)) {
_dynamicText = ""
_dynamicIcon = ""
_dynamicTooltip = ""
return
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
return;
}
_dynamicText = contentStr
_dynamicIcon = ""
_dynamicTooltip = ""
_dynamicText = contentStr;
_dynamicIcon = "";
_dynamicTooltip = "";
}
function checkCollapse(text) {
if (!textCollapse || textCollapse.length === 0) {
return false
return false;
}
if (textCollapse.startsWith("/") && textCollapse.endsWith("/") && textCollapse.length > 1) {
// Treat as regex
var pattern = textCollapse.substring(1, textCollapse.length - 1)
var pattern = textCollapse.substring(1, textCollapse.length - 1);
try {
var regex = new RegExp(pattern)
return regex.test(text)
var regex = new RegExp(pattern);
return regex.test(text);
} catch (e) {
Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`)
return (textCollapse === text) // Fallback to exact match on invalid regex
Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`);
return (textCollapse === text); // Fallback to exact match on invalid regex
}
} else {
// Treat as plain string
return (textCollapse === text)
return (textCollapse === text);
}
}
function onClicked() {
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec])
Logger.i("CustomButton", `Executing command: ${leftClickExec}`)
Quickshell.execDetached(["sh", "-c", leftClickExec]);
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
} else if (!hasExec && !leftClickUpdateText) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
settingsPanel.open()
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Bar;
settingsPanel.open();
}
if (!textStream && leftClickUpdateText) {
runTextCommand()
runTextCommand();
}
}
function onRightClicked() {
if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", rightClickExec])
Logger.i("CustomButton", `Executing command: ${rightClickExec}`)
Quickshell.execDetached(["sh", "-c", rightClickExec]);
Logger.i("CustomButton", `Executing command: ${rightClickExec}`);
}
if (!textStream && rightClickUpdateText) {
runTextCommand()
runTextCommand();
}
}
function onMiddleClicked() {
if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", middleClickExec])
Logger.i("CustomButton", `Executing command: ${middleClickExec}`)
Quickshell.execDetached(["sh", "-c", middleClickExec]);
Logger.i("CustomButton", `Executing command: ${middleClickExec}`);
}
if (!textStream && middleClickUpdateText) {
runTextCommand()
runTextCommand();
}
}
function runTextCommand() {
if (!textCommand || textCommand.length === 0)
return
return;
if (textProc.running)
return
textProc.command = ["sh", "-lc", textCommand]
textProc.running = true
return;
textProc.command = ["sh", "-lc", textCommand];
textProc.running = true;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
+11 -11
View File
@@ -2,10 +2,10 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Power
import qs.Services.UI
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
@@ -21,12 +21,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -47,20 +47,20 @@ Item {
forceOpen: IdleInhibitorService.timeout !== null
forceClose: IdleInhibitorService.timeout == null
onWheel: function (delta) {
var sign = delta > 0 ? 1 : -1
var sign = delta > 0 ? 1 : -1;
// the offset makes scrolling down feel symmetrical to scrolling up
var timeout = IdleInhibitorService.timeout - (delta < 0 ? 60 : 0)
var timeout = IdleInhibitorService.timeout - (delta < 0 ? 60 : 0);
if (timeout == null || timeout < 600) {
delta = 60 // <= 10m, increment at 1m interval
delta = 60; // <= 10m, increment at 1m interval
} else if (timeout >= 600 && timeout < 1800) {
delta = 300 // >= 10m, increment at 5m interval
delta = 300; // >= 10m, increment at 5m interval
} else if (timeout >= 1800 && timeout < 3600) {
delta = 600 // >= 30m, increment at 10m interval
delta = 600; // >= 30m, increment at 10m interval
} else if (timeout >= 3600) {
delta = 1800 // > 1h, increment at 30m interval
delta = 1800; // > 1h, increment at 30m interval
}
IdleInhibitorService.changeTimeout(delta * sign)
IdleInhibitorService.changeTimeout(delta * sign);
}
}
}
+9 -9
View File
@@ -1,13 +1,13 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Services.Keyboard
import qs.Widgets
import qs.Modules.Bar.Extras
import qs.Services.Keyboard
import qs.Services.UI
import qs.Widgets
Item {
id: root
@@ -23,12 +23,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
@@ -53,9 +53,9 @@ Item {
})
forceOpen: root.displayMode === "forceOpen"
forceClose: root.displayMode === "alwaysHide"
onClicked: {
onClicked:
// You could open keyboard settings here if needed.
}
{}
}
}
+3 -3
View File
@@ -20,12 +20,12 @@ Rectangle {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string barPosition: Settings.data.bar.position
+61 -61
View File
@@ -2,11 +2,11 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Widgets.AudioSpectrum
import qs.Commons
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
Item {
id: root
@@ -23,12 +23,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isVerticalBar: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right")
@@ -50,18 +50,18 @@ Item {
readonly property string placeholderText: I18n.tr("bar.widget-settings.media-mini.no-active-player")
readonly property string tooltipText: {
var title = getTitle()
var controls = ""
var title = getTitle();
var controls = "";
if (MediaService.canGoNext) {
controls += "Right click for next.\n"
controls += "Right click for next.\n";
}
if (MediaService.canGoPrevious) {
controls += "Middle click for previous."
controls += "Middle click for previous.";
}
if (controls !== "") {
return title + "\n\n" + controls
return title + "\n\n" + controls;
}
return title
return title;
}
// Hide conditions
@@ -103,53 +103,53 @@ Item {
}
function calculatedVerticalDimension() {
return Math.round((Style.baseWidgetSize - 5) * scaling)
return Math.round((Style.baseWidgetSize - 5) * scaling);
}
function calculateContentWidth() {
// Calculate the actual content width based on visible elements
var contentWidth = 0
var margins = Style.marginS * scaling * 2 // Left and right margins
var contentWidth = 0;
var margins = Style.marginS * scaling * 2; // Left and right margins
// Icon or album art width
if (!hasActivePlayer || !showAlbumArt) {
// Icon width
contentWidth += Math.round(18 * scaling)
contentWidth += Math.round(18 * scaling);
} else if (showAlbumArt && hasActivePlayer) {
// Album art width
contentWidth += 21 * scaling
contentWidth += 21 * scaling;
}
// Spacing between icon/art and text; only if there is text
if (fullTitleMetrics.contentWidth > 0) {
contentWidth += Style.marginS * scaling
contentWidth += Style.marginS * scaling;
// Text width (use the measured width)
contentWidth += fullTitleMetrics.contentWidth
contentWidth += fullTitleMetrics.contentWidth;
// Additional small margin for text
contentWidth += Style.marginXXS * 2
contentWidth += Style.marginXXS * 2;
}
// Add container margins
contentWidth += margins
contentWidth += margins;
return Math.ceil(contentWidth)
return Math.ceil(contentWidth);
}
// Dynamic width: adapt to content but respect maximum width setting
readonly property real dynamicWidth: {
// If using fixed width mode, always use maxWidth
if (useFixedWidth) {
return maxWidth
return maxWidth;
}
// Otherwise, adapt to content
if (!hasActivePlayer) {
// Keep compact when no active player
return calculateContentWidth()
return calculateContentWidth();
}
// Use content width but don't exceed user-set maximum width
return Math.min(calculateContentWidth(), maxWidth)
return Math.min(calculateContentWidth(), maxWidth);
}
// A hidden text element to safely measure the full title width
@@ -284,11 +284,11 @@ Item {
id: titleContainer
Layout.preferredWidth: {
// Calculate available width based on other elements in the row
var iconWidth = (windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0)
var albumArtWidth = (hasActivePlayer && showAlbumArt ? (21 * scaling + Style.marginS * scaling) : 0)
var totalMargins = Style.marginXXS * 2
var availableWidth = mainContainer.width - iconWidth - albumArtWidth - totalMargins
return Math.max(20, availableWidth)
var iconWidth = (windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0);
var albumArtWidth = (hasActivePlayer && showAlbumArt ? (21 * scaling + Style.marginS * scaling) : 0);
var totalMargins = Style.marginXXS * 2;
var availableWidth = mainContainer.width - iconWidth - albumArtWidth - totalMargins;
return Math.max(20, availableWidth);
}
Layout.maximumWidth: Layout.preferredWidth
Layout.alignment: Qt.AlignVCenter
@@ -309,8 +309,8 @@ Item {
repeat: false
onTriggered: {
if (scrollingMode === "always" && titleContainer.needsScrolling) {
titleContainer.isScrolling = true
titleContainer.isResetting = false
titleContainer.isScrolling = true;
titleContainer.isResetting = false;
}
}
}
@@ -318,48 +318,48 @@ Item {
// Update scrolling state based on mode
property var updateScrollingState: function () {
if (scrollingMode === "never") {
isScrolling = false
isResetting = false
isScrolling = false;
isResetting = false;
} else if (scrollingMode === "always") {
if (needsScrolling) {
if (mouseArea.containsMouse) {
isScrolling = false
isResetting = true
isScrolling = false;
isResetting = true;
} else {
scrollStartTimer.restart()
scrollStartTimer.restart();
}
} else {
scrollStartTimer.stop()
isScrolling = false
isResetting = false
scrollStartTimer.stop();
isScrolling = false;
isResetting = false;
}
} else if (scrollingMode === "hover") {
if (mouseArea.containsMouse && needsScrolling) {
isScrolling = true
isResetting = false
isScrolling = true;
isResetting = false;
} else {
isScrolling = false
isScrolling = false;
if (needsScrolling) {
isResetting = true
isResetting = true;
}
}
}
}
onWidthChanged: {
containerWidth = width
updateScrollingState()
containerWidth = width;
updateScrollingState();
}
Component.onCompleted: {
containerWidth = width
updateScrollingState()
containerWidth = width;
updateScrollingState();
}
Connections {
target: mouseArea
function onContainsMouseChanged() {
titleContainer.updateScrollingState()
titleContainer.updateScrollingState();
}
}
@@ -385,11 +385,11 @@ Item {
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
onTextChanged: {
titleContainer.isScrolling = false
titleContainer.isResetting = false
scrollContainer.scrollX = 0
titleContainer.isScrolling = false;
titleContainer.isResetting = false;
scrollContainer.scrollX = 0;
if (titleContainer.needsScrolling) {
scrollStartTimer.restart()
scrollStartTimer.restart();
}
}
}
@@ -413,7 +413,7 @@ Item {
duration: 300
easing.type: Easing.OutQuad
onFinished: {
titleContainer.isResetting = false
titleContainer.isResetting = false;
}
}
@@ -467,28 +467,28 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!hasActivePlayer || !MediaService.currentPlayer || !MediaService.canPlay) {
return
return;
}
if (mouse.button === Qt.LeftButton) {
MediaService.playPause()
MediaService.playPause();
} else if (mouse.button == Qt.RightButton) {
MediaService.next()
TooltipService.hide()
MediaService.next();
TooltipService.hide();
} else if (mouse.button == Qt.MiddleButton) {
MediaService.previous()
TooltipService.hide()
MediaService.previous();
TooltipService.hide();
}
}
onEntered: {
var textToShow = hasActivePlayer ? tooltipText : placeholderText
var textToShow = hasActivePlayer ? tooltipText : placeholderText;
if ((textToShow !== "") && isVerticalBar || (scrollingMode === "never")) {
TooltipService.show(Screen, root, textToShow, BarService.getTooltipDirection())
TooltipService.show(Screen, root, textToShow, BarService.getTooltipDirection());
}
}
onExited: {
TooltipService.hide()
TooltipService.hide();
}
}
}
+21 -21
View File
@@ -23,12 +23,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -48,13 +48,13 @@ Item {
// Logger.i("Bar:Microphone", "onInputVolumeChanged")
if (!firstInputVolumeReceived) {
// Ignore the first volume change
firstInputVolumeReceived = true
firstInputVolumeReceived = true;
} else {
// If a tooltip is visible while we show the pill
// hide it so it doesn't overlap the volume slider.
TooltipService.hide()
pill.show()
externalHideTimer.restart()
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
@@ -66,11 +66,11 @@ Item {
// Logger.i("Bar:Microphone", "onInputMutedChanged")
if (!firstInputVolumeReceived) {
// Ignore the first mute change
firstInputVolumeReceived = true
firstInputVolumeReceived = true;
} else {
TooltipService.hide()
pill.show()
externalHideTimer.restart()
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
@@ -80,7 +80,7 @@ Item {
running: false
interval: 1500
onTriggered: {
pill.hide()
pill.hide();
}
}
@@ -101,25 +101,25 @@ Item {
onWheel: function (delta) {
// As soon as we start scrolling to adjust volume, hide the tooltip
TooltipService.hide()
TooltipService.hide();
wheelAccumulator += delta
wheelAccumulator += delta;
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume)
wheelAccumulator = 0;
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume);
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume)
wheelAccumulator = 0;
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume);
}
}
onClicked: {
PanelService.getPanel("audioPanel", screen)?.toggle(this)
PanelService.getPanel("audioPanel", screen)?.toggle(this);
}
onRightClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
AudioService.setInputMuted(!AudioService.inputMuted);
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])
Quickshell.execDetached(["pwvucontrol"]);
}
}
}
+11 -11
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
@@ -28,24 +28,24 @@ NIconButton {
onClicked: {
// Check if wlsunset is available before enabling night light
if (!ProgramCheckerService.wlsunsetAvailable) {
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
return
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"));
return;
}
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
Settings.data.nightLight.enabled = true;
Settings.data.nightLight.forced = false;
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
Settings.data.nightLight.forced = true;
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
Settings.data.nightLight.enabled = false;
Settings.data.nightLight.forced = false;
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
@@ -26,6 +26,6 @@ NIconButton {
tooltipText: PowerProfileService.noctaliaPerformanceMode ? I18n.tr("tooltips.noctalia-performance-enabled") : I18n.tr("tooltips.noctalia-performance-disabled")
tooltipDirection: BarService.getTooltipDirection()
onClicked: {
PowerProfileService.toggleNoctaliaPerformance()
PowerProfileService.toggleNoctaliaPerformance();
}
}
+14 -14
View File
@@ -1,11 +1,11 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
@@ -22,27 +22,27 @@ NIconButton {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
function computeUnreadCount() {
var since = NotificationService.lastSeenTs
var count = 0
var model = NotificationService.historyList
var since = NotificationService.lastSeenTs;
var count = 0;
var model = NotificationService.historyList;
for (var i = 0; i < model.count; i++) {
var item = model.get(i)
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
var item = model.get(i);
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp;
if (ts > since)
count++
count++;
}
return count
return count;
}
baseSize: Style.capsuleHeight
@@ -57,8 +57,8 @@ NIconButton {
colorBorderHover: Color.transparent
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel", screen)
panel?.toggle(this)
var panel = PanelService.getPanel("notificationHistoryPanel", screen);
panel?.toggle(this);
}
onRightClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb
+4 -4
View File
@@ -1,8 +1,8 @@
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Services.Media
import qs.Services.System
import qs.Services.UI
import qs.Widgets
// Screen Recording Indicator
@@ -24,10 +24,10 @@ NIconButton {
function handleClick() {
if (!ScreenRecorderService.isAvailable) {
ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc"))
return
ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc"));
return;
}
ScreenRecorderService.toggleRecording()
ScreenRecorderService.toggleRecording();
}
onClicked: handleClick()
+8 -8
View File
@@ -19,12 +19,12 @@ NIconButton {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string colorName: widgetSettings.colorName !== undefined ? widgetSettings.colorName : widgetMetadata.colorName
@@ -32,16 +32,16 @@ NIconButton {
readonly property color iconColor: {
switch (colorName) {
case "primary":
return Color.mPrimary
return Color.mPrimary;
case "secondary":
return Color.mSecondary
return Color.mSecondary;
case "tertiary":
return Color.mTertiary
return Color.mTertiary;
case "error":
return Color.mError
return Color.mError;
case "onSurface":
default:
return Color.mOnSurface
return Color.mOnSurface;
}
}
+3 -3
View File
@@ -19,12 +19,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
// Use settings or defaults from BarWidgetRegistry
+29 -29
View File
@@ -3,8 +3,8 @@ import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.UI
import qs.Services.System
import qs.Services.UI
import qs.Widgets
Rectangle {
@@ -21,12 +21,12 @@ Rectangle {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string barPosition: Settings.data.bar.position
@@ -43,8 +43,8 @@ Rectangle {
readonly property real iconSize: textSize * 1.4
readonly property real textSize: {
var base = isVertical ? width * 0.82 : height
return Math.max(1, (density === "compact") ? base * 0.43 : base * 0.33)
var base = isVertical ? width * 0.82 : height;
return Math.max(1, (density === "compact") ? base * 0.43 : base * 0.33);
}
// Match Workspace widget pill sizing: base ratio depends on bar density
@@ -178,11 +178,11 @@ Rectangle {
anchors.centerIn: parent
onLoaded: {
item.warning = Qt.binding(() => cpuWarning)
item.critical = Qt.binding(() => cpuCritical)
item.indicatorWidth = Qt.binding(() => cpuUsageContainer.width)
item.warningColor = Qt.binding(() => root.warningColor)
item.criticalColor = Qt.binding(() => root.criticalColor)
item.warning = Qt.binding(() => cpuWarning);
item.critical = Qt.binding(() => cpuCritical);
item.indicatorWidth = Qt.binding(() => cpuUsageContainer.width);
item.warningColor = Qt.binding(() => root.warningColor);
item.criticalColor = Qt.binding(() => root.criticalColor);
}
}
@@ -214,11 +214,11 @@ Rectangle {
NText {
text: {
let usage = Math.round(SystemStatService.cpuUsage)
let usage = Math.round(SystemStatService.cpuUsage);
if (usage < 100) {
return `${usage}%`
return `${usage}%`;
} else {
return usage
return usage;
}
}
family: Settings.data.ui.fontFixed
@@ -252,11 +252,11 @@ Rectangle {
anchors.centerIn: parent
onLoaded: {
item.warning = Qt.binding(() => tempWarning)
item.critical = Qt.binding(() => tempCritical)
item.indicatorWidth = Qt.binding(() => cpuTempContainer.width)
item.warningColor = Qt.binding(() => root.warningColor)
item.criticalColor = Qt.binding(() => root.criticalColor)
item.warning = Qt.binding(() => tempWarning);
item.critical = Qt.binding(() => tempCritical);
item.indicatorWidth = Qt.binding(() => cpuTempContainer.width);
item.warningColor = Qt.binding(() => root.warningColor);
item.criticalColor = Qt.binding(() => root.criticalColor);
}
}
@@ -319,11 +319,11 @@ Rectangle {
anchors.centerIn: parent
onLoaded: {
item.warning = Qt.binding(() => memWarning)
item.critical = Qt.binding(() => memCritical)
item.indicatorWidth = Qt.binding(() => memoryContainer.width)
item.warningColor = Qt.binding(() => root.warningColor)
item.criticalColor = Qt.binding(() => root.criticalColor)
item.warning = Qt.binding(() => memWarning);
item.critical = Qt.binding(() => memCritical);
item.indicatorWidth = Qt.binding(() => memoryContainer.width);
item.warningColor = Qt.binding(() => root.warningColor);
item.criticalColor = Qt.binding(() => root.criticalColor);
}
}
@@ -472,11 +472,11 @@ Rectangle {
anchors.centerIn: parent
onLoaded: {
item.warning = Qt.binding(() => diskWarning)
item.critical = Qt.binding(() => diskCritical)
item.indicatorWidth = Qt.binding(() => diskContainer.width)
item.warningColor = Qt.binding(() => root.warningColor)
item.criticalColor = Qt.binding(() => root.criticalColor)
item.warning = Qt.binding(() => diskWarning);
item.critical = Qt.binding(() => diskCritical);
item.indicatorWidth = Qt.binding(() => diskContainer.width);
item.warningColor = Qt.binding(() => root.warningColor);
item.criticalColor = Qt.binding(() => root.criticalColor);
}
}
+25 -27
View File
@@ -2,8 +2,8 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
@@ -27,12 +27,12 @@ Rectangle {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
property bool hasWindow: false
@@ -42,35 +42,35 @@ Rectangle {
function updateHasWindow() {
try {
var total = CompositorService.windows.count || 0
var total = CompositorService.windows.count || 0;
var activeIds = CompositorService.getActiveWorkspaces().map(function (ws) {
return ws.id
})
var found = false
return ws.id;
});
var found = false;
for (var i = 0; i < total; i++) {
var w = CompositorService.windows.get(i)
var w = CompositorService.windows.get(i);
if (!w)
continue
var passOutput = (!onlySameOutput) || (w.output == screen.name)
var passWorkspace = (!onlyActiveWorkspaces) || (activeIds.includes(w.workspaceId))
continue;
var passOutput = (!onlySameOutput) || (w.output == screen.name);
var passWorkspace = (!onlyActiveWorkspaces) || (activeIds.includes(w.workspaceId));
if (passOutput && passWorkspace) {
found = true
break
found = true;
break;
}
}
hasWindow = found
hasWindow = found;
} catch (e) {
hasWindow = false
hasWindow = false;
}
}
Connections {
target: CompositorService
function onWindowListChanged() {
updateHasWindow()
updateHasWindow();
}
function onWorkspaceChanged() {
updateHasWindow()
updateHasWindow();
}
}
@@ -117,7 +117,7 @@ Rectangle {
property ShellScreen screen: root.screen
visible: (!onlySameOutput || modelData.output == screen.name) && (!onlyActiveWorkspaces || CompositorService.getActiveWorkspaces().map(function (ws) {
return ws.id
return ws.id;
}).includes(modelData.workspaceId))
Layout.preferredWidth: root.itemSize
@@ -125,7 +125,6 @@ Rectangle {
Layout.alignment: Qt.AlignCenter
IconImage {
id: appIcon
width: parent.width
height: parent.height
@@ -144,10 +143,10 @@ Rectangle {
}
Rectangle {
id: iconBackground
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
id: iconBackground
width: 4
height: 4
color: modelData.isFocused ? Color.mPrimary : Color.transparent
@@ -163,19 +162,18 @@ Rectangle {
onPressed: function (mouse) {
if (!taskbarItem.modelData)
return
return;
if (mouse.button === Qt.LeftButton) {
try {
CompositorService.focusWindow(taskbarItem.modelData)
CompositorService.focusWindow(taskbarItem.modelData);
} catch (error) {
Logger.e("Taskbar", "Failed to activate toplevel: " + error)
Logger.e("Taskbar", "Failed to activate toplevel: " + error);
}
} else if (mouse.button === Qt.RightButton) {
try {
CompositorService.closeWindow(taskbarItem.modelData)
CompositorService.closeWindow(taskbarItem.modelData);
} catch (error) {
Logger.e("Taskbar", "Failed to close toplevel: " + error)
Logger.e("Taskbar", "Failed to close toplevel: " + error);
}
}
}
+77 -74
View File
@@ -26,18 +26,17 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
readonly property int characterCount: 2
readonly property bool showWorkspaceNumbers: (widgetSettings.showWorkspaceNumbers !== undefined) ? widgetSettings.showWorkspaceNumbers : true
readonly property bool showNumbersOnlyWhenOccupied: (widgetSettings.showNumbersOnlyWhenOccupied !== undefined) ? widgetSettings.showNumbersOnlyWhenOccupied : true
readonly property bool showLabelsOnlyWhenOccupied: (widgetSettings.showLabelsOnlyWhenOccupied !== undefined) ? widgetSettings.showLabelsOnlyWhenOccupied : true
readonly property bool colorizeIcons: (widgetSettings.colorizeIcons !== undefined) ? widgetSettings.colorizeIcons : widgetMetadata.colorizeIcons
property ListModel localWorkspaces: ListModel {}
property real masterProgress: 0.0
@@ -49,40 +48,39 @@ Item {
property bool wheelCooldown: false
function refreshWorkspaces() {
localWorkspaces.clear()
localWorkspaces.clear();
if (!screen)
return
const screenName = screen.name.toLowerCase()
return;
const screenName = screen.name.toLowerCase();
for (var i = 0; i < CompositorService.workspaces.count; i++) {
const ws = CompositorService.workspaces.get(i)
const ws = CompositorService.workspaces.get(i);
if (ws.output.toLowerCase() !== screenName)
continue
continue;
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused)
continue
continue;
// Copy all properties from ws and add windows
var workspaceData = Object.assign({}, ws)
workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id)
var workspaceData = Object.assign({}, ws);
workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id);
localWorkspaces.append(workspaceData)
localWorkspaces.append(workspaceData);
}
updateWorkspaceFocus()
updateWorkspaceFocus();
}
function triggerUnifiedWave() {
effectColor = Color.mPrimary
masterAnimation.restart()
effectColor = Color.mPrimary;
masterAnimation.restart();
}
function updateWorkspaceFocus() {
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave()
break
root.triggerUnifiedWave();
break;
}
}
}
@@ -90,27 +88,27 @@ Item {
function getFocusedLocalIndex() {
for (var i = 0; i < localWorkspaces.count; i++) {
if (localWorkspaces.get(i).isFocused === true)
return i
return i;
}
return -1
return -1;
}
function switchByOffset(offset) {
if (localWorkspaces.count === 0)
return
var current = getFocusedLocalIndex()
return;
var current = getFocusedLocalIndex();
if (current < 0)
current = 0
var next = (current + offset) % localWorkspaces.count
current = 0;
var next = (current + offset) % localWorkspaces.count;
if (next < 0)
next = localWorkspaces.count - 1
const ws = localWorkspaces.get(next)
next = localWorkspaces.count - 1;
const ws = localWorkspaces.get(next);
if (ws && ws.idx !== undefined)
CompositorService.switchToWorkspace(ws)
CompositorService.switchToWorkspace(ws);
}
Component.onCompleted: {
refreshWorkspaces()
refreshWorkspaces();
}
onScreenChanged: refreshWorkspaces()
@@ -123,11 +121,11 @@ Item {
target: CompositorService
function onWorkspacesChanged() {
refreshWorkspaces()
refreshWorkspaces();
}
function onWindowListChanged() {
refreshWorkspaces()
refreshWorkspaces();
}
}
@@ -164,8 +162,8 @@ Item {
interval: 150
repeat: false
onTriggered: {
root.wheelCooldown = false
root.wheelAccumulatedDelta = 0
root.wheelCooldown = false;
root.wheelAccumulatedDelta = 0;
}
}
@@ -176,24 +174,24 @@ Item {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function (event) {
if (root.wheelCooldown)
return
return;
// Prefer vertical delta, fall back to horizontal if needed
var dy = event.angleDelta.y
var dx = event.angleDelta.x
var useDy = Math.abs(dy) >= Math.abs(dx)
var delta = useDy ? dy : dx
var dy = event.angleDelta.y;
var dx = event.angleDelta.x;
var useDy = Math.abs(dy) >= Math.abs(dx);
var delta = useDy ? dy : dx;
// One notch is typically 120
root.wheelAccumulatedDelta += delta
var step = 120
root.wheelAccumulatedDelta += delta;
var step = 120;
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1;
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
// For horizontal layout, same mapping using vertical wheel
root.switchByOffset(direction)
root.wheelCooldown = true
wheelDebounce.restart()
root.wheelAccumulatedDelta = 0
event.accepted = true
root.switchByOffset(direction);
root.wheelCooldown = true;
wheelDebounce.restart();
root.wheelAccumulatedDelta = 0;
event.accepted = true;
}
}
}
@@ -221,7 +219,7 @@ Item {
enabled: !hasWindows
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
CompositorService.switchToWorkspace(workspaceModel)
CompositorService.switchToWorkspace(workspaceModel);
}
}
@@ -297,22 +295,22 @@ Item {
onPressed: function (mouse) {
if (!model) {
return
return;
}
if (mouse.button === Qt.LeftButton) {
CompositorService.focusWindow(model)
CompositorService.focusWindow(model);
} else if (mouse.button === Qt.RightButton) {
CompositorService.closeWindow(model)
CompositorService.closeWindow(model);
}
}
onEntered: {
taskbarItem.itemHovered = true
TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection())
taskbarItem.itemHovered = true;
TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection());
}
onExited: {
taskbarItem.itemHovered = false
TooltipService.hide()
taskbarItem.itemHovered = false;
TooltipService.hide();
}
}
}
@@ -322,7 +320,7 @@ Item {
Item {
id: workspaceNumberContainer
visible: root.labelMode !== "none" && root.showWorkspaceNumbers && (!root.showNumbersOnlyWhenOccupied || container.hasWindows)
visible: root.labelMode !== "none" && (!root.showLabelsOnlyWhenOccupied || container.hasWindows)
anchors {
left: parent.left
@@ -331,7 +329,8 @@ Item {
topMargin: -Style.fontSizeXS * 0.5
}
width: Math.max(workspaceNumber.implicitWidth + Style.marginXS, Style.fontSizeXXS * 2)
// Doube width margin necessary here for Name or Name+Index, but double height not needed.
width: Math.max(workspaceNumber.implicitWidth + (Style.marginXS * 2), Style.fontSizeXXS * 2)
height: Math.max(workspaceNumber.implicitHeight + Style.marginXS, Style.fontSizeXXS * 2)
Rectangle {
@@ -342,13 +341,13 @@ Item {
color: {
if (workspaceModel.isFocused)
return Color.mPrimary
return Color.mPrimary;
if (workspaceModel.isUrgent)
return Color.mError
return Color.mError;
if (hasWindows)
return Color.mSecondary
return Color.mSecondary;
return Qt.alpha(Color.mOutline, 0.3)
return Qt.alpha(Color.mOutline, 0.3);
}
scale: workspaceModel.isActive ? 1.0 : 0.9
@@ -389,11 +388,15 @@ Item {
anchors.centerIn: parent
text: {
if (root.labelMode === "name" && workspaceModel.name && workspaceModel.name.length > 0) {
return workspaceModel.name.substring(0, root.characterCount)
} else {
return workspaceModel.idx.toString()
if (workspaceModel.name && workspaceModel.name.length > 0) {
if (root.labelMode === "name") {
return workspaceModel.name.substring(0, root.characterCount)
}
if (root.labelMode === "index+name") {
return (workspaceModel.idx.toString() + workspaceModel.name.substring(0, 1))
}
}
return workspaceModel.idx.toString()
}
family: Settings.data.ui.fontFixed
@@ -406,24 +409,24 @@ Item {
color: {
if (workspaceModel.isFocused)
return Color.mOnPrimary
return Color.mOnPrimary;
if (workspaceModel.isUrgent)
return Color.mOnError
return Color.mOnError;
if (hasWindows)
return Color.mOnSecondary
return Color.mOnSecondary;
return Color.mOnSurface
return Color.mOnSurface;
}
opacity: {
if (workspaceModel.isFocused)
return 1.0
return 1.0;
if (workspaceModel.isUrgent)
return 0.9
return 0.9;
if (hasWindows)
return 0.8
return 0.8;
return 0.6
return 0.6;
}
Behavior on opacity {
+95 -95
View File
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
@@ -21,8 +21,8 @@ Rectangle {
// Get shared tray menu window from PanelService (reactive to trigger changes)
readonly property var trayMenuWindow: {
// Reference trigger to force re-evaluation
var _ = trayMenuUpdateTrigger
return PanelService.getTrayMenuWindow(screen)
var _ = trayMenuUpdateTrigger;
return PanelService.getTrayMenuWindow(screen);
}
readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null
@@ -31,7 +31,7 @@ Rectangle {
target: PanelService
function onTrayMenuWindowRegistered(registeredScreen) {
if (registeredScreen === screen) {
root.trayMenuUpdateTrigger++
root.trayMenuUpdateTrigger++;
}
}
}
@@ -45,12 +45,12 @@ Rectangle {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string barPosition: Settings.data.bar.position
@@ -76,149 +76,149 @@ Rectangle {
}
function _performFilteredItemsUpdate() {
let newItems = []
let newItems = [];
if (SystemTray.items && SystemTray.items.values) {
const trayItems = SystemTray.items.values
const trayItems = SystemTray.items.values;
for (var i = 0; i < trayItems.length; i++) {
const item = trayItems[i]
const item = trayItems[i];
if (!item) {
continue
continue;
}
const title = item.tooltipTitle || item.name || item.id || ""
const title = item.tooltipTitle || item.name || item.id || "";
// Check if blacklisted
let isBlacklisted = false
let isBlacklisted = false;
if (root.blacklist && root.blacklist.length > 0) {
for (var j = 0; j < root.blacklist.length; j++) {
const rule = root.blacklist[j]
const rule = root.blacklist[j];
if (wildCardMatch(title, rule)) {
isBlacklisted = true
break
isBlacklisted = true;
break;
}
}
}
if (!isBlacklisted) {
newItems.push(item)
newItems.push(item);
}
}
}
// If drawer is disabled, show all items inline
if (!root.drawerEnabled) {
filteredItems = newItems
dropdownItems = []
filteredItems = newItems;
dropdownItems = [];
} else {
// Build inline (pinned) and drawer (unpinned) lists
// If pinned list is empty, all items go to drawer (none inline)
// If pinned list has items, pinned items are inline, rest go to drawer
if (pinned && pinned.length > 0) {
let pinnedItems = []
let pinnedItems = [];
for (var k = 0; k < newItems.length; k++) {
const item2 = newItems[k]
const title2 = item2.tooltipTitle || item2.name || item2.id || ""
const item2 = newItems[k];
const title2 = item2.tooltipTitle || item2.name || item2.id || "";
for (var m = 0; m < pinned.length; m++) {
const rule2 = pinned[m]
const rule2 = pinned[m];
if (wildCardMatch(title2, rule2)) {
pinnedItems.push(item2)
break
pinnedItems.push(item2);
break;
}
}
}
filteredItems = pinnedItems
filteredItems = pinnedItems;
// Unpinned items go to drawer
let unpinnedItems = []
let unpinnedItems = [];
for (var v = 0; v < newItems.length; v++) {
const cand = newItems[v]
let isPinned = false
const cand = newItems[v];
let isPinned = false;
for (var f = 0; f < filteredItems.length; f++) {
if (filteredItems[f] === cand) {
isPinned = true
break
isPinned = true;
break;
}
}
if (!isPinned)
unpinnedItems.push(cand)
unpinnedItems.push(cand);
}
dropdownItems = unpinnedItems
dropdownItems = unpinnedItems;
} else {
// No pinned items: all items go to drawer (none inline)
filteredItems = []
dropdownItems = newItems
filteredItems = [];
dropdownItems = newItems;
}
}
}
function updateFilteredItems() {
updateDebounceTimer.restart()
updateDebounceTimer.restart();
}
function wildCardMatch(str, rule) {
if (!str || !rule) {
return false
return false;
}
//Logger.d("Tray", "wildCardMatch - Input str:", str, "rule:", rule)
// Escape all special regex characters in the rule
let escapedRule = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
let escapedRule = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Convert '*' to '.*' for wildcard matching
let pattern = escapedRule.replace(/\\\*/g, '.*')
let pattern = escapedRule.replace(/\\\*/g, '.*');
// Add ^ and $ to match the entire string
pattern = '^' + pattern + '$'
pattern = '^' + pattern + '$';
//Logger.d("Tray", "wildCardMatch - Generated pattern:", pattern)
try {
const regex = new RegExp(pattern, 'i')
const regex = new RegExp(pattern, 'i');
// 'i' for case-insensitive
//Logger.d("Tray", "wildCardMatch - Regex test result:", regex.test(str))
return regex.test(str)
return regex.test(str);
} catch (e) {
Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message)
return false // If regex is invalid, it won't match
Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message);
return false; // If regex is invalid, it won't match
}
}
function toggleDrawer(button) {
TooltipService.hideImmediately()
TooltipService.hideImmediately();
// Close the tray menu if it's open
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close()
trayMenuWindow.close();
}
const panel = PanelService.getPanel("trayDrawerPanel", root.screen)
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
if (panel) {
panel.widgetSection = root.section
panel.widgetIndex = root.sectionWidgetIndex
panel.toggle(this)
panel.widgetSection = root.section;
panel.widgetIndex = root.sectionWidgetIndex;
panel.toggle(this);
}
}
function onLoaded() {
// When the widget is fully initialized with its props set the screen for the trayMenu
if (trayMenu && trayMenu.item) {
trayMenu.item.screen = screen
trayMenu.item.screen = screen;
}
}
Connections {
target: SystemTray.items
function onValuesChanged() {
root.updateFilteredItems()
root.updateFilteredItems();
}
}
Connections {
target: Settings
function onSettingsSaved() {
root.updateFilteredItems()
root.updateFilteredItems();
}
}
Component.onCompleted: {
root.updateFilteredItems() // Initial update
root.updateFilteredItems(); // Initial update
}
visible: filteredItems.length > 0 || dropdownItems.length > 0
@@ -248,14 +248,14 @@ Rectangle {
icon: {
switch (barPosition) {
case "bottom":
return "caret-up"
return "caret-up";
case "left":
return "caret-right"
return "caret-right";
case "right":
return "caret-left"
return "caret-left";
case "top":
default:
return "caret-down"
return "caret-down";
}
}
onClicked: toggleDrawer(this)
@@ -283,20 +283,20 @@ Rectangle {
property bool menuJustOpened: false
source: {
let icon = modelData?.icon || ""
let icon = modelData?.icon || "";
if (!icon) {
return ""
return "";
}
// Process icon path
if (icon.includes("?path=")) {
const chunks = icon.split("?path=")
const name = chunks[0]
const path = chunks[1]
const fileName = name.substring(name.lastIndexOf("/") + 1)
return `file://${path}/${fileName}`
const chunks = icon.split("?path=");
const name = chunks[0];
const path = chunks[1];
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon
return icon;
}
opacity: status === Image.Ready ? 1 : 0
@@ -315,71 +315,71 @@ Rectangle {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!modelData) {
return
return;
}
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (trayMenuWindow) {
trayMenuWindow.close()
trayMenuWindow.close();
}
if (!modelData.onlyMenu) {
modelData.activate()
modelData.activate();
}
} else if (mouse.button === Qt.MiddleButton) {
// Close the menu if it was visible
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close()
return
trayMenuWindow.close();
return;
}
modelData.secondaryActivate && modelData.secondaryActivate()
modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) {
TooltipService.hideImmediately()
TooltipService.hideImmediately();
// Close the menu if it was visible
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close()
return
trayMenuWindow.close();
return;
}
// Close any opened panel
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close()
PanelService.openedPanel.close();
}
if (modelData.hasMenu && modelData.menu && trayMenuWindow && trayMenu && trayMenu.item) {
trayMenuWindow.open()
trayMenuWindow.open();
// Position menu based on bar position
let menuX, menuY
let menuX, menuY;
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM
menuY = 0
menuX = width + Style.marginM;
menuY = 0;
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM
menuY = 0
menuX = -trayMenu.item.width - Style.marginM;
menuY = 0;
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2)
menuY = (barPosition === "top") ? Style.barHeight : -Style.barHeight
menuX = (width / 2) - (trayMenu.item.width / 2);
menuY = (barPosition === "top") ? Style.barHeight : -Style.barHeight;
}
trayMenu.item.trayItem = modelData
trayMenu.item.widgetSection = root.section
trayMenu.item.widgetIndex = root.sectionWidgetIndex
trayMenu.item.showAt(parent, menuX, menuY)
trayMenu.item.trayItem = modelData;
trayMenu.item.widgetSection = root.section;
trayMenu.item.widgetIndex = root.sectionWidgetIndex;
trayMenu.item.showAt(parent, menuX, menuY);
} else {
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set")
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
}
}
}
onEntered: {
if (trayMenuWindow) {
trayMenuWindow.close()
trayMenuWindow.close();
}
TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection())
TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection());
}
onExited: TooltipService.hide()
}
@@ -403,14 +403,14 @@ Rectangle {
icon: {
switch (barPosition) {
case "bottom":
return "caret-up"
return "caret-up";
case "left":
return "caret-right"
return "caret-right";
case "right":
return "caret-left"
return "caret-left";
case "top":
default:
return "caret-down"
return "caret-down";
}
}
onClicked: toggleDrawer(this)
+18 -18
View File
@@ -5,8 +5,8 @@ import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.UI
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
Item {
@@ -23,12 +23,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -48,12 +48,12 @@ Item {
// Logger.i("Bar:Volume", "onVolumeChanged")
if (!firstVolumeReceived) {
// Ignore the first volume change
firstVolumeReceived = true
firstVolumeReceived = true;
} else {
// Hide any tooltip while the pill is visible / being updated
TooltipService.hide()
pill.show()
externalHideTimer.restart()
TooltipService.hide();
pill.show();
externalHideTimer.restart();
}
}
}
@@ -63,7 +63,7 @@ Item {
running: false
interval: 1500
onTriggered: {
pill.hide()
pill.hide();
}
}
@@ -84,25 +84,25 @@ Item {
onWheel: function (delta) {
// Hide tooltip as soon as the user starts scrolling to adjust volume
TooltipService.hide()
TooltipService.hide();
wheelAccumulator += delta
wheelAccumulator += delta;
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.increaseVolume()
wheelAccumulator = 0;
AudioService.increaseVolume();
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.decreaseVolume()
wheelAccumulator = 0;
AudioService.decreaseVolume();
}
}
onClicked: {
PanelService.getPanel("audioPanel", screen)?.toggle(this)
PanelService.getPanel("audioPanel", screen)?.toggle(this);
}
onRightClicked: {
AudioService.setOutputMuted(!AudioService.muted)
AudioService.setOutputMuted(!AudioService.muted);
}
onMiddleClicked: {
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"])
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"]);
}
}
}
+3 -3
View File
@@ -21,11 +21,11 @@ NIconButton {
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen)
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
if (Settings.data.wallpaper.panelPosition === "follow_bar") {
wallpaperPanel?.toggle(this)
wallpaperPanel?.toggle(this);
} else {
wallpaperPanel?.toggle()
wallpaperPanel?.toggle();
}
}
}
+20 -20
View File
@@ -1,9 +1,9 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.Networking
import qs.Services.UI
import qs.Modules.Bar.Extras
Item {
id: root
@@ -19,12 +19,12 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
@@ -41,37 +41,37 @@ Item {
icon: {
try {
if (NetworkService.ethernetConnected) {
return NetworkService.internetConnectivity ? "ethernet" : "ethernet-off"
return NetworkService.internetConnectivity ? "ethernet" : "ethernet-off";
}
let connected = false
let signalStrength = 0
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
connected = true;
signalStrength = NetworkService.networks[net].signal;
break;
}
}
return connected ? NetworkService.signalIcon(signalStrength, true) : "wifi-off"
return connected ? NetworkService.signalIcon(signalStrength, true) : "wifi-off";
} catch (error) {
Logger.e("Wi-Fi", "Error getting icon:", error)
return "wifi-off"
Logger.e("Wi-Fi", "Error getting icon:", error);
return "wifi-off";
}
}
text: {
try {
if (NetworkService.ethernetConnected) {
return ""
return "";
}
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
return net
return net;
}
}
return ""
return "";
} catch (error) {
Logger.e("Wi-Fi", "Error getting ssid:", error)
return "error"
Logger.e("Wi-Fi", "Error getting ssid:", error);
return "error";
}
}
autoHide: false
@@ -81,9 +81,9 @@ Item {
onRightClicked: NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled)
tooltipText: {
if (pill.text !== "") {
return pill.text
return pill.text;
}
return I18n.tr("tooltips.manage-wifi")
return I18n.tr("tooltips.manage-wifi");
}
}
}
+114 -97
View File
@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Window
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Commons
@@ -24,23 +24,23 @@ Item {
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
return widgets[sectionWidgetIndex];
}
}
return {}
return {};
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool density: Settings.data.bar.density
readonly property real baseDimensionRatio: {
const b = (density === "compact") ? 0.85 : 0.65
const b = (density === "compact") ? 0.85 : 0.65;
if (widgetSettings.labelMode === "none") {
return b * 0.75
return b * 0.75;
}
return b
return b;
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
@@ -68,76 +68,85 @@ Item {
implicitHeight: isVertical ? computeHeight() : Style.barHeight
function getWorkspaceWidth(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
const factor = ws.isActive ? 2.2 : 1
const d = Style.capsuleHeight * root.baseDimensionRatio;
const factor = ws.isActive ? 2.2 : 1;
// For name mode, calculate width based on actual text content
if (labelMode === "name" && ws.name && ws.name.length > 0) {
const displayText = ws.name.substring(0, characterCount)
const textWidth = displayText.length * (d * 0.4) // Approximate width per character
const padding = d * 0.6 // Padding on both sides
return Math.max(d * factor, textWidth + padding)
// Don't calculate text width if labels are off
if (labelMode === "none") {
return d * factor;
}
return d * factor
var displayText = ws.idx.toString();
if (ws.name && ws.name.length > 0) {
if (root.labelMode === "name") {
displayText = ws.name.substring(0, characterCount);
} else if (root.labelMode === "index+name") {
displayText = ws.idx.toString() + " " + ws.name.substring(0, characterCount);
}
}
const textWidth = displayText.length * (d * 0.4); // Approximate width per character
const padding = d * 0.6;
return Math.max(d * factor, textWidth + padding);
}
function getWorkspaceHeight(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
const factor = ws.isActive ? 2.2 : 1
return d * factor
const d = Style.capsuleHeight * root.baseDimensionRatio;
const factor = ws.isActive ? 2.2 : 1;
return d * factor;
}
function computeWidth() {
let total = 0
let total = 0;
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += getWorkspaceWidth(ws)
const ws = localWorkspaces.get(i);
total += getWorkspaceWidth(ws);
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return Math.round(total)
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
total += horizontalPadding * 2;
return Math.round(total);
}
function computeHeight() {
let total = 0
let total = 0;
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += getWorkspaceHeight(ws)
const ws = localWorkspaces.get(i);
total += getWorkspaceHeight(ws);
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return Math.round(total)
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
total += horizontalPadding * 2;
return Math.round(total);
}
function getFocusedLocalIndex() {
for (var i = 0; i < localWorkspaces.count; i++) {
if (localWorkspaces.get(i).isFocused === true)
return i
return i;
}
return -1
return -1;
}
function switchByOffset(offset) {
if (localWorkspaces.count === 0)
return
var current = getFocusedLocalIndex()
return;
var current = getFocusedLocalIndex();
if (current < 0)
current = 0
var next = (current + offset) % localWorkspaces.count
current = 0;
var next = (current + offset) % localWorkspaces.count;
if (next < 0)
next = localWorkspaces.count - 1
const ws = localWorkspaces.get(next)
next = localWorkspaces.count - 1;
const ws = localWorkspaces.get(next);
if (ws && ws.idx !== undefined)
CompositorService.switchToWorkspace(ws)
CompositorService.switchToWorkspace(ws);
}
Component.onCompleted: {
refreshWorkspaces()
refreshWorkspaces();
}
Component.onDestruction: {
root.isDestroying = true
root.isDestroying = true;
}
onScreenChanged: refreshWorkspaces()
@@ -146,40 +155,40 @@ Item {
Connections {
target: CompositorService
function onWorkspacesChanged() {
refreshWorkspaces()
refreshWorkspaces();
}
}
function refreshWorkspaces() {
localWorkspaces.clear()
localWorkspaces.clear();
if (screen !== null) {
for (var i = 0; i < CompositorService.workspaces.count; i++) {
const ws = CompositorService.workspaces.get(i)
const ws = CompositorService.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
continue
continue;
}
localWorkspaces.append(ws)
localWorkspaces.append(ws);
}
}
}
workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus()
workspaceRepeaterHorizontal.model = localWorkspaces;
workspaceRepeaterVertical.model = localWorkspaces;
updateWorkspaceFocus();
}
function triggerUnifiedWave() {
effectColor = Color.mPrimary
masterAnimation.restart()
effectColor = Color.mPrimary;
masterAnimation.restart();
}
function updateWorkspaceFocus() {
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave()
root.workspaceChanged(ws.id, Color.mPrimary)
break
root.triggerUnifiedWave();
root.workspaceChanged(ws.id, Color.mPrimary);
break;
}
}
}
@@ -228,8 +237,8 @@ Item {
interval: 150
repeat: false
onTriggered: {
root.wheelCooldown = false
root.wheelAccumulatedDelta = 0
root.wheelCooldown = false;
root.wheelAccumulatedDelta = 0;
}
}
@@ -240,24 +249,24 @@ Item {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function (event) {
if (root.wheelCooldown)
return
return;
// Prefer vertical delta, fall back to horizontal if needed
var dy = event.angleDelta.y
var dx = event.angleDelta.x
var useDy = Math.abs(dy) >= Math.abs(dx)
var delta = useDy ? dy : dx
var dy = event.angleDelta.y;
var dx = event.angleDelta.x;
var useDy = Math.abs(dy) >= Math.abs(dx);
var delta = useDy ? dy : dx;
// One notch is typically 120
root.wheelAccumulatedDelta += delta
var step = 120
root.wheelAccumulatedDelta += delta;
var step = 120;
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1;
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
// For horizontal layout, same mapping using vertical wheel
root.switchByOffset(direction)
root.wheelCooldown = true
wheelDebounce.restart()
root.wheelAccumulatedDelta = 0
event.accepted = true
root.switchByOffset(direction);
root.wheelCooldown = true;
wheelDebounce.restart();
root.wheelAccumulatedDelta = 0;
event.accepted = true;
}
}
}
@@ -289,11 +298,15 @@ Item {
x: (pill.width - width) / 2
y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, characterCount)
} else {
return model.idx.toString()
if (model.name && model.name.length > 0) {
if (root.labelMode === "name") {
return model.name.substring(0, characterCount);
}
if (root.labelMode === "index+name") {
return (model.idx.toString() + " " + model.name.substring(0, characterCount))
}
}
return model.idx.toString();
}
family: Settings.data.ui.fontFixed
pointSize: model.isActive ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
@@ -303,13 +316,13 @@ Item {
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
return Color.mOnPrimary;
if (model.isUrgent)
return Color.mOnError
return Color.mOnError;
if (model.isOccupied)
return Color.mOnSecondary
return Color.mOnSecondary;
return Color.mOnSecondary
return Color.mOnSecondary;
}
}
}
@@ -318,13 +331,13 @@ Item {
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
return Color.mPrimary;
if (model.isUrgent)
return Color.mError
return Color.mError;
if (model.isOccupied)
return Color.mSecondary
return Color.mSecondary;
return Qt.alpha(Color.mSecondary, 0.3)
return Qt.alpha(Color.mSecondary, 0.3);
}
scale: model.isActive ? 1.0 : 0.9
z: 0
@@ -334,7 +347,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
CompositorService.switchToWorkspace(model)
CompositorService.switchToWorkspace(model);
}
hoverEnabled: true
}
@@ -434,11 +447,15 @@ Item {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, characterCount)
} else {
return model.idx.toString()
if (model.name && model.name.length > 0) {
if (root.labelMode === "name") {
return model.name.substring(0, characterCount);
}
if (root.labelMode === "index+name") {
return (model.idx.toString() + model.name.substring(0, 1))
}
}
return model.idx.toString();
}
family: Settings.data.ui.fontFixed
pointSize: model.isActive ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
@@ -448,13 +465,13 @@ Item {
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
return Color.mOnPrimary;
if (model.isUrgent)
return Color.mOnError
return Color.mOnError;
if (model.isOccupied)
return Color.mOnSecondary
return Color.mOnSecondary;
return Color.mOnSurface
return Color.mOnSurface;
}
}
}
@@ -463,13 +480,13 @@ Item {
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
return Color.mPrimary;
if (model.isUrgent)
return Color.mError
return Color.mError;
if (model.isOccupied)
return Color.mSecondary
return Color.mSecondary;
return Color.mOutline
return Color.mOutline;
}
scale: model.isActive ? 1.0 : 0.9
z: 0
@@ -479,7 +496,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
CompositorService.switchToWorkspace(model)
CompositorService.switchToWorkspace(model);
}
hoverEnabled: true
}
+87 -87
View File
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
@@ -26,7 +26,7 @@ Loader {
target: BarService
function onBarReadyChanged(screenName) {
if (screenName === modelData.name) {
barIsReady = true
barIsReady = true;
}
}
}
@@ -35,7 +35,7 @@ Loader {
Connections {
target: ToplevelManager ? ToplevelManager.toplevels : null
function onValuesChanged() {
updateDockApps()
updateDockApps();
}
}
@@ -43,17 +43,17 @@ Loader {
Connections {
target: Settings.data.dock
function onPinnedAppsChanged() {
updateDockApps()
updateDockApps();
}
function onOnlySameOutputChanged() {
updateDockApps()
updateDockApps();
}
}
// Initial update when component is ready
Component.onCompleted: {
if (ToplevelManager) {
updateDockApps()
updateDockApps();
}
}
@@ -93,33 +93,33 @@ Loader {
// Function to close any open context menu
function closeAllContextMenus() {
if (currentContextMenu && currentContextMenu.visible) {
currentContextMenu.hide()
currentContextMenu.hide();
}
}
// Function to update the combined dock apps model
function updateDockApps() {
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []
const pinnedApps = Settings.data.dock.pinnedApps || []
const combined = []
const processedAppIds = new Set()
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [];
const pinnedApps = Settings.data.dock.pinnedApps || [];
const combined = [];
const processedAppIds = new Set();
// Strategy: Maintain app positions as much as possible
// 1. First pass: Add all running apps (both pinned and non-pinned) in their current order
runningApps.forEach(toplevel => {
if (toplevel && toplevel.appId && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) {
const isPinned = pinnedApps.includes(toplevel.appId)
const appType = isPinned ? "pinned-running" : "running"
const isPinned = pinnedApps.includes(toplevel.appId);
const appType = isPinned ? "pinned-running" : "running";
combined.push({
"type": appType,
"toplevel": toplevel,
"appId": toplevel.appId,
"title": toplevel.title
})
processedAppIds.add(toplevel.appId)
});
processedAppIds.add(toplevel.appId);
}
})
});
// 2. Second pass: Add non-running pinned apps at the end
pinnedApps.forEach(pinnedAppId => {
@@ -130,11 +130,11 @@ Loader {
"toplevel": null,
"appId": pinnedAppId,
"title": pinnedAppId
})
});
}
})
});
dockApps = combined
dockApps = combined;
}
// Timer to unload dock after hide animation completes
@@ -143,7 +143,7 @@ Loader {
interval: hideAnimationDuration + 50 // Add small buffer
onTriggered: {
if (hidden && autoHide) {
dockLoaded = false
dockLoaded = false;
}
}
}
@@ -155,14 +155,14 @@ Loader {
onTriggered: {
// Force menuHovered to false if no menu is current or visible
if (!root.currentContextMenu || !root.currentContextMenu.visible) {
menuHovered = false
menuHovered = false;
}
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
hidden = true
unloadTimer.restart() // Start unload timer when hiding
hidden = true;
unloadTimer.restart(); // Start unload timer when hiding
} else if (autoHide && !dockHovered && !peekHovered) {
// Restart timer if menu is closing (handles race condition)
restart()
restart();
}
}
}
@@ -173,9 +173,9 @@ Loader {
interval: showDelay
onTriggered: {
if (autoHide) {
dockLoaded = true // Load dock immediately
hidden = false // Then trigger show animation
unloadTimer.stop() // Cancel any pending unload
dockLoaded = true; // Load dock immediately
hidden = false; // Then trigger show animation
unloadTimer.stop(); // Cancel any pending unload
}
}
}
@@ -183,14 +183,14 @@ Loader {
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
hidden = false
dockLoaded = true
hideTimer.stop()
showTimer.stop()
unloadTimer.stop()
hidden = false;
dockLoaded = true;
hideTimer.stop();
showTimer.stop();
unloadTimer.stop();
} else {
hidden = true
unloadTimer.restart() // Schedule unload after animation
hidden = true;
unloadTimer.restart(); // Schedule unload after animation
}
}
@@ -218,16 +218,16 @@ Loader {
hoverEnabled: true
onEntered: {
peekHovered = true
peekHovered = true;
if (hidden) {
showTimer.start()
showTimer.start();
}
}
onExited: {
peekHovered = false
peekHovered = false;
if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
hideTimer.restart()
hideTimer.restart();
}
}
}
@@ -260,9 +260,9 @@ Loader {
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL + floatingMargin : floatingMargin)
return (Style.barHeight + Style.marginM) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL + floatingMargin : floatingMargin);
default:
return floatingMargin
return floatingMargin;
}
}
@@ -313,24 +313,24 @@ Loader {
hoverEnabled: true
onEntered: {
dockHovered = true
dockHovered = true;
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering
showTimer.stop();
hideTimer.stop();
unloadTimer.stop(); // Cancel unload if hovering
}
}
onExited: {
dockHovered = false
dockHovered = false;
if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
hideTimer.restart();
}
}
onClicked: {
// Close any open context menu when clicking on the dock background
closeAllContextMenus()
closeAllContextMenus();
}
}
@@ -342,8 +342,8 @@ Loader {
function getAppIcon(appData): string {
if (!appData || !appData.appId)
return ""
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase())
return "";
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase());
}
RowLayout {
@@ -371,7 +371,7 @@ Loader {
Connections {
target: modelData?.toplevel
function onClosed() {
Qt.callLater(root.updateDockApps)
Qt.callLater(root.updateDockApps);
}
}
@@ -452,9 +452,9 @@ Loader {
onHoveredChanged: {
// Only update menuHovered if this menu is current and visible
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
menuHovered = hovered
menuHovered = hovered;
} else {
menuHovered = false
menuHovered = false;
}
}
@@ -462,26 +462,26 @@ Loader {
target: contextMenu
function onRequestClose() {
// Clear current menu immediately to prevent hover updates
root.currentContextMenu = null
hideTimer.stop()
contextMenu.hide()
menuHovered = false
anyAppHovered = false
root.currentContextMenu = null;
hideTimer.stop();
contextMenu.hide();
menuHovered = false;
anyAppHovered = false;
}
}
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
onVisibleChanged: {
if (visible) {
root.currentContextMenu = contextMenu
anyAppHovered = false
root.currentContextMenu = contextMenu;
anyAppHovered = false;
} else if (root.currentContextMenu === contextMenu) {
root.currentContextMenu = null
hideTimer.stop()
menuHovered = false
anyAppHovered = false
root.currentContextMenu = null;
hideTimer.stop();
menuHovered = false;
anyAppHovered = false;
// Restart hide timer after menu closes
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
hideTimer.restart();
}
}
}
@@ -496,28 +496,28 @@ Loader {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
anyAppHovered = true;
const appName = appButton.appTitle || appButton.appId || "Unknown";
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName;
if (!contextMenu.visible) {
TooltipService.show(Screen, appButton, tooltipText, "top")
TooltipService.show(Screen, appButton, tooltipText, "top");
}
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering app
showTimer.stop();
hideTimer.stop();
unloadTimer.stop(); // Cancel unload if hovering app
}
}
onExited: {
anyAppHovered = false
TooltipService.hide()
anyAppHovered = false;
TooltipService.hide();
// Clear menuHovered if no current menu or menu not visible
if (!root.currentContextMenu || !root.currentContextMenu.visible) {
menuHovered = false
menuHovered = false;
}
if (autoHide && !dockHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
hideTimer.restart();
}
}
@@ -525,33 +525,33 @@ Loader {
if (mouse.button === Qt.RightButton) {
// If right-clicking on the same app with an open context menu, close it
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
root.closeAllContextMenus()
return
root.closeAllContextMenus();
return;
}
// Close any other existing context menu first
root.closeAllContextMenus()
root.closeAllContextMenus();
// Hide tooltip when showing context menu
TooltipService.hideImmediately()
contextMenu.show(appButton, modelData.toplevel || modelData)
return
TooltipService.hideImmediately();
contextMenu.show(appButton, modelData.toplevel || modelData);
return;
}
// Close any existing context menu for non-right-click actions
root.closeAllContextMenus()
root.closeAllContextMenus();
// Check if toplevel is still valid (not a stale reference)
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel)
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel);
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
modelData.toplevel.close()
Qt.callLater(root.updateDockApps) // Force immediate dock update
modelData.toplevel.close();
Qt.callLater(root.updateDockApps); // Force immediate dock update
} else if (mouse.button === Qt.LeftButton) {
if (isValidToplevel && modelData.toplevel.activate) {
// Running app - activate it
modelData.toplevel.activate()
modelData.toplevel.activate();
} else if (modelData?.appId) {
// Pinned app not running - launch it
Quickshell.execDetached(["gtk-launch", modelData.appId])
Quickshell.execDetached(["gtk-launch", modelData.appId]);
}
}
}
+56 -57
View File
@@ -31,21 +31,21 @@ PopupWindow {
function initItems() {
// Is this a running app?
const isRunning = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel)
const isRunning = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel);
// Is this a pinned app?
const isPinned = root.toplevel && root.isAppPinned(root.toplevel.appId)
const isPinned = root.toplevel && root.isAppPinned(root.toplevel.appId);
var next = []
var next = [];
if (isRunning) {
// Focus item
next.push({
"icon": "eye",
"text": I18n.tr("dock.menu.focus"),
"action": function () {
handleFocus()
handleFocus();
}
})
});
}
// Pin/Unpin item
@@ -53,9 +53,9 @@ PopupWindow {
"icon": !isPinned ? "pin" : "unpin",
"text": !isPinned ? I18n.tr("dock.menu.pin") : I18n.tr("dock.menu.unpin"),
"action": function () {
handlePin()
handlePin();
}
})
});
if (isRunning) {
// Close item
@@ -63,55 +63,54 @@ PopupWindow {
"icon": "close",
"text": I18n.tr("dock.menu.close"),
"action": function () {
handleClose()
handleClose();
}
})
});
}
// Create a menu entry for each app-specific action definied in its .desktop file
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) {
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
if (entry != null) {
entry.actions.forEach(function (action) {
next.push({
"icon": "",
"text": action.name,
"action": function () {
action.execute()
action.execute();
}
})
})
});
});
}
}
root.items = next
root.items = next;
}
// Helper functions for pin/unpin functionality
function isAppPinned(appId) {
if (!appId)
return false
const pinnedApps = Settings.data.dock.pinnedApps || []
return pinnedApps.includes(appId)
return false;
const pinnedApps = Settings.data.dock.pinnedApps || [];
return pinnedApps.includes(appId);
}
function toggleAppPin(appId) {
if (!appId)
return
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice() // Create a copy
const isPinned = pinnedApps.includes(appId)
return;
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); // Create a copy
const isPinned = pinnedApps.includes(appId);
if (isPinned) {
// Unpin: remove from array
pinnedApps = pinnedApps.filter(id => id !== appId)
pinnedApps = pinnedApps.filter(id => id !== appId);
} else {
// Pin: add to array
pinnedApps.push(appId)
pinnedApps.push(appId);
}
// Update the settings
Settings.data.dock.pinnedApps = pinnedApps
Settings.data.dock.pinnedApps = pinnedApps;
}
anchor.item: anchorItem
@@ -120,62 +119,62 @@ PopupWindow {
function show(item, toplevelData) {
if (!item) {
return
return;
}
anchorItem = item
toplevel = toplevelData
initItems()
visible = true
canAutoClose = false
gracePeriodTimer.restart()
anchorItem = item;
toplevel = toplevelData;
initItems();
visible = true;
canAutoClose = false;
gracePeriodTimer.restart();
}
function hide() {
visible = false
root.items.length = 0
visible = false;
root.items.length = 0;
}
// Helper function to determine which menu item is under the mouse
function getHoveredItem(mouseY) {
const itemHeight = 32
const startY = Style.marginM
const relativeY = mouseY - startY
const itemHeight = 32;
const startY = Style.marginM;
const relativeY = mouseY - startY;
if (relativeY < 0)
return -1
return -1;
const itemIndex = Math.floor(relativeY / itemHeight)
return itemIndex >= 0 && itemIndex < root.items.length ? itemIndex : -1
const itemIndex = Math.floor(relativeY / itemHeight);
return itemIndex >= 0 && itemIndex < root.items.length ? itemIndex : -1;
}
function handleFocus() {
if (root.toplevel?.activate) {
root.toplevel.activate()
root.toplevel.activate();
}
root.requestClose()
root.requestClose();
}
function handlePin() {
if (root.toplevel?.appId) {
root.toggleAppPin(root.toplevel.appId)
root.toggleAppPin(root.toplevel.appId);
}
root.requestClose()
root.requestClose();
}
function handleClose() {
// Check if toplevel is still valid before trying to close it
const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel)
const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel);
if (isValidToplevel && root.toplevel.close) {
root.toplevel.close()
root.toplevel.close();
// Trigger immediate dock update callback if provided
if (root.onAppClosed && typeof root.onAppClosed === "function") {
Qt.callLater(root.onAppClosed)
Qt.callLater(root.onAppClosed);
}
}
root.hide()
root.requestClose()
root.hide();
root.requestClose();
}
// Short delay to ignore spurious events
@@ -184,9 +183,9 @@ PopupWindow {
interval: 1500
repeat: false
onTriggered: {
root.canAutoClose = true
root.canAutoClose = true;
if (!menuMouseArea.containsMouse) {
closeTimer.start()
closeTimer.start();
}
}
}
@@ -197,7 +196,7 @@ PopupWindow {
repeat: false
running: false
onTriggered: {
root.hide()
root.hide();
}
}
@@ -216,25 +215,25 @@ PopupWindow {
cursorShape: root.hoveredItem >= 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
closeTimer.stop()
closeTimer.stop();
}
onExited: {
root.hoveredItem = -1
root.hoveredItem = -1;
if (root.canAutoClose) {
// Only close if grace period has passed
closeTimer.start()
closeTimer.start();
}
}
onPositionChanged: mouse => {
root.hoveredItem = root.getHoveredItem(mouse.y)
root.hoveredItem = root.getHoveredItem(mouse.y);
}
onClicked: mouse => {
const clickedItem = root.getHoveredItem(mouse.y)
const clickedItem = root.getHoveredItem(mouse.y);
if (clickedItem >= 0) {
root.items[clickedItem].action.call()
root.items[clickedItem].action.call();
}
}
}
+31 -31
View File
@@ -18,24 +18,24 @@ Scope {
onCurrentTextChanged: {
if (currentText !== "") {
showFailure = false
errorMessage = ""
showFailure = false;
errorMessage = "";
}
}
function tryUnlock() {
if (!pamAvailable) {
errorMessage = "PAM not available"
showFailure = true
return
errorMessage = "PAM not available";
showFailure = true;
return;
}
root.unlockInProgress = true
errorMessage = ""
showFailure = false
root.unlockInProgress = true;
errorMessage = "";
showFailure = false;
Logger.i("LockContext", "Starting PAM authentication for user:", pam.user)
pam.start()
Logger.i("LockContext", "Starting PAM authentication for user:", pam.user);
pam.start();
}
PamContext {
@@ -44,48 +44,48 @@ Scope {
user: HostService.username
onPamMessage: {
Logger.i("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
Logger.i("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired);
if (messageIsError) {
errorMessage = message
errorMessage = message;
} else {
infoMessage = message
infoMessage = message;
}
if (responseRequired) {
Logger.i("LockContext", "Responding to PAM with password")
respond(root.currentText)
Logger.i("LockContext", "Responding to PAM with password");
respond(root.currentText);
}
}
onResponseRequiredChanged: {
Logger.i("LockContext", "Response required changed:", responseRequired)
Logger.i("LockContext", "Response required changed:", responseRequired);
if (responseRequired && root.unlockInProgress) {
Logger.i("LockContext", "Automatically responding to PAM")
respond(root.currentText)
Logger.i("LockContext", "Automatically responding to PAM");
respond(root.currentText);
}
}
onCompleted: result => {
Logger.i("LockContext", "PAM completed with result:", result)
Logger.i("LockContext", "PAM completed with result:", result);
if (result === PamResult.Success) {
Logger.i("LockContext", "Authentication successful")
root.unlocked()
Logger.i("LockContext", "Authentication successful");
root.unlocked();
} else {
Logger.i("LockContext", "Authentication failed")
errorMessage = "Authentication failed"
showFailure = true
root.failed()
Logger.i("LockContext", "Authentication failed");
errorMessage = "Authentication failed";
showFailure = true;
root.failed();
}
root.unlockInProgress = false
root.unlockInProgress = false;
}
onError: {
Logger.i("LockContext", "PAM error:", error, "message:", message)
errorMessage = message || "Authentication error"
showFailure = true
root.unlockInProgress = false
root.failed()
Logger.i("LockContext", "PAM error:", error, "message:", message);
errorMessage = message || "Authentication error";
showFailure = true;
root.unlockInProgress = false;
root.failed();
}
}
}
+86 -90
View File
@@ -1,21 +1,21 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.Pam
import Quickshell.Services.UPower
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Services.Keyboard
import qs.Services.Location
import qs.Services.Media
import qs.Services.Compositor
import qs.Services.UI
import qs.Services.System
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
@@ -31,14 +31,14 @@ Loader {
interval: 250
repeat: false
onTriggered: {
lockScreen.active = false
lockScreen.active = false;
// Reset the deprecation flag when unlocking
lockScreen.triggeredViaDeprecatedCall = false
lockScreen.triggeredViaDeprecatedCall = false;
}
}
function scheduleUnloadAfterUnlock() {
unloadAfterUnlockTimer.start()
unloadAfterUnlockTimer.start();
}
sourceComponent: Component {
@@ -48,12 +48,12 @@ Loader {
LockContext {
id: lockContext
onUnlocked: {
lockSession.locked = false
lockScreen.scheduleUnloadAfterUnlock()
lockContext.currentText = ""
lockSession.locked = false;
lockScreen.scheduleUnloadAfterUnlock();
lockContext.currentText = "";
}
onFailed: {
lockContext.currentText = ""
lockContext.currentText = "";
}
}
@@ -130,21 +130,20 @@ Loader {
smooth: false
onPaint: {
const ctx = getContext("2d")
const ctx = getContext("2d");
if (!ctx)
return
return;
ctx.reset();
ctx.clearRect(0, 0, width, height);
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = parent.cornerColor;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = parent.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, height, parent.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(width, height, parent.cornerRadius, 0, 2 * Math.PI);
ctx.fill();
}
onWidthChanged: if (available)
@@ -164,21 +163,20 @@ Loader {
smooth: true
onPaint: {
const ctx = getContext("2d")
const ctx = getContext("2d");
if (!ctx)
return
return;
ctx.reset();
ctx.clearRect(0, 0, width, height);
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = parent.cornerColor;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = parent.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, height, parent.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(0, height, parent.cornerRadius, 0, 2 * Math.PI);
ctx.fill();
}
onWidthChanged: if (available)
@@ -198,21 +196,20 @@ Loader {
smooth: true
onPaint: {
const ctx = getContext("2d")
const ctx = getContext("2d");
if (!ctx)
return
return;
ctx.reset();
ctx.clearRect(0, 0, width, height);
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = parent.cornerColor;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = parent.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, 0, parent.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(width, 0, parent.cornerRadius, 0, 2 * Math.PI);
ctx.fill();
}
onWidthChanged: if (available)
@@ -232,21 +229,20 @@ Loader {
smooth: true
onPaint: {
const ctx = getContext("2d")
const ctx = getContext("2d");
if (!ctx)
return
return;
ctx.reset();
ctx.clearRect(0, 0, width, height);
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = parent.cornerColor;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = parent.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, 0, parent.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(0, 0, parent.cornerRadius, 0, 2 * Math.PI);
ctx.fill();
}
onWidthChanged: if (available)
@@ -347,7 +343,7 @@ Loader {
// Date below
NText {
text: {
var lang = I18n.locale.name.split("_")[0]
var lang = I18n.locale.name.split("_")[0];
var formats = {
"de": "dddd, d. MMMM",
"es": "dddd, d 'de' MMMM",
@@ -356,8 +352,8 @@ Loader {
"zh": "yyyy年M月d日 dddd",
"uk": "dddd, d MMMM",
"tr": "dddd, d MMMM"
}
return I18n.locale.toString(Time.now, formats[lang] || "dddd, MMMM d")
};
return I18n.locale.toString(Time.now, formats[lang] || "dddd, MMMM d");
}
pointSize: Style.fontSizeXL
font.weight: Font.Medium
@@ -487,15 +483,15 @@ Loader {
// Compact status indicators container (compact mode only)
Rectangle {
width: {
var hasBattery = UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent
var hasKeyboard = keyboardLayout.currentLayout !== "Unknown"
var hasBattery = UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent;
var hasKeyboard = keyboardLayout.currentLayout !== "Unknown";
if (hasBattery && hasKeyboard) {
return 200
return 200;
} else if (hasBattery || hasKeyboard) {
return 120
return 120;
} else {
return 0
return 0;
}
}
height: 40
@@ -708,14 +704,14 @@ Loader {
NText {
text: {
var temp = LocationService.data.weather.current_weather.temperature
var suffix = "C"
var temp = LocationService.data.weather.current_weather.temperature;
var suffix = "C";
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp)
suffix = "F"
temp = LocationService.celsiusToFahrenheit(temp);
suffix = "F";
}
temp = Math.round(temp)
return temp + "°" + suffix
temp = Math.round(temp);
return temp + "°" + suffix;
}
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
@@ -724,14 +720,14 @@ Loader {
NText {
text: {
var wind = LocationService.data.weather.current_weather.windspeed
var unit = "km/h"
var wind = LocationService.data.weather.current_weather.windspeed;
var unit = "km/h";
if (Settings.data.location.useFahrenheit) {
wind = wind * 0.621371 // Convert km/h to mph
unit = "mph"
wind = wind * 0.621371; // Convert km/h to mph
unit = "mph";
}
wind = Math.round(wind)
return wind + " " + unit
wind = Math.round(wind);
return wind + " " + unit;
}
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
@@ -773,8 +769,8 @@ Loader {
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
return I18n.locale.toString(weatherDate, "ddd")
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
return I18n.locale.toString(weatherDate, "ddd");
}
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
@@ -791,15 +787,15 @@ Loader {
NText {
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index]
var min = LocationService.data.weather.daily.temperature_2m_min[index]
var max = LocationService.data.weather.daily.temperature_2m_max[index];
var min = LocationService.data.weather.daily.temperature_2m_min[index];
if (Settings.data.location.useFahrenheit) {
max = LocationService.celsiusToFahrenheit(max)
min = LocationService.celsiusToFahrenheit(min)
max = LocationService.celsiusToFahrenheit(max);
min = LocationService.celsiusToFahrenheit(min);
}
max = Math.round(max)
min = Math.round(min)
return max + "°/" + min + "°"
max = Math.round(max);
min = Math.round(min);
return max + "°/" + min + "°";
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
@@ -916,7 +912,7 @@ Loader {
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
lockContext.tryUnlock();
}
}
+34 -16
View File
@@ -3,8 +3,8 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Modules.MainScreen
import qs.Services.UI
// ------------------------------
// MainScreen for each screen (manages bar + all panels)
@@ -16,14 +16,25 @@ Variants {
property bool shouldBeActive: {
if (!modelData || !modelData.name) {
return false
return false;
}
Logger.d("Shell", "MainScreen activated for", modelData?.name)
return true
let shouldLoad = true;
if (!Settings.data.general.allowPanelsOnScreenWithoutBar) {
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
shouldLoad = monitors.length === 0 || monitors.includes(modelData?.name);
}
if (shouldLoad) {
Logger.d("AllScreens", "Screen activated: ", modelData?.name);
}
return shouldLoad;
}
property bool windowLoaded: false
// Main Screen loader - Bar and panels backgrounds
Loader {
id: windowLoader
active: parent.shouldBeActive
@@ -33,7 +44,7 @@ Variants {
onLoaded: {
// Signal that window is loaded so exclusion zone can be created
parent.windowLoaded = true
parent.windowLoaded = true;
}
sourceComponent: MainScreen {
@@ -45,11 +56,11 @@ Variants {
Loader {
active: {
if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.isVisible)
return false
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || []
return monitors.length === 0 || monitors.includes(modelData?.name)
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
@@ -58,7 +69,7 @@ Variants {
}
onLoaded: {
Logger.d("Shell", "BarContentWindow created for", modelData?.name)
Logger.d("AllScreens", "BarContentWindow created for", modelData?.name);
}
}
@@ -67,11 +78,11 @@ Variants {
Loader {
active: {
if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.isVisible)
return false
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || []
return monitors.length === 0 || monitors.includes(modelData?.name)
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
@@ -80,14 +91,21 @@ Variants {
}
onLoaded: {
Logger.d("Shell", "BarExclusionZone created for", modelData?.name)
Logger.d("AllScreens", "BarExclusionZone created for", modelData?.name);
}
}
// TrayMenuWindow - separate window for tray context menus
// This must be a top-level PanelWindow.
// Disabled when bar is hidden or not configured for this screen
Loader {
active: parent.windowLoaded && parent.shouldBeActive
active: {
if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.isVisible)
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
sourceComponent: TrayMenuWindow {
@@ -95,7 +113,7 @@ Variants {
}
onLoaded: {
Logger.d("Shell", "TrayMenuWindow created for", modelData?.name)
Logger.d("AllScreens", "TrayMenuWindow created for", modelData?.name);
}
}
}
@@ -3,17 +3,16 @@ import QtQuick.Shapes
import qs.Commons
import qs.Widgets
/**
* AllBackgrounds - Unified Shape container for all bar and panel backgrounds
*
* Unified shadow system. This component contains a single Shape
* with multiple ShapePath children (one for bar, one for each panel type).
*
* Benefits:
* - Single GPU-accelerated rendering pass for all backgrounds
* - Unified shadow system (one MultiEffect for everything)
*/
* AllBackgrounds - Unified Shape container for all bar and panel backgrounds
*
* Unified shadow system. This component contains a single Shape
* with multiple ShapePath children (one for bar, one for each panel type).
*
* Benefits:
* - Single GPU-accelerated rendering pass for all backgrounds
* - Unified shadow system (one MultiEffect for everything)
*/
Item {
id: root
@@ -46,15 +45,12 @@ Item {
enabled: false // Disable mouse input on the Shape itself
Component.onCompleted: {
Logger.d("AllBackgrounds", "AllBackgrounds initialized")
Logger.d("AllBackgrounds", " bar:", root.bar)
Logger.d("AllBackgrounds", " windowRoot:", root.windowRoot)
Logger.d("AllBackgrounds", "AllBackgrounds initialized");
}
/**
* Bar
*/
* Bar
*/
BarBackground {
bar: root.bar
shapeContainer: backgroundsShape
@@ -62,10 +58,9 @@ Item {
backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
}
/**
* Panels
*/
* Panels
*/
// Audio
PanelBackground {
@@ -74,13 +69,6 @@ Item {
backgroundColor: panelBackgroundColor
}
// Battery
PanelBackground {
panel: root.windowRoot.batteryPanelPlaceholder
shapeContainer: backgroundsShape
backgroundColor: panelBackgroundColor
}
// Bluetooth
PanelBackground {
panel: root.windowRoot.bluetoothPanelPlaceholder
@@ -1,22 +1,21 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Services.UI
import qs.Modules.MainScreen.Backgrounds
import qs.Services.UI
/**
* BarBackground - ShapePath component for rendering the bar background
*
* Unified shadow system. This component is a ShapePath that will be
* a child of the unified AllBackgrounds Shape container.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
* BarBackground - ShapePath component for rendering the bar background
*
* Unified shadow system. This component is a ShapePath that will be
* a child of the unified AllBackgrounds Shape container.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
@@ -35,15 +34,15 @@ ShapePath {
readonly property bool shouldShow: {
// Check global bar visibility
if (!BarService.isVisible)
return false
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || []
var screenName = windowRoot?.screen?.name || ""
var monitors = Settings.data.bar.monitors || [];
var screenName = windowRoot?.screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName)
return monitors.length === 0 || monitors.includes(screenName);
}
// Corner radius (from Style)
@@ -65,9 +64,9 @@ ShapePath {
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
if (cornerState === -1)
return 0
return 0;
// All other states use effectiveRadius
return effectiveRadius
return effectiveRadius;
}
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
@@ -3,22 +3,21 @@ import QtQuick.Shapes
import qs.Commons
import qs.Modules.MainScreen.Backgrounds
/**
* PanelBackground - ShapePath component for rendering a single background
*
* Unified shadow system. This component is a ShapePath that will
* be a child of the unified AllBackgrounds Shape container.
*
* Reads positioning and geometry from PanelPlaceholder (via panel.panelItem).
* The actual panel content lives in a separate SmartPanelWindow.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
* PanelBackground - ShapePath component for rendering a single background
*
* Unified shadow system. This component is a ShapePath that will
* be a child of the unified AllBackgrounds Shape container.
*
* Reads positioning and geometry from PanelPlaceholder (via panel.panelItem).
* The actual panel content lives in a separate SmartPanelWindow.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
@@ -51,9 +50,9 @@ ShapePath {
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
if (cornerState === -1)
return 0
return 0;
// All other states use effectiveRadius
return effectiveRadius
return effectiveRadius;
}
// Per-corner multipliers and radii based on panelBg's corner states
@@ -4,78 +4,71 @@ import QtQuick
import QtQuick.Shapes
import Quickshell
/**
* ShapeCornerHelper - Utility singleton for shape corner calculations
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*
* The key technique: Using PathArc direction control (Clockwise vs Counterclockwise)
* combined with multipliers to create both inner and outer corner curves.
*/
* ShapeCornerHelper - Utility singleton for shape corner calculations
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*
* The key technique: Using PathArc direction control (Clockwise vs Counterclockwise)
* combined with multipliers to create both inner and outer corner curves.
*/
Singleton {
id: root
/**
* Get X-axis multiplier for a corner state
* State 1 (horizontal invert) returns -1, others return 1
*/
* Get X-axis multiplier for a corner state
* State 1 (horizontal invert) returns -1, others return 1
*/
function getMultX(cornerState) {
return cornerState === 1 ? -1 : 1
return cornerState === 1 ? -1 : 1;
}
/**
* Get Y-axis multiplier for a corner state
* State 2 (vertical invert) returns -1, others return 1
*/
* Get Y-axis multiplier for a corner state
* State 2 (vertical invert) returns -1, others return 1
*/
function getMultY(cornerState) {
return cornerState === 2 ? -1 : 1
return cornerState === 2 ? -1 : 1;
}
/**
* Get PathArc direction for a corner based on its multipliers
* Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise
* This creates the outer curve effect for inverted corners
*/
* Get PathArc direction for a corner based on its multipliers
* Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise
* This creates the outer curve effect for inverted corners
*/
function getArcDirection(multX, multY) {
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise;
}
/**
* Convenience function to get arc direction directly from corner state
*/
* Convenience function to get arc direction directly from corner state
*/
function getArcDirectionFromState(cornerState) {
const multX = getMultX(cornerState)
const multY = getMultY(cornerState)
return getArcDirection(multX, multY)
const multX = getMultX(cornerState);
const multY = getMultY(cornerState);
return getArcDirection(multX, multY);
}
/**
* Get the "flattening" radius when shape dimensions are too small
* Prevents visual artifacts when radius exceeds dimensions
*/
* Get the "flattening" radius when shape dimensions are too small
* Prevents visual artifacts when radius exceeds dimensions
*/
function getFlattenedRadius(dimension, requestedRadius) {
if (dimension < requestedRadius * 2) {
return dimension / 2
return dimension / 2;
}
return requestedRadius
return requestedRadius;
}
/**
* Check if a shape should use flattened corners
* Returns true if width or height is too small for the requested radius
*/
* Check if a shape should use flattened corners
* Returns true if width or height is too small for the requested radius
*/
function shouldFlatten(width, height, radius) {
return width < radius * 2 || height < radius * 2
return width < radius * 2 || height < radius * 2;
}
}
+10 -11
View File
@@ -2,19 +2,18 @@ import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Modules.Bar
import qs.Services.UI
/**
* BarContentWindow - Separate transparent PanelWindow for bar content
*
* This window contains only the bar widgets (content), while the background
* is rendered in MainScreen's unified Shape system. This separation prevents
* fullscreen redraws when bar widgets redraw.
*
* This component should be instantiated once per screen by AllScreens.qml
*/
* BarContentWindow - Separate transparent PanelWindow for bar content
*
* This window contains only the bar widgets (content), while the background
* is rendered in MainScreen's unified Shape system. This separation prevents
* fullscreen redraws when bar widgets redraw.
*
* This component should be instantiated once per screen by AllScreens.qml
*/
PanelWindow {
id: barWindow
@@ -22,7 +21,7 @@ PanelWindow {
color: Color.transparent // Transparent - background is in MainScreen below
Component.onCompleted: {
Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name)
Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name);
}
// Wayland layer configuration
+12 -16
View File
@@ -3,13 +3,12 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
/**
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
*
* This is a minimal window that works with the compositor to reserve space,
* while the actual bar UI is rendered in MainScreen.
*/
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
*
* This is a minimal window that works with the compositor to reserve space,
* while the actual bar UI is rendered in MainScreen.
*/
PanelWindow {
id: root
@@ -46,11 +45,11 @@ PanelWindow {
// Vertical bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For left bar, reserve left margin; for right bar, reserve right margin
return Style.barHeight + barMarginH
return Style.barHeight + barMarginH;
}
return Style.barHeight
return Style.barHeight;
}
return 0 // Auto-width when left/right anchors are true
return 0; // Auto-width when left/right anchors are true
}
implicitHeight: {
@@ -58,17 +57,14 @@ PanelWindow {
// Horizontal bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For top bar, reserve top margin; for bottom bar, reserve bottom margin
return Style.barHeight + barMarginV
return Style.barHeight + barMarginV;
}
return Style.barHeight
return Style.barHeight;
}
return 0 // Auto-height when top/bottom anchors are true
return 0; // Auto-height when top/bottom anchors are true
}
Component.onCompleted: {
Logger.d("BarExclusionZone", "Created for screen:", screen?.name)
Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating)
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right)
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight)
Logger.d("BarExclusionZone", "Created for screen:", screen?.name);
}
}
+53 -138
View File
@@ -2,16 +2,14 @@ import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import "Backgrounds" as Backgrounds
import qs.Commons
import qs.Services.UI
import "Backgrounds" as Backgrounds
// All panels
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Audio
import qs.Modules.Panels.Battery
import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Calendar
import qs.Modules.Panels.ControlCenter
@@ -23,17 +21,16 @@ import qs.Modules.Panels.SetupWizard
import qs.Modules.Panels.Tray
import qs.Modules.Panels.Wallpaper
import qs.Modules.Panels.WiFi
import qs.Services.UI
/**
* MainScreen - Single PanelWindow per screen that manages all panels and the bar
*/
* MainScreen - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
// Expose panels as readonly property aliases
readonly property alias audioPanel: audioPanel
readonly property alias batteryPanel: batteryPanel
readonly property alias bluetoothPanel: bluetoothPanel
readonly property alias calendarPanel: calendarPanel
readonly property alias controlCenterPanel: controlCenterPanel
@@ -48,7 +45,6 @@ PanelWindow {
// Expose panel placeholders for AllBackgrounds
readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder
readonly property var batteryPanelPlaceholder: batteryPanel.panelPlaceholder
readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder
readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder
readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder
@@ -62,7 +58,7 @@ PanelWindow {
readonly property var wifiPanelPlaceholder: wifiPanel.panelPlaceholder
Component.onCompleted: {
Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y)
Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y);
}
// Wayland
@@ -85,9 +81,9 @@ PanelWindow {
color: {
if (dimmerOpacity > 0 && isPanelOpen && !isPanelClosing) {
return Qt.alpha(Color.mShadow, dimmerOpacity)
return Qt.alpha(Color.mShadow, dimmerOpacity);
}
return Color.transparent
return Color.transparent;
}
Behavior on color {
@@ -101,15 +97,15 @@ PanelWindow {
readonly property bool barShouldShow: {
// Check global bar visibility
if (!BarService.isVisible)
return false
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || []
var screenName = screen?.name || ""
var monitors = Settings.data.bar.monitors || [];
var screenName = screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName)
return monitors.length === 0 || monitors.includes(screenName);
}
// Make everything click-through except bar
@@ -162,145 +158,74 @@ PanelWindow {
// ---------------------------------------
AudioPanel {
id: audioPanel
objectName: "audioPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "audioPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(audioPanel)
}
}
BatteryPanel {
id: batteryPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "batteryPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(batteryPanel)
}
}
BluetoothPanel {
id: bluetoothPanel
objectName: "bluetoothPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "bluetoothPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(bluetoothPanel)
}
}
ControlCenterPanel {
id: controlCenterPanel
objectName: "controlCenterPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "controlCenterPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(controlCenterPanel)
}
}
CalendarPanel {
id: calendarPanel
objectName: "calendarPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "calendarPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(calendarPanel)
}
}
Launcher {
id: launcherPanel
objectName: "launcherPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "launcherPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(launcherPanel)
}
}
NotificationHistoryPanel {
id: notificationHistoryPanel
objectName: "notificationHistoryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "notificationHistoryPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(notificationHistoryPanel)
}
}
SessionMenu {
id: sessionMenuPanel
objectName: "sessionMenuPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "sessionMenuPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(sessionMenuPanel)
}
}
SettingsPanel {
id: settingsPanel
objectName: "settingsPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "settingsPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(settingsPanel)
}
}
SetupWizard {
id: setupWizardPanel
objectName: "setupWizardPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "setupWizardPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(setupWizardPanel)
}
}
TrayDrawerPanel {
id: trayDrawerPanel
objectName: "trayDrawerPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "trayDrawerPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(trayDrawerPanel)
}
}
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "wallpaperPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(wallpaperPanel)
}
}
WiFiPanel {
id: wifiPanel
objectName: "wifiPanel-" + (root.screen?.name || "unknown")
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "wifiPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(wifiPanel)
}
}
// ----------------------------------------------
@@ -326,94 +251,84 @@ PanelWindow {
// Use screen dimensions directly
x: {
if (barPosition === "right")
return screen.width - Style.barHeight - barMarginH - attachmentOverlap // Extend left towards panels
return barMarginH
return screen.width - Style.barHeight - barMarginH - attachmentOverlap; // Extend left towards panels
return barMarginH;
}
y: {
if (barPosition === "bottom")
return screen.height - Style.barHeight - barMarginV - attachmentOverlap
return barMarginV
return screen.height - Style.barHeight - barMarginV - attachmentOverlap;
return barMarginV;
}
width: {
if (barIsVertical) {
return Style.barHeight + attachmentOverlap
return Style.barHeight + attachmentOverlap;
}
return screen.width - barMarginH * 2
return screen.width - barMarginH * 2;
}
height: {
if (barIsVertical) {
return screen.height - barMarginV * 2
return screen.height - barMarginV * 2;
}
return Style.barHeight + attachmentOverlap
return Style.barHeight + attachmentOverlap;
}
// Corner states (same as Bar.qml)
readonly property int topLeftCornerState: {
if (barFloating)
return 0
return 0;
if (barPosition === "top")
return -1
return -1;
if (barPosition === "left")
return -1
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
return -1
return -1;
}
readonly property int topRightCornerState: {
if (barFloating)
return 0
return 0;
if (barPosition === "top")
return -1
return -1;
if (barPosition === "right")
return -1
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
return -1
return -1;
}
readonly property int bottomLeftCornerState: {
if (barFloating)
return 0
return 0;
if (barPosition === "bottom")
return -1
return -1;
if (barPosition === "left")
return -1
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
return -1
return -1;
}
readonly property int bottomRightCornerState: {
if (barFloating)
return 0
return 0;
if (barPosition === "bottom")
return -1
return -1;
if (barPosition === "right")
return -1
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2
return barIsVertical ? 1 : 2;
}
return -1
}
Component.onCompleted: {
Logger.d("MainScreen", "===== Bar placeholder loaded =====")
Logger.d("MainScreen", " Screen:", screen?.name, "Size:", screen?.width, "x", screen?.height)
Logger.d("MainScreen", " Bar position:", barPosition, "| isVertical:", barIsVertical)
Logger.d("MainScreen", " Bar dimensions: x=" + x, "y=" + y, "width=" + width, "height=" + height)
Logger.d("MainScreen", " Style.barHeight =", Style.barHeight)
Logger.d("MainScreen", " Margins: H=" + barMarginH, "V=" + barMarginV, "| Floating:", barFloating)
return -1;
}
}
/**
* Screen Corners
*/
* Screen Corners
*/
ScreenCorners {}
}
}
+241 -242
View File
@@ -3,14 +3,13 @@ import Quickshell
import qs.Commons
import qs.Services.UI
/**
* PanelPlaceholder - Lightweight positioning logic for panel backgrounds
*
* This component stays in MainScreen and provides geometry for PanelBackground rendering.
* It contains only positioning calculations and animations, no visual content.
* The actual panel content lives in a separate SmartPanelWindow.
*/
* PanelPlaceholder - Lightweight positioning logic for panel backgrounds
*
* This component stays in MainScreen and provides geometry for PanelBackground rendering.
* It contains only positioning calculations and animations, no visual content.
* The actual panel content lives in a separate SmartPanelWindow.
*/
Item {
id: root
@@ -71,13 +70,13 @@ Item {
readonly property bool allowAttach: Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar
readonly property bool allowAttachToBar: {
if (!(Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar) || Settings.data.bar.backgroundOpacity < 1.0) {
return false
return false;
}
// A panel can only be attached to a bar if there is a bar on that screen
var monitors = Settings.data.bar.monitors || []
var result = monitors.length === 0 || monitors.includes(root.screen?.name || "")
return result
var monitors = Settings.data.bar.monitors || [];
var result = monitors.length === 0 || monitors.includes(root.screen?.name || "");
return result;
}
// Effective anchor properties (depend on allowAttach)
@@ -97,17 +96,17 @@ Item {
function onUiScaleRatioChanged() {
if (root.isPanelVisible) {
root.setPosition()
root.setPosition();
}
}
}
// Public function to update content size from SmartPanelWindow
function updateContentSize(w, h) {
contentPreferredWidth = w
contentPreferredHeight = h
contentPreferredWidth = w;
contentPreferredHeight = h;
if (isPanelVisible) {
setPosition()
setPosition();
}
}
@@ -115,45 +114,45 @@ Item {
function setPosition() {
// Don't calculate position if parent dimensions aren't available yet
if (!root.width || !root.height) {
Logger.d("PanelPlaceholder", "Skipping setPosition - dimensions not ready:", root.width, "x", root.height, panelName)
Qt.callLater(setPosition)
return
Logger.d("PanelPlaceholder", "Skipping setPosition - dimensions not ready:", root.width, "x", root.height, panelName);
Qt.callLater(setPosition);
return;
}
// Calculate panel dimensions first (needed for positioning)
var w
var w;
// Priority 1: Content-driven size (dynamic)
if (contentPreferredWidth > 0) {
w = contentPreferredWidth
w = contentPreferredWidth;
} // Priority 2: Ratio-based size
else if (root.preferredWidthRatio !== undefined) {
w = Math.round(Math.max(root.width * root.preferredWidthRatio, root.preferredWidth))
w = Math.round(Math.max(root.width * root.preferredWidthRatio, root.preferredWidth));
} // Priority 3: Static preferred width
else {
w = root.preferredWidth
w = root.preferredWidth;
}
var panelWidth = Math.min(w, root.width - Style.marginL * 2)
var panelWidth = Math.min(w, root.width - Style.marginL * 2);
var h
var h;
// Priority 1: Content-driven size (dynamic)
if (contentPreferredHeight > 0) {
h = contentPreferredHeight
h = contentPreferredHeight;
} // Priority 2: Ratio-based size
else if (root.preferredHeightRatio !== undefined) {
h = Math.round(Math.max(root.height * root.preferredHeightRatio, root.preferredHeight))
h = Math.round(Math.max(root.height * root.preferredHeightRatio, root.preferredHeight));
} // Priority 3: Static preferred height
else {
h = root.preferredHeight
h = root.preferredHeight;
}
var panelHeight = Math.min(h, root.height - Style.barHeight - Style.marginL * 2)
var panelHeight = Math.min(h, root.height - Style.barHeight - Style.marginL * 2);
// Update panelBackground target size (will be animated)
panelBackground.targetWidth = panelWidth
panelBackground.targetHeight = panelHeight
panelBackground.targetWidth = panelWidth;
panelBackground.targetHeight = panelHeight;
// Calculate position
var calculatedX
var calculatedY
var calculatedX;
var calculatedY;
// ===== X POSITIONING =====
if (root.useButtonPosition && root.width > 0 && panelWidth > 0) {
@@ -162,111 +161,111 @@ Item {
if (allowAttach) {
// Attached panels: align with bar edge (left or right side)
if (root.barPosition === "left") {
var leftBarEdge = root.barMarginH + Style.barHeight
calculatedX = leftBarEdge
var leftBarEdge = root.barMarginH + Style.barHeight;
calculatedX = leftBarEdge;
} else {
// right
var rightBarEdge = root.width - root.barMarginH - Style.barHeight
calculatedX = rightBarEdge - panelWidth
var rightBarEdge = root.width - root.barMarginH - Style.barHeight;
calculatedX = rightBarEdge - panelWidth;
}
} else {
// Detached panels: center on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2
var minX = Style.marginL
var maxX = root.width - panelWidth - Style.marginL
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2;
var minX = Style.marginL;
var maxX = root.width - panelWidth - Style.marginL;
// Account for vertical bar taking up space
if (root.barPosition === "left") {
minX = root.barMarginH + Style.barHeight + Style.marginL
minX = root.barMarginH + Style.barHeight + Style.marginL;
} else if (root.barPosition === "right") {
maxX = root.width - root.barMarginH - Style.barHeight - panelWidth - Style.marginL
maxX = root.width - root.barMarginH - Style.barHeight - panelWidth - Style.marginL;
}
panelX = Math.max(minX, Math.min(panelX, maxX))
calculatedX = panelX
panelX = Math.max(minX, Math.min(panelX, maxX));
calculatedX = panelX;
}
} else {
// For horizontal bars, center panel on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2;
if (allowAttach) {
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = root.width - root.barMarginH - cornerInset
panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - panelWidth))
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0;
var barLeftEdge = root.barMarginH + cornerInset;
var barRightEdge = root.width - root.barMarginH - cornerInset;
panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - panelWidth));
} else {
panelX = Math.max(Style.marginL, Math.min(panelX, root.width - panelWidth - Style.marginL))
panelX = Math.max(Style.marginL, Math.min(panelX, root.width - panelWidth - Style.marginL));
}
calculatedX = panelX
calculatedX = panelX;
}
} else {
// Standard anchor positioning
if (root.panelAnchorHorizontalCenter) {
if (root.barIsVertical) {
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = root.width - availableStart
calculatedX = availableStart + (availableWidth - panelWidth) / 2
var availableStart = root.barMarginH + Style.barHeight;
var availableWidth = root.width - availableStart;
calculatedX = availableStart + (availableWidth - panelWidth) / 2;
} else if (root.barPosition === "right") {
var availableWidth = root.width - root.barMarginH - Style.barHeight
calculatedX = (availableWidth - panelWidth) / 2
var availableWidth = root.width - root.barMarginH - Style.barHeight;
calculatedX = (availableWidth - panelWidth) / 2;
} else {
calculatedX = (root.width - panelWidth) / 2
calculatedX = (root.width - panelWidth) / 2;
}
} else {
calculatedX = (root.width - panelWidth) / 2
calculatedX = (root.width - panelWidth) / 2;
}
} else if (root.effectivePanelAnchorRight) {
if (allowAttach && root.barIsVertical && root.barPosition === "right") {
var rightBarEdge = root.width - root.barMarginH - Style.barHeight
calculatedX = rightBarEdge - panelWidth
var rightBarEdge = root.width - root.barMarginH - Style.barHeight;
calculatedX = rightBarEdge - panelWidth;
} else if (allowAttach) {
// Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar
var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom)
var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom);
if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) {
var rightCornerInset = Style.radiusL * 2
calculatedX = root.width - root.barMarginH - rightCornerInset - panelWidth
var rightCornerInset = Style.radiusL * 2;
calculatedX = root.width - root.barMarginH - rightCornerInset - panelWidth;
} else {
calculatedX = root.width - panelWidth
calculatedX = root.width - panelWidth;
}
} else {
calculatedX = root.width - panelWidth - Style.marginL
calculatedX = root.width - panelWidth - Style.marginL;
}
} else if (root.effectivePanelAnchorLeft) {
if (allowAttach && root.barIsVertical && root.barPosition === "left") {
var leftBarEdge = root.barMarginH + Style.barHeight
calculatedX = leftBarEdge
var leftBarEdge = root.barMarginH + Style.barHeight;
calculatedX = leftBarEdge;
} else if (allowAttach) {
// Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar
var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom)
var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom);
if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) {
var leftCornerInset = Style.radiusL * 2
calculatedX = root.barMarginH + leftCornerInset
var leftCornerInset = Style.radiusL * 2;
calculatedX = root.barMarginH + leftCornerInset;
} else {
calculatedX = 0
calculatedX = 0;
}
} else {
calculatedX = Style.marginL
calculatedX = Style.marginL;
}
} else {
// No explicit anchor: default to centering on bar
if (root.barIsVertical) {
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = root.width - availableStart - Style.marginL
calculatedX = availableStart + (availableWidth - panelWidth) / 2
var availableStart = root.barMarginH + Style.barHeight;
var availableWidth = root.width - availableStart - Style.marginL;
calculatedX = availableStart + (availableWidth - panelWidth) / 2;
} else {
var availableWidth = root.width - root.barMarginH - Style.barHeight - Style.marginL
calculatedX = Style.marginL + (availableWidth - panelWidth) / 2
var availableWidth = root.width - root.barMarginH - Style.barHeight - Style.marginL;
calculatedX = Style.marginL + (availableWidth - panelWidth) / 2;
}
} else {
if (allowAttach) {
var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0)
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = root.width - root.barMarginH - cornerInset
var centeredX = (root.width - panelWidth) / 2
calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - panelWidth))
var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0);
var barLeftEdge = root.barMarginH + cornerInset;
var barRightEdge = root.width - root.barMarginH - cornerInset;
var centeredX = (root.width - panelWidth) / 2;
calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - panelWidth));
} else {
calculatedX = (root.width - panelWidth) / 2
calculatedX = (root.width - panelWidth) / 2;
}
}
}
@@ -274,76 +273,76 @@ Item {
// Edge snapping for X
if (allowAttach && !root.barFloating && root.width > 0 && panelWidth > 0) {
var leftEdgePos = root.barMarginH
var leftEdgePos = root.barMarginH;
if (root.barPosition === "left") {
leftEdgePos = root.barMarginH + Style.barHeight
leftEdgePos = root.barMarginH + Style.barHeight;
}
var rightEdgePos = root.width - root.barMarginH - panelWidth
var rightEdgePos = root.width - root.barMarginH - panelWidth;
if (root.barPosition === "right") {
rightEdgePos = root.width - root.barMarginH - Style.barHeight - panelWidth
rightEdgePos = root.width - root.barMarginH - Style.barHeight - panelWidth;
}
// Only snap to left edge if panel is actually meant to be at left
var shouldSnapToLeft = root.effectivePanelAnchorLeft || (!root.hasExplicitHorizontalAnchor && root.barPosition === "left")
var shouldSnapToLeft = root.effectivePanelAnchorLeft || (!root.hasExplicitHorizontalAnchor && root.barPosition === "left");
// Only snap to right edge if panel is actually meant to be at right
var shouldSnapToRight = root.effectivePanelAnchorRight || (!root.hasExplicitHorizontalAnchor && root.barPosition === "right")
var shouldSnapToRight = root.effectivePanelAnchorRight || (!root.hasExplicitHorizontalAnchor && root.barPosition === "right");
if (shouldSnapToLeft && Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) {
calculatedX = leftEdgePos
calculatedX = leftEdgePos;
} else if (shouldSnapToRight && Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) {
calculatedX = rightEdgePos
calculatedX = rightEdgePos;
}
}
// ===== Y POSITIONING =====
if (root.useButtonPosition && root.height > 0 && panelHeight > 0) {
if (root.barPosition === "top") {
var topBarEdge = root.barMarginV + Style.barHeight
var topBarEdge = root.barMarginV + Style.barHeight;
if (allowAttach) {
calculatedY = topBarEdge
calculatedY = topBarEdge;
} else {
calculatedY = topBarEdge + Style.marginM
calculatedY = topBarEdge + Style.marginM;
}
} else if (root.barPosition === "bottom") {
var bottomBarEdge = root.height - root.barMarginV - Style.barHeight
var bottomBarEdge = root.height - root.barMarginV - Style.barHeight;
if (allowAttach) {
calculatedY = bottomBarEdge - panelHeight
calculatedY = bottomBarEdge - panelHeight;
} else {
calculatedY = bottomBarEdge - panelHeight - Style.marginM
calculatedY = bottomBarEdge - panelHeight - Style.marginM;
}
} else if (root.barIsVertical) {
var panelY = root.buttonPosition.y + root.buttonHeight / 2 - panelHeight / 2
var extraPadding = (allowAttach && root.barFloating) ? Style.radiusL : 0
var panelY = root.buttonPosition.y + root.buttonHeight / 2 - panelHeight / 2;
var extraPadding = (allowAttach && root.barFloating) ? Style.radiusL : 0;
if (allowAttach) {
var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0)
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = root.height - root.barMarginV - cornerInset
panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - panelHeight))
var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0);
var barTopEdge = root.barMarginV + cornerInset;
var barBottomEdge = root.height - root.barMarginV - cornerInset;
panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - panelHeight));
} else {
panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, root.height - panelHeight - Style.marginL - extraPadding))
panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, root.height - panelHeight - Style.marginL - extraPadding));
}
calculatedY = panelY
calculatedY = panelY;
}
} else {
// Standard anchor positioning
var barOffset = 0
var barOffset = 0;
if (!allowAttach) {
if (root.barPosition === "top") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
barOffset = root.barMarginV + Style.barHeight + Style.marginM;
} else if (root.barPosition === "bottom") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
barOffset = root.barMarginV + Style.barHeight + Style.marginM;
}
} else {
if (root.effectivePanelAnchorTop && root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
calculatedY = root.barMarginV + Style.barHeight;
} else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight;
} else if (!root.hasExplicitVerticalAnchor) {
if (root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
calculatedY = root.barMarginV + Style.barHeight;
} else if (root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight;
}
}
}
@@ -352,57 +351,57 @@ Item {
if (root.panelAnchorVerticalCenter) {
if (!root.barIsVertical) {
if (root.barPosition === "top") {
var availableStart = root.barMarginV + Style.barHeight
var availableHeight = root.height - availableStart
calculatedY = availableStart + (availableHeight - panelHeight) / 2
var availableStart = root.barMarginV + Style.barHeight;
var availableHeight = root.height - availableStart;
calculatedY = availableStart + (availableHeight - panelHeight) / 2;
} else if (root.barPosition === "bottom") {
var availableHeight = root.height - root.barMarginV - Style.barHeight
calculatedY = (availableHeight - panelHeight) / 2
var availableHeight = root.height - root.barMarginV - Style.barHeight;
calculatedY = (availableHeight - panelHeight) / 2;
} else {
calculatedY = (root.height - panelHeight) / 2
calculatedY = (root.height - panelHeight) / 2;
}
} else {
calculatedY = (root.height - panelHeight) / 2
calculatedY = (root.height - panelHeight) / 2;
}
} else if (root.effectivePanelAnchorTop) {
if (allowAttach) {
calculatedY = 0
calculatedY = 0;
} else {
var topBarOffset = (root.barPosition === "top") ? barOffset : 0
calculatedY = topBarOffset + Style.marginL
var topBarOffset = (root.barPosition === "top") ? barOffset : 0;
calculatedY = topBarOffset + Style.marginL;
}
} else if (root.effectivePanelAnchorBottom) {
if (allowAttach) {
calculatedY = root.height - panelHeight
calculatedY = root.height - panelHeight;
} else {
var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0
calculatedY = root.height - panelHeight - bottomBarOffset - Style.marginL
var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0;
calculatedY = root.height - panelHeight - bottomBarOffset - Style.marginL;
}
} else {
if (root.barIsVertical) {
if (allowAttach) {
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = root.height - root.barMarginV - cornerInset
var centeredY = (root.height - panelHeight) / 2
calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - panelHeight))
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0;
var barTopEdge = root.barMarginV + cornerInset;
var barBottomEdge = root.height - root.barMarginV - cornerInset;
var centeredY = (root.height - panelHeight) / 2;
calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - panelHeight));
} else {
calculatedY = (root.height - panelHeight) / 2
calculatedY = (root.height - panelHeight) / 2;
}
} else {
if (allowAttach && !root.barIsVertical) {
if (root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
calculatedY = root.barMarginV + Style.barHeight;
} else if (root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight;
}
} else {
if (root.barPosition === "top") {
calculatedY = barOffset + Style.marginL
calculatedY = barOffset + Style.marginL;
} else if (root.barPosition === "bottom") {
calculatedY = Style.marginL
calculatedY = Style.marginL;
} else {
calculatedY = Style.marginL
calculatedY = Style.marginL;
}
}
}
@@ -412,34 +411,34 @@ Item {
// Edge snapping for Y
if (allowAttach && !root.barFloating && root.height > 0 && panelHeight > 0) {
var topEdgePos = root.barMarginV
var topEdgePos = root.barMarginV;
if (root.barPosition === "top") {
topEdgePos = root.barMarginV + Style.barHeight
topEdgePos = root.barMarginV + Style.barHeight;
}
var bottomEdgePos = root.height - root.barMarginV - panelHeight
var bottomEdgePos = root.height - root.barMarginV - panelHeight;
if (root.barPosition === "bottom") {
bottomEdgePos = root.height - root.barMarginV - Style.barHeight - panelHeight
bottomEdgePos = root.height - root.barMarginV - Style.barHeight - panelHeight;
}
// Only snap to top edge if panel is actually meant to be at top
var shouldSnapToTop = root.effectivePanelAnchorTop || (!root.hasExplicitVerticalAnchor && root.barPosition === "top")
var shouldSnapToTop = root.effectivePanelAnchorTop || (!root.hasExplicitVerticalAnchor && root.barPosition === "top");
// Only snap to bottom edge if panel is actually meant to be at bottom
var shouldSnapToBottom = root.effectivePanelAnchorBottom || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom")
var shouldSnapToBottom = root.effectivePanelAnchorBottom || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom");
if (shouldSnapToTop && Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) {
calculatedY = topEdgePos
calculatedY = topEdgePos;
} else if (shouldSnapToBottom && Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) {
calculatedY = bottomEdgePos
calculatedY = bottomEdgePos;
}
}
// Apply calculated positions (set targets for animation)
panelBackground.targetX = calculatedX
panelBackground.targetY = calculatedY
panelBackground.targetX = calculatedX;
panelBackground.targetY = calculatedY;
Logger.d("PanelPlaceholder", "Position calculated:", calculatedX, calculatedY, panelName)
Logger.d("PanelPlaceholder", " Panel size:", panelWidth, "x", panelHeight)
Logger.d("PanelPlaceholder", "Position calculated:", calculatedX, calculatedY, panelName);
Logger.d("PanelPlaceholder", " Panel size:", panelWidth, "x", panelHeight);
}
// The panel background geometry item
@@ -469,35 +468,35 @@ Item {
// Animation direction determination (using target position to avoid binding loops)
readonly property bool willTouchTopBar: {
if (!isPanelVisible)
return false
return false;
if (!allowAttachToBar || root.barPosition !== "top" || root.barIsVertical)
return false
var targetTopBarY = root.barMarginV + Style.barHeight
return Math.abs(panelBackground.targetY - targetTopBarY) <= 1
return false;
var targetTopBarY = root.barMarginV + Style.barHeight;
return Math.abs(panelBackground.targetY - targetTopBarY) <= 1;
}
readonly property bool willTouchBottomBar: {
if (!isPanelVisible)
return false
return false;
if (!allowAttachToBar || root.barPosition !== "bottom" || root.barIsVertical)
return false
var targetBottomBarY = root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight
return Math.abs(panelBackground.targetY - targetBottomBarY) <= 1
return false;
var targetBottomBarY = root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight;
return Math.abs(panelBackground.targetY - targetBottomBarY) <= 1;
}
readonly property bool willTouchLeftBar: {
if (!isPanelVisible)
return false
return false;
if (!allowAttachToBar || root.barPosition !== "left" || !root.barIsVertical)
return false
var targetLeftBarX = root.barMarginH + Style.barHeight
return Math.abs(panelBackground.targetX - targetLeftBarX) <= 1
return false;
var targetLeftBarX = root.barMarginH + Style.barHeight;
return Math.abs(panelBackground.targetX - targetLeftBarX) <= 1;
}
readonly property bool willTouchRightBar: {
if (!isPanelVisible)
return false
return false;
if (!allowAttachToBar || root.barPosition !== "right" || !root.barIsVertical)
return false
var targetRightBarX = root.width - root.barMarginH - Style.barHeight - panelBackground.targetWidth
return Math.abs(panelBackground.targetX - targetRightBarX) <= 1
return false;
var targetRightBarX = root.width - root.barMarginH - Style.barHeight - panelBackground.targetWidth;
return Math.abs(panelBackground.targetX - targetRightBarX) <= 1;
}
readonly property bool willTouchTopEdge: isPanelVisible && allowAttach && panelBackground.targetY <= 1
readonly property bool willTouchBottomEdge: isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1)
@@ -506,59 +505,59 @@ Item {
readonly property bool isActuallyAttachedToAnyEdge: {
if (!isPanelVisible)
return false
return willTouchTopBar || willTouchBottomBar || willTouchLeftBar || willTouchRightBar || willTouchTopEdge || willTouchBottomEdge || willTouchLeftEdge || willTouchRightEdge
return false;
return willTouchTopBar || willTouchBottomBar || willTouchLeftBar || willTouchRightBar || willTouchTopEdge || willTouchBottomEdge || willTouchLeftEdge || willTouchRightEdge;
}
readonly property bool animateFromTop: {
if (!isPanelVisible)
return true
return true;
if (willTouchTopBar)
return true
return true;
if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar)
return true
return true;
if (!isActuallyAttachedToAnyEdge)
return true
return false
return true;
return false;
}
readonly property bool animateFromBottom: {
if (!isPanelVisible)
return false
return false;
if (willTouchBottomBar)
return true
return true;
if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar)
return true
return false
return true;
return false;
}
readonly property bool animateFromLeft: {
if (!isPanelVisible)
return false
return false;
if (willTouchTopBar || willTouchBottomBar)
return false
return false;
if (willTouchLeftBar)
return true
var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1
var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1)
return true;
var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1;
var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1);
if (touchingTopEdge || touchingBottomEdge)
return false
return false;
if (willTouchLeftEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar)
return true
return false
return true;
return false;
}
readonly property bool animateFromRight: {
if (!isPanelVisible)
return false
return false;
if (willTouchTopBar || willTouchBottomBar)
return false
return false;
if (willTouchRightBar)
return true
var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1
var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1)
return true;
var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1;
var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1);
if (touchingTopEdge || touchingBottomEdge)
return false
return false;
if (willTouchRightEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar)
return true
return false
return true;
return false;
}
readonly property bool shouldAnimateWidth: !shouldAnimateHeight && (animateFromLeft || animateFromRight)
@@ -567,17 +566,17 @@ Item {
// Current animated width/height
readonly property real currentWidth: {
if (isClosing && opacityFadeComplete && shouldAnimateWidth)
return 0
return 0;
if (isClosing || isPanelVisible)
return targetWidth
return 0
return targetWidth;
return 0;
}
readonly property real currentHeight: {
if (isClosing && opacityFadeComplete && shouldAnimateHeight)
return 0
return 0;
if (isClosing || isPanelVisible)
return targetHeight
return 0
return targetHeight;
return 0;
}
width: currentWidth
@@ -586,28 +585,28 @@ Item {
x: {
if (animateFromRight) {
if (isPanelVisible || isClosing) {
var targetRightEdge = targetX + targetWidth
return targetRightEdge - width
var targetRightEdge = targetX + targetWidth;
return targetRightEdge - width;
}
}
return targetX
return targetX;
}
y: {
if (animateFromBottom) {
if (isPanelVisible || isClosing) {
var targetBottomEdge = targetY + targetHeight
return targetBottomEdge - height
var targetBottomEdge = targetY + targetHeight;
return targetBottomEdge - height;
}
}
return targetY
return targetY;
}
Behavior on width {
NumberAnimation {
duration: {
if (!panelBackground.shouldAnimateWidth)
return 0
return root.isClosing ? Style.animationFast : Style.animationNormal
return 0;
return root.isClosing ? Style.animationFast : Style.animationNormal;
}
easing.type: Easing.BezierSpline
easing.bezierCurve: panelBackground.bezierCurve
@@ -618,8 +617,8 @@ Item {
NumberAnimation {
duration: {
if (!panelBackground.shouldAnimateHeight)
return 0
return root.isClosing ? Style.animationFast : Style.animationNormal
return 0;
return root.isClosing ? Style.animationFast : Style.animationNormal;
}
easing.type: Easing.BezierSpline
easing.bezierCurve: panelBackground.bezierCurve
@@ -628,75 +627,75 @@ Item {
// Corner states for PanelBackground to read
property int topLeftCornerState: {
var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft))
var barTouchInverted = touchingTopBar || touchingLeftBar
var edgeInverted = allowAttach && (touchingLeftEdge || touchingTopEdge)
var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft));
var barTouchInverted = touchingTopBar || touchingLeftBar;
var edgeInverted = allowAttach && (touchingLeftEdge || touchingTopEdge);
var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top");
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (touchingLeftEdge && touchingTopEdge)
return 0
return 0;
if (touchingLeftEdge)
return 2
return 2;
if (touchingTopEdge)
return 1
return root.barIsVertical ? 2 : 1
return 1;
return root.barIsVertical ? 2 : 1;
}
return 0
return 0;
}
property int topRightCornerState: {
var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight))
var barTouchInverted = touchingTopBar || touchingRightBar
var edgeInverted = allowAttach && (touchingRightEdge || touchingTopEdge)
var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight));
var barTouchInverted = touchingTopBar || touchingRightBar;
var edgeInverted = allowAttach && (touchingRightEdge || touchingTopEdge);
var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top");
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (touchingRightEdge && touchingTopEdge)
return 0
return 0;
if (touchingRightEdge)
return 2
return 2;
if (touchingTopEdge)
return 1
return root.barIsVertical ? 2 : 1
return 1;
return root.barIsVertical ? 2 : 1;
}
return 0
return 0;
}
property int bottomLeftCornerState: {
var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft))
var barTouchInverted = touchingBottomBar || touchingLeftBar
var edgeInverted = allowAttach && (touchingLeftEdge || touchingBottomEdge)
var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft));
var barTouchInverted = touchingBottomBar || touchingLeftBar;
var edgeInverted = allowAttach && (touchingLeftEdge || touchingBottomEdge);
var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom");
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (touchingLeftEdge && touchingBottomEdge)
return 0
return 0;
if (touchingLeftEdge)
return 2
return 2;
if (touchingBottomEdge)
return 1
return root.barIsVertical ? 2 : 1
return 1;
return root.barIsVertical ? 2 : 1;
}
return 0
return 0;
}
property int bottomRightCornerState: {
var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight))
var barTouchInverted = touchingBottomBar || touchingRightBar
var edgeInverted = allowAttach && (touchingRightEdge || touchingBottomEdge)
var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight));
var barTouchInverted = touchingBottomBar || touchingRightBar;
var edgeInverted = allowAttach && (touchingRightEdge || touchingBottomEdge);
var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom");
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (touchingRightEdge && touchingBottomEdge)
return 0
return 0;
if (touchingRightEdge)
return 2
return 2;
if (touchingBottomEdge)
return 1
return root.barIsVertical ? 2 : 1
return 1;
return root.barIsVertical ? 2 : 1;
}
return 0
return 0;
}
}
}
+5 -6
View File
@@ -2,13 +2,12 @@ import QtQuick
import QtQuick.Shapes
import qs.Commons
/**
* ScreenCorners - Shape component for rendering screen corners
*
* Renders concave corners at the screen edges to create a rounded screen effect.
* Self-contained Shape component (no shadows).
*/
* ScreenCorners - Shape component for rendering screen corners
*
* Renders concave corners at the screen edges to create a rounded screen effect.
* Self-contained Shape component (no shadows).
*/
Item {
id: root
+50 -44
View File
@@ -3,15 +3,14 @@ import Quickshell
import qs.Commons
import qs.Services.UI
/**
* SmartPanel - Wrapper that creates placeholder + content window
*
* This component is a thin wrapper that maintains backward compatibility
* while splitting panel rendering into:
* 1. PanelPlaceholder (in MainScreen, for background rendering)
* 2. SmartPanelWindow (separate window, for content)
*/
* SmartPanel - Wrapper that creates placeholder + content window
*
* This component is a thin wrapper that maintains backward compatibility
* while splitting panel rendering into:
* 1. PanelPlaceholder (in MainScreen, for background rendering)
* 2. SmartPanelWindow (separate window, for content)
*/
Item {
id: root
@@ -59,60 +58,74 @@ Item {
// Keyboard event handlers - these can be overridden by panel implementations
// Note: SmartPanelWindow directly calls these functions via panelWrapper reference
function onEscapePressed() {}
function onTabPressed() {}
function onBackTabPressed() {}
function onUpPressed() {}
function onDownPressed() {}
function onLeftPressed() {}
function onRightPressed() {}
function onReturnPressed() {}
function onHomePressed() {}
function onEndPressed() {}
function onPageUpPressed() {}
function onPageDownPressed() {}
function onCtrlJPressed() {}
function onCtrlKPressed() {}
function onEscapePressed() {
}
function onTabPressed() {
}
function onBackTabPressed() {
}
function onUpPressed() {
}
function onDownPressed() {
}
function onLeftPressed() {
}
function onRightPressed() {
}
function onReturnPressed() {
}
function onHomePressed() {
}
function onEndPressed() {
}
function onPageUpPressed() {
}
function onPageDownPressed() {
}
function onCtrlJPressed() {
}
function onCtrlKPressed() {
}
// Public control functions
function toggle(buttonItem, buttonName) {
// Ensure window is created before toggling
if (!root.windowActive) {
root.windowActive = true
root.windowActive = true;
Qt.callLater(function () {
if (windowLoader.item) {
windowLoader.item.toggle(buttonItem, buttonName)
windowLoader.item.toggle(buttonItem, buttonName);
}
})
});
} else if (windowLoader.item) {
windowLoader.item.toggle(buttonItem, buttonName)
windowLoader.item.toggle(buttonItem, buttonName);
}
}
function open(buttonItem, buttonName) {
// Ensure window is created before opening
if (!root.windowActive) {
root.windowActive = true
root.windowActive = true;
Qt.callLater(function () {
if (windowLoader.item) {
windowLoader.item.open(buttonItem, buttonName)
windowLoader.item.open(buttonItem, buttonName);
}
})
});
} else if (windowLoader.item) {
windowLoader.item.open(buttonItem, buttonName)
windowLoader.item.open(buttonItem, buttonName);
}
}
function close() {
if (windowLoader.item) {
windowLoader.item.close()
windowLoader.item.close();
}
}
// Expose setPosition for panels that need to recalculate on settings changes
function setPosition() {
if (panelPlaceholder) {
panelPlaceholder.setPosition()
panelPlaceholder.setPosition();
}
}
@@ -157,24 +170,17 @@ Item {
// Forward signals
onPanelOpened: root.opened()
onPanelClosed: {
root.closed()
root.closed();
// Destroy the window after close animation completes
Qt.callLater(function () {
root.windowActive = false
})
root.windowActive = false;
});
}
}
}
// Register with PanelService (backward compatibility)
// Note: Registration happens in MainScreen after objectName is set
// Register with PanelService
Component.onCompleted: {
// Use Qt.callLater to ensure objectName is set by parent before registering
Qt.callLater(function () {
if (!objectName) {
Logger.w("SmartPanel", "Panel created without objectName - PanelService registration may fail")
}
PanelService.registerPanel(root)
})
PanelService.registerPanel(root);
}
}
+126 -126
View File
@@ -5,13 +5,12 @@ import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
/**
* SmartPanelWindow - Separate window for panel content
*
* This component runs in its own window, separate from MainScreen.
* It follows the PanelPlaceholder for positioning and contains the actual panel content.
*/
* SmartPanelWindow - Separate window for panel content
*
* This component runs in its own window, separate from MainScreen.
* It follows the PanelPlaceholder for positioning and contains the actual panel content.
*/
PanelWindow {
id: root
@@ -56,6 +55,7 @@ PanelWindow {
color: Color.transparent
mask: null // No mask - content window is rectangular
visible: isPanelOpen
screen: placeholder.screen // Explicitly set screen to match placeholder
// Wayland layer shell configuration - fullscreen window
WlrLayershell.layer: WlrLayer.Top
@@ -63,13 +63,13 @@ PanelWindow {
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: {
if (!root.isPanelOpen) {
return WlrKeyboardFocus.None
return WlrKeyboardFocus.None;
}
if (CompositorService.isHyprland) {
// Exclusive focus on hyprland is too restrictive.
return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.OnDemand;
} else {
return root.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand
return root.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand;
}
}
@@ -91,95 +91,95 @@ PanelWindow {
// Sync state to placeholder
onIsPanelVisibleChanged: {
placeholder.isPanelVisible = isPanelVisible
placeholder.isPanelVisible = isPanelVisible;
}
onIsClosingChanged: {
placeholder.isClosing = isClosing
placeholder.isClosing = isClosing;
}
onOpacityFadeCompleteChanged: {
placeholder.opacityFadeComplete = opacityFadeComplete
placeholder.opacityFadeComplete = opacityFadeComplete;
}
// Panel control functions
function toggle(buttonItem, buttonName) {
if (!isPanelOpen) {
open(buttonItem, buttonName)
open(buttonItem, buttonName);
} else {
close()
close();
}
}
function open(buttonItem, buttonName) {
if (!buttonItem && buttonName) {
buttonItem = BarService.lookupWidget(buttonName, placeholder.screen.name)
buttonItem = BarService.lookupWidget(buttonName, placeholder.screen.name);
}
if (buttonItem) {
placeholder.buttonItem = buttonItem
placeholder.buttonItem = buttonItem;
// Map button position to screen coordinates
var buttonPos = buttonItem.mapToItem(null, 0, 0)
placeholder.buttonPosition = Qt.point(buttonPos.x, buttonPos.y)
placeholder.buttonWidth = buttonItem.width
placeholder.buttonHeight = buttonItem.height
placeholder.useButtonPosition = true
var buttonPos = buttonItem.mapToItem(null, 0, 0);
placeholder.buttonPosition = Qt.point(buttonPos.x, buttonPos.y);
placeholder.buttonWidth = buttonItem.width;
placeholder.buttonHeight = buttonItem.height;
placeholder.useButtonPosition = true;
} else {
// No button provided: reset button position mode
placeholder.buttonItem = null
placeholder.useButtonPosition = false
placeholder.buttonItem = null;
placeholder.useButtonPosition = false;
}
// Set isPanelOpen to trigger content loading
isPanelOpen = true
isPanelOpen = true;
// Notify PanelService
PanelService.willOpenPanel(root)
PanelService.willOpenPanel(root);
}
function close() {
// Start close sequence: fade opacity first
isClosing = true
sizeAnimationComplete = false
closeFinalized = false
isClosing = true;
sizeAnimationComplete = false;
closeFinalized = false;
// Stop the open animation timer if it's still running
opacityTrigger.stop()
openWatchdogActive = false
openWatchdogTimer.stop()
opacityTrigger.stop();
openWatchdogActive = false;
openWatchdogTimer.stop();
// Start close watchdog timer
closeWatchdogActive = true
closeWatchdogTimer.restart()
closeWatchdogActive = true;
closeWatchdogTimer.restart();
// If opacity is already 0, skip directly to size animation
if (contentWrapper.opacity === 0.0) {
opacityFadeComplete = true
opacityFadeComplete = true;
} else {
opacityFadeComplete = false
opacityFadeComplete = false;
}
Logger.d("SmartPanelWindow", "Closing panel", placeholder.panelName)
Logger.d("SmartPanelWindow", "Closing panel", placeholder.panelName);
}
function finalizeClose() {
// Prevent double-finalization
if (root.closeFinalized) {
Logger.w("SmartPanelWindow", "finalizeClose called but already finalized - ignoring", placeholder.panelName)
return
Logger.w("SmartPanelWindow", "finalizeClose called but already finalized - ignoring", placeholder.panelName);
return;
}
// Complete the close sequence after animations finish
root.closeFinalized = true
root.closeWatchdogActive = false
closeWatchdogTimer.stop()
root.closeFinalized = true;
root.closeWatchdogActive = false;
closeWatchdogTimer.stop();
root.isPanelVisible = false
root.isPanelOpen = false
root.isClosing = false
root.opacityFadeComplete = false
PanelService.closedPanel(root)
panelClosed()
root.isPanelVisible = false;
root.isPanelOpen = false;
root.isClosing = false;
root.opacityFadeComplete = false;
PanelService.closedPanel(root);
panelClosed();
Logger.d("SmartPanelWindow", "Panel close finalized", placeholder.panelName)
Logger.d("SmartPanelWindow", "Panel close finalized", placeholder.panelName);
}
// Fullscreen container for click-to-close and content
@@ -189,59 +189,59 @@ PanelWindow {
// Handle keyboard events directly via Keys handler
Keys.onPressed: event => {
Logger.d("SmartPanelWindow", "Key pressed:", event.key, "for panel:", placeholder.panelName)
Logger.d("SmartPanelWindow", "Key pressed:", event.key, "for panel:", placeholder.panelName);
if (event.key === Qt.Key_Escape) {
panelWrapper.onEscapePressed()
panelWrapper.onEscapePressed();
if (closeWithEscape) {
root.close()
event.accepted = true
root.close();
event.accepted = true;
}
} else if (panelWrapper) {
if (event.key === Qt.Key_Up && panelWrapper.onUpPressed) {
panelWrapper.onUpPressed()
event.accepted = true
panelWrapper.onUpPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Down && panelWrapper.onDownPressed) {
panelWrapper.onDownPressed()
event.accepted = true
panelWrapper.onDownPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Left && panelWrapper.onLeftPressed) {
panelWrapper.onLeftPressed()
event.accepted = true
panelWrapper.onLeftPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Right && panelWrapper.onRightPressed) {
panelWrapper.onRightPressed()
event.accepted = true
panelWrapper.onRightPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Tab && panelWrapper.onTabPressed) {
panelWrapper.onTabPressed()
event.accepted = true
panelWrapper.onTabPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab && panelWrapper.onBackTabPressed) {
panelWrapper.onBackTabPressed()
event.accepted = true
panelWrapper.onBackTabPressed();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && panelWrapper.onReturnPressed) {
panelWrapper.onReturnPressed()
event.accepted = true
panelWrapper.onReturnPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Home && panelWrapper.onHomePressed) {
panelWrapper.onHomePressed()
event.accepted = true
panelWrapper.onHomePressed();
event.accepted = true;
} else if (event.key === Qt.Key_End && panelWrapper.onEndPressed) {
panelWrapper.onEndPressed()
event.accepted = true
panelWrapper.onEndPressed();
event.accepted = true;
} else if (event.key === Qt.Key_PageUp && panelWrapper.onPageUpPressed) {
panelWrapper.onPageUpPressed()
event.accepted = true
panelWrapper.onPageUpPressed();
event.accepted = true;
} else if (event.key === Qt.Key_PageDown && panelWrapper.onPageDownPressed) {
panelWrapper.onPageDownPressed()
event.accepted = true
panelWrapper.onPageDownPressed();
event.accepted = true;
} else if (event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlJPressed) {
panelWrapper.onCtrlJPressed()
event.accepted = true
panelWrapper.onCtrlJPressed();
event.accepted = true;
} else if (event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlKPressed) {
panelWrapper.onCtrlKPressed()
event.accepted = true
panelWrapper.onCtrlKPressed();
event.accepted = true;
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlNPressed) {
panelWrapper.onCtrlNPressed()
event.accepted = true
panelWrapper.onCtrlNPressed();
event.accepted = true;
} else if (event.key === Qt.Key_P && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlPPressed) {
panelWrapper.onCtrlPPressed()
event.accepted = true
panelWrapper.onCtrlPPressed();
event.accepted = true;
}
}
}
@@ -252,8 +252,8 @@ PanelWindow {
enabled: root.isPanelOpen && !root.isClosing
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
root.close()
mouse.accepted = true
root.close();
mouse.accepted = true;
}
z: 0
}
@@ -271,10 +271,10 @@ PanelWindow {
// Opacity animation
opacity: {
if (isClosing)
return 0.0
return 0.0;
if (isPanelVisible && sizeAnimationComplete)
return 1.0
return 0.0
return 1.0;
return 0.0;
}
Behavior on opacity {
@@ -287,33 +287,33 @@ PanelWindow {
// Safety: Zero-duration animation handling
if (!running && duration === 0) {
if (root.isClosing && contentWrapper.opacity === 0.0) {
root.opacityFadeComplete = true
var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight
root.opacityFadeComplete = true;
var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight;
if (shouldFinalizeNow) {
Logger.d("SmartPanelWindow", "Zero-duration opacity + no size animation - finalizing", placeholder.panelName)
Qt.callLater(root.finalizeClose)
Logger.d("SmartPanelWindow", "Zero-duration opacity + no size animation - finalizing", placeholder.panelName);
Qt.callLater(root.finalizeClose);
}
} else if (root.isPanelVisible && contentWrapper.opacity === 1.0) {
root.openWatchdogActive = false
openWatchdogTimer.stop()
root.openWatchdogActive = false;
openWatchdogTimer.stop();
}
return
return;
}
// When opacity fade completes during close, trigger size animation
if (!running && root.isClosing && contentWrapper.opacity === 0.0) {
root.opacityFadeComplete = true
var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight
root.opacityFadeComplete = true;
var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight;
if (shouldFinalizeNow) {
Logger.d("SmartPanelWindow", "No animation - finalizing immediately", placeholder.panelName)
Qt.callLater(root.finalizeClose)
Logger.d("SmartPanelWindow", "No animation - finalizing immediately", placeholder.panelName);
Qt.callLater(root.finalizeClose);
} else {
Logger.d("SmartPanelWindow", "Animation will run - waiting for size animation", placeholder.panelName)
Logger.d("SmartPanelWindow", "Animation will run - waiting for size animation", placeholder.panelName);
}
} // When opacity fade completes during open, stop watchdog
else if (!running && root.isPanelVisible && contentWrapper.opacity === 1.0) {
root.openWatchdogActive = false
openWatchdogTimer.stop()
root.openWatchdogActive = false;
openWatchdogTimer.stop();
}
}
}
@@ -330,31 +330,31 @@ PanelWindow {
onLoaded: {
// Capture initial content-driven size if available
if (contentLoader.item) {
var hasWidthProp = contentLoader.item.hasOwnProperty('contentPreferredWidth')
var hasHeightProp = contentLoader.item.hasOwnProperty('contentPreferredHeight')
var hasWidthProp = contentLoader.item.hasOwnProperty('contentPreferredWidth');
var hasHeightProp = contentLoader.item.hasOwnProperty('contentPreferredHeight');
if (hasWidthProp || hasHeightProp) {
var initialWidth = hasWidthProp ? contentLoader.item.contentPreferredWidth : 0
var initialHeight = hasHeightProp ? contentLoader.item.contentPreferredHeight : 0
placeholder.updateContentSize(initialWidth, initialHeight)
Logger.d("SmartPanelWindow", "Initial content size:", initialWidth, "x", initialHeight, placeholder.panelName)
var initialWidth = hasWidthProp ? contentLoader.item.contentPreferredWidth : 0;
var initialHeight = hasHeightProp ? contentLoader.item.contentPreferredHeight : 0;
placeholder.updateContentSize(initialWidth, initialHeight);
Logger.d("SmartPanelWindow", "Initial content size:", initialWidth, "x", initialHeight, placeholder.panelName);
}
}
// Calculate position in placeholder
placeholder.setPosition()
placeholder.setPosition();
// Make panel visible on the next frame
Qt.callLater(function () {
root.isPanelVisible = true
opacityTrigger.start()
root.isPanelVisible = true;
opacityTrigger.start();
// Start open watchdog timer
root.openWatchdogActive = true
openWatchdogTimer.start()
root.openWatchdogActive = true;
openWatchdogTimer.start();
panelOpened()
})
panelOpened();
});
}
}
@@ -363,7 +363,7 @@ PanelWindow {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
mouse.accepted = true // Eat the click to prevent propagation to background
mouse.accepted = true; // Eat the click to prevent propagation to background
}
z: -1 // Behind content but above background click-to-close
}
@@ -375,13 +375,13 @@ PanelWindow {
function onContentPreferredWidthChanged() {
if (root.isPanelOpen && root.isPanelVisible && contentLoader.item) {
placeholder.updateContentSize(contentLoader.item.contentPreferredWidth, placeholder.contentPreferredHeight)
placeholder.updateContentSize(contentLoader.item.contentPreferredWidth, placeholder.contentPreferredHeight);
}
}
function onContentPreferredHeightChanged() {
if (root.isPanelOpen && root.isPanelVisible && contentLoader.item) {
placeholder.updateContentSize(placeholder.contentPreferredWidth, contentLoader.item.contentPreferredHeight)
placeholder.updateContentSize(placeholder.contentPreferredWidth, contentLoader.item.contentPreferredHeight);
}
}
}
@@ -395,7 +395,7 @@ PanelWindow {
repeat: false
onTriggered: {
if (root.isPanelVisible) {
root.sizeAnimationComplete = true
root.sizeAnimationComplete = true;
}
}
}
@@ -407,11 +407,11 @@ PanelWindow {
repeat: false
onTriggered: {
if (root.openWatchdogActive) {
Logger.w("SmartPanelWindow", "Open watchdog timeout - forcing panel visible state", placeholder.panelName)
root.openWatchdogActive = false
Logger.w("SmartPanelWindow", "Open watchdog timeout - forcing panel visible state", placeholder.panelName);
root.openWatchdogActive = false;
if (root.isPanelOpen && !root.isPanelVisible) {
root.isPanelVisible = true
root.sizeAnimationComplete = true
root.isPanelVisible = true;
root.sizeAnimationComplete = true;
}
}
}
@@ -424,8 +424,8 @@ PanelWindow {
repeat: false
onTriggered: {
if (root.closeWatchdogActive && !root.closeFinalized) {
Logger.w("SmartPanelWindow", "Close watchdog timeout - forcing panel close", placeholder.panelName)
Qt.callLater(root.finalizeClose)
Logger.w("SmartPanelWindow", "Close watchdog timeout - forcing panel close", placeholder.panelName);
Qt.callLater(root.finalizeClose);
}
}
}
@@ -437,14 +437,14 @@ PanelWindow {
function onWidthChanged() {
// When width shrinks to 0 during close and we're animating width, finalize
if (root.isClosing && placeholder.panelItem.width === 0 && placeholder.panelItem.shouldAnimateWidth) {
Qt.callLater(root.finalizeClose)
Qt.callLater(root.finalizeClose);
}
}
function onHeightChanged() {
// When height shrinks to 0 during close and we're animating height, finalize
if (root.isClosing && placeholder.panelItem.height === 0 && placeholder.panelItem.shouldAnimateHeight) {
Qt.callLater(root.finalizeClose)
Qt.callLater(root.finalizeClose);
}
}
}
+7 -7
View File
@@ -2,8 +2,8 @@ import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Modules.Bar.Extras
import qs.Services.UI
// Separate window for TrayMenu context menus
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
@@ -30,18 +30,18 @@ PanelWindow {
// Register with PanelService so panels can find this window
Component.onCompleted: {
objectName = "trayMenuWindow-" + (screen?.name || "unknown")
PanelService.registerTrayMenuWindow(screen, root)
objectName = "trayMenuWindow-" + (screen?.name || "unknown");
PanelService.registerTrayMenuWindow(screen, root);
}
function open() {
visible = true
visible = true;
}
function close() {
visible = false
visible = false;
if (trayMenu.item) {
trayMenu.item.hideMenu()
trayMenu.item.hideMenu();
}
}
@@ -56,7 +56,7 @@ PanelWindow {
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
onLoaded: {
if (item) {
item.screen = root.screen
item.screen = root.screen;
}
}
}
+67 -67
View File
@@ -2,9 +2,9 @@ import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Services.Notifications
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services.System
import qs.Widgets
@@ -35,7 +35,7 @@ Variants {
target: notificationModel
function onCountChanged() {
if (notificationModel.count === 0 && root.active) {
delayTimer.restart()
delayTimer.restart();
}
}
}
@@ -66,30 +66,30 @@ Variants {
// Calculate bar offsets for each edge separately
readonly property int barOffsetTop: {
if (barPos !== "top")
return 0
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
return Style.barHeight + floatMarginV
return 0;
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0;
return Style.barHeight + floatMarginV;
}
readonly property int barOffsetBottom: {
if (barPos !== "bottom")
return 0
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
return Style.barHeight + floatMarginV
return 0;
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0;
return Style.barHeight + floatMarginV;
}
readonly property int barOffsetLeft: {
if (barPos !== "left")
return 0
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
return floatMarginH
return 0;
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0;
return floatMarginH;
}
readonly property int barOffsetRight: {
if (barPos !== "right")
return 0
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
return floatMarginH
return 0;
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0;
return floatMarginH;
}
// Anchoring
@@ -111,31 +111,31 @@ Variants {
Component.onCompleted: {
animateConnection = function (notificationId) {
var delegate = null
var delegate = null;
if (notificationRepeater) {
for (var i = 0; i < notificationRepeater.count; i++) {
var item = notificationRepeater.itemAt(i)
var item = notificationRepeater.itemAt(i);
if (item?.notificationId === notificationId) {
delegate = item
break
delegate = item;
break;
}
}
}
if (delegate?.animateOut) {
delegate.animateOut()
delegate.animateOut();
} else {
NotificationService.dismissActiveNotification(notificationId)
NotificationService.dismissActiveNotification(notificationId);
}
}
};
NotificationService.animateAndRemove.connect(animateConnection)
NotificationService.animateAndRemove.connect(animateConnection);
}
Component.onDestruction: {
if (animateConnection) {
NotificationService.animateAndRemove.disconnect(animateConnection)
animateConnection = null
NotificationService.animateAndRemove.disconnect(animateConnection);
animateConnection = null;
}
}
@@ -223,8 +223,8 @@ Variants {
width: parent.availableWidth * model.progress
color: {
var baseColor = model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary
return Qt.alpha(baseColor, Settings.data.notifications.backgroundOpacity || 1.0)
var baseColor = model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary;
return Qt.alpha(baseColor, Settings.data.notifications.backgroundOpacity || 1.0);
}
antialiasing: true
@@ -257,10 +257,10 @@ Variants {
// Hover handling
onHoverCountChanged: {
if (hoverCount > 0) {
resumeTimer.stop()
NotificationService.pauseTimeout(notificationId)
resumeTimer.stop();
NotificationService.pauseTimeout(notificationId);
} else {
resumeTimer.start()
resumeTimer.start();
}
}
@@ -270,7 +270,7 @@ Variants {
repeat: false
onTriggered: {
if (hoverCount === 0) {
NotificationService.resumeTimeout(notificationId)
NotificationService.resumeTimeout(notificationId);
}
}
}
@@ -284,30 +284,30 @@ Variants {
onExited: parent.hoverCount--
onClicked: {
if (mouse.button === Qt.RightButton) {
animateOut()
animateOut();
}
}
}
// Animation setup
function triggerEntryAnimation() {
animInDelayTimer.stop()
removalTimer.stop()
resumeTimer.stop()
isRemoving = false
hoverCount = 0
animInDelayTimer.stop();
removalTimer.stop();
resumeTimer.stop();
isRemoving = false;
hoverCount = 0;
if (Settings.data.general.animationDisabled) {
slideOffset = 0
scaleValue = 1.0
opacityValue = 1.0
return
slideOffset = 0;
scaleValue = 1.0;
opacityValue = 1.0;
return;
}
slideOffset = slideInOffset
scaleValue = 0.8
opacityValue = 0.0
animInDelayTimer.interval = animationDelay
animInDelayTimer.start()
slideOffset = slideInOffset;
scaleValue = 0.8;
opacityValue = 0.0;
animInDelayTimer.interval = animationDelay;
animInDelayTimer.start();
}
Component.onCompleted: triggerEntryAnimation()
@@ -320,23 +320,23 @@ Variants {
repeat: false
onTriggered: {
if (card.isRemoving)
return
slideOffset = 0
scaleValue = 1.0
opacityValue = 1.0
return;
slideOffset = 0;
scaleValue = 1.0;
opacityValue = 1.0;
}
}
function animateOut() {
if (isRemoving)
return
animInDelayTimer.stop()
resumeTimer.stop()
isRemoving = true
return;
animInDelayTimer.stop();
resumeTimer.stop();
isRemoving = true;
if (!Settings.data.general.animationDisabled) {
slideOffset = slideOutOffset
scaleValue = 0.8
opacityValue = 0.0
slideOffset = slideOutOffset;
scaleValue = 0.8;
opacityValue = 0.0;
}
}
@@ -345,13 +345,13 @@ Variants {
interval: Style.animationSlow
repeat: false
onTriggered: {
NotificationService.dismissActiveNotification(notificationId)
NotificationService.dismissActiveNotification(notificationId);
}
}
onIsRemovingChanged: {
if (isRemoving) {
removalTimer.start()
removalTimer.start();
}
}
@@ -478,9 +478,9 @@ Variants {
property string parentNotificationId: notificationId
property var parsedActions: {
try {
return model.actionsJson ? JSON.parse(model.actionsJson) : []
return model.actionsJson ? JSON.parse(model.actionsJson) : [];
} catch (e) {
return []
return [];
}
}
visible: parsedActions.length > 0
@@ -495,11 +495,11 @@ Variants {
onExited: card.hoverCount--
text: {
var actionText = actionData.text || "Open"
var actionText = actionData.text || "Open";
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
return actionText.split(",")[1] || actionText;
}
return actionText
return actionText;
}
fontSize: Style.fontSizeS
backgroundColor: Color.mPrimary
@@ -508,7 +508,7 @@ Variants {
outlined: false
implicitHeight: 24
onClicked: {
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier)
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier);
}
}
}
@@ -528,8 +528,8 @@ Variants {
anchors.rightMargin: Style.marginXL
onClicked: {
NotificationService.removeFromHistory(model.id)
animateOut()
NotificationService.removeFromHistory(model.id);
animateOut();
}
}
}
+142 -133
View File
@@ -1,15 +1,15 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import Quickshell.Wayland
import qs.Commons
import qs.Widgets
import qs.Services.Hardware
import qs.Services.Media
import qs.Services.System
import qs.Widgets
// Unified OSD component that displays volume, input volume, and brightness changes
Variants {
@@ -52,54 +52,54 @@ Variants {
switch (currentOSDType) {
case "volume":
if (isMuted)
return "volume-mute"
return "volume-mute";
if (currentVolume <= Number.EPSILON)
return "volume-zero"
return currentVolume <= 0.5 ? "volume-low" : "volume-high"
return "volume-zero";
return currentVolume <= 0.5 ? "volume-low" : "volume-high";
case "inputVolume":
return isInputMuted ? "microphone-off" : "microphone"
return isInputMuted ? "microphone-off" : "microphone";
case "brightness":
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high";
default:
return ""
return "";
}
}
function getCurrentValue() {
switch (currentOSDType) {
case "volume":
return isMuted ? 0 : currentVolume
return isMuted ? 0 : currentVolume;
case "inputVolume":
return isInputMuted ? 0 : currentInputVolume
return isInputMuted ? 0 : currentInputVolume;
case "brightness":
return currentBrightness
return currentBrightness;
default:
return 0
return 0;
}
}
function getMaxValue() {
if (currentOSDType === "volume" || currentOSDType === "inputVolume") {
return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
}
return 1.0
return 1.0;
}
function getDisplayPercentage() {
const value = getCurrentValue()
const max = getMaxValue()
const pct = Math.round(Math.min(max, value) * 100)
return pct + "%"
const value = getCurrentValue();
const max = getMaxValue();
const pct = Math.round(Math.min(max, value) * 100);
return pct + "%";
}
function getProgressColor() {
const isMutedState = (currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted)
return isMutedState ? Color.mError : Color.mPrimary
const isMutedState = (currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted);
return isMutedState ? Color.mError : Color.mPrimary;
}
function getIconColor() {
const isMutedState = (currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted)
return isMutedState ? Color.mError : Color.mOnSurface
const isMutedState = (currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted);
return isMutedState ? Color.mError : Color.mOnSurface;
}
// ============================================================================
@@ -108,35 +108,35 @@ Variants {
function initializeAudioValues() {
// Initialize output volume
if (AudioService.sink?.ready && AudioService.sink?.audio && lastKnownVolume < 0) {
const vol = AudioService.volume
const vol = AudioService.volume;
if (vol !== undefined && !isNaN(vol)) {
lastKnownVolume = vol
volumeInitialized = true
muteInitialized = true
lastKnownVolume = vol;
volumeInitialized = true;
muteInitialized = true;
}
}
// Initialize input volume
if (AudioService.hasInput && AudioService.source?.ready && AudioService.source?.audio && lastKnownInputVolume < 0) {
const inputVol = AudioService.inputVolume
const inputVol = AudioService.inputVolume;
if (inputVol !== undefined && !isNaN(inputVol)) {
lastKnownInputVolume = inputVol
inputInitialized = true
lastKnownInputVolume = inputVol;
inputInitialized = true;
}
}
}
function resetOutputInit() {
lastKnownVolume = -1
volumeInitialized = false
muteInitialized = false
Qt.callLater(initializeAudioValues)
lastKnownVolume = -1;
volumeInitialized = false;
muteInitialized = false;
Qt.callLater(initializeAudioValues);
}
function resetInputInit() {
lastKnownInputVolume = -1
inputInitialized = false
Qt.callLater(initializeAudioValues)
lastKnownInputVolume = -1;
inputInitialized = false;
Qt.callLater(initializeAudioValues);
}
// ============================================================================
@@ -144,48 +144,48 @@ Variants {
// ============================================================================
function connectBrightnessMonitors() {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
const monitor = BrightnessService.monitors[i]
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
monitor.brightnessUpdated.connect(onBrightnessChanged)
const monitor = BrightnessService.monitors[i];
monitor.brightnessUpdated.disconnect(onBrightnessChanged);
monitor.brightnessUpdated.connect(onBrightnessChanged);
}
}
function onBrightnessChanged(newBrightness) {
lastUpdatedBrightness = newBrightness
lastUpdatedBrightness = newBrightness;
if (!brightnessInitialized) {
brightnessInitialized = true
return
brightnessInitialized = true;
return;
}
showOSD("brightness")
showOSD("brightness");
}
// ============================================================================
// OSD Display Control
// ============================================================================
function showOSD(type) {
currentOSDType = type
currentOSDType = type;
if (!root.active) {
root.active = true
root.active = true;
}
if (root.item) {
root.item.showOSD()
root.item.showOSD();
} else {
Qt.callLater(() => {
if (root.item)
root.item.showOSD()
})
root.item.showOSD();
});
}
}
function hideOSD() {
if (root.item?.osdItem) {
root.item.osdItem.hideImmediately()
root.item.osdItem.hideImmediately();
} else if (root.active) {
root.active = false
root.active = false;
}
}
@@ -199,15 +199,15 @@ Variants {
function onReadyChanged() {
if (Pipewire.ready)
Qt.callLater(initializeAudioValues)
Qt.callLater(initializeAudioValues);
}
function onDefaultAudioSinkChanged() {
resetOutputInit()
resetOutputInit();
}
function onDefaultAudioSourceChanged() {
resetInputInit()
resetInputInit();
}
}
@@ -217,72 +217,68 @@ Variants {
function onSinkChanged() {
if (AudioService.sink?.ready && AudioService.sink?.audio) {
resetOutputInit()
resetOutputInit();
}
}
function onSourceChanged() {
if (AudioService.hasInput && AudioService.source?.ready && AudioService.source?.audio) {
resetInputInit()
resetInputInit();
}
}
function onVolumeChanged() {
if (lastKnownVolume < 0) {
initializeAudioValues()
initializeAudioValues();
if (lastKnownVolume < 0)
return
return;
}
if (!volumeInitialized)
return
return;
if (Math.abs(AudioService.volume - lastKnownVolume) > 0.001) {
lastKnownVolume = AudioService.volume
showOSD("volume")
lastKnownVolume = AudioService.volume;
showOSD("volume");
}
}
function onMutedChanged() {
if (lastKnownVolume < 0) {
initializeAudioValues()
initializeAudioValues();
if (lastKnownVolume < 0)
return
return;
}
if (!muteInitialized)
return
showOSD("volume")
return;
showOSD("volume");
}
function onInputVolumeChanged() {
if (!AudioService.hasInput)
return
return;
if (lastKnownInputVolume < 0) {
initializeAudioValues()
initializeAudioValues();
if (lastKnownInputVolume < 0)
return
return;
}
if (!inputInitialized)
return
return;
if (Math.abs(AudioService.inputVolume - lastKnownInputVolume) > 0.001) {
lastKnownInputVolume = AudioService.inputVolume
showOSD("inputVolume")
lastKnownInputVolume = AudioService.inputVolume;
showOSD("inputVolume");
}
}
function onInputMutedChanged() {
if (!AudioService.hasInput)
return
return;
if (lastKnownInputVolume < 0) {
initializeAudioValues()
initializeAudioValues();
if (lastKnownInputVolume < 0)
return
return;
}
if (!inputInitialized)
return
showOSD("inputVolume")
return;
showOSD("inputVolume");
}
}
@@ -290,7 +286,7 @@ Variants {
Connections {
target: BrightnessService
function onMonitorsChanged() {
connectBrightnessMonitors()
connectBrightnessMonitors();
}
}
@@ -301,9 +297,9 @@ Variants {
running: true
onTriggered: {
if (Pipewire.ready)
initializeAudioValues()
muteInitialized = true
connectBrightnessMonitors()
initializeAudioValues();
muteInitialized = true;
connectBrightnessMonitors();
}
}
@@ -314,22 +310,21 @@ Variants {
repeat: true
onTriggered: {
if (!Pipewire.ready)
return
const needsOutputInit = lastKnownVolume < 0
const needsInputInit = AudioService.hasInput && lastKnownInputVolume < 0
return;
const needsOutputInit = lastKnownVolume < 0;
const needsInputInit = AudioService.hasInput && lastKnownInputVolume < 0;
if (needsOutputInit || needsInputInit) {
initializeAudioValues()
initializeAudioValues();
// Stop timer if both are initialized
const outputDone = lastKnownVolume >= 0
const inputDone = !AudioService.hasInput || lastKnownInputVolume >= 0
const outputDone = lastKnownVolume >= 0;
const inputDone = !AudioService.hasInput || lastKnownInputVolume >= 0;
if (outputDone && inputDone) {
running = false
running = false;
}
} else {
running = false
running = false;
}
}
}
@@ -352,11 +347,11 @@ Variants {
// Dimensions
readonly property int hWidth: Math.round(320 * Style.uiScaleRatio)
readonly property int hHeight: Math.round(72 * Style.uiScaleRatio)
readonly property int vWidth: Math.round(72 * Style.uiScaleRatio)
readonly property int vWidth: Math.round(80 * Style.uiScaleRatio)
readonly property int vHeight: Math.round(280 * Style.uiScaleRatio)
readonly property int barThickness: {
const base = Math.max(8, Math.round(8 * Style.uiScaleRatio))
return base % 2 === 0 ? base : base + 1
const base = Math.max(8, Math.round(8 * Style.uiScaleRatio));
return base % 2 === 0 ? base : base + 1;
}
anchors.top: isTop
@@ -366,15 +361,15 @@ Variants {
function calculateMargin(isAnchored, position) {
if (!isAnchored)
return 0
return 0;
let base = Style.marginM
let base = Style.marginM;
if (Settings.data.bar.position === position) {
const isVertical = position === "top" || position === "bottom"
const floatExtra = Settings.data.bar.floating ? (isVertical ? Settings.data.bar.marginVertical : Settings.data.bar.marginHorizontal) * Style.marginXL : 0
return Style.barHeight + base + floatExtra
const isVertical = position === "top" || position === "bottom";
const floatExtra = Settings.data.bar.floating ? (isVertical ? Settings.data.bar.marginVertical : Settings.data.bar.marginHorizontal) * Style.marginXL : 0;
return Style.barHeight + base + floatExtra;
}
return base
return base;
}
margins.top: calculateMargin(anchors.top, "top")
@@ -389,7 +384,7 @@ Variants {
WlrLayershell.namespace: "noctalia-osd-" + (screen?.name || "unknown")
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.layer: Settings.data.osd?.overlayLayer ? WlrLayer.Overlay : WlrLayer.Top
exclusionMode: PanelWindow.ExclusionMode.Ignore
WlrLayershell.exclusionMode: ExclusionMode.Ignore
Item {
id: osdItem
@@ -422,9 +417,9 @@ Variants {
id: visibilityTimer
interval: Style.animationNormal + 50
onTriggered: {
osdItem.visible = false
root.currentOSDType = ""
root.active = false
osdItem.visible = false;
root.currentOSDType = "";
root.active = false;
}
}
@@ -436,8 +431,8 @@ Variants {
color: Qt.alpha(Color.mSurface, Settings.data.osd.backgroundOpacity || 1.0)
border.color: Qt.alpha(Color.mOutline, Settings.data.osd.backgroundOpacity || 1.0)
border.width: {
const bw = Math.max(2, Style.borderM)
return bw % 2 === 0 ? bw : bw + 1
const bw = Math.max(2, Style.borderM);
return bw % 2 === 0 ? bw : bw + 1;
}
}
@@ -458,11 +453,20 @@ Variants {
Component {
id: horizontalContent
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginL
anchors.fill: parent
anchors.leftMargin: Style.marginL
anchors.rightMargin: Style.marginL
spacing: Style.marginM
clip: true
// TextMetrics to measure the maximum possible percentage width (150%)
TextMetrics {
id: percentageMetrics
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightMedium
font.pointSize: Style.fontSizeS * (Settings.data.ui.fontFixedScale * Style.uiScaleRatio)
text: "150%" // Maximum possible value with volumeOverdrive
}
NIcon {
icon: root.getIcon()
@@ -480,6 +484,7 @@ Variants {
Rectangle {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
height: panel.barThickness
radius: Math.round(panel.barThickness / 2)
color: Color.mSurfaceVariant
@@ -516,7 +521,10 @@ Variants {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
Layout.preferredWidth: Math.round(50 * Style.uiScaleRatio)
Layout.fillWidth: false
Layout.preferredWidth: Math.ceil(percentageMetrics.width) + Math.round(8 * Style.uiScaleRatio)
Layout.maximumWidth: Math.ceil(percentageMetrics.width) + Math.round(8 * Style.uiScaleRatio)
Layout.minimumWidth: Math.ceil(percentageMetrics.width)
}
}
}
@@ -528,6 +536,7 @@ Variants {
anchors.topMargin: Style.marginL
anchors.bottomMargin: Style.marginL
spacing: Style.marginS
clip: true
NText {
text: root.getDisplayPercentage()
@@ -594,39 +603,39 @@ Variants {
}
function show() {
hideTimer.stop()
visibilityTimer.stop()
osdItem.visible = true
hideTimer.stop();
visibilityTimer.stop();
osdItem.visible = true;
Qt.callLater(() => {
osdItem.opacity = 1
osdItem.scale = 1.0
})
osdItem.opacity = 1;
osdItem.scale = 1.0;
});
hideTimer.start()
hideTimer.start();
}
function hide() {
hideTimer.stop()
visibilityTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.85
visibilityTimer.start()
hideTimer.stop();
visibilityTimer.stop();
osdItem.opacity = 0;
osdItem.scale = 0.85;
visibilityTimer.start();
}
function hideImmediately() {
hideTimer.stop()
visibilityTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.85
osdItem.visible = false
root.currentOSDType = ""
root.active = false
hideTimer.stop();
visibilityTimer.stop();
osdItem.opacity = 0;
osdItem.scale = 0.85;
osdItem.visible = false;
root.currentOSDType = "";
root.active = false;
}
}
function showOSD() {
osdItem.show()
osdItem.show();
}
}
}
+11 -11
View File
@@ -1,12 +1,12 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Widgets
import qs.Modules.MainScreen
import qs.Services.Media
import qs.Widgets
SmartPanel {
id: root
@@ -25,7 +25,7 @@ SmartPanel {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
if (!localOutputVolumeChanging) {
localOutputVolume = AudioService.volume
localOutputVolume = AudioService.volume;
}
}
}
@@ -34,7 +34,7 @@ SmartPanel {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
if (!localInputVolumeChanging) {
localInputVolume = AudioService.inputVolume
localInputVolume = AudioService.inputVolume;
}
}
}
@@ -46,10 +46,10 @@ SmartPanel {
repeat: true
onTriggered: {
if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localOutputVolume)
AudioService.setVolume(localOutputVolume);
}
if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) {
AudioService.setInputVolume(localInputVolume)
AudioService.setInputVolume(localInputVolume);
}
}
}
@@ -95,7 +95,7 @@ SmartPanel {
tooltipText: I18n.tr("tooltips.output-muted")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setOutputMuted(!AudioService.muted)
AudioService.setOutputMuted(!AudioService.muted);
}
}
@@ -104,7 +104,7 @@ SmartPanel {
tooltipText: I18n.tr("tooltips.input-muted")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
AudioService.setInputMuted(!AudioService.inputMuted);
}
}
@@ -113,7 +113,7 @@ SmartPanel {
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
root.close();
}
}
}
@@ -179,8 +179,8 @@ SmartPanel {
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData)
localOutputVolume = AudioService.volume
AudioService.setAudioSink(modelData);
localOutputVolume = AudioService.volume;
}
Layout.fillWidth: true
}
-153
View File
@@ -1,153 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.Hardware
import qs.Widgets
import qs.Modules.MainScreen
SmartPanel {
id: root
property var optionsModel: []
function updateOptionsModel() {
let newOptions = [{
"id": BatteryService.ChargingMode.Full,
"label": "battery.panel.full"
}, {
"id": BatteryService.ChargingMode.Balanced,
"label": "battery.panel.balanced"
}, {
"id": BatteryService.ChargingMode.Lifespan,
"label": "battery.panel.lifespan"
}]
root.optionsModel = newOptions
}
onOpened: {
updateOptionsModel()
}
ButtonGroup {
id: batteryGroup
}
Component {
id: optionsComponent
ColumnLayout {
spacing: Style.marginM
Repeater {
model: root.optionsModel
delegate: NRadioButton {
ButtonGroup.group: batteryGroup
required property var modelData
text: I18n.tr(modelData.label, {
"percent": BatteryService.getThresholdValue(modelData.id)
})
checked: BatteryService.chargingMode === modelData.id
onClicked: {
BatteryService.setChargingMode(modelData.id)
}
Layout.fillWidth: true
}
}
}
}
Component {
id: disabledComponent
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM
NIcon {
icon: "recharging"
pointSize: 48
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("battery.panel.disabled")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
}
}
panelContent: Item {
anchors.fill: parent
property real contentPreferredWidth: Math.round(340 * Style.uiScaleRatio)
property real contentPreferredHeight: Math.round(mainLayout.implicitHeight + Style.marginM * 2)
ColumnLayout {
id: mainLayout
anchors.centerIn: parent
width: parent.contentPreferredWidth - Style.marginM * 2
anchors.margins: Style.marginM
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
Layout.preferredHeight: header.implicitHeight + Style.marginM * 2
RowLayout {
id: header
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: "battery-4"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("battery.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
id: batteryManagerSwitch
checked: BatteryService.chargingMode !== BatteryService.ChargingMode.Disabled
onToggled: checked => BatteryService.toggleEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
}
}
}
NBox {
Layout.fillWidth: true
Layout.preferredHeight: loader.implicitHeight + Style.marginM * 2
Loader {
id: loader
anchors.centerIn: parent
width: parent.width - Style.marginM * 2
sourceComponent: BatteryService.chargingMode === BatteryService.ChargingMode.Disabled ? disabledComponent : optionsComponent
}
}
}
}
}
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
@@ -13,9 +13,7 @@ NBox {
property string label: ""
property string tooltipText: ""
property var model: {
}
property var model: {}
Layout.fillWidth: true
Layout.preferredHeight: column.implicitHeight + Style.marginM * 2
@@ -52,10 +50,10 @@ NBox {
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
return Color.mPrimary;
if (modelData.blocked)
return Color.mError
return defaultColor
return Color.mError;
return defaultColor;
}
Layout.fillWidth: true
@@ -154,33 +152,33 @@ NBox {
fontWeight: Style.fontWeightMedium
backgroundColor: {
if (device.canDisconnect && !isBusy) {
return Color.mError
return Color.mError;
}
return Color.mPrimary
return Color.mPrimary;
}
tooltipText: root.tooltipText
text: {
if (modelData.pairing) {
return I18n.tr("bluetooth.panel.pairing")
return I18n.tr("bluetooth.panel.pairing");
}
if (modelData.blocked) {
return I18n.tr("bluetooth.panel.blocked")
return I18n.tr("bluetooth.panel.blocked");
}
if (modelData.connected) {
return I18n.tr("bluetooth.panel.disconnect")
return I18n.tr("bluetooth.panel.disconnect");
}
return I18n.tr("bluetooth.panel.connect")
return I18n.tr("bluetooth.panel.connect");
}
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)
BluetoothService.disconnectDevice(modelData);
} else {
BluetoothService.connectDeviceWithTrust(modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
onRightClicked: {
BluetoothService.forgetDevice(modelData)
BluetoothService.forgetDevice(modelData);
}
}
}
+19 -19
View File
@@ -1,13 +1,13 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
import qs.Services.UI
import qs.Services.Networking
import qs.Widgets
import qs.Modules.MainScreen
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
SmartPanel {
id: root
@@ -65,7 +65,7 @@ SmartPanel {
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
}
}
@@ -75,7 +75,7 @@ SmartPanel {
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
root.close();
}
}
}
@@ -143,9 +143,9 @@ SmartPanel {
label: I18n.tr("bluetooth.panel.connected-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected)
return BluetoothService.sortDevices(filtered)
return [];
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
return BluetoothService.sortDevices(filtered);
}
model: items
visible: items.length > 0
@@ -158,9 +158,9 @@ SmartPanel {
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
return BluetoothService.sortDevices(filtered)
return [];
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
return BluetoothService.sortDevices(filtered);
}
model: items
visible: items.length > 0
@@ -172,9 +172,9 @@ SmartPanel {
label: I18n.tr("bluetooth.panel.available-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted)
return BluetoothService.sortDevices(filtered)
return [];
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
return BluetoothService.sortDevices(filtered);
}
model: items
visible: items.length > 0
@@ -187,13 +187,13 @@ SmartPanel {
Layout.preferredHeight: columnScanning.implicitHeight + Style.marginM * 2
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
return false
return false;
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
}).length
return (availableCount === 0)
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return (availableCount === 0);
}
ColumnLayout {
+133 -133
View File
@@ -21,22 +21,22 @@ SmartPanel {
// Helper function to calculate ISO week number
function getISOWeekNumber(date) {
const target = new Date(date.valueOf())
const dayNr = (date.getDay() + 6) % 7
target.setDate(target.getDate() - dayNr + 3)
const firstThursday = new Date(target.getFullYear(), 0, 4)
const diff = target - firstThursday
const oneWeek = 1000 * 60 * 60 * 24 * 7
const weekNumber = 1 + Math.round(diff / oneWeek)
return weekNumber
const target = new Date(date.valueOf());
const dayNr = (date.getDay() + 6) % 7;
target.setDate(target.getDate() - dayNr + 3);
const firstThursday = new Date(target.getFullYear(), 0, 4);
const diff = target - firstThursday;
const oneWeek = 1000 * 60 * 60 * 24 * 7;
const weekNumber = 1 + Math.round(diff / oneWeek);
return weekNumber;
}
// 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
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;
}
panelContent: Item {
@@ -50,12 +50,12 @@ SmartPanel {
property real calendarGridHeight: {
// Calculate number of weeks in the calendar grid
const numWeeks = grid.daysModel ? Math.ceil(grid.daysModel.length / 7) : 5
const numWeeks = grid.daysModel ? Math.ceil(grid.daysModel.length / 7) : 5;
// Calendar grid height (dynamic based on number of weeks)
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS;
return numWeeks * rowHeight
return numWeeks * rowHeight;
}
ColumnLayout {
id: content
@@ -69,17 +69,17 @@ SmartPanel {
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
function checkIsCurrentMonth() {
return (now.getMonth() === grid.month) && (now.getFullYear() === grid.year)
return (now.getMonth() === grid.month) && (now.getFullYear() === grid.year);
}
Component.onCompleted: {
isCurrentMonth = checkIsCurrentMonth()
isCurrentMonth = checkIsCurrentMonth();
}
Connections {
target: Time
function onNowChanged() {
content.isCurrentMonth = content.checkIsCurrentMonth()
content.isCurrentMonth = content.checkIsCurrentMonth();
}
}
@@ -87,7 +87,7 @@ SmartPanel {
target: I18n
function onLanguageChanged() {
// Force update of day names when language changes
grid.month = grid.month
grid.month = grid.month;
}
}
@@ -180,11 +180,11 @@ SmartPanel {
NText {
text: {
if (!Settings.data.location.weatherEnabled)
return ""
return "";
if (!content.weatherReady)
return I18n.tr("calendar.weather.loading")
const chunks = Settings.data.location.name.split(",")
return chunks[0]
return I18n.tr("calendar.weather.loading");
const chunks = Settings.data.location.name.split(",");
return chunks[0];
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
@@ -227,11 +227,11 @@ SmartPanel {
id: calendar
Layout.fillWidth: true
Layout.preferredHeight: {
const navigationHeight = Style.baseWidgetSize // Navigation buttons row
const dayNamesHeight = Style.baseWidgetSize * 0.6 // Day names header row
const innerMargins = Style.marginM * 2 // Top and bottom margins inside NBox
const innerSpacing = Style.marginS * 2 // Spacing between nav, dayNames, and grid (2 gaps)
return navigationHeight + dayNamesHeight + calendarGridHeight + innerMargins + innerSpacing
const navigationHeight = Style.baseWidgetSize; // Navigation buttons row
const dayNamesHeight = Style.baseWidgetSize * 0.6; // Day names header row
const innerMargins = Style.marginM * 2; // Top and bottom margins inside NBox
const innerSpacing = Style.marginS * 2; // Spacing between nav, dayNames, and grid (2 gaps)
return navigationHeight + dayNamesHeight + calendarGridHeight + innerMargins + innerSpacing;
}
Behavior on Layout.preferredWidth {
@@ -257,46 +257,46 @@ SmartPanel {
NIconButton {
icon: "chevron-left"
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
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)
let newDate = new Date(grid.year, grid.month - 1, 1);
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)))
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)
CalendarService.loadEvents(daysAhead + 30, daysBehind + 30);
}
}
NIconButton {
icon: "calendar"
onClicked: {
grid.month = now.getMonth()
grid.year = now.getFullYear()
content.isCurrentMonth = true
CalendarService.loadEvents()
grid.month = now.getMonth();
grid.year = now.getFullYear();
content.isCurrentMonth = true;
CalendarService.loadEvents();
}
}
NIconButton {
icon: "chevron-right"
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
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)
let newDate = new Date(grid.year, grid.month + 1, 1);
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)))
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)
CalendarService.loadEvents(daysAhead + 30, daysBehind + 30);
}
}
}
@@ -324,9 +324,9 @@ SmartPanel {
NText {
anchors.centerIn: parent
text: {
let dayIndex = (content.firstDayOfWeek + index) % 7
const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat)
return dayName.substring(0, 2).toUpperCase()
let dayIndex = (content.firstDayOfWeek + index) % 7;
const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat);
return dayName.substring(0, 2).toUpperCase();
}
color: Color.mPrimary
pointSize: Style.fontSizeS
@@ -345,55 +345,55 @@ SmartPanel {
// Helper function to check if a date has events
function hasEventsOnDate(year, month, day) {
if (!CalendarService.available || CalendarService.events.length === 0)
return false
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
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)
})
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 []
return [];
const targetDate = new Date(year, month, day)
const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000)
const targetEnd = targetStart + 86400 // +24 hours
const targetDate = new Date(year, month, day);
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);
});
}
// Helper function to check if an event is multi-day
function isMultiDayEvent(event) {
if (isAllDayEvent(event)) {
return false
return false;
}
const startDate = new Date(event.start * 1000)
const endDate = new Date(event.end * 1000)
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())
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()
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
return isToday ? Color.mOnSecondary : Color.mTertiary;
} else if (isAllDayEvent(event)) {
return isToday ? Color.mOnSecondary : Color.mSecondary
return isToday ? Color.mOnSecondary : Color.mSecondary;
} else {
return isToday ? Color.mOnSecondary : Color.mPrimary
return isToday ? Color.mOnSecondary : Color.mPrimary;
}
}
@@ -402,9 +402,9 @@ SmartPanel {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
Layout.preferredHeight: {
const numWeeks = weekNumbers ? weekNumbers.length : 5
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
return numWeeks * rowHeight
const numWeeks = weekNumbers ? weekNumbers.length : 5;
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS;
return numWeeks * rowHeight;
}
spacing: Style.marginXXS
@@ -417,33 +417,33 @@ SmartPanel {
property var weekNumbers: {
if (!grid.daysModel || grid.daysModel.length === 0)
return []
return [];
const weeks = []
const numWeeks = Math.ceil(grid.daysModel.length / 7)
const weeks = [];
const numWeeks = Math.ceil(grid.daysModel.length / 7);
for (var i = 0; i < numWeeks; i++) {
const dayIndex = i * 7
const dayIndex = i * 7;
if (dayIndex < grid.daysModel.length) {
const weekDay = grid.daysModel[dayIndex]
const date = new Date(weekDay.year, weekDay.month, weekDay.day)
const weekDay = grid.daysModel[dayIndex];
const date = new Date(weekDay.year, weekDay.month, weekDay.day);
// Get Thursday of this week for ISO week calculation
const firstDayOfWeek = content.firstDayOfWeek
let thursday = new Date(date)
const firstDayOfWeek = content.firstDayOfWeek;
let thursday = new Date(date);
if (firstDayOfWeek === 0) {
thursday.setDate(date.getDate() + 4)
thursday.setDate(date.getDate() + 4);
} else if (firstDayOfWeek === 1) {
thursday.setDate(date.getDate() + 3)
thursday.setDate(date.getDate() + 3);
} else {
let daysToThursday = (4 - firstDayOfWeek + 7) % 7
thursday.setDate(date.getDate() + daysToThursday)
let daysToThursday = (4 - firstDayOfWeek + 7) % 7;
thursday.setDate(date.getDate() + daysToThursday);
}
weeks.push(root.getISOWeekNumber(thursday))
weeks.push(root.getISOWeekNumber(thursday));
}
}
return weeks
return weeks;
}
Repeater {
@@ -466,9 +466,9 @@ SmartPanel {
id: grid
Layout.fillWidth: true
Layout.preferredHeight: {
const numWeeks = daysModel ? Math.ceil(daysModel.length / 7) : 5
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS
return numWeeks * rowHeight
const numWeeks = daysModel ? Math.ceil(daysModel.length / 7) : 5;
const rowHeight = Style.baseWidgetSize * 0.9 + Style.marginXXS;
return numWeeks * rowHeight;
}
columns: 7
columnSpacing: Style.marginXXS
@@ -486,51 +486,51 @@ SmartPanel {
// Calculate days to display
property var daysModel: {
const firstOfMonth = new Date(year, month, 1)
const lastOfMonth = new Date(year, month + 1, 0)
const daysInMonth = lastOfMonth.getDate()
const firstOfMonth = new Date(year, month, 1);
const lastOfMonth = new Date(year, month + 1, 0);
const daysInMonth = lastOfMonth.getDate();
// Get first day of week (0 = Sunday, 1 = Monday, etc.)
const firstDayOfWeek = content.firstDayOfWeek
const firstOfMonthDayOfWeek = firstOfMonth.getDay()
const firstDayOfWeek = content.firstDayOfWeek;
const firstOfMonthDayOfWeek = firstOfMonth.getDay();
// Calculate days before first of month
let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
// Calculate days after last of month to complete the week
const lastOfMonthDayOfWeek = lastOfMonth.getDay()
const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7
const lastOfMonthDayOfWeek = lastOfMonth.getDay();
const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7;
// Build array of day objects
const days = []
const today = new Date()
const days = [];
const today = new Date();
// Previous month days
const prevMonth = new Date(year, month, 0)
const prevMonthDays = prevMonth.getDate()
const prevMonth = new Date(year, month, 0);
const prevMonthDays = prevMonth.getDate();
for (var i = daysBefore - 1; i >= 0; i--) {
const day = prevMonthDays - i
const date = new Date(year, month - 1, day)
const day = prevMonthDays - i;
const date = new Date(year, month - 1, day);
days.push({
"day": day,
"month": month - 1,
"year": month === 0 ? year - 1 : year,
"today": false,
"currentMonth": false
})
});
}
// Current month days
for (var day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day)
const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate()
const date = new Date(year, month, day);
const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
days.push({
"day": day,
"month": month,
"year": year,
"today": isToday,
"currentMonth": true
})
});
}
// Next month days (only if needed to complete the week)
@@ -541,10 +541,10 @@ SmartPanel {
"year": month === 11 ? year + 1 : year,
"today": false,
"currentMonth": false
})
});
}
return days
return days;
}
Repeater {
@@ -566,10 +566,10 @@ SmartPanel {
text: modelData.day
color: {
if (modelData.today)
return Color.mOnSecondary
return Color.mOnSecondary;
if (modelData.currentMonth)
return Color.mOnSurface
return Color.mOnSurfaceVariant
return Color.mOnSurface;
return Color.mOnSurfaceVariant;
}
opacity: modelData.currentMonth ? 1.0 : 0.4
pointSize: Style.fontSizeM
@@ -602,35 +602,35 @@ SmartPanel {
enabled: Settings.data.location.showCalendarEvents
onEntered: {
const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day)
const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day);
if (events.length > 0) {
const summaries = events.map(event => {
if (isAllDayEvent(event)) {
return event.summary
return event.summary;
} else {
// Always format with '0' padding to ensure proper horizontal alignment
const timeFormat = Settings.data.location.use12hourFormat ? "hh:mm AP" : "HH:mm"
const start = new Date(event.start * 1000)
const startFormatted = I18n.locale.toString(start, timeFormat)
const end = new Date(event.end * 1000)
const endFormatted = I18n.locale.toString(end, timeFormat)
return `${startFormatted}-${endFormatted} ${event.summary}`
const timeFormat = Settings.data.location.use12hourFormat ? "hh:mm AP" : "HH:mm";
const start = new Date(event.start * 1000);
const startFormatted = I18n.locale.toString(start, timeFormat);
const end = new Date(event.end * 1000);
const endFormatted = I18n.locale.toString(end, timeFormat);
return `${startFormatted}-${endFormatted} ${event.summary}`;
}
}).join('\n')
TooltipService.show(screen, parent, summaries, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
}).join('\n');
TooltipService.show(screen, parent, summaries, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed);
}
}
onClicked: {
const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`
const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`;
if (ProgramCheckerService.gnomeCalendarAvailable) {
Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes])
root.close()
Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]);
root.close();
}
}
onExited: {
TooltipService.hide()
TooltipService.hide();
}
}
@@ -16,10 +16,10 @@ NBox {
property bool localInputVolumeChanging: false
Component.onCompleted: {
var vol = AudioService.volume
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0
var inputVol = AudioService.inputVolume
localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0
var vol = AudioService.volume;
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
var inputVol = AudioService.inputVolume;
localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0;
}
// Timer to debounce volume changes
@@ -29,10 +29,10 @@ NBox {
repeat: true
onTriggered: {
if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localOutputVolume)
AudioService.setVolume(localOutputVolume);
}
if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) {
AudioService.setInputVolume(localInputVolume)
AudioService.setInputVolume(localInputVolume);
}
}
}
@@ -42,8 +42,8 @@ NBox {
target: AudioService
function onVolumeChanged() {
if (!localOutputVolumeChanging) {
var vol = AudioService.volume
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0
var vol = AudioService.volume;
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
}
}
}
@@ -52,8 +52,8 @@ NBox {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
if (!localOutputVolumeChanging) {
var vol = AudioService.volume
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0
var vol = AudioService.volume;
localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
}
}
}
@@ -62,8 +62,8 @@ NBox {
target: AudioService
function onInputVolumeChanged() {
if (!localInputVolumeChanging) {
var vol = AudioService.inputVolume
localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0
var vol = AudioService.inputVolume;
localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
}
}
}
@@ -72,8 +72,8 @@ NBox {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
if (!localInputVolumeChanging) {
var vol = AudioService.inputVolume
localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0
var vol = AudioService.inputVolume;
localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0;
}
}
}
@@ -105,7 +105,7 @@ NBox {
colorFgHover: Color.mOnHover
onClicked: {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.muted
AudioService.sink.audio.muted = !AudioService.muted;
}
}
}
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Media
@@ -22,7 +22,7 @@ NBox {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === screen.name) {
wallpaper = path
wallpaper = path;
}
}
}
@@ -48,8 +48,8 @@ NBox {
// Background image that covers everything
Image {
readonly property int dim: Math.round(256 * Style.uiScaleRatio)
id: bgImage
readonly property int dim: Math.round(256 * Style.uiScaleRatio)
anchors.fill: parent
source: MediaService.trackArtUrl || wallpaper
sourceSize: Qt.size(dim, dim)
@@ -81,13 +81,13 @@ NBox {
sourceComponent: {
switch (Settings.data.audio.visualizerType) {
case "linear":
return linearComponent
return linearComponent;
case "mirrored":
return mirroredComponent
return mirroredComponent;
case "wave":
return waveComponent
return waveComponent;
default:
return null
return null;
}
}
@@ -164,8 +164,8 @@ NBox {
cursorShape: Qt.PointingHandCursor
onClicked: {
var menuItems = []
var players = MediaService.getAvailablePlayers()
var menuItems = [];
var players = MediaService.getAvailablePlayers();
for (var i = 0; i < players.length; i++) {
menuItems.push({
"label": players[i].identity,
@@ -173,10 +173,10 @@ NBox {
"icon": "disc",
"enabled": true,
"visible": true
})
});
}
playerContextMenu.model = menuItems
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
playerContextMenu.model = menuItems;
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height);
}
}
@@ -187,9 +187,9 @@ NBox {
verticalPolicy: ScrollBar.AlwaysOff
onTriggered: function (action) {
var index = parseInt(action)
var index = parseInt(action);
if (!isNaN(index)) {
MediaService.switchToPlayer(index)
MediaService.switchToPlayer(index);
}
}
}
@@ -276,11 +276,11 @@ NBox {
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0
const r = MediaService.currentPosition / MediaService.trackLength
return 0;
const r = MediaService.currentPosition / MediaService.trackLength;
if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
return 0;
return Math.max(0, Math.min(1, r));
}
property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
@@ -290,10 +290,10 @@ NBox {
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio));
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
MediaService.seekByRatio(next);
progressWrapper.lastSentSeekRatio = next;
}
}
}
@@ -310,21 +310,21 @@ NBox {
heightRatio: 0.6
onMoved: {
progressWrapper.localSeekRatio = value
seekDebounce.restart()
progressWrapper.localSeekRatio = value;
seekDebounce.restart();
}
onPressedChanged: {
if (pressed) {
MediaService.isSeeking = true
progressWrapper.localSeekRatio = value
MediaService.seekByRatio(value)
progressWrapper.lastSentSeekRatio = value
MediaService.isSeeking = true;
progressWrapper.localSeekRatio = value;
MediaService.seekByRatio(value);
progressWrapper.lastSentSeekRatio = value;
} else {
seekDebounce.stop()
MediaService.seekByRatio(value)
MediaService.isSeeking = false
progressWrapper.localSeekRatio = -1
progressWrapper.lastSentSeekRatio = -1
seekDebounce.stop();
MediaService.seekByRatio(value);
MediaService.isSeeking = false;
progressWrapper.localSeekRatio = -1;
progressWrapper.lastSentSeekRatio = -1;
}
}
}
@@ -61,9 +61,9 @@ NBox {
icon: "settings"
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
var panel = PanelService.getPanel("settingsPanel", screen)
panel.requestedTab = SettingsPanel.Tab.General
panel.open()
var panel = PanelService.getPanel("settingsPanel", screen);
panel.requestedTab = SettingsPanel.Tab.General;
panel.open();
}
}
@@ -71,8 +71,8 @@ NBox {
icon: "power"
tooltipText: I18n.tr("tooltips.session-menu")
onClicked: {
PanelService.getPanel("sessionMenuPanel", screen)?.open()
PanelService.getPanel("controlCenterPanel", screen)?.close()
PanelService.getPanel("sessionMenuPanel", screen)?.open();
PanelService.getPanel("controlCenterPanel", screen)?.close();
}
}
@@ -80,7 +80,7 @@ NBox {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
onClicked: {
PanelService.getPanel("controlCenterPanel", screen)?.close()
PanelService.getPanel("controlCenterPanel", screen)?.close();
}
}
}
@@ -102,14 +102,14 @@ NBox {
stdout: StdioCollector {
onStreamFinished: {
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0])
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds)
uptimeProcess.running = false
var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]);
uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds);
uptimeProcess.running = false;
}
}
}
function updateSystemInfo() {
uptimeProcess.running = true
uptimeProcess.running = true;
}
}
@@ -3,9 +3,9 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Modules.Panels.ControlCenter
import qs.Modules.Panels.ControlCenter.Cards
import qs.Widgets
RowLayout {
Layout.fillWidth: true
@@ -28,9 +28,11 @@ NBox {
height: content.widgetHeight
Layout.alignment: Qt.AlignHCenter
// Highlight color based on thresholds
fillColor: (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor
|| Color.mError) : Color.mError) : (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor
|| Color.mTertiary) : Color.mTertiary) : Color.mPrimary
fillColor: (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError) : (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (
Settings.data.systemMonitor.warningColor
|| Color.mTertiary) :
Color.mTertiary) :
Color.mPrimary
textColor: (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuCriticalThreshold) ? Color.mSurfaceVariant : (SystemStatService.cpuUsage > Settings.data.systemMonitor.cpuWarningThreshold) ? Color.mSurfaceVariant : Color.mOnSurface
}
NCircleStat {
@@ -42,9 +44,11 @@ NBox {
height: content.widgetHeight
Layout.alignment: Qt.AlignHCenter
// Highlight color based on thresholds
fillColor: (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor
|| Color.mError) : Color.mError) : (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor
|| Color.mTertiary) : Color.mTertiary) : Color.mPrimary
fillColor: (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError) : (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (
Settings.data.systemMonitor.warningColor
|| Color.mTertiary) :
Color.mTertiary) :
Color.mPrimary
textColor: (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempCriticalThreshold) ? Color.mSurfaceVariant : (SystemStatService.cpuTemp > Settings.data.systemMonitor.tempWarningThreshold) ? Color.mSurfaceVariant : Color.mOnSurface
}
NCircleStat {
@@ -55,9 +59,11 @@ NBox {
height: content.widgetHeight
Layout.alignment: Qt.AlignHCenter
// Highlight color based on thresholds
fillColor: (SystemStatService.memPercent > Settings.data.systemMonitor.memCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor
|| Color.mError) : Color.mError) : (SystemStatService.memPercent > Settings.data.systemMonitor.memWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor
|| Color.mTertiary) : Color.mTertiary) : Color.mPrimary
fillColor: (SystemStatService.memPercent > Settings.data.systemMonitor.memCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError) : (SystemStatService.memPercent > Settings.data.systemMonitor.memWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (
Settings.data.systemMonitor.warningColor
|| Color.mTertiary) :
Color.mTertiary) :
Color.mPrimary
textColor: (SystemStatService.memPercent > Settings.data.systemMonitor.memCriticalThreshold) ? Color.mSurfaceVariant : (SystemStatService.memPercent > Settings.data.systemMonitor.memWarningThreshold) ? Color.mSurfaceVariant : Color.mOnSurface
}
NCircleStat {
@@ -68,11 +74,12 @@ NBox {
height: content.widgetHeight
Layout.alignment: Qt.AlignHCenter
// Highlight color based on thresholds
fillColor: ((SystemStatService.diskPercents["/"]
?? 0) > Settings.data.systemMonitor.diskCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor
|| Color.mError) : Color.mError) : ((SystemStatService.diskPercents["/"]
?? 0) > Settings.data.systemMonitor.diskWarningThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor
|| Color.mTertiary) : Color.mTertiary) : Color.mPrimary
fillColor: ((SystemStatService.diskPercents["/"] ?? 0) > Settings.data.systemMonitor.diskCriticalThreshold) ? (Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError) : ((SystemStatService.diskPercents["/"] ?? 0) > Settings.data.systemMonitor.diskWarningThreshold) ? (
Settings.data.systemMonitor.useCustomColors
? (Settings.data.systemMonitor.warningColor
|| Color.mTertiary) :
Color.mTertiary) :
Color.mPrimary
textColor: ((SystemStatService.diskPercents["/"] ?? 0) > Settings.data.systemMonitor.diskCriticalThreshold) ? Color.mSurfaceVariant : ((SystemStatService.diskPercents["/"] ?? 0) > Settings.data.systemMonitor.diskWarningThreshold) ? Color.mSurfaceVariant : Color.mOnSurface
}
}
@@ -43,8 +43,8 @@ NBox {
NText {
text: {
// Ensure the name is not too long if one had to specify the country
const chunks = Settings.data.location.name.split(",")
return chunks[0]
const chunks = Settings.data.location.name.split(",");
return chunks[0];
}
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
@@ -56,16 +56,16 @@ NBox {
visible: weatherReady
text: {
if (!weatherReady) {
return ""
return "";
}
var temp = LocationService.data.weather.current_weather.temperature
var suffix = "C"
var temp = LocationService.data.weather.current_weather.temperature;
var suffix = "C";
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp)
var suffix = "F"
temp = LocationService.celsiusToFahrenheit(temp);
var suffix = "F";
}
temp = Math.round(temp)
return `${temp}°${suffix}`
temp = Math.round(temp);
return `${temp}°${suffix}`;
}
pointSize: showLocation ? Style.fontSizeXL : Style.fontSizeXL * 1.6
font.weight: Style.fontWeightBold
@@ -103,8 +103,8 @@ NBox {
NText {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
return I18n.locale.toString(weatherDate, "ddd")
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
return I18n.locale.toString(weatherDate, "ddd");
}
color: Color.mOnSurface
}
@@ -117,15 +117,15 @@ NBox {
NText {
Layout.alignment: Qt.AlignHCenter
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index]
var min = LocationService.data.weather.daily.temperature_2m_min[index]
var max = LocationService.data.weather.daily.temperature_2m_max[index];
var min = LocationService.data.weather.daily.temperature_2m_min[index];
if (Settings.data.location.useFahrenheit) {
max = LocationService.celsiusToFahrenheit(max)
min = LocationService.celsiusToFahrenheit(min)
max = LocationService.celsiusToFahrenheit(max);
min = LocationService.celsiusToFahrenheit(min);
}
max = Math.round(max)
min = Math.round(min)
return `${max}°/${min}°`
max = Math.round(max);
min = Math.round(min);
return `${max}°/${min}°`;
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
@@ -17,8 +17,8 @@ SmartPanel {
// Check if there's a bar on this screen
readonly property bool hasBarOnScreen: {
var monitors = Settings.data.bar.monitors || []
return monitors.length === 0 || monitors.includes(screen?.name)
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(screen?.name);
}
// When position is "close_to_bar_button" but there's no bar, fall back to center
@@ -33,39 +33,37 @@ SmartPanel {
preferredWidth: Math.round(460 * Style.uiScaleRatio)
preferredHeight: {
var height = 0
var count = 0
var height = 0;
var count = 0;
for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) {
const card = Settings.data.controlCenter.cards[i]
const card = Settings.data.controlCenter.cards[i];
if (!card.enabled)
continue
const contributes = (card.id !== "weather-card" || Settings.data.location.weatherEnabled)
continue;
const contributes = (card.id !== "weather-card" || Settings.data.location.weatherEnabled);
if (!contributes)
continue
count++
continue;
count++;
switch (card.id) {
case "profile-card":
height += profileHeight
break
height += profileHeight;
break;
case "shortcuts-card":
height += shortcutsHeight
break
height += shortcutsHeight;
break;
case "audio-card":
height += audioHeight
break
height += audioHeight;
break;
case "weather-card":
height += weatherHeight
break
height += weatherHeight;
break;
case "media-sysmon-card":
height += mediaSysMonHeight
break
height += mediaSysMonHeight;
break;
default:
break
break;
}
}
return height + (count + 1) * Style.marginL
return height + (count + 1) * Style.marginL;
}
readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio)
@@ -77,11 +75,11 @@ SmartPanel {
property int weatherHeight: Math.round(210 * Style.uiScaleRatio)
onOpened: {
MediaService.autoSwitchingPaused = true
MediaService.autoSwitchingPaused = true;
}
onClosed: {
MediaService.autoSwitchingPaused = false
MediaService.autoSwitchingPaused = false;
}
panelContent: Item {
@@ -103,31 +101,31 @@ SmartPanel {
Layout.preferredHeight: {
switch (modelData.id) {
case "profile-card":
return profileHeight
return profileHeight;
case "shortcuts-card":
return shortcutsHeight
return shortcutsHeight;
case "audio-card":
return audioHeight
return audioHeight;
case "weather-card":
return weatherHeight
return weatherHeight;
case "media-sysmon-card":
return mediaSysMonHeight
return mediaSysMonHeight;
default:
return 0
return 0;
}
}
sourceComponent: {
switch (modelData.id) {
case "profile-card":
return profileCard
return profileCard;
case "shortcuts-card":
return shortcutsCard
return shortcutsCard;
case "audio-card":
return audioCard
return audioCard;
case "weather-card":
return weatherCard
return weatherCard;
case "media-sysmon-card":
return mediaSysMonCard
return mediaSysMonCard;
}
}
}
@@ -153,7 +151,7 @@ SmartPanel {
id: weatherCard
WeatherCard {
Component.onCompleted: {
root.weatherHeight = this.height
root.weatherHeight = this.height;
}
}
}
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import qs.Services.UI
import qs.Commons
import qs.Services.UI
Item {
id: root
@@ -18,7 +18,7 @@ Item {
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
function getImplicitSize(item, prop) {
return (item && item.visible) ? item[prop] : 0
return (item && item.visible) ? item[prop] : 0;
}
Loader {
@@ -29,36 +29,36 @@ Item {
onLoaded: {
if (!item)
return
return;
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
item[prop] = widgetProps[prop];
}
}
// Set screen property
if (item.hasOwnProperty("screen")) {
item.screen = widgetScreen
item.screen = widgetScreen;
}
// Call custom onLoaded if it exists
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
item.onLoaded();
}
}
Component.onDestruction: {
// Explicitly clear references
widgetProps = null
widgetProps = null;
}
}
// Error handling
Component.onCompleted: {
if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) {
Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId)
Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId);
}
}
}
@@ -33,28 +33,28 @@ Item {
function _updatePropertiesFromSettings() {
if (!widgetSettings) {
return
return;
}
onClickedCommand = widgetSettings.onClicked || ""
onRightClickedCommand = widgetSettings.onRightClicked || ""
onMiddleClickedCommand = widgetSettings.onMiddleClicked || ""
stateChecksJson = widgetSettings.stateChecksJson || "[]" // Populate from widgetSettings
onClickedCommand = widgetSettings.onClicked || "";
onRightClickedCommand = widgetSettings.onRightClicked || "";
onMiddleClickedCommand = widgetSettings.onMiddleClicked || "";
stateChecksJson = widgetSettings.stateChecksJson || "[]"; // Populate from widgetSettings
try {
_parsedStateChecks = JSON.parse(stateChecksJson)
_parsedStateChecks = JSON.parse(stateChecksJson);
} catch (e) {
console.error("CustomButton: Failed to parse stateChecksJson:", e.message)
_parsedStateChecks = []
console.error("CustomButton: Failed to parse stateChecksJson:", e.message);
_parsedStateChecks = [];
}
generalTooltipText = widgetSettings.generalTooltipText || "Custom Button"
enableOnStateLogic = widgetSettings.enableOnStateLogic || false
generalTooltipText = widgetSettings.generalTooltipText || "Custom Button";
enableOnStateLogic = widgetSettings.enableOnStateLogic || false;
updateState()
updateState();
}
function onWidgetSettingsChanged() {
if (widgetSettings) {
_updatePropertiesFromSettings()
_updatePropertiesFromSettings();
}
}
}
@@ -64,16 +64,16 @@ Item {
running: false
command: _currentStateCheckIndex !== -1 && _parsedStateChecks.length > _currentStateCheckIndex ? ["sh", "-c", _parsedStateChecks[_currentStateCheckIndex].command] : []
onExited: function (exitCode, stdout, stderr) {
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex]
var currentCommand = currentCheckItem.command
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex];
var currentCommand = currentCheckItem.command;
if (exitCode === 0) {
// Command succeeded, this is the active state
_isHot = true
_activeStateIcon = currentCheckItem.icon || widgetSettings.icon || "heart"
_isHot = true;
_activeStateIcon = currentCheckItem.icon || widgetSettings.icon || "heart";
} else {
// Command failed, try next one
_currentStateCheckIndex++
_checkNextState()
_currentStateCheckIndex++;
_checkNextState();
}
}
}
@@ -85,53 +85,53 @@ Item {
repeat: false
onTriggered: {
if (enableOnStateLogic && _parsedStateChecks.length > 0) {
_currentStateCheckIndex = 0
_checkNextState()
_currentStateCheckIndex = 0;
_checkNextState();
} else {
_isHot = false
_activeStateIcon = widgetSettings.icon || "heart"
_isHot = false;
_activeStateIcon = widgetSettings.icon || "heart";
}
}
}
function _checkNextState() {
if (_currentStateCheckIndex < _parsedStateChecks.length) {
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex]
var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex];
if (currentCheckItem && currentCheckItem.command) {
stateCheckProcessExecutor.running = true
stateCheckProcessExecutor.running = true;
} else {
_currentStateCheckIndex++
_checkNextState()
_currentStateCheckIndex++;
_checkNextState();
}
} else {
// All checks failed
_isHot = false
_activeStateIcon = widgetSettings.icon || "heart"
_isHot = false;
_activeStateIcon = widgetSettings.icon || "heart";
}
}
function updateState() {
if (!enableOnStateLogic || _parsedStateChecks.length === 0) {
_isHot = false
_activeStateIcon = widgetSettings.icon || "heart"
return
_isHot = false;
_activeStateIcon = widgetSettings.icon || "heart";
return;
}
stateUpdateTimer.restart()
stateUpdateTimer.restart();
}
function _buildTooltipText() {
let tooltip = generalTooltipText
let tooltip = generalTooltipText;
if (onClickedCommand) {
tooltip += `\nLeft click: ${onClickedCommand}`
tooltip += `\nLeft click: ${onClickedCommand}`;
}
if (onRightClickedCommand) {
tooltip += `\nRight click: ${onRightClickedCommand}`
tooltip += `\nRight click: ${onRightClickedCommand}`;
}
if (onMiddleClickedCommand) {
tooltip += `\nMiddle click: ${onMiddleClickedCommand}`
tooltip += `\nMiddle click: ${onMiddleClickedCommand}`;
}
return tooltip
return tooltip;
}
NIconButtonHot {
@@ -141,20 +141,20 @@ Item {
tooltipText: _buildTooltipText()
onClicked: {
if (onClickedCommand) {
Quickshell.execDetached(["sh", "-c", onClickedCommand])
updateState()
Quickshell.execDetached(["sh", "-c", onClickedCommand]);
updateState();
}
}
onRightClicked: {
if (onRightClickedCommand) {
Quickshell.execDetached(["sh", "-c", onRightClickedCommand])
updateState()
Quickshell.execDetached(["sh", "-c", onRightClickedCommand]);
updateState();
}
}
onMiddleClicked: {
if (onMiddleClickedCommand) {
Quickshell.execDetached(["sh", "-c", onMiddleClickedCommand])
updateState()
Quickshell.execDetached(["sh", "-c", onMiddleClickedCommand]);
updateState();
}
}
}
@@ -16,19 +16,19 @@ NIconButtonHot {
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
Settings.data.nightLight.enabled = true;
Settings.data.nightLight.forced = false;
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
Settings.data.nightLight.forced = true;
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
Settings.data.nightLight.enabled = false;
Settings.data.nightLight.forced = false;
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
}
}

Some files were not shown because too many files have changed in this diff Show More