cleaned and optimized launcher + added actionsview on listview items on launcher. TAB to open it or right click. Added typing animation for Crypto.qml and also anchored the icon on the left so it doesnt keep moving around when changing sizes.
This commit is contained in:
@@ -16,7 +16,9 @@ ModuleChip {
|
||||
id: configWriter
|
||||
}
|
||||
|
||||
width: chipRow.implicitWidth + 24
|
||||
width: cryptoIcon.visible
|
||||
? (12 + cryptoIcon.implicitWidth + 7 + chipRow.implicitWidth + 12)
|
||||
: (chipRow.implicitWidth + 24)
|
||||
radius: panelRadius
|
||||
|
||||
chipColor: t.cryptoBg
|
||||
@@ -152,6 +154,7 @@ ModuleChip {
|
||||
if (root.localPairs.length <= 1) return
|
||||
root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length
|
||||
root._applyFromCache()
|
||||
root._restartTyping()
|
||||
rotateTimer.restart()
|
||||
}
|
||||
|
||||
@@ -159,6 +162,7 @@ ModuleChip {
|
||||
if (root.localPairs.length <= 1) return
|
||||
root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length
|
||||
root._applyFromCache()
|
||||
root._restartTyping()
|
||||
rotateTimer.restart()
|
||||
}
|
||||
|
||||
@@ -184,6 +188,51 @@ ModuleChip {
|
||||
sparklineData = entry.sparkline || []
|
||||
change7d = entry.change7d || 0
|
||||
hasError = false
|
||||
root._restartTyping()
|
||||
}
|
||||
|
||||
property bool _typing: false
|
||||
property int _typedLabel: 0
|
||||
property int _typedValue: 0
|
||||
property int _typedIcon: 0
|
||||
|
||||
function _restartTyping() {
|
||||
typeAnim.stop()
|
||||
_typing = true
|
||||
_typedLabel = 0
|
||||
_typedValue = 0
|
||||
_typedIcon = 0
|
||||
typeAnimLabel.to = (root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": ").length
|
||||
typeAnimValue.to = (root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)).length
|
||||
typeAnimIcon.to = (root.price >= 0 ? 1 : 0)
|
||||
typeAnim.start()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: typeAnim
|
||||
onStopped: { root._typing = false }
|
||||
|
||||
NumberAnimation {
|
||||
id: typeAnimLabel
|
||||
target: root
|
||||
property: "_typedLabel"
|
||||
duration: 200
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
NumberAnimation {
|
||||
id: typeAnimValue
|
||||
target: root
|
||||
property: "_typedValue"
|
||||
duration: 200
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
NumberAnimation {
|
||||
id: typeAnimIcon
|
||||
target: root
|
||||
property: "_typedIcon"
|
||||
duration: 50
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
function fetchPrice() {
|
||||
@@ -291,9 +340,26 @@ ModuleChip {
|
||||
onLocalRefreshSecChanged: refreshTimer.restart()
|
||||
onLocalRotateSecChanged: rotateTimer.restart()
|
||||
|
||||
Text {
|
||||
id: cryptoIcon
|
||||
visible: root.localShowIcon
|
||||
text: ""
|
||||
color: t.cryptoIcon
|
||||
font.pixelSize: 14
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 12
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: chipRow
|
||||
anchors.centerIn: parent
|
||||
anchors {
|
||||
left: cryptoIcon.visible ? cryptoIcon.right : parent.left
|
||||
leftMargin: cryptoIcon.visible ? 7 : 12
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
@@ -310,34 +376,61 @@ ModuleChip {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.localShowIcon
|
||||
text: ""
|
||||
color: t.cryptoIcon
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 7
|
||||
Item {
|
||||
implicitWidth: labelSizer.implicitWidth
|
||||
implicitHeight: labelSizer.implicitHeight
|
||||
|
||||
Text {
|
||||
id: labelSizer
|
||||
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
||||
color: "transparent"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root._typing
|
||||
? (root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": ").substring(0, root._typedLabel)
|
||||
: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
||||
color: t.cryptoLabel
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.fill: labelSizer
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: valueSizer.implicitWidth
|
||||
implicitHeight: valueSizer.implicitHeight
|
||||
|
||||
Text {
|
||||
id: valueSizer
|
||||
text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
||||
color: "transparent"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root._typing
|
||||
? (root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)).substring(0, root._typedValue)
|
||||
: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
||||
color: t.cryptoValue
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.fill: valueSizer
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
||||
color: t.cryptoLabel
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
||||
color: t.cryptoValue
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.price >= 0
|
||||
text: root.change24h >= 0 ? "▲" : "▼"
|
||||
visible: root._typing || root.price >= 0
|
||||
text: root._typing
|
||||
? (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "").substring(0, root._typedIcon)
|
||||
: (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "")
|
||||
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
|
||||
@@ -21,7 +21,13 @@ PanelWindow {
|
||||
property bool popupOpen: false
|
||||
property real popupCenterX: -1
|
||||
|
||||
function _doOpen() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function _doOpen() {
|
||||
hideTimer.stop()
|
||||
if (chipRoot) {
|
||||
popupCenterX = chipRoot.mapToItem(null, 0, 0).x + chipRoot.width / 2
|
||||
}
|
||||
visible = true; popupOpen = true
|
||||
}
|
||||
function open() {
|
||||
_doOpen()
|
||||
}
|
||||
|
||||
@@ -118,8 +118,6 @@ QtObject {
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// LAUNCHER
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh"
|
||||
property string launcherCacheDir: "/tmp/qs_launcher"
|
||||
property bool launcherCloseOnClickOutside: true
|
||||
property bool launcherSelectFirst: false
|
||||
property bool launcherMipmapIcons: false
|
||||
@@ -142,7 +140,7 @@ QtObject {
|
||||
//
|
||||
// This list is the *default*; pairs can be added/removed live in the
|
||||
// Settings popup and those changes are persisted across restarts.
|
||||
property var cryptoPairs: [{ coin: "bitcoin", currency: "usd" }, { coin: "ethereum", currency: "eur" }, { coin: "monero", currency: "usd" }, { coin: "bitcoin", currency: "eur" }, { coin: "bitcoin", currency: "brl" }, { coin: "ethereum", currency: "usd" }]
|
||||
property var cryptoPairs: [{ coin: "bitcoin", currency: "usd" }, { coin: "ethereum", currency: "eur" }, { coin: "monero", currency: "usd" }, { coin: "bitcoin", currency: "eur" }, { coin: "bitcoin", currency: "brl" }, { coin: "ethereum", currency: "usd" }, { coin: "zcash", currency: "usd" }]
|
||||
|
||||
// ── Compact label decimals ────────────────────────────────────────────────
|
||||
// 0 → "80k"
|
||||
|
||||
@@ -3,8 +3,8 @@ import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
|
||||
import "../config" as Cfg
|
||||
import "../components"
|
||||
@@ -21,6 +21,78 @@ PanelWindow {
|
||||
|
||||
readonly property int launcherGap: 12
|
||||
|
||||
property var allItems: []
|
||||
property var favoriteIds: []
|
||||
property var accessCounts: ({})
|
||||
property bool _loaded: false
|
||||
|
||||
readonly property string _statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
|
||||
|
||||
function buildItems() {
|
||||
const apps = DesktopEntries.applications.values
|
||||
const items = []
|
||||
for (let i = 0; i < apps.length; i++) {
|
||||
const e = apps[i]
|
||||
items.push({
|
||||
name: e.name,
|
||||
exec: e.execString,
|
||||
id: e.id,
|
||||
icon: e.icon,
|
||||
entry: e,
|
||||
actions: e.actions
|
||||
})
|
||||
}
|
||||
items.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||
root.allItems = items
|
||||
}
|
||||
|
||||
Component.onCompleted: buildItems()
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() { root.buildItems() }
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stateFile
|
||||
path: root._statePath
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onAdapterUpdated: writeAdapter()
|
||||
adapter: JsonAdapter {
|
||||
id: stateAdapter
|
||||
property list<string> favorites: []
|
||||
property var access: ({})
|
||||
}
|
||||
onLoaded: {
|
||||
root.favoriteIds = stateAdapter.favorites
|
||||
root.accessCounts = stateAdapter.access
|
||||
root._loaded = true
|
||||
}
|
||||
onLoadFailed: {
|
||||
root._loaded = true
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFavorite(itemId) {
|
||||
const arr = root.favoriteIds.slice()
|
||||
const idx = arr.indexOf(itemId)
|
||||
if (idx >= 0)
|
||||
arr.splice(idx, 1)
|
||||
else
|
||||
arr.push(itemId)
|
||||
root.favoriteIds = arr
|
||||
stateAdapter.favorites = root.favoriteIds
|
||||
}
|
||||
|
||||
function incrementAccess(itemId) {
|
||||
const counts = Object.assign({}, root.accessCounts)
|
||||
counts[itemId] = (counts[itemId] || 0) + 1
|
||||
root.accessCounts = counts
|
||||
stateAdapter.access = root.accessCounts
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
id: launcherShortcut
|
||||
name: "toggle"
|
||||
@@ -30,13 +102,14 @@ PanelWindow {
|
||||
property bool _isOpen: false
|
||||
readonly property bool isOpen: _isOpen
|
||||
property bool _keyboardNav: false
|
||||
property var allItems: []
|
||||
property bool loading: false
|
||||
property int lastCacheUpdate: 0
|
||||
property var favoriteIds: []
|
||||
property var accessCounts: ({})
|
||||
readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
|
||||
property string _expandingActions: ""
|
||||
property string _savedExpandId: ""
|
||||
property real _savedContentY: 0
|
||||
|
||||
property bool _pendingRestore: false
|
||||
property bool _inRestore: false
|
||||
|
||||
property string expandedItemId: ""
|
||||
|
||||
readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1
|
||||
|
||||
@@ -52,15 +125,33 @@ PanelWindow {
|
||||
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 = []
|
||||
|
||||
function pushItem(item) {
|
||||
result.push(Object.assign({}, item, { isHeader: false, isAction: false }))
|
||||
if (root.expandedItemId && item.id === root.expandedItemId && item.actions && item.actions.length > 0) {
|
||||
for (let j = 0; j < item.actions.length; j++) {
|
||||
const a = item.actions[j]
|
||||
result.push({
|
||||
isHeader: false,
|
||||
isAction: true,
|
||||
name: a.name,
|
||||
id: item.id + ":" + a.id,
|
||||
icon: a.icon,
|
||||
actionObj: a
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }))
|
||||
pushItem(favs[i])
|
||||
}
|
||||
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 }))
|
||||
pushItem(rest[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -88,16 +179,16 @@ PanelWindow {
|
||||
closeTimer.stop()
|
||||
visible = true
|
||||
_isOpen = true
|
||||
expandedItemId = ""
|
||||
searchInput.text = ""
|
||||
resultsList.currentIndex = root.defaultIndex
|
||||
Qt.callLater(() => resultsList.positionViewAtBeginning())
|
||||
loadItems()
|
||||
loadState()
|
||||
Qt.callLater(() => searchInput.forceActiveFocus())
|
||||
}
|
||||
|
||||
function close() {
|
||||
_isOpen = false
|
||||
expandedItemId = ""
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
@@ -108,108 +199,18 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems() {
|
||||
loading = true
|
||||
var now = Date.now()
|
||||
var staleMs = 300000
|
||||
if (now - lastCacheUpdate < staleMs) {
|
||||
readCache()
|
||||
return
|
||||
}
|
||||
readCache()
|
||||
lastCacheUpdate = now
|
||||
updateProc.command = [Cfg.Config.launcherScript, "--update-only"]
|
||||
updateProc.running = true
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
const path = Cfg.Config.launcherCacheDir + "/drun.txt"
|
||||
dataProc.rawOutput = ""
|
||||
dataProc.command = ["cat", path]
|
||||
dataProc.running = true
|
||||
}
|
||||
|
||||
function shellQuote(str) {
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
function parseExec(str) {
|
||||
const args = []
|
||||
let current = ""
|
||||
let inSingle = false
|
||||
let inDouble = false
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str[i]
|
||||
|
||||
if (inSingle) {
|
||||
if (ch === "'") inSingle = false
|
||||
else current += ch
|
||||
} else if (inDouble) {
|
||||
if (ch === '"') inDouble = false
|
||||
else current += ch
|
||||
} else if (ch === "'") {
|
||||
inSingle = true
|
||||
} else if (ch === '"') {
|
||||
inDouble = true
|
||||
} else if (/\s/.test(ch)) {
|
||||
if (current.length > 0) {
|
||||
args.push(current)
|
||||
current = ""
|
||||
}
|
||||
} else {
|
||||
current += ch
|
||||
}
|
||||
}
|
||||
if (current.length > 0) args.push(current)
|
||||
return args
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
stateReadProc.command = ["sh", "-c", "cat " + shellQuote(statePath) + " 2>/dev/null || true"]
|
||||
stateReadProc.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) {
|
||||
const arr = favoriteIds.slice()
|
||||
const idx = arr.indexOf(itemId)
|
||||
if (idx >= 0) {
|
||||
arr.splice(idx, 1)
|
||||
} else {
|
||||
arr.push(itemId)
|
||||
}
|
||||
favoriteIds = arr
|
||||
saveState()
|
||||
}
|
||||
|
||||
function incrementAccess(itemId) {
|
||||
const counts = Object.assign({}, accessCounts)
|
||||
counts[itemId] = (counts[itemId] || 0) + 1
|
||||
accessCounts = counts
|
||||
saveState()
|
||||
}
|
||||
|
||||
function launch(item) {
|
||||
if (!item || !item.exec) return
|
||||
if (!item || !item.entry) return
|
||||
item.entry.execute()
|
||||
console.log("Launching: " + item.name)
|
||||
root.incrementAccess(item.id)
|
||||
close()
|
||||
}
|
||||
|
||||
let cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim()
|
||||
const logPath = "/tmp/qml_launch.log"
|
||||
|
||||
const args = parseExec(cleanExec)
|
||||
const cmd = args.map(a => shellQuote(a)).join(" ") + " > " + shellQuote(logPath) + " 2>&1 < /dev/null &"
|
||||
|
||||
launchProc.command = ["sh", "-c", cmd]
|
||||
launchProc.running = true
|
||||
|
||||
console.log("Launching: " + cleanExec)
|
||||
incrementAccess(item.id)
|
||||
function executeAction(item) {
|
||||
if (!item || !item.actionObj) return
|
||||
item.actionObj.execute()
|
||||
console.log("Executing action: " + item.name)
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -228,61 +229,9 @@ PanelWindow {
|
||||
|
||||
PopupHideTimer {
|
||||
id: closeTimer
|
||||
onTriggered: {
|
||||
root.visible = false
|
||||
root.allItems = []
|
||||
}
|
||||
onTriggered: root.visible = false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateProc
|
||||
onRunningChanged: if (!running) root.readCache()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dataProc
|
||||
property string rawOutput: ""
|
||||
stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" }
|
||||
onRunningChanged: {
|
||||
if (!running && rawOutput) {
|
||||
const lines = rawOutput.trim().split("\n")
|
||||
const items = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const p = lines[i].split("\t")
|
||||
if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" })
|
||||
}
|
||||
root.allItems = items
|
||||
root.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: launchProc }
|
||||
|
||||
Process {
|
||||
id: stateReadProc
|
||||
property string rawData: ""
|
||||
stdout: SplitParser { onRead: data => stateReadProc.rawData += data + "\n" }
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (rawData.trim()) {
|
||||
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: stateWriteProc }
|
||||
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -355,12 +304,48 @@ PanelWindow {
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectionColor: t.launcherAccent
|
||||
|
||||
onTextChanged: resultsList.currentIndex = root.defaultIndex
|
||||
onTextChanged: {
|
||||
resultsList.currentIndex = root.defaultIndex
|
||||
root.expandedItemId = ""
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
||||
if (idx >= 0)
|
||||
root.launch(root.filteredItems[idx])
|
||||
if (idx < 0) return
|
||||
const item = root.filteredItems[idx]
|
||||
if (item.isAction)
|
||||
root.executeAction(item)
|
||||
else
|
||||
root.launch(item)
|
||||
}
|
||||
|
||||
Keys.onTabPressed: (event) => {
|
||||
event.accepted = true
|
||||
root._keyboardNav = true
|
||||
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
||||
if (idx < 0) return
|
||||
const item = root.filteredItems[idx]
|
||||
if (!item || item.isHeader || item.isAction) return
|
||||
root._savedContentY = resultsList.contentY
|
||||
root._savedExpandId = item.id
|
||||
root._expandingActions = item.id
|
||||
if (root.expandedItemId === item.id) {
|
||||
root._pendingRestore = true
|
||||
root.expandedItemId = ""
|
||||
} else if (item.actions && item.actions.length > 0) {
|
||||
root._pendingRestore = true
|
||||
root.expandedItemId = item.id
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
for (let i = 0; i < resultsList.count; i++) {
|
||||
const it = root.filteredItems[i]
|
||||
if (it.id === item.id && !it.isAction && !it.isHeader) {
|
||||
resultsList.currentIndex = i
|
||||
resultsList.positionViewAtIndex(i, ListView.Contain)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
@@ -432,19 +417,51 @@ PanelWindow {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
onModelChanged: {
|
||||
currentIndex = root.defaultIndex
|
||||
for (let i = 0; i < count; i++) {
|
||||
const md = root.filteredItems[i]
|
||||
if (md && !md.isHeader) {
|
||||
currentIndex = i
|
||||
break
|
||||
if (root._expandingActions !== "") {
|
||||
root._expandingActions = ""
|
||||
if (root._savedExpandId !== "") {
|
||||
Qt.callLater(() => {
|
||||
for (let i = 0; i < resultsList.count; i++) {
|
||||
const it = root.filteredItems[i]
|
||||
if (it.id === root._savedExpandId && !it.isAction && !it.isHeader) {
|
||||
resultsList.currentIndex = i
|
||||
resultsList.positionViewAtIndex(i, ListView.Contain)
|
||||
break
|
||||
}
|
||||
}
|
||||
root._pendingRestore = false
|
||||
root._savedExpandId = ""
|
||||
})
|
||||
} else {
|
||||
root._pendingRestore = false
|
||||
}
|
||||
} else {
|
||||
currentIndex = root.defaultIndex
|
||||
for (let i = 0; i < count; i++) {
|
||||
const md = root.filteredItems[i]
|
||||
if (md && !md.isHeader && !md.isAction) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
Qt.callLater(() => positionViewAtBeginning())
|
||||
}
|
||||
Qt.callLater(() => positionViewAtBeginning())
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Connections {
|
||||
target: resultsList
|
||||
function onContentYChanged() {
|
||||
if (root._pendingRestore && !root._inRestore && Math.abs(resultsList.contentY - root._savedContentY) > 1) {
|
||||
root._inRestore = true
|
||||
resultsList.contentY = root._savedContentY
|
||||
root._inRestore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: itemDelegate
|
||||
width: resultsList.width
|
||||
height: 38
|
||||
radius: modelData.isHeader ? 0 : 8
|
||||
@@ -452,6 +469,7 @@ PanelWindow {
|
||||
: (starHovered || hovered) ? t.launcherHover
|
||||
: (resultsList.currentIndex === index && index !== -1
|
||||
? t.launcherSelected : "transparent")
|
||||
clip: modelData.isAction
|
||||
property bool hovered: false
|
||||
property bool starHovered: false
|
||||
|
||||
@@ -467,11 +485,12 @@ PanelWindow {
|
||||
RowLayout {
|
||||
visible: !modelData.isHeader
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.leftMargin: modelData.isAction ? 28 : 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Item {
|
||||
visible: !modelData.isAction
|
||||
width: 22; height: 22
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
@@ -496,14 +515,31 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: t.launcherText
|
||||
visible: modelData.isAction
|
||||
text: "›"
|
||||
color: t.launcherDim
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: modelData.isAction ? t.launcherDim : t.launcherText
|
||||
font.pixelSize: modelData.isAction ? 13 : 14
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !modelData.isAction && modelData.actions && modelData.actions.length > 0
|
||||
text: root.expandedItemId === modelData.id ? "▲" : "▼"
|
||||
color: t.launcherDim
|
||||
font.pixelSize: 9
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !modelData.isAction
|
||||
width: 26; height: 26
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
@@ -525,20 +561,40 @@ PanelWindow {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: { parent.hovered = false; parent.starHovered = false }
|
||||
onPositionChanged: mouse => {
|
||||
parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46)
|
||||
parent.starHovered = !modelData.isHeader && !modelData.isAction && (mouse.x >= width - 46)
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (modelData.isHeader) return
|
||||
if (mouse.x >= width - 46) {
|
||||
root.toggleFavorite(modelData.id)
|
||||
} else {
|
||||
root.launch(modelData)
|
||||
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (!modelData.isAction && modelData.actions && modelData.actions.length > 0) {
|
||||
root._savedContentY = resultsList.contentY
|
||||
root._savedExpandId = modelData.id
|
||||
root._expandingActions = modelData.id
|
||||
root._pendingRestore = true
|
||||
if (root.expandedItemId === modelData.id)
|
||||
root.expandedItemId = ""
|
||||
else
|
||||
root.expandedItemId = modelData.id
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.isAction) {
|
||||
root.executeAction(modelData)
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.x >= width - 46)
|
||||
root.toggleFavorite(modelData.id)
|
||||
else
|
||||
root.launch(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CACHE_DIR="/tmp/qs_launcher"
|
||||
|
||||
DRUN_CACHE="$CACHE_DIR/drun.txt"
|
||||
RUN_CACHE="$CACHE_DIR/run.txt"
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
|
||||
get_desktop_files() {
|
||||
IFS=':' read -ra DIRS <<< \
|
||||
"${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
|
||||
for dir in "${DIRS[@]}"; do
|
||||
APP_DIR="$dir/applications"
|
||||
|
||||
[ -d "$APP_DIR" ] || continue
|
||||
|
||||
find "$APP_DIR" \
|
||||
-maxdepth 1 \
|
||||
-name "*.desktop" \
|
||||
2>/dev/null
|
||||
done | while read -r f; do
|
||||
|
||||
name=$(
|
||||
grep -m1 '^Name=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
exec=$(
|
||||
grep -m1 '^Exec=' "$f" \
|
||||
| cut -d= -f2- \
|
||||
| sed 's/%[fFuUik]//g'
|
||||
)
|
||||
|
||||
type=$(
|
||||
grep -m1 '^Type=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
nodisplay=$(
|
||||
grep -m1 '^NoDisplay=' "$f" \
|
||||
| cut -d= -f2- \
|
||||
| tr '[:upper:]' '[:lower:]'
|
||||
)
|
||||
|
||||
icon=$(
|
||||
grep -m1 '^Icon=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
[[ "$type" == "Application" ]] || continue
|
||||
[[ "$nodisplay" != "true" ]] || continue
|
||||
[[ -n "$name" ]] || continue
|
||||
|
||||
printf "%s\t%s\t%s\t%s\n" \
|
||||
"$name" \
|
||||
"$exec" \
|
||||
"$(basename "$f" .desktop)" \
|
||||
"$icon"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
get_binaries() {
|
||||
compgen -c \
|
||||
| sort -u \
|
||||
| head -600 \
|
||||
| awk '{
|
||||
print $1 "\t" $1 "\t" $1
|
||||
}'
|
||||
}
|
||||
|
||||
|
||||
sort_and_dedup() {
|
||||
sort -f -t$'\t' -k1,1 \
|
||||
| awk -F'\t' '!seen[$1]++'
|
||||
}
|
||||
|
||||
|
||||
build_drun() {
|
||||
get_desktop_files | sort_and_dedup
|
||||
}
|
||||
|
||||
build_run() {
|
||||
{ get_desktop_files; get_binaries; } | sort_and_dedup
|
||||
}
|
||||
|
||||
|
||||
update_cache() {
|
||||
build_drun > "$DRUN_CACHE.tmp"
|
||||
build_run > "$RUN_CACHE.tmp"
|
||||
|
||||
mv "$DRUN_CACHE.tmp" "$DRUN_CACHE"
|
||||
mv "$RUN_CACHE.tmp" "$RUN_CACHE"
|
||||
}
|
||||
|
||||
|
||||
if [ "$1" == "--update-only" ]; then
|
||||
update_cache
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user