import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Widgets import "Plugins" import "../../../Helpers/FuzzySort.js" as Fuzzysort import qs.Commons import qs.Modules.MainScreen import qs.Services.Keyboard import qs.Widgets SmartPanel { id: root readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview && ClipboardService.items.length > 0 && selectedIndex >= 0 && results[selectedIndex] && results[selectedIndex].clipboardId // Panel configuration readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) preferredWidth: totalBaseWidth preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 // Positioning readonly property string panelPosition: { if (Settings.data.appLauncher.position === "follow_bar") { if (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") { return `center_${Settings.data.bar.position}`; } else { return `${Settings.data.bar.position}_center`; } } else { return Settings.data.appLauncher.position; } } panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") panelAnchorVerticalCenter: panelPosition === "center" panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") panelAnchorBottom: panelPosition.startsWith("bottom_") panelAnchorTop: panelPosition.startsWith("top_") // Core state property string searchText: "" property int selectedIndex: 0 property var results: [] property var plugins: [] property var activePlugin: null property bool resultsReady: false property bool ignoreMouseHover: false readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6) readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) readonly property bool isGridView: { // Always use list view for clipboard and calculator to better display content if (searchText.startsWith(">clip") || searchText.startsWith(">calc")) { return false; } return Settings.data.appLauncher.viewMode === "grid"; } // Target columns, but actual columns may vary based on available width readonly property int targetGridColumns: 5 readonly property int gridCellSize: Math.floor((listPanelWidth - Style.marginS - (targetGridColumns * Style.marginXXS)) / targetGridColumns) // Actual columns that fit in the GridView // This gets updated dynamically by the GridView when its actual width is known property int gridColumns: 5 // Override keyboard handlers from SmartPanel for navigation. // Launcher specific: onTabPressed() and onBackTabPressed() are special here. // They are not coming from SmartPanelWindow as they are consumed by the search field before reaching the panel. // They are instead being forwared from the search field NTextInput below. function onTabPressed() { selectNextWrapped(); } function onBackTabPressed() { selectPreviousWrapped(); } function onUpPressed() { if (isGridView) { // Force update to prevent GridView interference Qt.callLater(() => { selectPreviousRow(); }); } else { selectPreviousWrapped(); } } function onDownPressed() { if (isGridView) { // Force update to prevent GridView interference Qt.callLater(() => { selectNextRow(); }); } else { selectNextWrapped(); } } function onLeftPressed() { if (isGridView) { selectPreviousColumn(); } else { // In list view, left = previous item selectPreviousWrapped(); } } function onRightPressed() { if (isGridView) { selectNextColumn(); } else { // In list view, right = next item selectNextWrapped(); } } function onReturnPressed() { activate(); } function onHomePressed() { selectFirst(); } function onEndPressed() { selectLast(); } function onPageUpPressed() { selectPreviousPage(); } function onPageDownPressed() { selectNextPage(); } function onCtrlJPressed() { selectNextWrapped(); } function onCtrlKPressed() { selectPreviousWrapped(); } function onCtrlNPressed() { selectNextWrapped(); } function onCtrlPPressed() { selectPreviousWrapped(); } // Public API for plugins function setSearchText(text) { searchText = text; } // Plugin registration function registerPlugin(plugin) { plugins.push(plugin); plugin.launcher = root; if (plugin.init) plugin.init(); } // Search handling function updateResults() { results = []; activePlugin = null; // Check for command mode if (searchText.startsWith(">")) { // Find plugin that handles this command for (let plugin of plugins) { if (plugin.handleCommand && plugin.handleCommand(searchText)) { activePlugin = plugin; results = plugin.getResults(searchText); break; } } // Show available commands if just ">" or filter commands if partial match if (!activePlugin) { // Collect all commands from all plugins let allCommands = []; for (let plugin of plugins) { if (plugin.commands) { allCommands = allCommands.concat(plugin.commands()); } } if (searchText === ">") { // Show all commands when just ">" results = allCommands; } else if (searchText.length > 1) { // Filter commands using fuzzy search when typing partial command const query = searchText.substring(1); // Remove the ">" prefix if (typeof Fuzzysort !== 'undefined') { // Use fuzzy search to filter commands const fuzzyResults = Fuzzysort.go(query, allCommands, { "keys": ["name"], "threshold": -1000, "limit": 50 }); // Convert fuzzy results back to command objects results = fuzzyResults.map(result => result.obj); } else { // Fallback to simple substring matching const queryLower = query.toLowerCase(); results = allCommands.filter(cmd => { const cmdName = (cmd.name || "").toLowerCase(); return cmdName.includes(queryLower); }); } } } } else { // Regular search - let plugins contribute results for (let plugin of plugins) { if (plugin.handleSearch) { const pluginResults = plugin.getResults(searchText); results = results.concat(pluginResults); } } } selectedIndex = 0; } onSearchTextChanged: updateResults() // Lifecycle onOpened: { resultsReady = false; ignoreMouseHover = true; // Notify plugins and update results // Use Qt.callLater to ensure plugins are registered (Component.onCompleted runs first) Qt.callLater(() => { for (let plugin of plugins) { if (plugin.onOpened) plugin.onOpened(); } updateResults(); resultsReady = true; }); } onClosed: { // Reset search text searchText = ""; ignoreMouseHover = true; // Notify plugins for (let plugin of plugins) { if (plugin.onClosed) plugin.onClosed(); } } // Plugin components - declared inline so imports work correctly ApplicationsPlugin { id: appsPlugin Component.onCompleted: { registerPlugin(this); Logger.d("Launcher", "Registered: ApplicationsPlugin"); } } CalculatorPlugin { id: calcPlugin Component.onCompleted: { registerPlugin(this); Logger.d("Launcher", "Registered: CalculatorPlugin"); } } ClipboardPlugin { id: clipPlugin Component.onCompleted: { if (Settings.data.appLauncher.enableClipboardHistory) { registerPlugin(this); Logger.d("Launcher", "Registered: ClipboardPlugin"); } } } EmojiPlugin { id: emojiPlugin Component.onCompleted: { registerPlugin(this); Logger.d("Launcher", "Registered: EmojiPlugin"); } } // Navigation functions function selectNextWrapped() { if (results.length > 0) { selectedIndex = (selectedIndex + 1) % results.length; } } function selectPreviousWrapped() { if (results.length > 0) { selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length; } } function selectFirst() { selectedIndex = 0; } function selectLast() { if (results.length > 0) { selectedIndex = results.length - 1; } else { selectedIndex = 0; } } function selectNextPage() { if (results.length > 0) { const page = Math.max(1, Math.floor(600 / entryHeight)); // Use approximate height selectedIndex = Math.min(selectedIndex + page, results.length - 1); } } function selectPreviousPage() { if (results.length > 0) { const page = Math.max(1, Math.floor(600 / entryHeight)); // Use approximate height selectedIndex = Math.max(selectedIndex - page, 0); } } // Grid view navigation functions function selectPreviousRow() { if (results.length > 0 && isGridView) { const currentRow = Math.floor(selectedIndex / gridColumns); const currentCol = selectedIndex % gridColumns; if (currentRow > 0) { // Move to previous row, same column const targetRow = currentRow - 1; const targetIndex = targetRow * gridColumns + currentCol; // Check if target column exists in target row const itemsInTargetRow = Math.min(gridColumns, results.length - targetRow * gridColumns); if (currentCol < itemsInTargetRow) { selectedIndex = targetIndex; } else { // Target column doesn't exist, go to last item in target row 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; } } } } function selectNextRow() { if (results.length > 0 && isGridView) { const currentRow = Math.floor(selectedIndex / gridColumns); const currentCol = selectedIndex % gridColumns; const totalRows = Math.ceil(results.length / gridColumns); if (currentRow < totalRows - 1) { // Move to next row, same column const targetRow = currentRow + 1; const targetIndex = targetRow * gridColumns + currentCol; // Check if target index is valid if (targetIndex < results.length) { selectedIndex = targetIndex; } else { // Target column doesn't exist in target row, go to last item in target row const itemsInTargetRow = results.length - targetRow * gridColumns; if (itemsInTargetRow > 0) { selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1; } else { // Target row is empty, wrap to first row selectedIndex = Math.min(currentCol, results.length - 1); } } } else { // Wrap to first row, same column selectedIndex = Math.min(currentCol, results.length - 1); } } } function selectPreviousColumn() { if (results.length > 0 && isGridView) { const currentRow = Math.floor(selectedIndex / gridColumns); const currentCol = selectedIndex % gridColumns; if (currentCol > 0) { // Move left in same row selectedIndex = currentRow * gridColumns + (currentCol - 1); } else { // Wrap to last column of previous row if (currentRow > 0) { selectedIndex = (currentRow - 1) * gridColumns + (gridColumns - 1); } else { // Wrap to last column of last row const totalRows = Math.ceil(results.length / gridColumns); const lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1); selectedIndex = Math.min(lastRowIndex, results.length - 1); } } } } 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) { // Move right in same row selectedIndex = currentRow * gridColumns + (currentCol + 1); } else { // Wrap to first column of next row const totalRows = Math.ceil(results.length / gridColumns); if (currentRow < totalRows - 1) { selectedIndex = (currentRow + 1) * gridColumns; } else { // Wrap to first item selectedIndex = 0; } } } } function activate() { if (results.length > 0 && results[selectedIndex]) { const item = results[selectedIndex]; if (item.onActivate) { item.onActivate(); } } } panelContent: Rectangle { id: ui color: Color.transparent opacity: resultsReady ? 1.0 : 0.0 // Preview Panel (external) NBox { id: previewBox visible: root.previewActive width: root.previewPanelWidth height: Math.round(400 * Style.uiScaleRatio) x: ui.width + Style.marginM y: { if (!resultsViewLoader.item) return Style.marginL; const view = resultsViewLoader.item; const row = root.isGridView ? Math.floor(root.selectedIndex / root.gridColumns) : root.selectedIndex; const itemHeight = root.isGridView ? (root.gridCellSize + Style.marginXXS) : (root.entryHeight + view.spacing); const yPos = row * itemHeight - view.contentY; const mapped = view.mapToItem(ui, 0, yPos); return Math.max(Style.marginL, Math.min(mapped.y, ui.height - previewBox.height - Style.marginL)); } z: -1 // Draw behind main panel content if it ever overlaps opacity: visible ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: Style.animationFast } } Loader { id: clipboardPreviewLoader anchors.fill: parent active: root.previewActive source: active ? "./ClipboardPreview.qml" : "" onLoaded: { if (selectedIndex >= 0 && results[selectedIndex] && item) { item.currentItem = results[selectedIndex]; } } onItemChanged: { if (item && selectedIndex >= 0 && results[selectedIndex]) { item.currentItem = results[selectedIndex]; } } } } MouseArea { id: mouseMovementDetector anchors.fill: parent z: -999 hoverEnabled: true propagateComposedEvents: true acceptedButtons: Qt.NoButton property real lastX: 0 property real lastY: 0 property bool initialized: false onPositionChanged: mouse => { if (!initialized) { lastX = mouse.x; lastY = mouse.y; initialized = true; return; } const deltaX = Math.abs(mouse.x - lastX); const deltaY = Math.abs(mouse.y - lastY); if (deltaX > 1 || deltaY > 1) { root.ignoreMouseHover = false; lastX = mouse.x; lastY = mouse.y; } } Connections { target: root function onOpened() { mouseMovementDetector.initialized = false; } } } // Focus management Connections { target: root function onOpened() { // Delay focus to ensure window has keyboard focus Qt.callLater(() => { if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus(); } }); } } Behavior on opacity { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutCirc } } RowLayout { anchors.fill: parent anchors.margins: Style.marginL // Apply overall margins here spacing: Style.marginM // Apply spacing between elements here // Left Pane ColumnLayout { id: leftPane Layout.fillHeight: true Layout.preferredWidth: root.listPanelWidth spacing: Style.marginM NTextInput { id: searchInput Layout.fillWidth: true fontSize: Style.fontSizeL fontWeight: Style.fontWeightSemiBold text: searchText placeholderText: I18n.tr("placeholders.search-launcher") onTextChanged: searchText = text Component.onCompleted: { if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus(); // Intercept keys before TextField handles them searchInput.inputItem.Keys.onPressed.connect(function (event) { if (event.key === Qt.Key_Tab) { root.onTabPressed(); event.accepted = true; } else if (event.key === Qt.Key_Backtab) { root.onBackTabPressed(); event.accepted = true; } else if (event.key === Qt.Key_Left && root.isGridView) { // In grid view, left arrow navigates the grid root.onLeftPressed(); event.accepted = true; } else if (event.key === Qt.Key_Right && root.isGridView) { // In grid view, right arrow navigates the grid root.onRightPressed(); event.accepted = true; } }); } } } Loader { id: resultsViewLoader Layout.fillWidth: true Layout.fillHeight: true sourceComponent: root.isGridView ? gridViewComponent : listViewComponent } Component { id: listViewComponent NListView { id: resultsList horizontalPolicy: ScrollBar.AlwaysOff verticalPolicy: ScrollBar.AsNeeded width: parent.width height: parent.height spacing: Style.marginXXS model: results currentIndex: selectedIndex cacheBuffer: resultsList.height * 2 onCurrentIndexChanged: { cancelFlick(); if (currentIndex >= 0) { positionViewAtIndex(currentIndex, ListView.Contain); } if (clipboardPreviewLoader.item) { clipboardPreviewLoader.item.currentItem = results[currentIndex] || null; } } onModelChanged: {} delegate: Rectangle { id: entry property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" // Pin helpers function togglePin(appId) { if (!appId) return; let arr = (Settings.data.dock.pinnedApps || []).slice(); const idx = arr.indexOf(appId); if (idx >= 0) arr.splice(idx, 1); else arr.push(appId); Settings.data.dock.pinnedApps = arr; } function isPinned(appId) { const arr = Settings.data.dock.pinnedApps || []; return appId && arr.indexOf(appId) >= 0; } // Property to reliably track the current item's ID. // This changes whenever the delegate is recycled for a new item. property var currentClipboardId: modelData.isImage ? modelData.clipboardId : "" // When this delegate is assigned a new image item, trigger the decode. onCurrentClipboardIdChanged: { // Check if it's a valid ID and if the data isn't already cached. if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) { ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null); } } width: resultsList.width - Style.marginS implicitHeight: entryHeight radius: Style.radiusM color: entry.isSelected ? Color.mHover : Color.mSurface Behavior on color { ColorAnimation { duration: Style.animationFast easing.type: Easing.OutCirc } } ColumnLayout { id: contentLayout anchors.fill: parent anchors.margins: Style.marginM spacing: Style.marginM // Top row - Main entry content with pin button RowLayout { Layout.fillWidth: true spacing: Style.marginM // Icon badge or Image preview or Emoji Rectangle { Layout.preferredWidth: badgeSize Layout.preferredHeight: badgeSize radius: Style.radiusM color: Color.mSurfaceVariant // Image preview for clipboard images NImageRounded { id: imagePreview anchors.fill: parent visible: modelData.isImage && !modelData.emojiChar radius: Style.radiusM // This property creates a dependency on the service's revision counter readonly property int _rev: ClipboardService.revision // Fetches from the service's cache. // The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated. imagePath: { _rev; return ClipboardService.getImageData(modelData.clipboardId) || ""; } 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.emojiChar || (modelData.isImage && imagePreview.status === Image.Error) active: visible sourceComponent: Component { IconImage { anchors.fill: parent source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" visible: modelData.icon && source !== "" && !modelData.emojiChar asynchronous: true } } } // Emoji display - takes precedence when emojiChar is present NText { id: emojiDisplay anchors.centerIn: parent visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible) text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis font.weight: Style.fontWeightBold color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis } // Image type indicator overlay Rectangle { visible: modelData.isImage && imagePreview.visible anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: 2 width: formatLabel.width + 6 height: formatLabel.height + 2 radius: Style.radiusM color: Color.mSurfaceVariant NText { id: formatLabel anchors.centerIn: parent text: { if (!modelData.isImage) return ""; const desc = modelData.description || ""; const parts = desc.split(" • "); return parts[0] || "IMG"; } pointSize: Style.fontSizeXXS color: Color.mPrimary } } } // 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 Layout.fillWidth: true } NText { text: modelData.description || "" pointSize: Style.fontSizeS color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" } } // Pin/Unpin action icon button NIconButton { visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0) Layout.alignment: Qt.AlignRight | Qt.AlignVCenter icon: entry.isPinned(entry.appId) ? "unpin" : "pin" tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin") onClicked: entry.togglePin(entry.appId) } } } MouseArea { id: mouseArea anchors.fill: parent z: -1 hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: { if (!root.ignoreMouseHover) { selectedIndex = index; } } onClicked: mouse => { if (mouse.button === Qt.LeftButton) { selectedIndex = index; root.activate(); mouse.accepted = true; } } acceptedButtons: Qt.LeftButton } } } } Component { id: gridViewComponent NGridView { id: resultsGrid horizontalPolicy: ScrollBar.AlwaysOff verticalPolicy: ScrollBar.AsNeeded width: parent.width height: parent.height cellWidth: gridCellSize + Style.marginXXS cellHeight: gridCellSize + Style.marginXXS model: results cacheBuffer: resultsGrid.height * 2 keyNavigationEnabled: false focus: false interactive: true onWidthChanged: { // Update gridColumns based on actual GridView width // This ensures navigation works correctly regardless of panel size const actualCols = Math.floor(width / cellWidth); if (actualCols > 0 && actualCols !== root.gridColumns) { root.gridColumns = actualCols; } } // Completely disable GridView key handling Keys.enabled: false // Don't sync selectedIndex to GridView's currentIndex // The visual selection is handled by the delegate based on selectedIndex // We only need to position the view to show the selected item onModelChanged: {} // Handle scrolling to show selected item when it changes Connections { target: root enabled: root.isGridView function onSelectedIndexChanged() { // Only process if we're still in grid view and component exists if (!root.isGridView || root.selectedIndex < 0 || !resultsGrid) { return; } Qt.callLater(() => { // Double-check we're still in grid view mode if (root.isGridView && resultsGrid && resultsGrid.cancelFlick) { resultsGrid.cancelFlick(); resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain); } }); // Update preview if (clipboardPreviewLoader.item && root.selectedIndex >= 0) { clipboardPreviewLoader.item.currentItem = results[root.selectedIndex] || null; } } } delegate: Rectangle { id: gridEntry property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" width: gridCellSize height: gridCellSize radius: Style.radiusM color: gridEntry.isSelected ? Color.mHover : Color.mSurface Behavior on color { ColorAnimation { duration: Style.animationFast easing.type: Easing.OutCirc } } ColumnLayout { anchors.fill: parent anchors.margins: Style.marginM spacing: Style.marginS // Icon badge or Image preview or Emoji Rectangle { Layout.preferredWidth: badgeSize * 1.5 Layout.preferredHeight: badgeSize * 1.5 Layout.alignment: Qt.AlignHCenter radius: Style.radiusM color: Color.mSurfaceVariant // Image preview for clipboard images NImageRounded { id: gridImagePreview anchors.fill: parent visible: modelData.isImage && !modelData.emojiChar radius: Style.radiusM readonly property int _rev: ClipboardService.revision imagePath: { _rev; return ClipboardService.getImageData(modelData.clipboardId) || ""; } 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.emojiChar || (modelData.isImage && gridImagePreview.status === Image.Error) active: visible sourceComponent: Component { IconImage { anchors.fill: parent source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" visible: modelData.icon && source !== "" && !modelData.emojiChar asynchronous: true } } } // Emoji display NText { id: gridEmojiDisplay anchors.centerIn: parent visible: modelData.emojiChar ? true : (!gridImagePreview.visible && !gridIconLoader.visible) text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") pointSize: modelData.emojiChar ? Style.fontSizeXXL : Style.fontSizeXL font.weight: Style.fontWeightBold color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary } } // Text content NText { text: modelData.name || "Unknown" pointSize: Style.fontSizeS font.weight: Style.fontWeightSemiBold color: gridEntry.isSelected ? Color.mOnHover : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true Layout.maximumWidth: gridCellSize - Style.marginM * 2 horizontalAlignment: Text.AlignHCenter } } MouseArea { id: mouseArea anchors.fill: parent z: -1 hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: { if (!root.ignoreMouseHover) { selectedIndex = index; } } onClicked: mouse => { if (mouse.button === Qt.LeftButton) { selectedIndex = index; root.activate(); mouse.accepted = true; } } acceptedButtons: Qt.LeftButton } } } } NDivider { Layout.fillWidth: true } NText { Layout.fillWidth: true text: { if (results.length === 0) return searchText ? "No results" : ""; const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""; return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`; } pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant horizontalAlignment: Text.AlignCenter } } } } }