Launcher: add app categories

NIconTabButton: add tooltip support
This commit is contained in:
Ly-sec
2025-12-01 17:39:11 +01:00
parent 1a37da8ae8
commit 759e354956
16 changed files with 539 additions and 61 deletions
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Unbekannt"
},
"launcher": {
"categories": {
"all": "Alle",
"audiovideo": "Audio & Video",
"chat": "Chat",
"development": "Entwicklung",
"education": "Bildung",
"game": "Spiele",
"graphics": "Grafiken",
"misc": "Verschiedenes",
"network": "Netzwerk",
"office": "Büro",
"system": "System",
"webbrowser": "Webbrowser"
},
"pin": "An das Dock anheften",
"unpin": "Vom Dock lösen"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Unknown"
},
"launcher": {
"categories": {
"all": "All",
"audiovideo": "Audio & Video",
"chat": "Chat",
"development": "Development",
"education": "Education",
"game": "Games",
"graphics": "Graphics",
"misc": "Misc",
"network": "Network",
"office": "Office",
"system": "System",
"webbrowser": "Web Browser"
},
"pin": "Pin to dock",
"unpin": "Unpin from dock"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Desconocido"
},
"launcher": {
"categories": {
"all": "Todo",
"audiovideo": "Audio y video",
"chat": "Chat",
"development": "Desarrollo",
"education": "Educación",
"game": "Juegos",
"graphics": "Gráficos",
"misc": "Varios",
"network": "Red",
"office": "Oficina",
"system": "Sistema",
"webbrowser": "Navegador web"
},
"pin": "Anclar al dock",
"unpin": "Desanclar del dock"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Inconnu"
},
"launcher": {
"categories": {
"all": "Tout",
"audiovideo": "Audio et vidéo",
"chat": "Conversation",
"development": "Développement",
"education": "Éducation",
"game": "Jeux",
"graphics": "Graphiques",
"misc": "Divers",
"network": "Réseau",
"office": "Bureau",
"system": "Système",
"webbrowser": "Navigateur web"
},
"pin": "Épingler au dock",
"unpin": "Retirer du dock"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "不明"
},
"launcher": {
"categories": {
"all": "すべて",
"audiovideo": "音声 & ビデオ",
"chat": "チャット",
"development": "開発",
"education": "教育",
"game": "ゲーム",
"graphics": "グラフィックス",
"misc": "その他",
"network": "ネットワーク",
"office": "オフィス",
"system": "システム",
"webbrowser": "ウェブブラウザ"
},
"pin": "ドックにピン留め",
"unpin": "ドックからピン留めを解除"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Onbekend"
},
"launcher": {
"categories": {
"all": "Alle",
"audiovideo": "Audio & Video",
"chat": "Chat",
"development": "Ontwikkeling",
"education": "Onderwijs",
"game": "Spellen",
"graphics": "Grafische weergave",
"misc": "Diversen",
"network": "Netwerk",
"office": "Kantoor",
"system": "Systeem",
"webbrowser": "Webbrowser"
},
"pin": "Aan dock vastmaken",
"unpin": "Van dock losmaken"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Desconhecido"
},
"launcher": {
"categories": {
"all": "Tudo",
"audiovideo": "Áudio e Vídeo",
"chat": "Bate-papo",
"development": "Desenvolvimento",
"education": "Educação",
"game": "Jogos",
"graphics": "Gráficos",
"misc": "Diversos",
"network": "Rede",
"office": "Escritório",
"system": "Sistema",
"webbrowser": "Navegador web"
},
"pin": "Fixar no dock",
"unpin": "Desafixar do dock"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Неизвестно"
},
"launcher": {
"categories": {
"all": "Всё",
"audiovideo": "Аудио и видео",
"chat": "Чат",
"development": "Разработка",
"education": "Образование",
"game": "Игры",
"graphics": "Графика",
"misc": "Разное",
"network": "Сеть",
"office": "Офис",
"system": "Система",
"webbrowser": "Веб-браузер"
},
"pin": "Закрепить на панели",
"unpin": "Открепить от панели"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Bilinmiyor"
},
"launcher": {
"categories": {
"all": "Tümü",
"audiovideo": "Ses ve Video",
"chat": "Sohbet",
"development": "Gelişim",
"education": "Eğitim",
"game": "Oyunlar",
"graphics": "Grafikler",
"misc": "Çeşitli",
"network": "Ağ",
"office": "Ofis",
"system": "Sistem",
"webbrowser": "Web tarayıcı"
},
"pin": "Dock'a sabitle",
"unpin": "Dock'dan sabitlemeyi kaldır"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Невідомо"
},
"launcher": {
"categories": {
"all": "Все",
"audiovideo": "Аудіо та відео",
"chat": "Чат",
"development": "Розвиток",
"education": "Освіта",
"game": "Ігри",
"graphics": "Графіка",
"misc": "Різне",
"network": "Мережа",
"office": "Офіс",
"system": "Система",
"webbrowser": "Веб-браузер"
},
"pin": "Закріпити в доці",
"unpin": "Відкріпити з доку"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "未知"
},
"launcher": {
"categories": {
"all": "全部",
"audiovideo": "音频和视频",
"chat": "聊天",
"development": "发展",
"education": "教育",
"game": "游戏",
"graphics": "图形",
"misc": "杂项",
"network": "网络",
"office": "办公室",
"system": "系统",
"webbrowser": "网页浏览器"
},
"pin": "固定到 Dock",
"unpin": "从 Dock 取消固定"
},
+60 -7
View File
@@ -17,7 +17,7 @@ SmartPanel {
readonly property bool previewActive: !!(searchText && searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview && ClipboardService.items && ClipboardService.items.length > 0 && selectedIndex >= 0 && results && results[selectedIndex] && results[selectedIndex].clipboardId)
// Panel configuration
readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio)
readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio)
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2)
@@ -68,8 +68,10 @@ SmartPanel {
}
// Target columns, but actual columns may vary based on available width
// Account for NTabBar margins (Style.marginXS on each side) to match category tabs width
readonly property int targetGridColumns: 5
readonly property int gridCellSize: Math.floor((listPanelWidth - Style.marginS - (targetGridColumns * Style.marginXXS)) / targetGridColumns)
readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS)
readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns)
// Actual columns that fit in the GridView
// This gets updated dynamically by the GridView when its actual width is known
@@ -85,6 +87,12 @@ SmartPanel {
var currentIndex = emojiPlugin.categories.indexOf(emojiPlugin.selectedCategory);
var nextIndex = (currentIndex + 1) % emojiPlugin.categories.length;
emojiPlugin.selectCategory(emojiPlugin.categories[nextIndex]);
} else if ((activePlugin === null || activePlugin === appsPlugin) && appsPlugin.isBrowsingMode) {
// In apps browsing mode (no search), Tab navigates between categories
var availableCategories = appsPlugin.availableCategories || ["all"];
var currentIndex = availableCategories.indexOf(appsPlugin.selectedCategory);
var nextIndex = (currentIndex + 1) % availableCategories.length;
appsPlugin.selectCategory(availableCategories[nextIndex]);
} else {
selectNextWrapped();
}
@@ -653,6 +661,34 @@ SmartPanel {
}
}
// App category tabs (shown when browsing apps without search)
NTabBar {
id: appCategoryTabs
visible: (root.activePlugin === null || root.activePlugin === appsPlugin) && appsPlugin.isBrowsingMode
Layout.fillWidth: true
currentIndex: {
if (visible && appsPlugin.availableCategories) {
return appsPlugin.availableCategories.indexOf(appsPlugin.selectedCategory);
}
return 0;
}
Repeater {
model: appsPlugin.availableCategories || []
NIconTabButton {
required property string modelData
required property int index
icon: appsPlugin.categoryIcons[modelData] || "apps"
tooltipText: appsPlugin.getCategoryName ? appsPlugin.getCategoryName(modelData) : modelData
tabIndex: index
checked: appCategoryTabs.currentIndex === index
onClicked: {
appsPlugin.selectCategory(modelData);
}
}
}
}
Loader {
id: resultsViewLoader
Layout.fillWidth: true
@@ -918,14 +954,31 @@ SmartPanel {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return parent.width / 5;
}
return gridCellSize + Style.marginXXS;
// Use gridCellSize which already accounts for NTabBar margins
return root.gridCellSize + Style.marginXL;
}
cellHeight: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return (parent.width / 5) * 1.2;
}
return gridCellSize + Style.marginXXS;
return gridCellSize + Style.marginXL;
}
leftMargin: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return 0;
}
// Match NTabBar margins (Style.marginXS on each side) to align with category tabs
return Style.marginXS;
}
rightMargin: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return 0;
}
// Match NTabBar margins (Style.marginXS on each side) to align with category tabs
return Style.marginXS;
}
topMargin: 0
bottomMargin: 0
model: results
cacheBuffer: resultsGrid.height * 2
keyNavigationEnabled: false
@@ -1018,13 +1071,13 @@ SmartPanel {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return resultsGrid.width / 5;
}
return gridCellSize;
return resultsGrid.cellWidth;
}
height: {
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
return (resultsGrid.width / 5) * 1.2;
}
return gridCellSize;
return resultsGrid.cellHeight;
}
radius: Style.radiusM
color: gridEntry.isSelected ? Color.mHover : Color.mSurface
@@ -1044,7 +1097,7 @@ SmartPanel {
}
return Style.marginM;
}
spacing: Style.marginS
spacing: Style.marginM
// Icon badge or Image preview or Emoji
Rectangle {
@@ -5,11 +5,54 @@ import "../../../../Helpers/FuzzySort.js" as Fuzzysort
import qs.Commons
Item {
id: root
property var launcher: null
property string name: I18n.tr("plugins.applications")
property bool handleSearch: true
property var entries: []
// Category support
property string selectedCategory: "all"
property bool isBrowsingMode: false
property var categories: ["all", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"]
property var availableCategories: ["all"] // Reactive property for available categories
property var categoryIcons: ({
"all": "apps",
"AudioVideo": "music",
"Chat": "message-circle",
"Development": "code",
"Education": "school" // Includes Science
,
"Game": "device-gamepad",
"Graphics": "brush",
"Network": "wifi",
"Office": "file-text",
"System": "device-desktop" // Includes Settings and Utility
,
"Misc": "dots",
"WebBrowser": "world"
})
function getCategoryName(category) {
const names = {
"all": I18n.tr("launcher.categories.all"),
"AudioVideo": I18n.tr("launcher.categories.audiovideo"),
"Chat": I18n.tr("launcher.categories.chat"),
"Development": I18n.tr("launcher.categories.development"),
"Education": I18n.tr("launcher.categories.education"),
"Game": I18n.tr("launcher.categories.game"),
"Graphics": I18n.tr("launcher.categories.graphics"),
"Network": I18n.tr("launcher.categories.network"),
"Office": I18n.tr("launcher.categories.office"),
"System": I18n.tr("launcher.categories.system"),
"Misc": I18n.tr("launcher.categories.misc"),
"WebBrowser": I18n.tr("launcher.categories.webbrowser")
};
return names[category] || category;
}
// Persistent usage tracking stored in cacheDir
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
@@ -49,6 +92,199 @@ Item {
function onOpened() {
// Refresh apps when launcher opens
loadApplications();
// Reset to "all" category when opening
selectedCategory = "all";
// Set browsing mode initially (will be updated when getResults is called)
isBrowsingMode = true;
}
function selectCategory(category) {
selectedCategory = category;
if (launcher) {
launcher.updateResults();
}
}
function getAppCategories(app) {
if (!app)
return [];
const result = [];
if (app.categories) {
if (Array.isArray(app.categories)) {
for (let cat of app.categories) {
if (cat && cat.trim && cat.trim() !== '') {
result.push(cat.trim());
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
result.push(cat.trim());
}
}
} else if (typeof app.categories === 'string') {
const cats = app.categories.split(';').filter(c => c && c.trim() !== '');
for (let cat of cats) {
const trimmed = cat.trim();
if (trimmed && !result.includes(trimmed)) {
result.push(trimmed);
}
}
} else if (app.categories.length !== undefined) {
try {
for (let i = 0; i < app.categories.length; i++) {
const cat = app.categories[i];
if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') {
result.push(cat.trim());
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
result.push(cat.trim());
}
}
} catch (e) {}
}
}
if (app.Categories) {
const cats = app.Categories.split(';').filter(c => c && c.trim() !== '');
for (let cat of cats) {
const trimmed = cat.trim();
if (trimmed && !result.includes(trimmed)) {
result.push(trimmed);
}
}
}
return result;
}
function getAppCategory(app) {
const appCategories = getAppCategories(app);
if (appCategories.length === 0)
return null;
const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"];
for (let cat of appCategories) {
if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") {
return "AudioVideo";
}
}
if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) {
return "Chat";
}
if (appCategories.includes("WebBrowser")) {
return "WebBrowser";
}
// Map Science to Education
if (appCategories.includes("Science")) {
return "Education";
}
// Map Settings to System
if (appCategories.includes("Settings")) {
return "System";
}
// Map Utility to System
if (appCategories.includes("Utility")) {
return "System";
}
for (let priorityCat of priorityCategories) {
if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) {
return priorityCat;
}
}
return "Misc";
}
function appMatchesCategory(app, category) {
// Check if app matches the selected category
if (category === "all")
return true;
// Get the primary category for this app (first matching standard category)
const primaryCategory = getAppCategory(app);
// If app has no matching standard category, don't show it in any category (only in "all")
if (!primaryCategory)
return false;
// Map Audio/Video to AudioVideo
if (category === "AudioVideo") {
const appCategories = getAppCategories(app);
// Show if app has AudioVideo, Audio, or Video
return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video");
}
// Map Science to Education
if (category === "Education") {
const appCategories = getAppCategories(app);
return appCategories.includes("Education") || appCategories.includes("Science");
}
// Map Settings and Utility to System
if (category === "System") {
const appCategories = getAppCategories(app);
return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility");
}
// Only show app in its primary category to avoid overlap
// This ensures each app appears in exactly one category tab
return category === primaryCategory;
}
function getAvailableCategories() {
const categorySet = new Set();
let hasAudioVideo = false;
let hasEducation = false;
let hasSystem = false;
for (let app of entries) {
const appCategories = getAppCategories(app);
const primaryCategory = getAppCategory(app);
if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) {
hasAudioVideo = true;
} else if (appCategories.includes("Education") || appCategories.includes("Science")) {
hasEducation = true;
} else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) {
hasSystem = true;
} else if (primaryCategory && root.categories.includes(primaryCategory)) {
categorySet.add(primaryCategory);
}
}
if (hasAudioVideo) {
categorySet.add("AudioVideo");
}
if (hasEducation) {
categorySet.add("Education");
}
if (hasSystem) {
categorySet.add("System");
}
const result = ["all"];
for (let cat of root.categories) {
if (cat !== "all" && cat !== "Misc" && categorySet.has(cat)) {
result.push(cat);
}
}
if (categorySet.has("Misc")) {
result.push("Misc");
}
if (result.length === 1) {
const fallback = root.categories.filter(c => c !== "Misc");
fallback.push("Misc");
return fallback;
}
return result;
}
function loadApplications() {
@@ -64,6 +300,12 @@ Item {
return app;
});
Logger.d("ApplicationsPlugin", `Loaded ${entries.length} applications`);
// Update available categories when apps are loaded
updateAvailableCategories();
}
function updateAvailableCategories() {
availableCategories = getAvailableCategories();
}
function getExecutableName(app) {
@@ -99,38 +341,47 @@ Item {
if (!entries || entries.length === 0)
return [];
// Set browsing mode based on whether there's a query
isBrowsingMode = !query || query.trim() === "";
// Filter by category first
let filteredEntries = entries;
if (selectedCategory && selectedCategory !== "all") {
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
}
if (!query || query.trim() === "") {
// Return all apps, optionally sorted by usage
// Return filtered apps, optionally sorted by usage
const favoriteApps = Settings.data.appLauncher.favoriteApps || [];
let sorted;
if (Settings.data.appLauncher.sortByMostUsed) {
sorted = entries.slice().sort((a, b) => {
// Favorites first
const aFav = favoriteApps.includes(getAppKey(a));
const bFav = favoriteApps.includes(getAppKey(b));
if (aFav !== bFav)
return aFav ? -1 : 1;
const ua = getUsageCount(a);
const ub = getUsageCount(b);
if (ub !== ua)
return ub - ua;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
sorted = filteredEntries.slice().sort((a, b) => {
// Favorites first
const aFav = favoriteApps.includes(getAppKey(a));
const bFav = favoriteApps.includes(getAppKey(b));
if (aFav !== bFav)
return aFav ? -1 : 1;
const ua = getUsageCount(a);
const ub = getUsageCount(b);
if (ub !== ua)
return ub - ua;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
} else {
sorted = entries.slice().sort((a, b) => {
const aFav = favoriteApps.includes(getAppKey(a));
const bFav = favoriteApps.includes(getAppKey(b));
if (aFav !== bFav)
return aFav ? -1 : 1;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
sorted = filteredEntries.slice().sort((a, b) => {
const aFav = favoriteApps.includes(getAppKey(a));
const bFav = favoriteApps.includes(getAppKey(b));
if (aFav !== bFav)
return aFav ? -1 : 1;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
}
return sorted.map(app => createResultEntry(app));
}
// Use fuzzy search if available, fallback to simple search
if (typeof Fuzzysort !== 'undefined') {
const fuzzyResults = Fuzzysort.go(query, entries, {
const fuzzyResults = Fuzzysort.go(query, filteredEntries, {
"keys": ["name", "comment", "genericName", "executableName"],
"threshold": -1000,
"limit": 20
@@ -151,37 +402,37 @@ Item {
} else {
// Fallback to simple search
const searchTerm = query.toLowerCase();
return entries.filter(app => {
const name = (app.name || "").toLowerCase();
const comment = (app.comment || "").toLowerCase();
const generic = (app.genericName || "").toLowerCase();
const executable = getExecutableName(app).toLowerCase();
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
}).sort((a, b) => {
// Prioritize name matches, then executable matches
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
const aExecutable = getExecutableName(a).toLowerCase();
const bExecutable = getExecutableName(b).toLowerCase();
const aStarts = aName.startsWith(searchTerm);
const bStarts = bName.startsWith(searchTerm);
const aExecStarts = aExecutable.startsWith(searchTerm);
const bExecStarts = bExecutable.startsWith(searchTerm);
return filteredEntries.filter(app => {
const name = (app.name || "").toLowerCase();
const comment = (app.comment || "").toLowerCase();
const generic = (app.genericName || "").toLowerCase();
const executable = getExecutableName(app).toLowerCase();
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
}).sort((a, b) => {
// Prioritize name matches, then executable matches
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
const aExecutable = getExecutableName(a).toLowerCase();
const bExecutable = getExecutableName(b).toLowerCase();
const aStarts = aName.startsWith(searchTerm);
const bStarts = bName.startsWith(searchTerm);
const aExecStarts = aExecutable.startsWith(searchTerm);
const bExecStarts = bExecutable.startsWith(searchTerm);
// Prioritize name matches first
if (aStarts && !bStarts)
return -1;
if (!aStarts && bStarts)
return 1;
// Prioritize name matches first
if (aStarts && !bStarts)
return -1;
if (!aStarts && bStarts)
return 1;
// Then prioritize executable matches
if (aExecStarts && !bExecStarts)
return -1;
if (!aExecStarts && bExecStarts)
return 1;
// Then prioritize executable matches
if (aExecStarts && !bExecStarts)
return -1;
if (!aExecStarts && bExecStarts)
return 1;
return aName.localeCompare(bName);
}).slice(0, 20).map(app => createResultEntry(app));
return aName.localeCompare(bName);
}).slice(0, 20).map(app => createResultEntry(app));
}
}
@@ -897,9 +897,9 @@ ColumnLayout {
NCheckbox {
label: "Emacs"
description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", {
"app": "emacs"
})
description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia t)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", {
"app": "emacs"
})
checked: Settings.data.templates.emacs
enabled: ProgramCheckerService.emacsAvailable
opacity: ProgramCheckerService.emacsAvailable ? 1.0 : 0.6
+4
View File
@@ -36,6 +36,10 @@ Item {
property alias delegate: gridView.delegate
property alias cellWidth: gridView.cellWidth
property alias cellHeight: gridView.cellHeight
property alias leftMargin: gridView.leftMargin
property alias rightMargin: gridView.rightMargin
property alias topMargin: gridView.topMargin
property alias bottomMargin: gridView.bottomMargin
property alias currentIndex: gridView.currentIndex
property alias count: gridView.count
property alias contentHeight: gridView.contentHeight
+18 -2
View File
@@ -1,7 +1,9 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
Rectangle {
@@ -9,6 +11,7 @@ Rectangle {
// Public properties
property string icon: ""
property string tooltipText: ""
property bool checked: false
property int tabIndex: 0
@@ -51,9 +54,22 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
onEntered: {
root.isHovered = true;
if (root.tooltipText) {
TooltipService.show(parent, root.tooltipText);
}
}
onExited: {
root.isHovered = false;
if (root.tooltipText) {
TooltipService.hide();
}
}
onClicked: {
if (root.tooltipText) {
TooltipService.hide();
}
root.clicked();
// Update parent NTabBar's currentIndex
if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) {