mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(launcher): splitted in smaller files for easier maintainability. improved record usage.
This commit is contained in:
+6
-11
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 0–1, 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() {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.mSurface
|
||||
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.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: 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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user