mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-26 14:32:52 -05:00
switch hto monorepo structure
This commit is contained in:
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
237
quickshell/Modals/Spotlight/FileSearchController.qml
Normal file
@@ -0,0 +1,237 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: controller
|
||||
|
||||
property string searchQuery: ""
|
||||
property alias model: fileModel
|
||||
property int selectedIndex: 0
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isSearching: false
|
||||
property int totalResults: 0
|
||||
property string searchField: "filename"
|
||||
|
||||
signal searchCompleted
|
||||
|
||||
ListModel {
|
||||
id: fileModel
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
if (!DSearchService.dsearchAvailable) {
|
||||
model.clear()
|
||||
totalResults = 0
|
||||
isSearching = false
|
||||
return
|
||||
}
|
||||
|
||||
if (searchQuery.length === 0) {
|
||||
model.clear()
|
||||
totalResults = 0
|
||||
isSearching = false
|
||||
return
|
||||
}
|
||||
|
||||
isSearching = true
|
||||
const params = {
|
||||
"limit": 50,
|
||||
"fuzzy": true,
|
||||
"sort": "score",
|
||||
"desc": true
|
||||
}
|
||||
|
||||
if (searchField && searchField !== "all") {
|
||||
params.field = searchField
|
||||
}
|
||||
|
||||
DSearchService.search(searchQuery, params, response => {
|
||||
if (response.error) {
|
||||
model.clear()
|
||||
totalResults = 0
|
||||
isSearching = false
|
||||
return
|
||||
}
|
||||
|
||||
if (response.result) {
|
||||
updateModel(response.result)
|
||||
}
|
||||
|
||||
isSearching = false
|
||||
searchCompleted()
|
||||
})
|
||||
}
|
||||
|
||||
function updateModel(result) {
|
||||
model.clear()
|
||||
totalResults = result.total_hits || 0
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = true
|
||||
|
||||
if (!result.hits || result.hits.length === 0) {
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
return
|
||||
}
|
||||
|
||||
for (var i = 0; i < result.hits.length; i++) {
|
||||
const hit = result.hits[i]
|
||||
const filePath = hit.id || ""
|
||||
const fileName = getFileName(filePath)
|
||||
const fileExt = getFileExtension(fileName)
|
||||
const fileType = determineFileType(fileName, filePath)
|
||||
const dirPath = getDirPath(filePath)
|
||||
|
||||
model.append({
|
||||
"filePath": filePath,
|
||||
"fileName": fileName,
|
||||
"fileExtension": fileExt,
|
||||
"fileType": fileType,
|
||||
"dirPath": dirPath,
|
||||
"score": hit.score || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
const parts = path.split('/')
|
||||
return parts[parts.length - 1] || path
|
||||
}
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.')
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getDirPath(path) {
|
||||
const lastSlash = path.lastIndexOf('/')
|
||||
if (lastSlash > 0) {
|
||||
return path.substring(0, lastSlash)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function determineFileType(fileName, filePath) {
|
||||
const ext = getFileExtension(fileName)
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image"
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video"
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio"
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code"
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
|
||||
if (docExts.includes(ext)) {
|
||||
return "document"
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive"
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary"
|
||||
}
|
||||
|
||||
return "file"
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (model.count === 0) {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (model.count === 0) {
|
||||
return
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
|
||||
signal fileOpened
|
||||
|
||||
function openFile(filePath) {
|
||||
if (!filePath || filePath.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let url = filePath
|
||||
if (!url.startsWith("file://")) {
|
||||
url = "file://" + filePath
|
||||
}
|
||||
|
||||
Qt.openUrlExternally(url)
|
||||
fileOpened()
|
||||
}
|
||||
|
||||
function openFolder(filePath) {
|
||||
if (!filePath || filePath.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastSlash = filePath.lastIndexOf('/')
|
||||
if (lastSlash <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const dirPath = filePath.substring(0, lastSlash)
|
||||
let url = dirPath
|
||||
if (!url.startsWith("file://")) {
|
||||
url = "file://" + dirPath
|
||||
}
|
||||
|
||||
Qt.openUrlExternally(url)
|
||||
fileOpened()
|
||||
}
|
||||
|
||||
function openSelected() {
|
||||
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = model.get(selectedIndex)
|
||||
if (item && item.filePath) {
|
||||
openFile(item.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
searchQuery = ""
|
||||
model.clear()
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
isSearching = false
|
||||
totalResults = 0
|
||||
}
|
||||
|
||||
onSearchQueryChanged: {
|
||||
performSearch()
|
||||
}
|
||||
|
||||
onSearchFieldChanged: {
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
155
quickshell/Modals/Spotlight/FileSearchEntry.qml
Normal file
@@ -0,0 +1,155 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: entry
|
||||
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property string fileExtension
|
||||
required property string fileType
|
||||
required property string dirPath
|
||||
required property bool isSelected
|
||||
required property int itemIndex
|
||||
|
||||
signal clicked()
|
||||
|
||||
readonly property int iconSize: 40
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Image {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
source: fileType === "image" ? `file://${filePath}` : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
cache: true
|
||||
asynchronous: true
|
||||
visible: fileType === "image" && status === Image.Ready
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: imagePreview
|
||||
maskEnabled: true
|
||||
maskSource: imageMask
|
||||
visible: fileType === "image" && imagePreview.status === Image.Ready
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: imageMask
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: getFileTypeColor()
|
||||
visible: fileType !== "image" || imagePreview.status !== Image.Ready
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: getFileIconText()
|
||||
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - iconSize - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: fileName
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideMiddle
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: dirPath
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: entry.clicked()
|
||||
}
|
||||
|
||||
function getFileTypeColor() {
|
||||
switch (fileType) {
|
||||
case "code":
|
||||
return Theme.codeFileColor || Theme.primarySelected
|
||||
case "document":
|
||||
return Theme.docFileColor || Theme.secondarySelected
|
||||
case "video":
|
||||
return Theme.videoFileColor || Theme.tertiarySelected
|
||||
case "audio":
|
||||
return Theme.audioFileColor || Theme.errorSelected
|
||||
case "archive":
|
||||
return Theme.archiveFileColor || Theme.warningSelected
|
||||
case "binary":
|
||||
return Theme.binaryFileColor || Theme.surfaceDim
|
||||
default:
|
||||
return Theme.surfaceLight
|
||||
}
|
||||
}
|
||||
|
||||
function getFileIconText() {
|
||||
if (fileType === "binary") {
|
||||
return "bin"
|
||||
}
|
||||
|
||||
if (fileExtension.length > 0) {
|
||||
return fileExtension
|
||||
}
|
||||
|
||||
return fileName.charAt(0).toUpperCase()
|
||||
}
|
||||
}
|
||||
246
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
246
quickshell/Modals/Spotlight/FileSearchResults.qml
Normal file
@@ -0,0 +1,246 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
property var fileSearchController: null
|
||||
|
||||
function resetScroll() {
|
||||
filesList.contentY = 0
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: filesList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index)
|
||||
signal itemRightClicked(int index)
|
||||
|
||||
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.margins: Theme.spacingS
|
||||
model: fileSearchController ? fileSearchController.model : null
|
||||
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
|
||||
onItemClicked: function (index) {
|
||||
if (fileSearchController) {
|
||||
const item = fileSearchController.model.get(index)
|
||||
fileSearchController.openFile(item.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
onItemRightClicked: function (index) {
|
||||
if (fileSearchController) {
|
||||
const item = fileSearchController.model.get(index)
|
||||
fileSearchController.openFolder(item.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardNavigationReset: {
|
||||
if (fileSearchController)
|
||||
fileSearchController.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property string fileExtension
|
||||
required property string fileType
|
||||
required property string dirPath
|
||||
|
||||
width: ListView.view.width
|
||||
height: filesList.itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: iconBackground
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.surfaceLight
|
||||
visible: fileType !== "image"
|
||||
|
||||
DankNFIcon {
|
||||
id: nerdIcon
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const lowerName = fileName.toLowerCase()
|
||||
if (lowerName.startsWith("dockerfile"))
|
||||
return "docker"
|
||||
if (lowerName.startsWith("makefile"))
|
||||
return "makefile"
|
||||
if (lowerName.startsWith("license"))
|
||||
return "license"
|
||||
if (lowerName.startsWith("readme"))
|
||||
return "readme"
|
||||
return fileExtension.toLowerCase()
|
||||
}
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
visible: !nerdIcon.visible
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: fileType === "image"
|
||||
sourceComponent: Image {
|
||||
anchors.fill: parent
|
||||
source: "file://" + filePath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
maskSource: ShaderEffectSource {
|
||||
sourceItem: Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 40 - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: fileName || ""
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: dirPath || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: fileMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
|
||||
filesList.currentIndex = index
|
||||
}
|
||||
onPositionChanged: {
|
||||
filesList.keyboardNavigationReset()
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
filesList.itemClicked(index)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
filesList.itemRightClicked(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
|
||||
|
||||
StyledText {
|
||||
property string displayText: {
|
||||
if (!fileSearchController) {
|
||||
return ""
|
||||
}
|
||||
if (!DSearchService.dsearchAvailable) {
|
||||
return I18n.tr("DankSearch not available")
|
||||
}
|
||||
if (fileSearchController.isSearching) {
|
||||
return I18n.tr("Searching...")
|
||||
}
|
||||
if (fileSearchController.searchQuery.length === 0) {
|
||||
return I18n.tr("Enter a search query")
|
||||
}
|
||||
if (!fileSearchController.model || fileSearchController.model.count === 0) {
|
||||
return I18n.tr("No files found")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
text: displayText
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: displayText.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
447
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
447
quickshell/Modals/Spotlight/SpotlightContent.qml
Normal file
@@ -0,0 +1,447 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: spotlightKeyHandler
|
||||
|
||||
property alias appLauncher: appLauncher
|
||||
property alias searchField: searchField
|
||||
property alias fileSearchController: fileSearchController
|
||||
property var parentModal: null
|
||||
property string searchMode: "apps"
|
||||
|
||||
function resetScroll() {
|
||||
if (searchMode === "apps") {
|
||||
resultsView.resetScroll()
|
||||
} else {
|
||||
fileSearchResults.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchMode() {
|
||||
if (searchField.text.startsWith("/")) {
|
||||
if (searchMode !== "files") {
|
||||
searchMode = "files"
|
||||
}
|
||||
const query = searchField.text.substring(1)
|
||||
fileSearchController.searchQuery = query
|
||||
} else {
|
||||
if (searchMode !== "apps") {
|
||||
searchMode = "apps"
|
||||
fileSearchController.reset()
|
||||
appLauncher.searchQuery = searchField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
if (searchMode === "files") {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
} else {
|
||||
fileSearchController.keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
clip: false
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.launchSelected()
|
||||
} else if (searchMode === "files") {
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: 4
|
||||
onAppLaunched: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
onViewModeSelected: mode => {
|
||||
SettingsData.set("spotlightModalViewMode", mode)
|
||||
}
|
||||
}
|
||||
|
||||
FileSearchController {
|
||||
id: fileSearchController
|
||||
|
||||
onFileOpened: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
clip: false
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: searchMode === "files" ? "folder" : "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||
placeholderText: ""
|
||||
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [spotlightKeyHandler]
|
||||
onTextChanged: {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.searchQuery = text
|
||||
}
|
||||
}
|
||||
onTextEdited: {
|
||||
updateSearchMode()
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||
appLauncher.launchSelected()
|
||||
else if (appLauncher.model.count > 0)
|
||||
appLauncher.launchApp(appLauncher.model.get(0))
|
||||
} else if (searchMode === "files") {
|
||||
if (fileSearchController.model.count > 0)
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
|
||||
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "apps" && appLauncher.model.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "files"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: filenameFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "title"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: filenameFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "filename"
|
||||
}
|
||||
onEntered: {
|
||||
filenameTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (filenameTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (filenameTooltipLoader.item)
|
||||
filenameTooltipLoader.item.hide()
|
||||
|
||||
filenameTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "description"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: contentFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "body"
|
||||
}
|
||||
onEntered: {
|
||||
contentTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (contentTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (contentTooltipLoader.item)
|
||||
contentTooltipLoader.item.hide()
|
||||
|
||||
contentTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
|
||||
SpotlightResults {
|
||||
id: resultsView
|
||||
anchors.fill: parent
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
contextMenu: contextMenu
|
||||
visible: searchMode === "apps"
|
||||
}
|
||||
|
||||
FileSearchResults {
|
||||
id: fileSearchResults
|
||||
anchors.fill: parent
|
||||
fileSearchController: spotlightKeyHandler.fileSearchController
|
||||
visible: searchMode === "files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: contextMenu.visible
|
||||
z: 999
|
||||
onClicked: () => {
|
||||
contextMenu.hide()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
x: contextMenu.x
|
||||
y: contextMenu.y
|
||||
width: contextMenu.width
|
||||
height: contextMenu.height
|
||||
onClicked: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: filenameTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
}
|
||||
338
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
338
quickshell/Modals/Spotlight/SpotlightContextMenu.qml
Normal file
@@ -0,0 +1,338 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: contextMenu
|
||||
|
||||
property var currentApp: null
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
|
||||
function show(x, y, app) {
|
||||
currentApp = app
|
||||
contextMenu.x = x + 4
|
||||
contextMenu.y = y + 4
|
||||
contextMenu.open()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
contextMenu.close()
|
||||
}
|
||||
|
||||
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
modal: false
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: pinRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!desktopEntry)
|
||||
return "push_pin"
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!desktopEntry)
|
||||
return I18n.tr("Pin to Dock")
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (!desktopEntry)
|
||||
return
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
if (SessionData.isPinnedApp(appId))
|
||||
SessionData.removePinnedApp(appId)
|
||||
else
|
||||
SessionData.addPinnedApp(appId)
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: actionRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.iconSize - 2
|
||||
height: Theme.iconSize - 2
|
||||
visible: modelData.icon && modelData.icon !== ""
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && desktopEntry) {
|
||||
SessionService.launchDesktopAction(desktopEntry, modelData)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: launchRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "launch"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launchMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (contextMenu.currentApp && appLauncher)
|
||||
appLauncher.launchApp(contextMenu.currentApp)
|
||||
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: primeRunRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch on dGPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: primeRunMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (desktopEntry) {
|
||||
SessionService.launchDesktopEntry(desktopEntry, true)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
quickshell/Modals/Spotlight/SpotlightModal.qml
Normal file
158
quickshell/Modals/Spotlight/SpotlightModal.qml
Normal file
@@ -0,0 +1,158 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
layerNamespace: "dms:spotlight"
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property alias spotlightContent: spotlightContentInstance
|
||||
|
||||
function show() {
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = query
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query
|
||||
}
|
||||
}
|
||||
|
||||
spotlightOpen = true
|
||||
open()
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
spotlightOpen = false
|
||||
close()
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = ""
|
||||
spotlightContent.appLauncher.selectedIndex = 0
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
||||
}
|
||||
if (spotlightContent.fileSearchController) {
|
||||
spotlightContent.fileSearchController.reset()
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll()
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (spotlightOpen) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: spotlightOpen
|
||||
width: 500
|
||||
height: 600
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
keepContentLoaded: true
|
||||
onVisibleChanged: () => {
|
||||
if (visible && !spotlightOpen) {
|
||||
show()
|
||||
}
|
||||
if (visible && spotlightContent) {
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return hide()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
|
||||
spotlightOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
target: ModalManager
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
spotlightModal.show()
|
||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
spotlightModal.hide()
|
||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
spotlightModal.toggle()
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
function openQuery(query: string): string {
|
||||
spotlightModal.showWithQuery(query)
|
||||
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
function toggleQuery(query: string): string {
|
||||
if (spotlightModal.spotlightOpen) {
|
||||
spotlightModal.hide()
|
||||
} else {
|
||||
spotlightModal.showWithQuery(query)
|
||||
}
|
||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
SpotlightContent {
|
||||
id: spotlightContentInstance
|
||||
|
||||
parentModal: spotlightModal
|
||||
}
|
||||
|
||||
directContent: spotlightContentInstance
|
||||
}
|
||||
179
quickshell/Modals/Spotlight/SpotlightResults.qml
Normal file
179
quickshell/Modals/Spotlight/SpotlightResults.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
property var appLauncher: null
|
||||
property var contextMenu: null
|
||||
|
||||
function resetScroll() {
|
||||
resultsList.contentY = 0
|
||||
resultsGrid.contentY = 0
|
||||
}
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
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.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "list"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherListDelegate {
|
||||
listView: resultsList
|
||||
itemHeight: resultsList.itemHeight
|
||||
iconSize: resultsList.iconSize
|
||||
showDescription: resultsList.showDescription
|
||||
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsList.keyboardNavigationActive
|
||||
isCurrentItem: ListView.isCurrentItem
|
||||
iconMaterialSizeAdjustment: 0
|
||||
iconUnicodeScale: 0.8
|
||||
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsList.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: resultsGrid
|
||||
|
||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
property int columns: 4
|
||||
property bool adaptiveColumns: false
|
||||
property int minCellWidth: 120
|
||||
property int maxCellWidth: 160
|
||||
property int cellPadding: 8
|
||||
property real iconSizeRatio: 0.55
|
||||
property int maxIconSize: 48
|
||||
property int minIconSize: 32
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||
const itemBottom = itemY + cellHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
clip: true
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherGridDelegate {
|
||||
gridView: resultsGrid
|
||||
cellWidth: resultsGrid.cellWidth
|
||||
cellHeight: resultsGrid.cellHeight
|
||||
cellPadding: resultsGrid.cellPadding
|
||||
minIconSize: resultsGrid.minIconSize
|
||||
maxIconSize: resultsGrid.maxIconSize
|
||||
iconSizeRatio: resultsGrid.iconSizeRatio
|
||||
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
|
||||
currentIndex: resultsGrid.currentIndex
|
||||
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsGrid.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user