Launcher: add pin to dock button if dock is enabled

This commit is contained in:
Ly-sec
2025-09-29 16:31:53 +02:00
parent b8b54825d5
commit 559609be64
9 changed files with 204 additions and 119 deletions
+4
View File
@@ -1155,6 +1155,10 @@
"close": "Schließen"
}
},
"launcher": {
"pin": "An das Dock anheften",
"unpin": "Vom Dock lösen"
},
"placeholders": {
"search-icons": "z.B. noctalia, niri, battery, cloud",
"profile-picture-path": "/home/benutzer/.face",
+4
View File
@@ -1138,6 +1138,10 @@
"close": "Close"
}
},
"launcher": {
"pin": "Pin to dock",
"unpin": "Unpin from dock"
},
"placeholders": {
"search-icons": "e.g., noctalia, niri, battery, cloud",
"profile-picture-path": "/home/user/.face",
+4
View File
@@ -1136,6 +1136,10 @@
"close": "Cerrar"
}
},
"launcher": {
"pin": "Anclar al dock",
"unpin": "Desanclar del dock"
},
"placeholders": {
"search-icons": "ej., noctalia, niri, battery, cloud",
"profile-picture-path": "/home/usuario/.face",
+4
View File
@@ -1136,6 +1136,10 @@
"close": "Fermer"
}
},
"launcher": {
"pin": "Épingler au dock",
"unpin": "Retirer du dock"
},
"placeholders": {
"search-icons": "ex: noctalia, niri, batterie, nuage",
"profile-picture-path": "/home/user/.face",
+4
View File
@@ -1136,6 +1136,10 @@
"close": "Fechar"
}
},
"launcher": {
"pin": "Fixar no dock",
"unpin": "Desafixar do dock"
},
"placeholders": {
"search-icons": "ex., noctalia, niri, battery, cloud",
"profile-picture-path": "/home/usuario/.face",
+4
View File
@@ -1136,6 +1136,10 @@
"close": "关闭"
}
},
"launcher": {
"pin": "固定到 Dock",
"unpin": "从 Dock 取消固定"
},
"placeholders": {
"search-icons": "例如:noctalia, niri, battery, cloud",
"profile-picture-path": "/home/user/.face",
+2
View File
@@ -71,6 +71,8 @@ Singleton {
"chevron-down": "chevron-down",
"caret-up": "caret-up-filled",
"caret-down": "caret-down-filled",
"star": "star",
"star-off": "star-off",
"battery-exclamation": "battery-exclamation",
"battery-charging": "battery-charging",
"battery-4": "battery-4",
+156 -117
View File
@@ -302,11 +302,29 @@ NPanel {
positionViewAtIndex(currentIndex, ListView.Contain)
}
}
onModelChanged: {}
delegate: Rectangle {
id: entry
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
// Accessor for app id
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.
@@ -321,7 +339,7 @@ NPanel {
}
width: resultsList.width - Style.marginS * scaling
height: entryHeight
implicitHeight: entryHeight
radius: Style.radiusM * scaling
color: entry.isSelected ? Color.mTertiary : Color.mSurface
@@ -332,136 +350,152 @@ NPanel {
}
}
RowLayout {
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// Icon badge or Image preview
Rectangle {
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
clip: true
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM * scaling
// 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) || ""
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5 * scaling
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true
imagePreview.visible = false
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
visible: !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 !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
// Icon badge or Image preview
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2 * scaling
width: formatLabel.width + 6 * scaling
height: formatLabel.height + 2 * scaling
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
clip: true
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM * scaling
// 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) || ""
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5 * scaling
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true
imagePreview.visible = false
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
visible: !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 !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return ""
const desc = modelData.description || ""
const parts = desc.split(" • ")
return parts[0] || "IMG"
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2 * scaling
width: formatLabel.width + 6 * scaling
height: formatLabel.height + 2 * scaling
radius: Style.radiusM * scaling
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"
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mPrimary
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mPrimary
}
}
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
elide: Text.ElideRight
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
font.pointSize: Style.fontSizeS * scaling
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
NText {
text: modelData.description || ""
font.pointSize: Style.fontSizeS * scaling
color: entry.isSelected ? Color.mOnTertiary : 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)
}
}
}
@@ -469,12 +503,17 @@ NPanel {
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index
ui.activate()
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index
ui.activate()
mouse.accepted = true
}
}
acceptedButtons: Qt.LeftButton
}
}
}
@@ -498,4 +537,4 @@ NPanel {
}
}
}
}
}
@@ -69,9 +69,14 @@ Item {
if (!query || query.trim() === "") {
// Return all 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)
@@ -79,7 +84,12 @@ Item {
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
})
} else {
sorted = entries.slice().sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()))
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())
})
}
return sorted.map(app => createResultEntry(app))
}
@@ -92,7 +102,16 @@ Item {
"limit": 20
})
return fuzzyResults.map(result => createResultEntry(result.obj))
// Sort favorites first within fuzzy results while preserving fuzzysort order otherwise
const favoriteApps = Settings.data.appLauncher.favoriteApps || []
const fav = []
const nonFav = []
for (const r of fuzzyResults) {
const app = r.obj
if (favoriteApps.includes(getAppKey(app))) fav.push(r)
else nonFav.push(r)
}
return fav.concat(nonFav).map(result => createResultEntry(result.obj))
} else {
// Fallback to simple search
const searchTerm = query.toLowerCase()
@@ -118,6 +137,7 @@ Item {
function createResultEntry(app) {
return {
"appId": getAppKey(app),
"name": app.name || "Unknown",
"description": app.genericName || app.comment || "",
"icon": app.icon || "application-x-executable",