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:
SomeElse
2026-06-09 04:06:40 +00:00
parent f59b2d6aa9
commit 8782fd2533
5 changed files with 366 additions and 316 deletions

View File

@@ -16,7 +16,9 @@ ModuleChip {
id: configWriter id: configWriter
} }
width: chipRow.implicitWidth + 24 width: cryptoIcon.visible
? (12 + cryptoIcon.implicitWidth + 7 + chipRow.implicitWidth + 12)
: (chipRow.implicitWidth + 24)
radius: panelRadius radius: panelRadius
chipColor: t.cryptoBg chipColor: t.cryptoBg
@@ -152,6 +154,7 @@ ModuleChip {
if (root.localPairs.length <= 1) return if (root.localPairs.length <= 1) return
root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length
root._applyFromCache() root._applyFromCache()
root._restartTyping()
rotateTimer.restart() rotateTimer.restart()
} }
@@ -159,6 +162,7 @@ ModuleChip {
if (root.localPairs.length <= 1) return if (root.localPairs.length <= 1) return
root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length
root._applyFromCache() root._applyFromCache()
root._restartTyping()
rotateTimer.restart() rotateTimer.restart()
} }
@@ -184,6 +188,51 @@ ModuleChip {
sparklineData = entry.sparkline || [] sparklineData = entry.sparkline || []
change7d = entry.change7d || 0 change7d = entry.change7d || 0
hasError = false 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() { function fetchPrice() {
@@ -291,9 +340,26 @@ ModuleChip {
onLocalRefreshSecChanged: refreshTimer.restart() onLocalRefreshSecChanged: refreshTimer.restart()
onLocalRotateSecChanged: rotateTimer.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 { RowLayout {
id: chipRow id: chipRow
anchors.centerIn: parent anchors {
left: cryptoIcon.visible ? cryptoIcon.right : parent.left
leftMargin: cryptoIcon.visible ? 7 : 12
verticalCenter: parent.verticalCenter
}
spacing: 0 spacing: 0
SequentialAnimation on opacity { SequentialAnimation on opacity {
@@ -310,34 +376,61 @@ ModuleChip {
} }
} }
Text { Item {
visible: root.localShowIcon implicitWidth: labelSizer.implicitWidth
text: "󰿤" implicitHeight: labelSizer.implicitHeight
color: t.cryptoIcon
font.pixelSize: 14 Text {
Layout.alignment: Qt.AlignVCenter id: labelSizer
Layout.rightMargin: 7 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 {
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": " visible: root._typing || root.price >= 0
color: t.cryptoLabel text: root._typing
font.pixelSize: 12 ? (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "").substring(0, root._typedIcon)
font.bold: true : (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "")
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 ? "▲" : "▼"
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown) color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
font.pixelSize: 9 font.pixelSize: 9
font.bold: true font.bold: true

View File

@@ -21,7 +21,13 @@ PanelWindow {
property bool popupOpen: false property bool popupOpen: false
property real popupCenterX: -1 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() { function open() {
_doOpen() _doOpen()
} }

View File

@@ -118,8 +118,6 @@ QtObject {
// ──────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────
// LAUNCHER // LAUNCHER
// ──────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────
property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh"
property string launcherCacheDir: "/tmp/qs_launcher"
property bool launcherCloseOnClickOutside: true property bool launcherCloseOnClickOutside: true
property bool launcherSelectFirst: false property bool launcherSelectFirst: false
property bool launcherMipmapIcons: false property bool launcherMipmapIcons: false
@@ -142,7 +140,7 @@ QtObject {
// //
// This list is the *default*; pairs can be added/removed live in the // This list is the *default*; pairs can be added/removed live in the
// Settings popup and those changes are persisted across restarts. // 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 ──────────────────────────────────────────────── // ── Compact label decimals ────────────────────────────────────────────────
// 0 → "80k" // 0 → "80k"

View File

@@ -3,8 +3,8 @@ import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Io
import "../config" as Cfg import "../config" as Cfg
import "../components" import "../components"
@@ -21,6 +21,78 @@ PanelWindow {
readonly property int launcherGap: 12 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 { GlobalShortcut {
id: launcherShortcut id: launcherShortcut
name: "toggle" name: "toggle"
@@ -30,13 +102,14 @@ PanelWindow {
property bool _isOpen: false property bool _isOpen: false
readonly property bool isOpen: _isOpen readonly property bool isOpen: _isOpen
property bool _keyboardNav: false property bool _keyboardNav: false
property var allItems: [] property string _expandingActions: ""
property bool loading: false property string _savedExpandId: ""
property int lastCacheUpdate: 0 property real _savedContentY: 0
property var favoriteIds: []
property var accessCounts: ({})
readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
property bool _pendingRestore: false
property bool _inRestore: false
property string expandedItemId: ""
readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1 readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1
@@ -52,15 +125,33 @@ PanelWindow {
let 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)) rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0))
const result = [] 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) { if (favs.length > 0) {
result.push({ isHeader: true, section: "Favorites" }) result.push({ isHeader: true, section: "Favorites" })
for (let i = 0; i < favs.length; i++) for (let i = 0; i < favs.length; i++)
result.push(Object.assign({}, favs[i], { isHeader: false })) pushItem(favs[i])
} }
if (rest.length > 0) { if (rest.length > 0) {
result.push({ isHeader: true, section: "All Applications" }) result.push({ isHeader: true, section: "All Applications" })
for (let i = 0; i < rest.length; i++) for (let i = 0; i < rest.length; i++)
result.push(Object.assign({}, rest[i], { isHeader: false })) pushItem(rest[i])
} }
return result return result
} }
@@ -88,16 +179,16 @@ PanelWindow {
closeTimer.stop() closeTimer.stop()
visible = true visible = true
_isOpen = true _isOpen = true
expandedItemId = ""
searchInput.text = "" searchInput.text = ""
resultsList.currentIndex = root.defaultIndex resultsList.currentIndex = root.defaultIndex
Qt.callLater(() => resultsList.positionViewAtBeginning()) Qt.callLater(() => resultsList.positionViewAtBeginning())
loadItems()
loadState()
Qt.callLater(() => searchInput.forceActiveFocus()) Qt.callLater(() => searchInput.forceActiveFocus())
} }
function close() { function close() {
_isOpen = false _isOpen = false
expandedItemId = ""
closeTimer.restart() 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) { 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() function executeAction(item) {
const logPath = "/tmp/qml_launch.log" if (!item || !item.actionObj) return
item.actionObj.execute()
const args = parseExec(cleanExec) console.log("Executing action: " + item.name)
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)
close() close()
} }
@@ -228,61 +229,9 @@ PanelWindow {
PopupHideTimer { PopupHideTimer {
id: closeTimer id: closeTimer
onTriggered: { onTriggered: root.visible = false
root.visible = false
root.allItems = []
}
} }
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 { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -355,12 +304,48 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
selectionColor: t.launcherAccent selectionColor: t.launcherAccent
onTextChanged: resultsList.currentIndex = root.defaultIndex onTextChanged: {
resultsList.currentIndex = root.defaultIndex
root.expandedItemId = ""
}
Keys.onReturnPressed: { Keys.onReturnPressed: {
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1) const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
if (idx >= 0) if (idx < 0) return
root.launch(root.filteredItems[idx]) 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: { Keys.onDownPressed: {
@@ -432,19 +417,51 @@ PanelWindow {
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
onModelChanged: { onModelChanged: {
currentIndex = root.defaultIndex if (root._expandingActions !== "") {
for (let i = 0; i < count; i++) { root._expandingActions = ""
const md = root.filteredItems[i] if (root._savedExpandId !== "") {
if (md && !md.isHeader) { Qt.callLater(() => {
currentIndex = i for (let i = 0; i < resultsList.count; i++) {
break 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 } 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 { delegate: Rectangle {
id: itemDelegate
width: resultsList.width width: resultsList.width
height: 38 height: 38
radius: modelData.isHeader ? 0 : 8 radius: modelData.isHeader ? 0 : 8
@@ -452,6 +469,7 @@ PanelWindow {
: (starHovered || hovered) ? t.launcherHover : (starHovered || hovered) ? t.launcherHover
: (resultsList.currentIndex === index && index !== -1 : (resultsList.currentIndex === index && index !== -1
? t.launcherSelected : "transparent") ? t.launcherSelected : "transparent")
clip: modelData.isAction
property bool hovered: false property bool hovered: false
property bool starHovered: false property bool starHovered: false
@@ -467,11 +485,12 @@ PanelWindow {
RowLayout { RowLayout {
visible: !modelData.isHeader visible: !modelData.isHeader
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 12 anchors.leftMargin: modelData.isAction ? 28 : 12
anchors.rightMargin: 12 anchors.rightMargin: 12
spacing: 12 spacing: 12
Item { Item {
visible: !modelData.isAction
width: 22; height: 22 width: 22; height: 22
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@@ -496,14 +515,31 @@ PanelWindow {
} }
Text { Text {
text: modelData.name visible: modelData.isAction
color: t.launcherText text: ""
color: t.launcherDim
font.pixelSize: 14 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 Layout.fillWidth: true
elide: Text.ElideRight 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 { Item {
visible: !modelData.isAction
width: 26; height: 26 width: 26; height: 26
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@@ -525,20 +561,40 @@ PanelWindow {
} }
MouseArea { MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onEntered: parent.hovered = true onEntered: parent.hovered = true
onExited: { parent.hovered = false; parent.starHovered = false } onExited: { parent.hovered = false; parent.starHovered = false }
onPositionChanged: mouse => { onPositionChanged: mouse => {
parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46) parent.starHovered = !modelData.isHeader && !modelData.isAction && (mouse.x >= width - 46)
} }
onClicked: mouse => { onClicked: mouse => {
if (modelData.isHeader) return if (modelData.isHeader) return
if (mouse.x >= width - 46) {
root.toggleFavorite(modelData.id) if (mouse.button === Qt.RightButton) {
} else { if (!modelData.isAction && modelData.actions && modelData.actions.length > 0) {
root.launch(modelData) 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)
} }
} }
} }

View File

@@ -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