Merge branch 'noctalia-dev:main' into pr/networking-refactor-pt1

This commit is contained in:
Turann_
2026-03-11 00:48:23 +03:00
committed by GitHub
54 changed files with 953 additions and 761 deletions
+7 -3
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Werde Unterstützer",
"changelog": "Änderungsprotokoll anzeigen",
"changelog-on-startup": "Änderungsprotokoll bei Update anzeigen",
"changelog-on-startup-desc": "Automatisch den Changelog anzeigen, wenn Noctalia aktualisiert wird.",
"contributors-description": "Ein Dankeschön an unseren {count} <b>großartigen</b> Mitwirkenden!",
"contributors-description-plural": "Ein Dankeschön an unsere {count} <b>großartigen</b> Mitwirkenden!",
"copy-info": "Informationen kopieren",
@@ -1671,8 +1673,8 @@
"panels-overlay-label": "Panels & Leiste oben behalten",
"scaling-description": "Ändert die Größe der allgemeinen Benutzeroberfläche, mit Ausnahme der Leiste.",
"scaling-label": "Oberflächenskalierung",
"scrollbar-always-visible-description": "Scrollbalken immer sichtbar lassen, wenn Inhalte scrollbar sind, anstatt sie nur beim Darüberfahren anzuzeigen.",
"scrollbar-always-visible-label": "Immer Bildlaufleisten anzeigen",
"scrollbar-always-visible-description": "Bildlaufleisten immer sichtbar lassen, wenn Inhalte scrollbar sind, anstatt sie nur beim Darüberfahren anzuzeigen.",
"scrollbar-always-visible-label": "Bildlaufleisten immer anzeigen",
"settings-panel-header": "Einstellungs-Panel",
"settings-panel-mode-description": "Wählen Sie das Layout der Einstellungen (möglicherweise ist ein Neustart erforderlich).",
"settings-panel-mode-label": "Einstellungs-Panel-Modus",
@@ -1684,7 +1686,9 @@
"shadows-label": "Schlagschatten",
"title": "Benutzeroberfläche",
"tooltips-description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.",
"tooltips-label": "Tooltips anzeigen"
"tooltips-label": "Tooltips anzeigen",
"translucent-widgets-description": "Mache Schaltflächen, Tabs und andere Widgets in Panels halbtransparent.",
"translucent-widgets-label": "Transluzente Widgets"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alphabetisch",
+5 -2
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Become a supporter",
"changelog": "View changelog",
"changelog-on-startup": "Show changelog on update",
"changelog-on-startup-desc": "Automatically show the changelog when Noctalia is updated",
"contributors-description": "Shout-out to our {count} <b>awesome</b> contributor!",
"contributors-description-plural": "Shout-out to our {count} <b>awesome</b> contributors!",
"copy-info": "Copy info",
@@ -1021,7 +1023,6 @@
"edit-mode-description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.",
"edit-mode-exit-button": "Exit edit mode",
"edit-mode-grid-snap-label": "Grid snap",
"edit-mode-grid-snap-scale-label": "Snap scale",
"edit-mode-label": "Edit mode",
"enabled-description": "Enable or disable desktop widgets entirely.",
"enabled-label": "Enable desktop widgets",
@@ -1686,7 +1687,9 @@
"shadows-label": "Drop shadows",
"title": "User Interface",
"tooltips-description": "Enable or disable tooltips throughout the interface.",
"tooltips-label": "Show tooltips"
"tooltips-label": "Show tooltips",
"translucent-widgets-description": "Make buttons, tabs, and other widgets inside panels semi-transparent.",
"translucent-widgets-label": "Translucent widgets"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alphabetical",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Hazte patrocinador",
"changelog": "Ver registro de cambios",
"changelog-on-startup": "Mostrar registro de cambios al actualizar",
"changelog-on-startup-desc": "Mostrar automáticamente el registro de cambios cuando Noctalia se actualice.",
"contributors-description": "¡Un saludo a nuestro <b>increíble</b> colaborador número {count}!",
"contributors-description-plural": "¡Un saludo a nuestros {count} <b>increíbles</b> colaboradores!",
"copy-info": "Copiar información",
@@ -1684,7 +1686,9 @@
"shadows-label": "Sombras paralelas",
"title": "Interfaz de usuario",
"tooltips-description": "Activar o desactivar los avisos emergentes en toda la interfaz.",
"tooltips-label": "Mostrar textos emergentes"
"tooltips-label": "Mostrar textos emergentes",
"translucent-widgets-description": "Haz que los botones, pestañas y otros widgets dentro de los paneles sean semitransparentes.",
"translucent-widgets-label": "Widgets translúcidos"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabético",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Devenir un soutien",
"changelog": "Afficher le journal des modifications",
"changelog-on-startup": "Afficher le journal des modifications lors de la mise à jour",
"changelog-on-startup-desc": "Afficher automatiquement le journal des modifications lors de la mise à jour de Noctalia.",
"contributors-description": "Un grand merci à notre {count} <b>super</b> contributeur !",
"contributors-description-plural": "Un grand merci à nos {count} <b>super</b> contributeurs !",
"copy-info": "Copier les informations",
@@ -1684,7 +1686,9 @@
"shadows-label": "Ombres portées",
"title": "Interface utilisateur",
"tooltips-description": "Activer ou désactiver les infobulles dans toute l'interface.",
"tooltips-label": "Afficher les infobulles"
"tooltips-label": "Afficher les infobulles",
"translucent-widgets-description": "Rendre les boutons, onglets et autres widgets à l'intérieur des panneaux semi-transparents.",
"translucent-widgets-label": "Widgets translucides"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alphabétique",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Legyél támogató",
"changelog": "Változások megtekintése",
"changelog-on-startup": "Változási napló megjelenítése frissítéskor",
"changelog-on-startup-desc": "Automatikusan mutassa a változási naplót, amikor a Noctalia frissül.",
"contributors-description": "Köszönet a(z) {count} <b>fantasztikus</b> közreműködőnknek!",
"contributors-description-plural": "Köszönet a(z) {count} <b>fantasztikus</b> közreműködőnknek!",
"copy-info": "Információk másolása",
@@ -1684,7 +1686,9 @@
"shadows-label": "Árnyékok",
"title": "Felhasználói felület",
"tooltips-description": "Engedélyezi vagy letiltja az eszköztippeket a felületen.",
"tooltips-label": "Eszköztippek megjelenítése"
"tooltips-label": "Eszköztippek megjelenítése",
"translucent-widgets-description": "Tedd a gombokat, füleket és egyéb widgeteket a paneleken belül félig átlátszóvá.",
"translucent-widgets-label": "Áttetsző widgetek"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Ábécérend",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Diventa sostenitore",
"changelog": "Visualizza changelog",
"changelog-on-startup": "Mostra il changelog all'aggiornamento",
"changelog-on-startup-desc": "Mostra automaticamente il changelog quando Noctalia viene aggiornato.",
"contributors-description": "Un ringraziamento al nostro {count} contributore <b>fantastico</b>!",
"contributors-description-plural": "Un ringraziamento ai nostri {count} contributori <b>fantastici</b>!",
"copy-info": "Copia info",
@@ -1684,7 +1686,9 @@
"shadows-label": "Ombre esterne",
"title": "Interfaccia utente",
"tooltips-description": "Abilita o disabilita i tooltip in tutta linterfaccia.",
"tooltips-label": "Mostra tooltip"
"tooltips-label": "Mostra tooltip",
"translucent-widgets-description": "Rendi semitrasparenti i pulsanti, le schede e gli altri widget all'interno dei pannelli.",
"translucent-widgets-label": "Widget traslucidi"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabetico",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "サポーターになる",
"changelog": "変更履歴を見る",
"changelog-on-startup": "アップデート時に変更履歴を表示",
"changelog-on-startup-desc": "Noctaliaが更新されたときに、自動的に変更履歴を表示する。",
"contributors-description": "{count}人の<b>素晴らしい</b>コントリビューターに感謝!",
"contributors-description-plural": "{count}人の<b>素晴らしい</b>コントリビューターに感謝!",
"copy-info": "情報をコピー",
@@ -1684,7 +1686,9 @@
"shadows-label": "ドロップシャドウ",
"title": "ユーザーインターフェース",
"tooltips-description": "インターフェース全体のツールチップの有効・無効を切り替えます。",
"tooltips-label": "ツールチップを表示"
"tooltips-label": "ツールチップを表示",
"translucent-widgets-description": "パネル内のボタン、タブ、その他のウィジェットを半透明にする。",
"translucent-widgets-label": "半透明のウィジェット"
},
"wallpaper": {
"automation-change-mode-alphabetical": "アルファベット順",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "후원자 되기",
"changelog": "변경 로그 보기",
"changelog-on-startup": "업데이트 시 변경 로그 표시",
"changelog-on-startup-desc": "Noctalia 업데이트 시 변경 로그를 자동으로 표시합니다.",
"contributors-description": "{count}명의 <b>멋진</b> 기여자에게 감사를 전합니다!",
"contributors-description-plural": "{count}명의 <b>멋진</b> 기여자들에게 감사를 전합니다!",
"copy-info": "정보 복사",
@@ -1684,7 +1686,9 @@
"shadows-label": "그림자",
"title": "사용자 인터페이스",
"tooltips-description": "인터페이스 전반에 툴팁을 활성화하거나 비활성화합니다.",
"tooltips-label": "툴팁 표시"
"tooltips-label": "툴팁 표시",
"translucent-widgets-description": "패널 내의 버튼, 탭 및 기타 위젯을 반투명하게 만듭니다.",
"translucent-widgets-label": "반투명 위젯"
},
"wallpaper": {
"automation-change-mode-alphabetical": "알파벳순",
+6 -2
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Word supporter",
"changelog": "Bekijk wijzigingslogboek",
"changelog-on-startup": "Toon wijzigingslogboek bij update",
"changelog-on-startup-desc": "Toon automatisch de changelog wanneer Noctalia wordt bijgewerkt.",
"contributors-description": "Een shout-out naar onze {count} <b>geweldige</b> bijdrager!",
"contributors-description-plural": "Een shout-out naar onze {count} <b>geweldige</b> bijdragers!",
"copy-info": "Kopieer info",
@@ -1672,7 +1674,7 @@
"scaling-description": "Wijzigt de grootte van de algemene gebruikersinterface, exclusief de balk.",
"scaling-label": "Interfaceschaling",
"scrollbar-always-visible-description": "Houd schuifbalken altijd zichtbaar wanneer inhoud scrollbaar is, in plaats van ze alleen bij hover te tonen.",
"scrollbar-always-visible-label": "Altijd schuifbalken tonen",
"scrollbar-always-visible-label": "Schuifbalken altijd weergeven",
"settings-panel-header": "Instellingenpaneel",
"settings-panel-mode-description": "Kies lay-out voor instellingen (mogelijk opnieuw openen vereist).",
"settings-panel-mode-label": "Instellingenpaneelmodus",
@@ -1684,7 +1686,9 @@
"shadows-label": "Slagschaduwen",
"title": "Gebruikersinterface",
"tooltips-description": "Schakel tooltips in of uit in de hele interface.",
"tooltips-label": "Tooltips tonen"
"tooltips-label": "Tooltips tonen",
"translucent-widgets-description": "Maak knoppen, tabbladen en andere widgets in panelen semi-transparant.",
"translucent-widgets-label": "Doorzichtige widgets"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabetisch",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Zostań wspierającym",
"changelog": "Zobacz dziennik zmian",
"changelog-on-startup": "Pokaż dziennik zmian po aktualizacji",
"changelog-on-startup-desc": "Automatycznie pokazuj dziennik zmian po aktualizacji Noctalia.",
"contributors-description": "Podziękowania dla naszego {count} <b>niesamowitego</b> współtwórcy!",
"contributors-description-plural": "Podziękowania dla naszych {count} <b>niesamowitych</b> współtwórców!",
"copy-info": "Kopiuj informacje",
@@ -1684,7 +1686,9 @@
"shadows-label": "Cienie",
"title": "Interfejs użytkownika",
"tooltips-description": "Włącz lub wyłącz podpowiedzi w całym interfejsie.",
"tooltips-label": "Pokaż podpowiedzi"
"tooltips-label": "Pokaż podpowiedzi",
"translucent-widgets-description": "Spraw, aby przyciski, zakładki i inne widżety w panelach były półprzezroczyste.",
"translucent-widgets-label": "Półprzezroczyste widżety"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabetyczny",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Torne-se um apoiador",
"changelog": "Ver histórico de alterações",
"changelog-on-startup": "Mostrar changelog na atualização",
"changelog-on-startup-desc": "Mostrar automaticamente o registo de alterações quando o Noctalia for atualizado.",
"contributors-description": "Agradecimentos ao nosso <b>incrível</b> colaborador!",
"contributors-description-plural": "Agradecimentos aos nossos {count} <b>incríveis</b> colaboradores!",
"copy-info": "Copiar informações",
@@ -1684,7 +1686,9 @@
"shadows-label": "Sombras projetadas",
"title": "Interface do usuário",
"tooltips-description": "Ativar ou desativar dicas de ferramentas em toda a interface.",
"tooltips-label": "Mostrar dicas de ferramenta"
"tooltips-label": "Mostrar dicas de ferramenta",
"translucent-widgets-description": "Torne botões, abas e outros widgets dentro dos painéis semitransparentes.",
"translucent-widgets-label": "Widgets translúcidos"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabético",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Стать спонсором",
"changelog": "Посмотреть список изменений",
"changelog-on-startup": "Показывать журнал изменений при обновлении",
"changelog-on-startup-desc": "Автоматически показывать журнал изменений при обновлении Noctalia.",
"contributors-description": "Благодарим нашего <b>замечательного</b> участника: {count}!",
"contributors-description-plural": "Благодарим наших <b>замечательных</b> участников: {count}!",
"copy-info": "Копировать информацию",
@@ -1684,7 +1686,9 @@
"shadows-label": "Отбрасываемые тени",
"title": "Пользовательский интерфейс",
"tooltips-description": "Включить или отключить всплывающие подсказки во всем интерфейсе.",
"tooltips-label": "Показывать всплывающие подсказки"
"tooltips-label": "Показывать всплывающие подсказки",
"translucent-widgets-description": "Сделайте кнопки, вкладки и другие виджеты внутри панелей полупрозрачными.",
"translucent-widgets-label": "Полупрозрачные виджеты"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Алфавитный",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Bli supporter",
"changelog": "Visa ändringslogg",
"changelog-on-startup": "Visa ändringslogg vid uppdatering",
"changelog-on-startup-desc": "Visa ändringsloggen automatiskt när Noctalia uppdateras.",
"contributors-description": "Tack till våra {count} <b>fantastiska</b> bidragsgivare!",
"contributors-description-plural": "Tack till våra {count} <b>fantastiska</b> bidragsgivare!",
"copy-info": "Kopiera info",
@@ -1684,7 +1686,9 @@
"shadows-label": "Fallskugga",
"title": "Användargränssnitt",
"tooltips-description": "Aktivera eller inaktivera verktygstips i hela gränssnittet.",
"tooltips-label": "Visa verktygstips"
"tooltips-label": "Visa verktygstips",
"translucent-widgets-description": "Gör knappar, flikar och andra widgets inuti paneler halvtransparenta.",
"translucent-widgets-label": "Genomskinliga widgetar"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabetiskt",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Destekçi ol",
"changelog": "Değişiklik günlüğünü görüntüle",
"changelog-on-startup": "Güncellemede değişiklik günlüğünü göster",
"changelog-on-startup-desc": "Noctalia güncellendiğinde değişiklik günlüğünü otomatik olarak göster.",
"contributors-description": "{count} <b>harika</b> katılımcımıza <b>teşekkürler</b>!",
"contributors-description-plural": "{count} <b>harika</b> katılımcımıza <b>teşekkürler</b>!",
"copy-info": "Bilgileri kopyala",
@@ -1684,7 +1686,9 @@
"shadows-label": "Gölge efektleri",
"title": "Kullanıcı arayüzü",
"tooltips-description": "Arayüz genelindeki ipuçlarını etkinleştirin veya devre dışı bırakın.",
"tooltips-label": "İpuçlarını göster"
"tooltips-label": "İpuçlarını göster",
"translucent-widgets-description": "Panellerin içindeki düğmeleri, sekmeleri ve diğer widget'ları yarı saydam yapın.",
"translucent-widgets-label": "Yarı saydam widget'lar"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Alfabetik",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "Стати прихильником",
"changelog": "Переглянути журнал змін",
"changelog-on-startup": "Показувати журнал змін під час оновлення",
"changelog-on-startup-desc": "Автоматично показувати журнал змін при оновленні Noctalia.",
"contributors-description": "Подяка нашому {count} <b>чудовому</b> учаснику!",
"contributors-description-plural": "Подяка нашим {count} <b>чудовим</b> учасникам!",
"copy-info": "Копіювати інформацію",
@@ -1684,7 +1686,9 @@
"shadows-label": "Тіні",
"title": "Користувацький інтерфейс",
"tooltips-description": "Увімкнути або вимкнути підказки в інтерфейсі.",
"tooltips-label": "Показувати підказки"
"tooltips-label": "Показувати підказки",
"translucent-widgets-description": "Зробіть кнопки, вкладки та інші віджети всередині панелей напівпрозорими.",
"translucent-widgets-label": "Напівпрозорі віджети"
},
"wallpaper": {
"automation-change-mode-alphabetical": "Алфавітний",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "成为支持者",
"changelog": "查看更改日志",
"changelog-on-startup": "更新时显示更新日志",
"changelog-on-startup-desc": "Noctalia 更新时自动显示更新日志。",
"contributors-description": "向我们 {count} 位<b>超棒的</b>贡献者致敬!",
"contributors-description-plural": "向我们 {count} 位<b>超棒的</b>贡献者致敬!",
"copy-info": "复制信息",
@@ -1684,7 +1686,9 @@
"shadows-label": "阴影",
"title": "用户界面",
"tooltips-description": "启用或禁用整个界面的提示信息。",
"tooltips-label": "显示提示信息(Tooltip"
"tooltips-label": "显示提示信息(Tooltip",
"translucent-widgets-description": "使面板内的按钮、选项卡和其他小部件半透明。",
"translucent-widgets-label": "半透明小部件"
},
"wallpaper": {
"automation-change-mode-alphabetical": "按字母顺序",
+5 -1
View File
@@ -714,6 +714,8 @@
"about": {
"become-supporter": "成為支持者",
"changelog": "檢視更新日誌",
"changelog-on-startup": "更新時顯示變更日誌",
"changelog-on-startup-desc": "Noctalia 更新時自動顯示更新日誌。",
"contributors-description": "特別感謝我們這{count}位<b>超讚</b>的貢獻者!!",
"contributors-description-plural": "特別感謝我們這{count}位<b>超讚</b>的貢獻者!!",
"copy-info": "複製資訊",
@@ -1684,7 +1686,9 @@
"shadows-label": "陰影",
"title": "使用者介面",
"tooltips-description": "在整個介面啟用或停用提示框",
"tooltips-label": "顯示提示框"
"tooltips-label": "顯示提示框",
"translucent-widgets-description": "使面板內的按鈕、分頁和其他小部件半透明。",
"translucent-widgets-label": "半透明小工具"
},
"wallpaper": {
"automation-change-mode-alphabetical": "照字母排序",
+1
View File
@@ -152,6 +152,7 @@
"scrollbarAlwaysVisible": true,
"boxBorderEnabled": false,
"panelBackgroundOpacity": 0.93,
"translucentWidgets": false,
"panelsAttachedToBar": true,
"settingsPanelMode": "attached",
"settingsPanelSideBarCardStyle": false
+18
View File
@@ -1,4 +1,13 @@
[
{
"labelKey": "panels.about.changelog-on-startup",
"descriptionKey": "panels.about.changelog-on-startup-desc",
"widget": "NToggle",
"tab": 21,
"tabLabel": "panels.about.title",
"subTab": 0,
"subTabLabel": "common.info"
},
{
"labelKey": "panels.about.telemetry-enabled",
"descriptionKey": "panels.about.telemetry-desc",
@@ -1971,6 +1980,15 @@
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.user-interface.translucent-widgets-label",
"descriptionKey": "panels.user-interface.translucent-widgets-description",
"widget": "NToggle",
"tab": 1,
"tabLabel": "panels.user-interface.title",
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.user-interface.shadows-direction-label",
"descriptionKey": "panels.user-interface.shadows-direction-description",
+14
View File
@@ -324,6 +324,20 @@ Singleton {
}
}
// Smart alpha calculation: automatically makes light mode more transparent
function smartAlpha(baseColor, minAlpha = 0.4) {
if (!Settings.data.ui.translucentWidgets)
return baseColor;
let baseOpacity = Settings.data.ui.panelBackgroundOpacity;
let targetOpacity = Settings.data.colorSchemes.darkMode ? baseOpacity : Math.pow(baseOpacity, 2);
let alpha = Math.max(targetOpacity, minAlpha);
// Combine with the base color's existing alpha
let resultAlpha = Math.max(0, baseColor.a - (1.0 - alpha));
return Qt.alpha(baseColor, resultAlpha);
}
readonly property var colorKeyModel: [
{
"key": "none",
+1
View File
@@ -330,6 +330,7 @@ Singleton {
property bool scrollbarAlwaysVisible: true
property bool boxBorderEnabled: false
property real panelBackgroundOpacity: 0.93
property bool translucentWidgets: false
property bool panelsAttachedToBar: true
property string settingsPanelMode: "attached" // "centered", "attached", "window"
property bool settingsPanelSideBarCardStyle: false
+6 -11
View File
@@ -109,18 +109,13 @@ Singleton {
save();
}
// Set a usage count directly (used for migration/merging)
function recordLauncherUsageMerge(key, count) {
// Migrate usage from one key to another, merging counts in a single save
function migrateLauncherUsage(fromKey, toKey) {
let counts = Object.assign({}, adapter.launcherUsage || {});
counts[key] = count;
adapter.launcherUsage = counts;
save();
}
// Remove a usage key (used for cleaning up legacy keys after migration)
function clearLauncherUsage(key) {
let counts = Object.assign({}, adapter.launcherUsage || {});
delete counts[key];
const fromCount = typeof counts[fromKey] === 'number' && isFinite(counts[fromKey]) ? counts[fromKey] : 0;
const toCount = typeof counts[toKey] === 'number' && isFinite(counts[toKey]) ? counts[toKey] : 0;
counts[toKey] = toCount + fromCount;
delete counts[fromKey];
adapter.launcherUsage = counts;
save();
}
+30 -35
View File
@@ -26,8 +26,18 @@ var constants = {
// Safe evaluation function that handles advanced math
function evaluate(expression) {
try {
// Replace mathematical constants
var processed = expression
// Fixes decimal arithmetic
var cleanExpr = expression.replace(/\s+/g, '').toLowerCase();
// Allows numbers (including decimals), basic operators, and explicitly permitted math terms only
var safeRegex = /^(\d*\.?\d+|[+\-*/()^%,]|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|log|ln|exp|pow|sqrt|cbrt|abs|floor|ceil|round|trunc|min|max|random|pi|e|sind|cosd|tand)+$/;
if (!safeRegex.test(cleanExpr)) {
throw new Error("Invalid characters or unauthorized functions in expression");
}
// Replace mathematical constants (Original Structure)
var processed = cleanExpr
.replace(/\bpi\b/gi, Math.PI)
.replace(/\be\b/gi, Math.E);
@@ -41,7 +51,7 @@ function evaluate(expression) {
.replace(/\bacos\s*\(/g, 'Math.acos(')
.replace(/\batan\s*\(/g, 'Math.atan(')
.replace(/\batan2\s*\(/g, 'Math.atan2(')
// Hyperbolic functions
.replace(/\bsinh\s*\(/g, 'Math.sinh(')
.replace(/\bcosh\s*\(/g, 'Math.cosh(')
@@ -49,28 +59,28 @@ function evaluate(expression) {
.replace(/\basinh\s*\(/g, 'Math.asinh(')
.replace(/\bacosh\s*\(/g, 'Math.acosh(')
.replace(/\batanh\s*\(/g, 'Math.atanh(')
// Logarithmic and exponential functions
.replace(/\blog\s*\(/g, 'Math.log10(')
.replace(/\bln\s*\(/g, 'Math.log(')
.replace(/\bexp\s*\(/g, 'Math.exp(')
.replace(/\bpow\s*\(/g, 'Math.pow(')
// Root functions
.replace(/\bsqrt\s*\(/g, 'Math.sqrt(')
.replace(/\bcbrt\s*\(/g, 'Math.cbrt(')
// Rounding and absolute
.replace(/\babs\s*\(/g, 'Math.abs(')
.replace(/\bfloor\s*\(/g, 'Math.floor(')
.replace(/\bceil\s*\(/g, 'Math.ceil(')
.replace(/\bround\s*\(/g, 'Math.round(')
.replace(/\btrunc\s*\(/g, 'Math.trunc(')
// Min/Max
.replace(/\bmin\s*\(/g, 'Math.min(')
.replace(/\bmax\s*\(/g, 'Math.max(')
// Random
.replace(/\brandom\s*\(\s*\)/g, 'Math.random()');
@@ -83,25 +93,10 @@ function evaluate(expression) {
// Handle ^ for exponentiation: convert 2^3 to Math.pow(2,3)
processed = processed.replace(/([\d.]+|\))\^([\d.]+|\([^)]*\))/g, 'Math.pow($1,$2)');
// Sanitize expression (only allow safe characters)
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
throw new Error("Invalid characters in expression");
}
// Replacing eval() with a scoped function constructor
// This is safe because the strict whitelist guarantees only math reaches this point
var result = new Function('return ' + processed)();
// Block dangerous identifiers (prototype chain traversal, code execution)
if (/\b(constructor|prototype|__proto__|__defineGetter__|__defineSetter__|__lookupGetter__|__lookupSetter__|Function|eval|require|import|process|global|window|this|self|globalThis|String|Object|Array|RegExp|Proxy|Reflect|setTimeout|setInterval)\b/.test(processed)) {
throw new Error("Invalid expression");
}
// Only allow Math.method property access - block any other dot-property chains
var withoutMathCalls = processed.replace(/\bMath\.\w+/g, '0');
if (/\./.test(withoutMathCalls)) {
throw new Error("Invalid expression");
}
// Evaluate the processed expression
var result = eval(processed);
if (!isFinite(result) || isNaN(result)) {
throw new Error("Invalid result");
}
@@ -117,12 +112,12 @@ function formatResult(result) {
if (Number.isInteger(result)) {
return result.toString();
}
// Handle very large or very small numbers
if (Math.abs(result) >= 1e15 || (Math.abs(result) < 1e-6 && result !== 0)) {
return result.toExponential(6);
}
// Normal decimal formatting
return parseFloat(result.toFixed(10)).toString();
}
@@ -131,35 +126,35 @@ function formatResult(result) {
function getAvailableFunctions() {
return [
// Basic arithmetic: +, -, *, /, %, ^, ()
// Trigonometric functions
"sin(x), cos(x), tan(x) - trigonometric functions (radians)",
"sind(x), cosd(x), tand(x) - trigonometric functions (degrees)",
"asin(x), acos(x), atan(x) - inverse trigonometric",
"atan2(y, x) - two-argument arctangent",
// Hyperbolic functions
"sinh(x), cosh(x), tanh(x) - hyperbolic functions",
"asinh(x), acosh(x), atanh(x) - inverse hyperbolic",
// Logarithmic and exponential
"log(x) - base 10 logarithm",
"ln(x) - natural logarithm",
"exp(x) - e^x",
"pow(x, y) - x^y",
// Root functions
"sqrt(x) - square root",
"cbrt(x) - cube root",
// Rounding and absolute
"abs(x) - absolute value",
"floor(x), ceil(x), round(x), trunc(x)",
// Min/Max/Random
"min(a, b, ...), max(a, b, ...)",
"random() - random number 0-1",
// Constants
"pi, e - mathematical constants"
];
@@ -44,6 +44,7 @@ Item {
id: unifiedBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
Component.onCompleted: {
@@ -111,6 +112,7 @@ Item {
id: panelBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
/**
@@ -159,6 +161,7 @@ Item {
id: barBackgroundShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
BarBackground {
+1
View File
@@ -25,6 +25,7 @@ Item {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false // Disable mouse input
visible: cornersPath.cornerRadius > 0 && width > 0 && height > 0
+21 -3
View File
@@ -28,8 +28,13 @@ Variants {
property ListModel notificationModel: NotificationService.activeList
// Always create window (but with 0x0 dimensions when no notifications)
active: notificationModel.count > 0 || delayTimer.running
// Deferred activation to prevent re-entrant QML incubation crash.
// Direct binding to notificationModel.count would activate the Loader
// synchronously during ListModel.insert(), causing nested incubation
// (Loader + inner Repeater both processing the model) which crashes
// the V4 engine in QV4::Object::insertMember.
property bool shouldBeActive: false
active: shouldBeActive || delayTimer.running
// Keep loader active briefly after last notification to allow animations to complete
Timer {
@@ -38,10 +43,23 @@ Variants {
repeat: false
}
// Deferred activation timer - activates Loader on next event loop iteration
Timer {
id: activationTimer
interval: 0
repeat: false
onTriggered: root.shouldBeActive = true
}
Connections {
target: notificationModel
function onCountChanged() {
if (notificationModel.count === 0 && root.active) {
if (notificationModel.count > 0) {
if (!root.shouldBeActive) {
activationTimer.restart();
}
} else if (root.shouldBeActive) {
root.shouldBeActive = false;
delayTimer.restart();
}
}
@@ -0,0 +1,134 @@
function selectNext(selectedIndex, resultsLength) {
if (resultsLength > 0 && selectedIndex < resultsLength - 1)
return selectedIndex + 1;
return selectedIndex;
}
function selectPrevious(selectedIndex, resultsLength) {
if (resultsLength > 0 && selectedIndex > 0)
return selectedIndex - 1;
return selectedIndex;
}
function selectNextWrapped(selectedIndex, resultsLength, allowWrap) {
if (resultsLength > 0) {
if (allowWrap)
return (selectedIndex + 1) % resultsLength;
return selectNext(selectedIndex, resultsLength);
}
return selectedIndex;
}
function selectPreviousWrapped(selectedIndex, resultsLength, allowWrap) {
if (resultsLength > 0) {
if (allowWrap)
return (((selectedIndex - 1) % resultsLength) + resultsLength) % resultsLength;
return selectPrevious(selectedIndex, resultsLength);
}
return selectedIndex;
}
function selectFirst() {
return 0;
}
function selectLast(resultsLength) {
return resultsLength > 0 ? resultsLength - 1 : 0;
}
function selectNextPage(selectedIndex, resultsLength, entryHeight) {
if (resultsLength > 0) {
var page = Math.max(1, Math.floor(600 / entryHeight));
return Math.min(selectedIndex + page, resultsLength - 1);
}
return selectedIndex;
}
function selectPreviousPage(selectedIndex, resultsLength, entryHeight) {
if (resultsLength > 0) {
var page = Math.max(1, Math.floor(600 / entryHeight));
return Math.max(selectedIndex - page, 0);
}
return selectedIndex;
}
function selectPreviousRow(selectedIndex, resultsLength, gridColumns) {
if (resultsLength <= 0 || gridColumns <= 0)
return selectedIndex;
var currentRow = Math.floor(selectedIndex / gridColumns);
var currentCol = selectedIndex % gridColumns;
if (currentRow > 0) {
var targetRow = currentRow - 1;
var itemsInTargetRow = Math.min(gridColumns, resultsLength - targetRow * gridColumns);
if (currentCol < itemsInTargetRow)
return targetRow * gridColumns + currentCol;
return targetRow * gridColumns + itemsInTargetRow - 1;
}
// Wrap to last row, same column
var totalRows = Math.ceil(resultsLength / gridColumns);
var lastRow = totalRows - 1;
var itemsInLastRow = Math.min(gridColumns, resultsLength - lastRow * gridColumns);
if (currentCol < itemsInLastRow)
return lastRow * gridColumns + currentCol;
return resultsLength - 1;
}
function selectNextRow(selectedIndex, resultsLength, gridColumns) {
if (resultsLength <= 0 || gridColumns <= 0)
return selectedIndex;
var currentRow = Math.floor(selectedIndex / gridColumns);
var currentCol = selectedIndex % gridColumns;
var totalRows = Math.ceil(resultsLength / gridColumns);
if (currentRow < totalRows - 1) {
var targetRow = currentRow + 1;
var targetIndex = targetRow * gridColumns + currentCol;
if (targetIndex < resultsLength)
return targetIndex;
var itemsInTargetRow = resultsLength - targetRow * gridColumns;
if (itemsInTargetRow > 0)
return targetRow * gridColumns + itemsInTargetRow - 1;
return Math.min(currentCol, resultsLength - 1);
}
// Wrap to first row, same column
return Math.min(currentCol, resultsLength - 1);
}
function selectPreviousColumn(selectedIndex, resultsLength, gridColumns) {
if (resultsLength <= 0)
return selectedIndex;
var currentRow = Math.floor(selectedIndex / gridColumns);
var currentCol = selectedIndex % gridColumns;
if (currentCol > 0)
return currentRow * gridColumns + (currentCol - 1);
if (currentRow > 0)
return (currentRow - 1) * gridColumns + (gridColumns - 1);
var totalRows = Math.ceil(resultsLength / gridColumns);
var lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1);
return Math.min(lastRowIndex, resultsLength - 1);
}
function selectNextColumn(selectedIndex, resultsLength, gridColumns) {
if (resultsLength <= 0)
return selectedIndex;
var currentRow = Math.floor(selectedIndex / gridColumns);
var currentCol = selectedIndex % gridColumns;
var itemsInCurrentRow = Math.min(gridColumns, resultsLength - currentRow * gridColumns);
if (currentCol < itemsInCurrentRow - 1)
return currentRow * gridColumns + (currentCol + 1);
var totalRows = Math.ceil(resultsLength / gridColumns);
if (currentRow < totalRows - 1)
return (currentRow + 1) * gridColumns;
return 0;
}
+27 -653
View File
@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import "Helpers/LauncherNavigation.js" as LauncherNav
import "Providers"
import qs.Commons
@@ -311,11 +312,14 @@ Rectangle {
let sb = b._score !== undefined ? b._score : 0;
// Boost scores for frequently used items from tracked providers
// _score is normalized 01, so boost is scaled to nudge, not overwhelm
if (boostByUsage) {
if (a.provider && a.provider.trackUsage && a.usageKey)
sa += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey));
if (b.provider && b.provider.trackUsage && b.usageKey)
sb += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey));
if (a.provider && a.provider.trackUsage && a.usageKey) {
sa += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey));
}
if (b.provider && b.provider.trackUsage && b.usageKey) {
sb += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey));
}
}
return sb - sa;
@@ -329,149 +333,42 @@ Rectangle {
selectedIndex = 0;
}
// Navigation functions
// Navigation functions (delegated to LauncherNavigation.js)
function selectNext() {
if (results.length > 0 && selectedIndex < results.length - 1) {
selectedIndex++;
}
selectedIndex = LauncherNav.selectNext(selectedIndex, results.length);
}
function selectPrevious() {
if (results.length > 0 && selectedIndex > 0) {
selectedIndex--;
}
selectedIndex = LauncherNav.selectPrevious(selectedIndex, results.length);
}
function selectNextWrapped() {
if (results.length > 0) {
if (allowWrapNavigation) {
selectedIndex = (selectedIndex + 1) % results.length;
} else {
selectNext();
}
}
selectedIndex = LauncherNav.selectNextWrapped(selectedIndex, results.length, allowWrapNavigation);
}
function selectPreviousWrapped() {
if (results.length > 0) {
if (allowWrapNavigation) {
selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length;
} else {
selectPrevious();
}
}
selectedIndex = LauncherNav.selectPreviousWrapped(selectedIndex, results.length, allowWrapNavigation);
}
function selectFirst() {
selectedIndex = 0;
selectedIndex = LauncherNav.selectFirst();
}
function selectLast() {
selectedIndex = results.length > 0 ? results.length - 1 : 0;
selectedIndex = LauncherNav.selectLast(results.length);
}
function selectNextPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(600 / entryHeight));
selectedIndex = Math.min(selectedIndex + page, results.length - 1);
}
selectedIndex = LauncherNav.selectNextPage(selectedIndex, results.length, entryHeight);
}
function selectPreviousPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(600 / entryHeight));
selectedIndex = Math.max(selectedIndex - page, 0);
}
selectedIndex = LauncherNav.selectPreviousPage(selectedIndex, results.length, entryHeight);
}
// Grid view navigation functions
function selectPreviousRow() {
if (results.length > 0 && isGridView && gridColumns > 0) {
const currentRow = Math.floor(selectedIndex / gridColumns);
const currentCol = selectedIndex % gridColumns;
if (currentRow > 0) {
const targetRow = currentRow - 1;
const targetIndex = targetRow * gridColumns + currentCol;
const itemsInTargetRow = Math.min(gridColumns, results.length - targetRow * gridColumns);
if (currentCol < itemsInTargetRow) {
selectedIndex = targetIndex;
} else {
selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1;
}
} else {
// Wrap to last row, same column
const totalRows = Math.ceil(results.length / gridColumns);
const lastRow = totalRows - 1;
const itemsInLastRow = Math.min(gridColumns, results.length - lastRow * gridColumns);
if (currentCol < itemsInLastRow) {
selectedIndex = lastRow * gridColumns + currentCol;
} else {
selectedIndex = results.length - 1;
}
}
}
selectedIndex = LauncherNav.selectPreviousRow(selectedIndex, results.length, gridColumns);
}
function selectNextRow() {
if (results.length > 0 && isGridView && gridColumns > 0) {
const currentRow = Math.floor(selectedIndex / gridColumns);
const currentCol = selectedIndex % gridColumns;
const totalRows = Math.ceil(results.length / gridColumns);
if (currentRow < totalRows - 1) {
const targetRow = currentRow + 1;
const targetIndex = targetRow * gridColumns + currentCol;
if (targetIndex < results.length) {
selectedIndex = targetIndex;
} else {
const itemsInTargetRow = results.length - targetRow * gridColumns;
if (itemsInTargetRow > 0) {
selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1;
} else {
selectedIndex = Math.min(currentCol, results.length - 1);
}
}
} else {
// Wrap to first row, same column
selectedIndex = Math.min(currentCol, results.length - 1);
}
}
selectedIndex = LauncherNav.selectNextRow(selectedIndex, results.length, gridColumns);
}
function selectPreviousColumn() {
if (results.length > 0 && isGridView) {
const currentRow = Math.floor(selectedIndex / gridColumns);
const currentCol = selectedIndex % gridColumns;
if (currentCol > 0) {
selectedIndex = currentRow * gridColumns + (currentCol - 1);
} else if (currentRow > 0) {
selectedIndex = (currentRow - 1) * gridColumns + (gridColumns - 1);
} else {
const totalRows = Math.ceil(results.length / gridColumns);
const lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1);
selectedIndex = Math.min(lastRowIndex, results.length - 1);
}
}
selectedIndex = LauncherNav.selectPreviousColumn(selectedIndex, results.length, gridColumns);
}
function selectNextColumn() {
if (results.length > 0 && isGridView) {
const currentRow = Math.floor(selectedIndex / gridColumns);
const currentCol = selectedIndex % gridColumns;
const itemsInCurrentRow = Math.min(gridColumns, results.length - currentRow * gridColumns);
if (currentCol < itemsInCurrentRow - 1) {
selectedIndex = currentRow * gridColumns + (currentCol + 1);
} else {
const totalRows = Math.ceil(results.length / gridColumns);
if (currentRow < totalRows - 1) {
selectedIndex = (currentRow + 1) * gridColumns;
} else {
selectedIndex = 0;
}
}
}
selectedIndex = LauncherNav.selectNextColumn(selectedIndex, results.length, gridColumns);
}
function activate() {
@@ -800,7 +697,7 @@ Rectangle {
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AlwaysOff
reserveScrollbarSpace: false
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurfaceVariant
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface
wheelScrollMultiplier: 4.0
width: parent.width
@@ -818,276 +715,8 @@ Rectangle {
}
onModelChanged: {}
delegate: NBox {
id: entry
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex)
width: resultsList.availableWidth
implicitHeight: root.entryHeight
clip: true
color: entry.isSelected ? Color.mHover : Color.mSurface
forceOpaque: entry.isSelected
// Prepare item when it becomes visible (e.g., decode images)
Component.onCompleted: {
var provider = modelData.provider;
if (provider && provider.prepareItem) {
provider.prepareItem(modelData);
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: root.isCompactDensity ? Style.marginXS : Style.marginM
spacing: root.isCompactDensity ? Style.marginXS : Style.marginM
// Top row - Main entry content with action buttons
RowLayout {
Layout.fillWidth: true
spacing: root.isCompactDensity ? Style.marginS : Style.marginM
// Icon badge or Image preview or Emoji
Item {
visible: !modelData.hideIcon
Layout.preferredWidth: modelData.hideIcon ? 0 : root.badgeSize
Layout.preferredHeight: modelData.hideIcon ? 0 : root.badgeSize
// Icon background
Rectangle {
anchors.fill: parent
radius: Style.radiusXS
color: Color.mSurfaceVariant
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
}
// Image preview - uses provider's getImageUrl if available
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: !!modelData.isImage && !modelData.displayString
radius: Style.radiusXS
borderColor: Color.mOnSurface
borderWidth: Style.borderM
imageFillMode: Image.PreserveAspectCrop
// Use provider's image revision for reactive updates
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
// Get image URL from provider
imagePath: {
_rev;
var provider = modelData.provider;
if (provider && provider.getImageUrl) {
return provider.getImageUrl(modelData);
}
return "";
}
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true;
imagePreview.visible = false;
}
}
}
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && imagePreview.status === Image.Error)
active: visible
sourceComponent: Component {
Loader {
anchors.fill: parent
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent
}
}
Component {
id: tablerIconComponent
NIcon {
icon: modelData.icon
pointSize: Style.fontSizeXXXL
visible: modelData.icon && !modelData.displayString
color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
}
}
Component {
id: systemIconComponent
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" && !modelData.displayString
asynchronous: true
}
}
}
// String display - takes precedence when displayString is present
NText {
id: stringDisplay
anchors.centerIn: parent
visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible)
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
}
// Image type indicator overlay
Rectangle {
visible: !!modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + Style.marginXS
height: formatLabel.height + Style.marginXXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" \u2022 ");
return parts[0] || "IMG";
}
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
// Badge icon overlay (generic indicator for any provider)
Rectangle {
visible: !!modelData.badgeIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: height
height: Style.fontSizeM + Style.marginXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NIcon {
anchors.centerIn: parent
icon: modelData.badgeIcon || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
maximumLineCount: 1
wrapMode: Text.Wrap
clip: true
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
visible: text !== "" && !root.isCompactDensity
}
}
// Action buttons row - dynamically populated from provider
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: Style.marginXS
visible: entry.isSelected && itemActions.length > 0
property var itemActions: {
if (!entry.isSelected)
return [];
var provider = modelData.provider || root.currentProvider;
if (provider && provider.getItemActions) {
return provider.getItemActions(modelData);
}
return [];
}
Repeater {
model: parent.itemActions
NIconButton {
icon: modelData.icon
baseSize: Style.baseWidgetSize * 0.75
tooltipText: modelData.tooltip
z: 1
handleWheel: true
onClicked: {
if (modelData.action) {
modelData.action();
}
}
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !Settings.data.appLauncher.ignoreMouseInput
onEntered: {
if (!root.ignoreMouseHover) {
root.selectedIndex = index;
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
root.selectedIndex = index;
root.activate();
mouse.accepted = true;
}
}
acceptedButtons: Qt.LeftButton
}
delegate: LauncherListDelegate {
launcher: root
}
}
}
@@ -1160,7 +789,7 @@ Rectangle {
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AlwaysOff
reserveScrollbarSpace: false
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurfaceVariant
gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface
wheelScrollMultiplier: 4.0
trackedSelectionIndex: root.selectedIndex
@@ -1206,263 +835,8 @@ Rectangle {
}
}
delegate: Item {
id: gridEntryContainer
width: resultsGrid.cellWidth
height: resultsGrid.cellHeight
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex)
// Prepare item when it becomes visible (e.g., decode images)
Component.onCompleted: {
var provider = modelData.provider;
if (provider && provider.prepareItem) {
provider.prepareItem(modelData);
}
}
NBox {
id: gridEntry
anchors.fill: parent
anchors.margins: Style.marginXXS
color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurface
forceOpaque: gridEntryContainer.isSelected
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: root.isCompactDensity ? Style.marginXS : Style.marginS
anchors.bottomMargin: root.isCompactDensity ? Style.marginXS : Style.marginS
spacing: root.isCompactDensity ? 0 : Style.marginXXS
// Icon badge or Image preview or Emoji
Item {
// Size image at 65% of cell dimensions.
Layout.preferredWidth: Math.round(gridEntry.width * 0.65)
Layout.preferredHeight: Math.round(gridEntry.height * 0.65)
Layout.alignment: Qt.AlignHCenter
// Icon background
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: Color.mSurfaceVariant
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
}
// Image preview - uses provider's getImageUrl if available
NImageRounded {
id: gridImagePreview
anchors.fill: parent
visible: !!modelData.isImage && !modelData.displayString
radius: Style.radiusM
// Use provider's image revision for reactive updates
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
// Get image URL from provider
imagePath: {
_rev;
var provider = modelData.provider;
if (provider && provider.getImageUrl) {
return provider.getImageUrl(modelData);
}
return "";
}
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
onStatusChanged: status => {
if (status === Image.Error) {
gridIconLoader.visible = true;
gridImagePreview.visible = false;
}
}
}
Loader {
id: gridIconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error)
active: visible
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent
Component {
id: gridTablerIconComponent
NIcon {
icon: modelData.icon
pointSize: Style.fontSizeXXXL
visible: modelData.icon && !modelData.displayString
color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
}
}
Component {
id: gridSystemIconComponent
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" && !modelData.displayString
asynchronous: true
}
}
}
// String display
NText {
id: gridStringDisplay
anchors.centerIn: parent
visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible)
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: {
if (modelData.displayString) {
// Use custom size if provided, otherwise default scaling
if (modelData.displayStringSize) {
return modelData.displayStringSize * Style.uiScaleRatio;
}
if (root.providerHasDisplayString) {
// Scale with cell width but cap at reasonable maximum
const cellBasedSize = gridEntry.width * 0.4;
const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio;
return Math.min(cellBasedSize, maxSize);
}
return Style.fontSizeXXL * 2 * Style.uiScaleRatio;
}
// Scale font size relative to cell width for low res, but cap at maximum
const cellBasedSize = gridEntry.width * 0.25;
const baseSize = Style.fontSizeXL * Style.uiScaleRatio;
const maxSize = Style.fontSizeXXL * Style.uiScaleRatio;
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
}
font.weight: Style.fontWeightBold
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
}
// Badge icon overlay (generic indicator for any provider)
Rectangle {
visible: !!modelData.badgeIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: height
height: Style.fontSizeM + Style.marginXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NIcon {
anchors.centerIn: parent
icon: modelData.badgeIcon || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
}
// Text content (hidden when hideLabel is true)
NText {
visible: !modelData.hideLabel
text: modelData.name || "Unknown"
pointSize: {
if (root.providerHasDisplayString && modelData.displayString) {
return Style.fontSizeS * Style.uiScaleRatio;
}
// Scale font size relative to cell width for low res, but cap at maximum
const cellBasedSize = gridEntry.width * 0.1;
const baseSize = Style.fontSizeXS * Style.uiScaleRatio;
const maxSize = Style.fontSizeS * Style.uiScaleRatio;
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
}
font.weight: Style.fontWeightSemiBold
color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
Layout.maximumWidth: gridEntry.width - 8
Layout.leftMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
Layout.rightMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
maximumLineCount: 1
}
}
// Action buttons (overlay in top-right corner) - dynamically populated from provider
Row {
visible: gridEntryContainer.isSelected && gridItemActions.length > 0
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginXS
z: 10
spacing: Style.marginXXS
property var gridItemActions: {
if (!gridEntryContainer.isSelected)
return [];
var provider = modelData.provider || root.currentProvider;
if (provider && provider.getItemActions) {
return provider.getItemActions(modelData);
}
return [];
}
Repeater {
model: parent.gridItemActions
NIconButton {
icon: modelData.icon
baseSize: Style.baseWidgetSize * 0.75
tooltipText: modelData.tooltip
z: 11
handleWheel: true
onClicked: {
if (modelData.action) {
modelData.action();
}
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !Settings.data.appLauncher.ignoreMouseInput
onEntered: {
if (!root.ignoreMouseHover) {
root.selectedIndex = index;
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
root.selectedIndex = index;
root.activate();
mouse.accepted = true;
}
}
acceptedButtons: Qt.LeftButton
}
delegate: LauncherGridDelegate {
launcher: root
}
}
}
@@ -0,0 +1,272 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
import qs.Commons
import qs.Widgets
Item {
id: gridEntryContainer
required property var modelData
required property int index
required property var launcher
width: GridView.view.cellWidth
height: GridView.view.cellHeight
property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex)
// Prepare item when it becomes visible (e.g., decode images)
Component.onCompleted: {
var provider = modelData.provider;
if (provider && provider.prepareItem) {
provider.prepareItem(modelData);
}
}
NBox {
id: gridEntry
anchors.fill: parent
anchors.margins: Style.marginXXS
color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurfaceVariant
forceOpaque: gridEntryContainer.isSelected
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginS
anchors.bottomMargin: launcher.isCompactDensity ? Style.marginXS : Style.marginS
spacing: launcher.isCompactDensity ? 0 : Style.marginXXS
// Icon badge or Image preview or Emoji
Item {
// Size image at 65% of cell dimensions.
Layout.preferredWidth: Math.round(gridEntry.width * 0.65)
Layout.preferredHeight: Math.round(gridEntry.height * 0.65)
Layout.alignment: Qt.AlignHCenter
// Icon background
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: Color.mSurfaceVariant
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
}
// Image preview - uses provider's getImageUrl if available
NImageRounded {
id: gridImagePreview
anchors.fill: parent
visible: !!modelData.isImage && !modelData.displayString
radius: Style.radiusM
// Use provider's image revision for reactive updates
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
// Get image URL from provider
imagePath: {
_rev;
var provider = modelData.provider;
if (provider && provider.getImageUrl) {
return provider.getImageUrl(modelData);
}
return "";
}
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
onStatusChanged: status => {
if (status === Image.Error) {
gridIconLoader.visible = true;
gridImagePreview.visible = false;
}
}
}
Loader {
id: gridIconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error)
active: visible
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent
Component {
id: gridTablerIconComponent
NIcon {
icon: modelData.icon
pointSize: Style.fontSizeXXXL
visible: modelData.icon && !modelData.displayString
color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
}
}
Component {
id: gridSystemIconComponent
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" && !modelData.displayString
asynchronous: true
}
}
}
// String display
NText {
id: gridStringDisplay
anchors.centerIn: parent
visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible)
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: {
if (modelData.displayString) {
// Use custom size if provided, otherwise default scaling
if (modelData.displayStringSize) {
return modelData.displayStringSize * Style.uiScaleRatio;
}
if (launcher.providerHasDisplayString) {
// Scale with cell width but cap at reasonable maximum
const cellBasedSize = gridEntry.width * 0.4;
const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio;
return Math.min(cellBasedSize, maxSize);
}
return Style.fontSizeXXL * 2 * Style.uiScaleRatio;
}
// Scale font size relative to cell width for low res, but cap at maximum
const cellBasedSize = gridEntry.width * 0.25;
const baseSize = Style.fontSizeXL * Style.uiScaleRatio;
const maxSize = Style.fontSizeXXL * Style.uiScaleRatio;
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
}
font.weight: Style.fontWeightBold
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
}
// Badge icon overlay (generic indicator for any provider)
Rectangle {
visible: !!modelData.badgeIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: height
height: Style.fontSizeM + Style.marginXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NIcon {
anchors.centerIn: parent
icon: modelData.badgeIcon || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
}
// Text content (hidden when hideLabel is true)
NText {
visible: !modelData.hideLabel
text: modelData.name || "Unknown"
pointSize: {
if (launcher.providerHasDisplayString && modelData.displayString) {
return Style.fontSizeS * Style.uiScaleRatio;
}
// Scale font size relative to cell width for low res, but cap at maximum
const cellBasedSize = gridEntry.width * 0.1;
const baseSize = Style.fontSizeXS * Style.uiScaleRatio;
const maxSize = Style.fontSizeS * Style.uiScaleRatio;
return Math.min(Math.max(cellBasedSize, baseSize), maxSize);
}
font.weight: Style.fontWeightSemiBold
color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
Layout.maximumWidth: gridEntry.width - 8
Layout.leftMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
Layout.rightMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
maximumLineCount: 1
}
}
// Action buttons (overlay in top-right corner) - dynamically populated from provider
Row {
visible: gridEntryContainer.isSelected && gridItemActions.length > 0
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginXS
z: 10
spacing: Style.marginXXS
property var gridItemActions: {
if (!gridEntryContainer.isSelected)
return [];
var provider = modelData.provider || launcher.currentProvider;
if (provider && provider.getItemActions) {
return provider.getItemActions(modelData);
}
return [];
}
Repeater {
model: parent.gridItemActions
NIconButton {
required property var modelData
icon: modelData.icon
baseSize: Style.baseWidgetSize * 0.75
tooltipText: modelData.tooltip
z: 11
handleWheel: true
onClicked: {
if (modelData.action) {
modelData.action();
}
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !Settings.data.appLauncher.ignoreMouseInput
onEntered: {
if (!launcher.ignoreMouseHover) {
launcher.selectedIndex = gridEntryContainer.index;
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
launcher.selectedIndex = gridEntryContainer.index;
launcher.activate();
mouse.accepted = true;
}
}
acceptedButtons: Qt.LeftButton
}
}
@@ -0,0 +1,284 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
import qs.Commons
import qs.Widgets
NBox {
id: entry
required property var modelData
required property int index
required property var launcher
property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex)
width: ListView.view.width
implicitHeight: launcher.entryHeight
clip: true
color: entry.isSelected ? Color.mHover : Color.mSurfaceVariant
forceOpaque: entry.isSelected
// Prepare item when it becomes visible (e.g., decode images)
Component.onCompleted: {
var provider = modelData.provider;
if (provider && provider.prepareItem) {
provider.prepareItem(modelData);
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginM
spacing: launcher.isCompactDensity ? Style.marginXS : Style.marginM
// Top row - Main entry content with action buttons
RowLayout {
Layout.fillWidth: true
spacing: launcher.isCompactDensity ? Style.marginS : Style.marginM
// Icon badge or Image preview or Emoji
Item {
visible: !modelData.hideIcon
Layout.preferredWidth: modelData.hideIcon ? 0 : launcher.badgeSize
Layout.preferredHeight: modelData.hideIcon ? 0 : launcher.badgeSize
// Icon background
Rectangle {
anchors.fill: parent
radius: Style.radiusXS
color: Color.mSurfaceVariant
visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage
}
// Image preview - uses provider's getImageUrl if available
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: !!modelData.isImage && !modelData.displayString
radius: Style.radiusXS
borderColor: Color.mOnSurface
borderWidth: Style.borderM
imageFillMode: Image.PreserveAspectCrop
// Use provider's image revision for reactive updates
readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0
// Get image URL from provider
imagePath: {
_rev;
var provider = modelData.provider;
if (provider && provider.getImageUrl) {
return provider.getImageUrl(modelData);
}
return "";
}
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5
height: width
}
}
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true;
imagePreview.visible = false;
}
}
}
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && imagePreview.status === Image.Error)
active: visible
sourceComponent: Component {
Loader {
anchors.fill: parent
sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent
}
}
Component {
id: tablerIconComponent
NIcon {
icon: modelData.icon
pointSize: Style.fontSizeXXXL
visible: modelData.icon && !modelData.displayString
color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface
}
}
Component {
id: systemIconComponent
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" && !modelData.displayString
asynchronous: true
}
}
}
// String display - takes precedence when displayString is present
NText {
id: stringDisplay
anchors.centerIn: parent
visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible)
text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary
}
// Image type indicator overlay
Rectangle {
visible: !!modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + Style.marginXS
height: formatLabel.height + Style.marginXXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" \u2022 ");
return parts[0] || "IMG";
}
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
// Badge icon overlay (generic indicator for any provider)
Rectangle {
visible: !!modelData.badgeIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: height
height: Style.fontSizeM + Style.marginXS
color: Color.mSurfaceVariant
radius: Style.radiusXXS
NIcon {
anchors.centerIn: parent
icon: modelData.badgeIcon || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
maximumLineCount: 1
wrapMode: Text.Wrap
clip: true
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
visible: text !== "" && !launcher.isCompactDensity
}
}
// Action buttons row - dynamically populated from provider
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: Style.marginXS
visible: entry.isSelected && itemActions.length > 0
property var itemActions: {
if (!entry.isSelected)
return [];
var provider = modelData.provider || launcher.currentProvider;
if (provider && provider.getItemActions) {
return provider.getItemActions(modelData);
}
return [];
}
Repeater {
model: parent.itemActions
NIconButton {
required property var modelData
icon: modelData.icon
baseSize: Style.baseWidgetSize * 0.75
tooltipText: modelData.tooltip
z: 1
handleWheel: true
onClicked: {
if (modelData.action) {
modelData.action();
}
}
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !Settings.data.appLauncher.ignoreMouseInput
onEntered: {
if (!launcher.ignoreMouseHover) {
launcher.selectedIndex = entry.index;
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
launcher.selectedIndex = entry.index;
launcher.activate();
mouse.accepted = true;
}
}
acceptedButtons: Qt.LeftButton
}
}
@@ -61,6 +61,7 @@ Item {
function init() {
loadApplications();
migrateLegacyUsageKeys();
}
function onOpened() {
@@ -639,32 +640,22 @@ Item {
return String(app && app.name ? app.name : "unknown");
}
// Returns the usage count for an app, checking both the canonical key (app.id)
// and the legacy command-based key. If a legacy key has usage but the canonical
// key doesn't, the counts are migrated automatically.
function getUsageCount(app) {
const key = getAppKey(app);
let count = ShellState.getLauncherUsageCount(key);
return ShellState.getLauncherUsageCount(getAppKey(app));
}
// Check for legacy command-based key if the primary key is the app ID
if (app && app.id && app.command && app.command.join) {
const legacyKey = app.command.join(" ");
if (legacyKey !== key) {
const legacyCount = ShellState.getLauncherUsageCount(legacyKey);
if (legacyCount > 0) {
// Migrate: merge legacy count into the canonical key
count += legacyCount;
ShellState.recordLauncherUsageMerge(key, count);
ShellState.clearLauncherUsage(legacyKey);
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" (${legacyCount}) "${key}" (${count})`);
// Migrate legacy command-based usage keys to canonical app-id keys at startup
function migrateLegacyUsageKeys() {
for (let i = 0; i < entries.length; i++) {
const app = entries[i];
if (app && app.id && app.command && app.command.join) {
const key = getAppKey(app);
const legacyKey = app.command.join(" ");
if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) {
ShellState.migrateLauncherUsage(legacyKey, key);
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" "${key}"`);
}
}
}
return count;
}
function recordUsage(app) {
ShellState.recordLauncherUsage(getAppKey(app));
}
}
@@ -145,7 +145,7 @@ NBox {
}
}
Component.onCompleted: updateAvailableWidgetsModel()
Component.onCompleted: Qt.callLater(updateAvailableWidgetsModel)
ListModel {
id: availableWidgetsModel
@@ -616,6 +616,14 @@ ColumnLayout {
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.about.changelog-on-startup")
description: I18n.tr("panels.about.changelog-on-startup-desc")
checked: Settings.data.general.showChangelogOnStartup
onToggled: checked => Settings.data.general.showChangelogOnStartup = checked
}
// System Information Section
NDivider {
Layout.fillWidth: true
+1 -1
View File
@@ -154,7 +154,7 @@ ColumnLayout {
}
Component.onCompleted: {
updateAvailableWidgetsModel();
Qt.callLater(updateAvailableWidgetsModel);
}
Connections {
@@ -279,7 +279,7 @@ ColumnLayout {
onClicked: root.toggleTemplate(chip.modelData.id)
onEntered: {
if (chip.modelData.tooltip) {
TooltipService.show(chip, chip.modelData.tooltip, "auto");
TooltipService.show(chip, chip.modelData.tooltip, "bottom");
}
}
onExited: {
@@ -58,7 +58,7 @@ ColumnLayout {
_saveFromModel();
}
Component.onCompleted: _loadToModel()
Component.onCompleted: Qt.callLater(_loadToModel)
Connections {
target: Settings.data.idle
@@ -49,6 +49,14 @@ ColumnLayout {
onToggled: checked => Settings.data.general.enableBlurBehind = checked
}
NToggle {
label: I18n.tr("panels.user-interface.translucent-widgets-label")
description: I18n.tr("panels.user-interface.translucent-widgets-description")
checked: Settings.data.ui.translucentWidgets
defaultValue: Settings.getDefaultValue("ui.translucentWidgets")
onToggled: checked => Settings.data.ui.translucentWidgets = checked
}
NComboBox {
visible: Settings.data.general.enableShadows
label: I18n.tr("panels.user-interface.shadows-direction-label")
+1 -1
View File
@@ -11,7 +11,7 @@ Singleton {
id: root
// Version properties
readonly property string baseVersion: "4.6.6"
readonly property string baseVersion: "4.6.7"
readonly property bool isDevelopment: true
readonly property string developmentSuffix: "-git"
readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}`
+1 -4
View File
@@ -24,10 +24,7 @@ Item {
return root.color;
}
// Reuse panel opacity, but limit it to 0.4
let alpha = Math.max(Settings.data.ui.panelBackgroundOpacity, 0.4);
alpha = Math.max(0, root.color.a - (1.0 - alpha));
return Qt.alpha(root.color, alpha);
return Color.smartAlpha(root.color);
}
}
}
+1
View File
@@ -36,6 +36,7 @@ RowLayout {
Rectangle {
id: box
Layout.margins: Style.borderS
implicitWidth: Math.round(root.baseSize)
implicitHeight: Math.round(root.baseSize)
radius: Style.iRadiusXS
+1
View File
@@ -12,6 +12,7 @@ Rectangle {
signal colorSelected(color color)
Layout.margins: Style.borderS
implicitWidth: 150
implicitHeight: Math.round(Style.baseWidgetSize * 1.1)
+1
View File
@@ -95,6 +95,7 @@ Slider {
height: bgContainer.height
visible: root.trackWidth > 0 && bgContainer.height > 0
preferredRendererType: Shape.CurveRenderer
asynchronous: true
ShapePath {
id: trackPath
+1
View File
@@ -136,6 +136,7 @@ RowLayout {
ComboBox {
id: combo
Layout.margins: Style.borderS
Layout.minimumWidth: Math.round(root.minimumWidth * Style.uiScaleRatio)
Layout.preferredHeight: Math.round(root.preferredHeight * Style.uiScaleRatio)
implicitWidth: Layout.minimumWidth
+1
View File
@@ -10,6 +10,7 @@ Rectangle {
signal tokenClicked(string token)
Layout.margins: Style.borderS
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
+1 -1
View File
@@ -17,7 +17,7 @@ Item {
property bool handleWheel: false
property bool hovering: false
property color colorBg: Color.mSurfaceVariant
property color colorBg: Color.smartAlpha(Color.mSurfaceVariant)
property color colorFg: Color.mPrimary
property color colorBgHover: Color.mHover
property color colorFgHover: Color.mOnHover
+1 -1
View File
@@ -22,7 +22,7 @@ Rectangle {
property bool pressed: false
// Color properties
property color colorBg: Color.mSurfaceVariant
property color colorBg: Color.smartAlpha(Color.mSurfaceVariant)
property color colorFg: Color.mPrimary
property color colorBgHover: Color.mHover
property color colorFgHover: Color.mOnHover
+1
View File
@@ -168,6 +168,7 @@ RowLayout {
ComboBox {
id: combo
Layout.margins: Style.borderS
Layout.minimumWidth: Math.round(root.minimumWidth * Style.uiScaleRatio)
Layout.preferredHeight: Math.round(root.preferredHeight * Style.uiScaleRatio)
implicitWidth: Layout.minimumWidth
+2
View File
@@ -43,6 +43,7 @@ Slider {
anchors.fill: parent
visible: bgContainer.width > 0 && bgContainer.height > 0
preferredRendererType: Shape.CurveRenderer
asynchronous: true
ShapePath {
id: bgPath
@@ -122,6 +123,7 @@ Slider {
height: bgContainer.height
visible: bgContainer.fillWidth > 0 && bgContainer.height > 0
preferredRendererType: Shape.CurveRenderer
asynchronous: true
clip: true
ShapePath {
+1
View File
@@ -91,6 +91,7 @@ RowLayout {
// Main spinbox container
Rectangle {
id: spinBoxContainer
Layout.margins: Style.borderS
implicitWidth: 120
implicitHeight: Math.round((root.baseSize - 4) / 2) * 2
radius: Style.iRadiusS
+2 -1
View File
@@ -66,9 +66,10 @@ Rectangle {
}
// Styling
Layout.margins: Style.borderS
implicitWidth: tabRow.implicitWidth + (margins * 2)
implicitHeight: tabHeight + (margins * 2)
color: Color.mSurfaceVariant
color: Color.smartAlpha(Color.mSurfaceVariant)
radius: Style.iRadiusM
RowLayout {
+2 -2
View File
@@ -33,8 +33,8 @@ Rectangle {
topRightRadius: isLast ? Style.iRadiusM : Style.iRadiusXXXS
bottomRightRadius: isLast ? Style.iRadiusM : Style.iRadiusXXXS
color: root.isHovered ? Color.mHover : (root.checked ? Color.mPrimary : Color.mSurface)
border.color: Color.mOutline
color: root.isHovered ? Color.mHover : (root.checked ? Color.mPrimary : Color.smartAlpha(Color.mSurface))
border.color: root.checked ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
Behavior on color {
+1 -1
View File
@@ -56,7 +56,7 @@ ColumnLayout {
Layout.fillWidth: true
Layout.minimumWidth: root.minimumInputWidth
Layout.margins: Math.ceil(Style.borderS)
Layout.margins: Style.borderS
implicitHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio
// This is important - makes the control accept focus
+1
View File
@@ -45,6 +45,7 @@ RowLayout {
id: switcher
Layout.alignment: Qt.AlignVCenter
Layout.margins: Style.borderS
implicitWidth: Math.round(root.baseSize * .85) * 2
implicitHeight: Math.round(root.baseSize * .5) * 2
+1 -1
View File
@@ -107,7 +107,6 @@ ShellRoot {
Qt.callLater(function () {
LocationService.init();
NightLightService.apply();
HooksService.init();
BluetoothService.init();
IdleInhibitorService.init();
IdleService.init();
@@ -179,6 +178,7 @@ ShellRoot {
running: false
interval: 1500
onTriggered: {
HooksService.init();
FontService.init();
UpdateService.init();
showWizardOrChangelog();