mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-08-15 18:43:59 +00:00
Launcher: add pin to dock button if dock is enabled
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1136,6 +1136,10 @@
|
||||
"close": "关闭"
|
||||
}
|
||||
},
|
||||
"launcher": {
|
||||
"pin": "固定到 Dock",
|
||||
"unpin": "从 Dock 取消固定"
|
||||
},
|
||||
"placeholders": {
|
||||
"search-icons": "例如:noctalia, niri, battery, cloud",
|
||||
"profile-picture-path": "/home/user/.face",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user