fixed sluggish scroll glitch on launcher.qml
This commit is contained in:
@@ -34,7 +34,8 @@ PanelWindow {
|
||||
property bool loading: false
|
||||
property int lastCacheUpdate: 0
|
||||
property var favoriteIds: []
|
||||
readonly property string favoritesPath: Quickshell.env("HOME") + "/.config/quickshell/launcher/favorites.txt"
|
||||
property var accessCounts: ({})
|
||||
readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
|
||||
|
||||
|
||||
readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1
|
||||
@@ -48,15 +49,31 @@ PanelWindow {
|
||||
filtered = allItems.filter(i => i.name.toLowerCase().startsWith(q))
|
||||
}
|
||||
const favs = filtered.filter(i => root.favoriteIds.includes(i.id))
|
||||
const rest = filtered.filter(i => !root.favoriteIds.includes(i.id))
|
||||
let rest = filtered.filter(i => !root.favoriteIds.includes(i.id))
|
||||
rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0))
|
||||
const result = []
|
||||
if (favs.length > 0)
|
||||
result.push(...favs.map(i => Object.assign({}, i, { section: "Favorites" })))
|
||||
if (rest.length > 0)
|
||||
result.push(...rest.map(i => Object.assign({}, i, { section: "All Applications" })))
|
||||
if (favs.length > 0) {
|
||||
result.push({ isHeader: true, section: "Favorites" })
|
||||
for (let i = 0; i < favs.length; i++)
|
||||
result.push(Object.assign({}, favs[i], { isHeader: false }))
|
||||
}
|
||||
if (rest.length > 0) {
|
||||
result.push({ isHeader: true, section: "All Applications" })
|
||||
for (let i = 0; i < rest.length; i++)
|
||||
result.push(Object.assign({}, rest[i], { isHeader: false }))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function skipHeader(idx, dir) {
|
||||
while (idx >= 0 && idx < root.filteredItems.length) {
|
||||
const item = root.filteredItems[idx]
|
||||
if (item && !item.isHeader) return idx
|
||||
idx += dir
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!_isOpen) {
|
||||
var fm = Hyprland.focusedMonitor
|
||||
@@ -73,8 +90,9 @@ PanelWindow {
|
||||
_isOpen = true
|
||||
searchInput.text = ""
|
||||
resultsList.currentIndex = root.defaultIndex
|
||||
Qt.callLater(() => resultsList.positionViewAtBeginning())
|
||||
loadItems()
|
||||
loadFavorites()
|
||||
loadState()
|
||||
Qt.callLater(() => searchInput.forceActiveFocus())
|
||||
}
|
||||
|
||||
@@ -147,20 +165,16 @@ PanelWindow {
|
||||
return args
|
||||
}
|
||||
|
||||
function loadFavorites() {
|
||||
favoritesReadProc.command = ["sh", "-c", "cat " + shellQuote(favoritesPath) + " 2>/dev/null || true"]
|
||||
favoritesReadProc.running = true
|
||||
function loadState() {
|
||||
stateReadProc.command = ["sh", "-c", "cat " + shellQuote(statePath) + " 2>/dev/null || true"]
|
||||
stateReadProc.running = true
|
||||
}
|
||||
|
||||
function saveFavorites() {
|
||||
if (favoriteIds.length === 0) {
|
||||
favoritesWriteProc.command = ["sh", "-c", ": > " + shellQuote(favoritesPath)]
|
||||
} else {
|
||||
const quoted = favoriteIds.map(id => shellQuote(id)).join(" ")
|
||||
favoritesWriteProc.command = ["sh", "-c",
|
||||
"printf '%s\\n' " + quoted + " > " + shellQuote(favoritesPath)]
|
||||
}
|
||||
favoritesWriteProc.running = true
|
||||
function saveState() {
|
||||
const data = JSON.stringify({ favorites: favoriteIds, access: accessCounts })
|
||||
stateWriteProc.command = ["sh", "-c",
|
||||
"printf '%s' " + shellQuote(data) + " > " + shellQuote(statePath)]
|
||||
stateWriteProc.running = true
|
||||
}
|
||||
|
||||
function toggleFavorite(itemId) {
|
||||
@@ -172,7 +186,14 @@ PanelWindow {
|
||||
arr.push(itemId)
|
||||
}
|
||||
favoriteIds = arr
|
||||
saveFavorites()
|
||||
saveState()
|
||||
}
|
||||
|
||||
function incrementAccess(itemId) {
|
||||
const counts = Object.assign({}, accessCounts)
|
||||
counts[itemId] = (counts[itemId] || 0) + 1
|
||||
accessCounts = counts
|
||||
saveState()
|
||||
}
|
||||
|
||||
function launch(item) {
|
||||
@@ -188,6 +209,7 @@ PanelWindow {
|
||||
launchProc.running = true
|
||||
|
||||
console.log("Launching: " + cleanExec)
|
||||
incrementAccess(item.id)
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -238,20 +260,28 @@ PanelWindow {
|
||||
Process { id: launchProc }
|
||||
|
||||
Process {
|
||||
id: favoritesReadProc
|
||||
id: stateReadProc
|
||||
property string rawData: ""
|
||||
stdout: SplitParser { onRead: data => favoritesReadProc.rawData += data + "\n" }
|
||||
stdout: SplitParser { onRead: data => stateReadProc.rawData += data + "\n" }
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (rawData.trim()) {
|
||||
root.favoriteIds = rawData.trim().split("\n").filter(l => l.length > 0)
|
||||
try {
|
||||
const state = JSON.parse(rawData.trim())
|
||||
if (Array.isArray(state.favorites))
|
||||
root.favoriteIds = state.favorites
|
||||
if (state.access && typeof state.access === "object")
|
||||
root.accessCounts = state.access
|
||||
} catch (e) {
|
||||
console.log("Failed to parse launcher state: " + e)
|
||||
}
|
||||
}
|
||||
rawData = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: favoritesWriteProc }
|
||||
Process { id: stateWriteProc }
|
||||
|
||||
|
||||
MouseArea {
|
||||
@@ -282,7 +312,7 @@ PanelWindow {
|
||||
height: targetHeight
|
||||
Behavior on height {
|
||||
enabled: root._isOpen
|
||||
NumberAnimation { duration: 154; easing.type: Easing.OutCubic }
|
||||
NumberAnimation { duration: 80; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
@@ -328,29 +358,33 @@ PanelWindow {
|
||||
onTextChanged: resultsList.currentIndex = root.defaultIndex
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (root.filteredItems.length > 0) {
|
||||
root.launch(root.filteredItems[Math.max(0, resultsList.currentIndex)])
|
||||
}
|
||||
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
||||
if (idx >= 0)
|
||||
root.launch(root.filteredItems[idx])
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = 0;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1)
|
||||
var next = resultsList.currentIndex < 0 ? 0 : resultsList.currentIndex + 1
|
||||
if (next >= resultsList.count) next = resultsList.count - 1
|
||||
next = root.skipHeader(next, 1)
|
||||
if (next < 0) next = root.skipHeader(0, 1)
|
||||
if (next >= 0) {
|
||||
resultsList.currentIndex = next
|
||||
resultsList.positionViewAtIndex(next, ListView.Contain)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = resultsList.count - 1;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0)
|
||||
var next = resultsList.currentIndex < 0 ? resultsList.count - 1 : resultsList.currentIndex - 1
|
||||
if (next < 0) next = 0
|
||||
next = root.skipHeader(next, -1)
|
||||
if (next < 0) next = root.skipHeader(resultsList.count - 1, -1)
|
||||
if (next >= 0) {
|
||||
resultsList.currentIndex = next
|
||||
resultsList.positionViewAtIndex(next, ListView.Contain)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,49 +395,77 @@ PanelWindow {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: listHeight
|
||||
|
||||
property int listHeight: Math.min(contentHeight, 400)
|
||||
Behavior on listHeight {
|
||||
NumberAnimation { duration: 154; easing.type: Easing.OutCubic }
|
||||
// Avoid reacting to contentHeight changes from lazy delegate
|
||||
// instantiation during scroll — only animate on model count changes.
|
||||
property int listHeight: 0
|
||||
|
||||
NumberAnimation {
|
||||
id: listHeightAnim
|
||||
target: resultsList
|
||||
property: "listHeight"
|
||||
duration: 80
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
function _syncListHeight() {
|
||||
var h = Math.min(contentHeight, 400)
|
||||
if (h === listHeight) return
|
||||
listHeightAnim.stop()
|
||||
listHeightAnim.from = listHeight
|
||||
listHeightAnim.to = h
|
||||
listHeightAnim.start()
|
||||
}
|
||||
|
||||
onCountChanged: Qt.callLater(_syncListHeight)
|
||||
Component.onCompleted: Qt.callLater(_syncListHeight)
|
||||
|
||||
clip: true
|
||||
model: root.filteredItems
|
||||
currentIndex: root.defaultIndex
|
||||
spacing: 2
|
||||
snapMode: ListView.NoSnap
|
||||
highlightRangeMode: ListView.NoHighlightRange
|
||||
cacheBuffer: 400
|
||||
reuseItems: true
|
||||
pixelAligned: false
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
section.property: "section"
|
||||
section.criteria: ViewSection.FullString
|
||||
section.delegate: Rectangle {
|
||||
width: resultsList.width
|
||||
height: 28
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors { left: parent.left; leftMargin: 12; verticalCenter: parent.verticalCenter }
|
||||
text: section
|
||||
color: t.launcherDim
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Bold
|
||||
onModelChanged: {
|
||||
currentIndex = root.defaultIndex
|
||||
for (let i = 0; i < count; i++) {
|
||||
const md = root.filteredItems[i]
|
||||
if (md && !md.isHeader) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
Qt.callLater(() => positionViewAtBeginning())
|
||||
}
|
||||
|
||||
onModelChanged: currentIndex = root.defaultIndex
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100; easing.type: Easing.InQuad }
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsList.width
|
||||
height: 38
|
||||
radius: 8
|
||||
property bool hovered: false
|
||||
|
||||
color: hovered ? t.launcherHover
|
||||
radius: modelData.isHeader ? 0 : 8
|
||||
color: modelData.isHeader ? "transparent"
|
||||
: (starHovered || hovered) ? t.launcherHover
|
||||
: (resultsList.currentIndex === index && index !== -1
|
||||
? t.launcherSelected : "transparent")
|
||||
property bool hovered: false
|
||||
property bool starHovered: false
|
||||
|
||||
Text {
|
||||
visible: modelData.isHeader
|
||||
anchors { left: parent.left; leftMargin: 12; verticalCenter: parent.verticalCenter }
|
||||
text: modelData.section || ""
|
||||
color: t.launcherDim
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: !modelData.isHeader
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
@@ -417,10 +479,10 @@ PanelWindow {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : ""
|
||||
sourceSize: Qt.size(64, 64)
|
||||
sourceSize: Qt.size(64, 64)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: Cfg.Config.launcherMipmapIcons
|
||||
mipmap: Cfg.Config.launcherMipmapIcons
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
@@ -442,14 +504,22 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 22; height: 22
|
||||
width: 26; height: 26
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
id: starText
|
||||
anchors.centerIn: parent
|
||||
text: root.favoriteIds.includes(modelData.id) ? "★" : "☆"
|
||||
color: root.favoriteIds.includes(modelData.id) ? t.launcherAccent : t.launcherDim
|
||||
font.pixelSize: 14
|
||||
font.pixelSize: 18
|
||||
|
||||
readonly property bool isFav: root.favoriteIds.includes(modelData.id)
|
||||
color: starHovered ? t.launcherStarHover
|
||||
: (isFav ? t.launcherAccent : t.launcherStar)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 180; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,8 +528,12 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onExited: { parent.hovered = false; parent.starHovered = false }
|
||||
onPositionChanged: mouse => {
|
||||
parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46)
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (modelData.isHeader) return
|
||||
if (mouse.x >= width - 46) {
|
||||
root.toggleFavorite(modelData.id)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user