mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
472 lines
18 KiB
QML
472 lines
18 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Modals.Common
|
|
import qs.Widgets
|
|
import qs.Services
|
|
|
|
DankModal {
|
|
id: root
|
|
|
|
property string title: I18n.tr("Select Application")
|
|
property string targetData: ""
|
|
property string targetDataLabel: ""
|
|
property string searchQuery: ""
|
|
property int selectedIndex: 0
|
|
property int gridColumns: SettingsData.appLauncherGridColumns
|
|
property bool keyboardNavigationActive: false
|
|
property string viewMode: "grid"
|
|
property var categoryFilter: []
|
|
property var usageHistoryKey: ""
|
|
property bool showTargetData: true
|
|
|
|
signal applicationSelected(var app, string targetData)
|
|
|
|
shouldBeVisible: false
|
|
allowStacking: true
|
|
modalWidth: 520
|
|
modalHeight: 500
|
|
|
|
onBackgroundClicked: close()
|
|
|
|
onDialogClosed: {
|
|
searchQuery = ""
|
|
selectedIndex = 0
|
|
keyboardNavigationActive = false
|
|
}
|
|
|
|
onOpened: {
|
|
searchQuery = ""
|
|
updateApplicationList()
|
|
selectedIndex = 0
|
|
Qt.callLater(() => {
|
|
if (contentLoader.item && contentLoader.item.searchField) {
|
|
contentLoader.item.searchField.text = ""
|
|
contentLoader.item.searchField.forceActiveFocus()
|
|
}
|
|
})
|
|
}
|
|
|
|
function updateApplicationList() {
|
|
applicationsModel.clear()
|
|
const apps = AppSearchService.applications
|
|
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}
|
|
let filteredApps = []
|
|
|
|
for (const app of apps) {
|
|
if (!app || !app.categories) continue
|
|
|
|
let matchesCategory = categoryFilter.length === 0
|
|
|
|
if (categoryFilter.length > 0) {
|
|
try {
|
|
for (const cat of app.categories) {
|
|
if (categoryFilter.includes(cat)) {
|
|
matchesCategory = true
|
|
break
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("AppPicker: Error iterating categories for", app.name, ":", e)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (matchesCategory) {
|
|
const name = app.name || ""
|
|
const lowerName = name.toLowerCase()
|
|
const lowerQuery = searchQuery.toLowerCase()
|
|
|
|
if (searchQuery === "" || lowerName.includes(lowerQuery)) {
|
|
filteredApps.push({
|
|
name: name,
|
|
icon: app.icon || "application-x-executable",
|
|
exec: app.exec || app.execString || "",
|
|
startupClass: app.startupWMClass || "",
|
|
appData: app
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
filteredApps.sort((a, b) => {
|
|
const aId = a.appData.id || a.appData.execString || a.appData.exec || ""
|
|
const bId = b.appData.id || b.appData.execString || b.appData.exec || ""
|
|
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0
|
|
const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0
|
|
if (aUsage !== bUsage) {
|
|
return bUsage - aUsage
|
|
}
|
|
return (a.name || "").localeCompare(b.name || "")
|
|
})
|
|
|
|
filteredApps.forEach(app => {
|
|
applicationsModel.append({
|
|
name: app.name,
|
|
icon: app.icon,
|
|
exec: app.exec,
|
|
startupClass: app.startupClass,
|
|
appId: app.appData.id || app.appData.execString || app.appData.exec || ""
|
|
})
|
|
})
|
|
|
|
console.log("AppPicker: Found " + filteredApps.length + " applications")
|
|
}
|
|
|
|
onSearchQueryChanged: updateApplicationList()
|
|
|
|
ListModel {
|
|
id: applicationsModel
|
|
}
|
|
|
|
content: Component {
|
|
FocusScope {
|
|
id: appContent
|
|
|
|
property alias searchField: searchField
|
|
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: event => {
|
|
root.close()
|
|
event.accepted = true
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (applicationsModel.count === 0) return
|
|
|
|
// Toggle view mode with Tab key
|
|
if (event.key === Qt.Key_Tab) {
|
|
root.viewMode = root.viewMode === "grid" ? "list" : "grid"
|
|
event.accepted = true
|
|
return
|
|
}
|
|
|
|
if (root.viewMode === "grid") {
|
|
if (event.key === Qt.Key_Left) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.max(0, root.selectedIndex - 1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Right) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Up) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Down) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns)
|
|
event.accepted = true
|
|
}
|
|
} else {
|
|
if (event.key === Qt.Key_Up) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.max(0, root.selectedIndex - 1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Down) {
|
|
root.keyboardNavigationActive = true
|
|
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) {
|
|
const app = applicationsModel.get(root.selectedIndex)
|
|
launchApplication(app)
|
|
}
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.spacingS * 2
|
|
height: parent.height - Theme.spacingS * 2
|
|
x: Theme.spacingS
|
|
y: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: 40
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.title
|
|
font.pixelSize: Theme.fontSizeLarge + 4
|
|
font.weight: Font.Bold
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
Row {
|
|
spacing: 4
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankActionButton {
|
|
buttonSize: 36
|
|
circular: false
|
|
iconName: "view_list"
|
|
iconSize: 20
|
|
iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
|
backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
onClicked: {
|
|
root.viewMode = "list"
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
buttonSize: 36
|
|
circular: false
|
|
iconName: "grid_view"
|
|
iconSize: 20
|
|
iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
|
backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
onClicked: {
|
|
root.viewMode = "grid"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DankTextField {
|
|
id: searchField
|
|
|
|
width: parent.width - Theme.spacingS * 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
height: 52
|
|
cornerRadius: Theme.cornerRadius
|
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
normalBorderColor: Theme.outlineMedium
|
|
focusedBorderColor: Theme.primary
|
|
leftIconName: "search"
|
|
leftIconSize: Theme.iconSize
|
|
leftIconColor: Theme.surfaceVariantText
|
|
leftIconFocusedColor: Theme.primary
|
|
showClearButton: true
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
enabled: root.shouldBeVisible
|
|
ignoreLeftRightKeys: root.viewMode !== "list"
|
|
ignoreTabKeys: true
|
|
keyForwardTargets: [appContent]
|
|
|
|
onTextEdited: {
|
|
root.searchQuery = text
|
|
}
|
|
|
|
Keys.onPressed: function (event) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.close()
|
|
event.accepted = true
|
|
return
|
|
}
|
|
|
|
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
|
const hasText = text.length > 0
|
|
|
|
if (isEnterKey && hasText) {
|
|
if (root.keyboardNavigationActive && applicationsModel.count > 0) {
|
|
const app = applicationsModel.get(root.selectedIndex)
|
|
launchApplication(app)
|
|
} else if (applicationsModel.count > 0) {
|
|
const app = applicationsModel.get(0)
|
|
launchApplication(app)
|
|
}
|
|
event.accepted = true
|
|
return
|
|
}
|
|
|
|
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]
|
|
const isNavigationKey = navigationKeys.includes(event.key)
|
|
const isEmptyEnter = isEnterKey && !hasText
|
|
|
|
event.accepted = !(isNavigationKey || isEmptyEnter)
|
|
}
|
|
|
|
Connections {
|
|
function onShouldBeVisibleChanged() {
|
|
if (!root.shouldBeVisible) {
|
|
searchField.focus = false
|
|
}
|
|
}
|
|
|
|
target: root
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: {
|
|
let usedHeight = 40 + Theme.spacingS
|
|
usedHeight += 52 + Theme.spacingS
|
|
if (root.showTargetData) {
|
|
usedHeight += 36 + Theme.spacingS
|
|
}
|
|
return parent.height - usedHeight
|
|
}
|
|
radius: Theme.cornerRadius
|
|
color: "transparent"
|
|
|
|
DankListView {
|
|
id: appList
|
|
|
|
property int itemHeight: 60
|
|
property int itemSpacing: Theme.spacingS
|
|
|
|
function ensureVisible(index) {
|
|
if (index < 0 || index >= count) return
|
|
|
|
const itemY = index * (itemHeight + itemSpacing)
|
|
const itemBottom = itemY + itemHeight
|
|
if (itemY < contentY) {
|
|
contentY = itemY
|
|
} else if (itemBottom > contentY + height) {
|
|
contentY = itemBottom - height
|
|
}
|
|
}
|
|
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.bottomMargin: Theme.spacingS
|
|
|
|
visible: root.viewMode === "list"
|
|
model: applicationsModel
|
|
currentIndex: root.selectedIndex
|
|
clip: true
|
|
spacing: itemSpacing
|
|
|
|
onCurrentIndexChanged: {
|
|
root.selectedIndex = currentIndex
|
|
if (root.keyboardNavigationActive) {
|
|
ensureVisible(currentIndex)
|
|
}
|
|
}
|
|
|
|
delegate: AppLauncherListDelegate {
|
|
listView: appList
|
|
itemHeight: 60
|
|
iconSize: 40
|
|
showDescription: false
|
|
|
|
isCurrentItem: index === root.selectedIndex
|
|
keyboardNavigationActive: root.keyboardNavigationActive
|
|
hoverUpdatesSelection: true
|
|
|
|
onItemClicked: (idx, modelData) => {
|
|
launchApplication(modelData)
|
|
}
|
|
|
|
onKeyboardNavigationReset: {
|
|
root.keyboardNavigationActive = false
|
|
}
|
|
}
|
|
}
|
|
|
|
DankGridView {
|
|
id: appGrid
|
|
|
|
function ensureVisible(index) {
|
|
if (index < 0 || index >= count) return
|
|
|
|
const itemY = Math.floor(index / root.gridColumns) * cellHeight
|
|
const itemBottom = itemY + cellHeight
|
|
if (itemY < contentY) {
|
|
contentY = itemY
|
|
} else if (itemBottom > contentY + height) {
|
|
contentY = itemBottom - height
|
|
}
|
|
}
|
|
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.bottomMargin: Theme.spacingS
|
|
|
|
visible: root.viewMode === "grid"
|
|
model: applicationsModel
|
|
cellWidth: width / root.gridColumns
|
|
cellHeight: 120
|
|
clip: true
|
|
currentIndex: root.selectedIndex
|
|
|
|
onCurrentIndexChanged: {
|
|
root.selectedIndex = currentIndex
|
|
if (root.keyboardNavigationActive) {
|
|
ensureVisible(currentIndex)
|
|
}
|
|
}
|
|
|
|
delegate: AppLauncherGridDelegate {
|
|
gridView: appGrid
|
|
cellWidth: appGrid.cellWidth
|
|
cellHeight: appGrid.cellHeight
|
|
|
|
currentIndex: root.selectedIndex
|
|
keyboardNavigationActive: root.keyboardNavigationActive
|
|
hoverUpdatesSelection: true
|
|
|
|
onItemClicked: (idx, modelData) => {
|
|
launchApplication(modelData)
|
|
}
|
|
|
|
onKeyboardNavigationReset: {
|
|
root.keyboardNavigationActive = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 36
|
|
radius: Theme.cornerRadius
|
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
|
|
border.color: Theme.outlineMedium
|
|
border.width: 1
|
|
visible: root.showTargetData && root.targetData.length > 0
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.targetDataLabel.length > 0 ? root.targetDataLabel + ": " + root.targetData : root.targetData
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
elide: Text.ElideMiddle
|
|
wrapMode: Text.NoWrap
|
|
maximumLineCount: 1
|
|
}
|
|
}
|
|
}
|
|
|
|
function launchApplication(app) {
|
|
if (!app) return
|
|
|
|
root.applicationSelected(app, root.targetData)
|
|
|
|
if (usageHistoryKey && app.appId) {
|
|
const usageHistory = SettingsData[usageHistoryKey] || {}
|
|
const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0
|
|
usageHistory[app.appId] = {
|
|
count: currentCount + 1,
|
|
lastUsed: Date.now(),
|
|
name: app.name
|
|
}
|
|
SettingsData.set(usageHistoryKey, usageHistory)
|
|
}
|
|
|
|
root.close()
|
|
}
|
|
}
|
|
}
|
|
}
|