1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

launcher v2: retire spotlight launcher in favor of dank launcher

This commit is contained in:
bbedward
2026-01-21 21:34:31 -05:00
parent 6735989455
commit b140afca8e
27 changed files with 311 additions and 4306 deletions

View File

@@ -24,8 +24,9 @@ Item {
property bool keyboardNavigationActive: false
property var sectionViewModes: ({})
property var pluginViewPreferences: ({})
property int gridColumns: 4
property int gridColumns: SettingsData.appLauncherGridColumns
property int viewModeVersion: 0
property string viewModeContext: "spotlight"
signal itemExecuted
signal searchCompleted
@@ -96,7 +97,9 @@ Item {
if (sectionViewModes[sectionId])
return sectionViewModes[sectionId];
var savedModes = SettingsData.spotlightSectionViewModes || {};
var savedModes = viewModeContext === "appDrawer"
? (SettingsData.appDrawerSectionViewModes || {})
: (SettingsData.spotlightSectionViewModes || {});
if (savedModes[sectionId])
return savedModes[sectionId];
@@ -116,10 +119,17 @@ Item {
[sectionId]: mode
});
viewModeVersion++;
var savedModes = Object.assign({}, SettingsData.spotlightSectionViewModes || {}, {
[sectionId]: mode
});
SettingsData.spotlightSectionViewModes = savedModes;
if (viewModeContext === "appDrawer") {
var savedModes = Object.assign({}, SettingsData.appDrawerSectionViewModes || {}, {
[sectionId]: mode
});
SettingsData.appDrawerSectionViewModes = savedModes;
} else {
var savedModes = Object.assign({}, SettingsData.spotlightSectionViewModes || {}, {
[sectionId]: mode
});
SettingsData.spotlightSectionViewModes = savedModes;
}
viewModeChanged(sectionId, mode);
}
@@ -273,10 +283,11 @@ Item {
activePluginName = "";
clearActivePluginViewPreference();
sections = cachedSections.map(function (s) {
var copy = Object.assign({}, s);
if (collapsedSections[s.id] !== undefined) {
var copy = Object.assign({}, s, {
items: s.items ? s.items.slice() : []
});
if (collapsedSections[s.id] !== undefined)
copy.collapsed = collapsedSections[s.id];
}
return copy;
});
flatModel = Scorer.flattenSections(sections);
@@ -344,6 +355,27 @@ Item {
}
if (searchMode === "apps") {
var cachedSections = AppSearchService.getCachedDefaultSections();
if (cachedSections && !searchQuery) {
var appSectionIds = ["favorites", "apps"];
sections = cachedSections.filter(function (s) {
return appSectionIds.indexOf(s.id) !== -1;
}).map(function (s) {
var copy = Object.assign({}, s, {
items: s.items ? s.items.slice() : []
});
if (collapsedSections[s.id] !== undefined)
copy.collapsed = collapsedSections[s.id];
return copy;
});
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
updateSelectedItem();
isSearching = false;
searchCompleted();
return;
}
var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) {
allItems.push(apps[i]);

View File

@@ -12,6 +12,7 @@ FocusScope {
LayoutMirroring.childrenInherit: true
property var parentModal: null
property string viewModeContext: "spotlight"
property alias searchField: searchField
property alias controller: controller
property alias resultsList: resultsList
@@ -87,6 +88,7 @@ FocusScope {
Controller {
id: controller
viewModeContext: root.viewModeContext
onItemExecuted: {
if (root.parentModal) {

View File

@@ -59,7 +59,7 @@ Item {
itemHeight = 52;
} else {
var cols = controller.getGridColumns(sectionId);
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor((mainFlickable.width - (root.gridColumns - 1) * 4) / root.gridColumns);
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
itemY = row * cellHeight;
@@ -88,7 +88,7 @@ Item {
} else {
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
var rows = Math.ceil((section.items?.length ?? 0) / cols);
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor((root.width - (cols - 1) * 4) / cols);
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
return 32 + rows * cellHeight;
}
@@ -129,7 +129,7 @@ Item {
itemH = 52;
} else {
var cols = controller.getGridColumns(entry.sectionId);
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor((width - (cols - 1) * 4) / cols);
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
var col = itemInSection % cols;
@@ -245,7 +245,7 @@ Item {
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor((width - (root.gridColumns - 1) * 4) / root.gridColumns)
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns)
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
Repeater {

View File

@@ -95,7 +95,7 @@ Item {
required property var modelData
required property int index
width: Math.floor((contentLoader.width - (root.gridColumns - 1) * 4) / root.gridColumns)
width: Math.floor(contentLoader.width / root.gridColumns)
height: width + 24
item: modelData
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex

View File

@@ -1,237 +0,0 @@
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()
}
}

View File

@@ -1,155 +0,0 @@
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()
}
}

View File

@@ -1,269 +0,0 @@
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
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: filesList.contentHeight > filesList.height && (filesList.currentIndex < filesList.count - 1 || filesList.contentY < filesList.contentHeight - filesList.height - 1)
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
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;
const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
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.widgetBaseHoverColor : fileMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
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
}
}
}

View File

@@ -1,858 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
Item {
id: spotlightKeyHandler
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property alias appLauncher: appLauncher
property alias searchField: searchField
property alias fileSearchController: fileSearchController
property alias resultsView: resultsView
property var parentModal: null
property string searchMode: "apps"
property bool usePopupContextMenu: false
property bool editMode: false
property var editingApp: null
property string editAppId: ""
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;
}
}
}
function openEditMode(app) {
if (!app)
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
const existing = SessionData.getAppOverride(editAppId);
editNameField.text = existing?.name || "";
editIconField.text = existing?.icon || "";
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
function closeEditMode() {
editMode = false;
editingApp = null;
editAppId = "";
Qt.callLater(() => searchField.forceActiveFocus());
}
function saveAppOverride() {
const override = {};
if (editNameField.text.trim())
override.name = editNameField.text.trim();
if (editIconField.text.trim())
override.icon = editIconField.text.trim();
if (editCommentField.text.trim())
override.comment = editCommentField.text.trim();
if (editEnvVarsField.text.trim())
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeEditMode();
}
onSearchModeChanged: {
if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false;
} else {
fileSearchController.keyboardNavigationActive = false;
}
}
anchors.fill: parent
focus: true
clip: false
Keys.onPressed: event => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
}
return;
}
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") {
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectNextInRow() : 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") {
I18n.isRtl ? appLauncher.selectPreviousInRow() : appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
I18n.isRtl ? appLauncher.selectNextInRow() : 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;
} else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10) {
if (searchMode === "apps" && appLauncher.model.count > 0) {
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (selectedApp && menu && resultsView) {
const itemPos = resultsView.getSelectedItemPosition();
const contentPos = resultsView.mapToItem(spotlightKeyHandler, itemPos.x, itemPos.y);
menu.show(contentPos.x, contentPos.y, selectedApp, true);
}
}
event.accepted = true;
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.spotlightModalViewMode
gridColumns: SettingsData.appLauncherGridColumns
onAppLaunched: () => {
if (parentModal)
parentModal.hide();
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview();
}
}
onViewModeSelected: mode => {
SettingsData.set("spotlightModalViewMode", mode);
}
}
FileSearchController {
id: fileSearchController
onFileOpened: () => {
if (parentModal)
parentModal.hide();
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview();
}
}
}
SpotlightContextMenuPopup {
id: popupContextMenu
parent: spotlightKeyHandler
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
searchField: spotlightKeyHandler.searchField
visible: false
z: 1000
}
MouseArea {
anchors.fill: parent
visible: usePopupContextMenu && popupContextMenu.visible
hoverEnabled: true
z: 999
onClicked: popupContextMenu.hide()
}
Loader {
id: layerContextMenuLoader
active: !spotlightKeyHandler.usePopupContextMenu
asynchronous: false
sourceComponent: Component {
SpotlightContextMenu {
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
parentModal: spotlightKeyHandler.parentModal
}
}
}
Connections {
target: parentModal
function onSpotlightOpenChanged() {
if (parentModal && !parentModal.spotlightOpen) {
if (layerContextMenuLoader.item)
layerContextMenuLoader.item.hide();
popupContextMenu.hide();
if (editMode)
closeEditMode();
}
}
enabled: parentModal !== null
}
Connections {
target: popupContextMenu
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
}
Connections {
target: layerContextMenuLoader.item
function onEditAppRequested(app) {
spotlightKeyHandler.openEditMode(app);
}
enabled: layerContextMenuLoader.item !== null
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
clip: false
visible: !editMode
Item {
id: searchRow
width: parent.width - Theme.spacingS * 2
height: 56
anchors.horizontalCenter: parent.horizontalCenter
DankTextField {
id: searchField
anchors.left: parent.left
anchors.right: buttonsContainer.left
anchors.rightMargin: Theme.spacingM
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;
}
}
}
Item {
id: buttonsContainer
width: viewModeButtons.visible ? viewModeButtons.width : (fileSearchButtons.visible ? fileSearchButtons.width : 0)
height: 36
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Row {
id: viewModeButtons
spacing: Theme.spacingXS
visible: searchMode === "apps"
anchors.right: parent.right
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 {
id: fileSearchButtons
spacing: Theme.spacingXS
visible: searchMode === "files"
anchors.right: parent.right
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
opacity: parentModal?.isClosing ? 0 : 1
SpotlightResults {
id: resultsView
anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher
visible: searchMode === "apps"
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
if (menu?.show) {
const isPopup = menu.contentItem !== undefined;
if (isPopup) {
const localPos = popupContextMenu.parent.mapFromItem(null, mouseX, mouseY);
menu.show(localPos.x, localPos.y, modelData, false);
} else {
menu.show(mouseX, mouseY, modelData, false);
}
}
}
}
FileSearchResults {
id: fileSearchResults
anchors.fill: parent
fileSearchController: spotlightKeyHandler.fileSearchController
visible: searchMode === "files"
}
}
}
FocusScope {
id: editView
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: editMode
focus: editMode
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
closeEditMode();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_S:
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
return;
case Qt.Key_R:
if ((event.modifiers & Qt.ControlModifier) && SessionData.getAppOverride(editAppId) !== null) {
resetAppOverride();
event.accepted = true;
}
return;
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 20
color: Theme.surfaceText
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Image {
width: 40
height: 40
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Edit App")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: editingApp?.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outlineMedium
}
Flickable {
width: parent.width
height: parent.height - y - buttonsRow.height - Theme.spacingM
contentHeight: editFieldsColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: editFieldsColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Name")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editNameField
width: parent.width
placeholderText: editingApp?.name || ""
keyNavigationTab: editIconField
keyNavigationBacktab: editExtraFlagsField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editIconField
width: parent.width
placeholderText: editingApp?.icon || ""
keyNavigationTab: editCommentField
keyNavigationBacktab: editNameField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editCommentField
width: parent.width
placeholderText: editingApp?.comment || ""
keyNavigationTab: editEnvVarsField
keyNavigationBacktab: editIconField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Environment Variables")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "KEY=value KEY2=value2"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankTextField {
id: editEnvVarsField
width: parent.width
placeholderText: "VAR=value"
keyNavigationTab: editExtraFlagsField
keyNavigationBacktab: editCommentField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Extra Arguments")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editExtraFlagsField
width: parent.width
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
Row {
id: buttonsRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
id: resetButton
width: 90
height: 40
radius: Theme.cornerRadius
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
visible: SessionData.getAppOverride(editAppId) !== null
StyledText {
text: I18n.tr("Reset")
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: resetButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: resetAppOverride()
}
}
Rectangle {
id: cancelButton
width: 90
height: 40
radius: Theme.cornerRadius
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
StyledText {
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Rectangle {
id: saveButton
width: 90
height: 40
radius: Theme.cornerRadius
color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: saveButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: saveAppOverride()
}
}
}
}
}
Loader {
id: filenameTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: contentTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -1,119 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals.Spotlight
PanelWindow {
id: root
WlrLayershell.namespace: "dms:spotlight-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
property var appLauncher: null
property var parentHandler: null
property var parentModal: null
property real menuPositionX: 0
property real menuPositionY: 0
signal editAppRequested(var app)
readonly property real shadowBuffer: 5
screen: parentModal?.effectiveScreen
function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false;
menuContent.currentApp = app;
let screenX = x;
let screenY = y;
if (parentModal) {
if (fromKeyboard) {
screenX = x + parentModal.alignedX;
screenY = y + parentModal.alignedY;
} else {
screenX = x + (parentModal.alignedX - shadowBuffer);
screenY = y + (parentModal.alignedY - shadowBuffer);
}
}
menuPositionX = screenX;
menuPositionY = screenY;
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true;
visible = true;
if (parentHandler) {
parentHandler.enabled = false;
}
Qt.callLater(() => {
menuContent.keyboardHandler.forceActiveFocus();
});
}
function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
visible = false;
}
visible: false
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
onVisibleChanged: {
if (!visible && parentHandler) {
parentHandler.enabled = true;
}
}
SpotlightContextMenuContent {
id: menuContent
x: {
const left = 10;
const right = root.width - width - 10;
const want = menuPositionX;
return Math.max(left, Math.min(right, want));
}
y: {
const top = 10;
const bottom = root.height - height - 10;
const want = menuPositionY;
return Math.max(top, Math.min(bottom, want));
}
appLauncher: root.appLauncher
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
}
MouseArea {
anchors.fill: parent
z: -1
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: root.hide()
}
}

View File

@@ -1,411 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var currentApp: null
property var appLauncher: null
property int selectedMenuIndex: 0
property bool keyboardNavigation: false
signal hideRequested
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property var actualItem: (currentApp && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function getPluginContextMenuActions() {
if (!currentApp || !currentApp.isPlugin || !actualItem)
return [];
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId) {
console.log("[ContextMenu] No pluginId found for item:", JSON.stringify(actualItem.categories));
return [];
}
const instance = PluginService.pluginInstances[pluginId];
if (!instance) {
console.log("[ContextMenu] No instance for pluginId:", pluginId);
return [];
}
if (typeof instance.getContextMenuActions !== "function") {
console.log("[ContextMenu] Instance has no getContextMenuActions:", pluginId);
return [];
}
const actions = instance.getContextMenuActions(actualItem);
if (!Array.isArray(actions))
return [];
return actions;
}
function executePluginAction(actionData) {
if (!currentApp || !actualItem)
return;
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId)
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof actionData === "function") {
actionData();
} else if (typeof instance.executeContextMenuAction === "function") {
instance.executeContextMenuAction(actualItem, actionData);
}
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
readonly property bool isRegularApp: desktopEntry && !currentApp?.isPlugin && !currentApp?.isCore && !currentApp?.isAction && !currentApp?.isBuiltInLauncher
signal editAppRequested(var app)
function hideCurrentApp() {
if (!desktopEntry)
return;
const appId = desktopEntry.id || desktopEntry.execString || "";
SessionData.hideApp(appId);
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
function editCurrentApp() {
if (!desktopEntry)
return;
editAppRequested(desktopEntry);
hideRequested();
}
readonly property var menuItems: {
const items = [];
if (currentApp && currentApp.isPlugin) {
const pluginActions = getPluginContextMenuActions();
for (let i = 0; i < pluginActions.length; i++) {
const act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "",
text: act.text || act.name || "",
action: () => executePluginAction(act.action)
});
}
if (items.length === 0) {
items.push({
type: "item",
icon: "content_copy",
text: I18n.tr("Copy"),
action: launchCurrentApp
});
}
return items;
}
const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : "";
const isPinned = SessionData.isPinnedApp(appId);
items.push({
type: "item",
icon: isPinned ? "keep_off" : "push_pin",
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
action: togglePin
});
if (isRegularApp) {
items.push({
type: "item",
icon: "visibility_off",
text: I18n.tr("Hide App"),
action: hideCurrentApp
});
items.push({
type: "item",
icon: "edit",
text: I18n.tr("Edit App"),
action: editCurrentApp
});
}
if (desktopEntry && desktopEntry.actions) {
items.push({
type: "separator"
});
for (let i = 0; i < desktopEntry.actions.length; i++) {
const act = desktopEntry.actions[i];
items.push({
type: "item",
text: act.name || "",
action: () => launchAction(act)
});
}
}
items.push({
type: "separator",
hidden: !desktopEntry || !desktopEntry.actions || desktopEntry.actions.length === 0
});
items.push({
type: "item",
icon: "launch",
text: I18n.tr("Launch"),
action: launchCurrentApp
});
if (SessionService.nvidiaCommand) {
items.push({
type: "separator"
});
items.push({
type: "item",
icon: "memory",
text: I18n.tr("Launch on dGPU"),
action: launchWithNvidia
});
}
return items;
}
readonly property int visibleItemCount: {
let count = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
count++;
}
}
return count;
}
function selectNext() {
if (visibleItemCount > 0) {
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
}
}
function selectPrevious() {
if (visibleItemCount > 0) {
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
}
}
function togglePin() {
if (!desktopEntry)
return;
const appId = desktopEntry.id || desktopEntry.execString || "";
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId);
else
SessionData.addPinnedApp(appId);
hideRequested();
}
function launchCurrentApp() {
if (currentApp && appLauncher)
appLauncher.launchApp(currentApp);
hideRequested();
}
function launchWithNvidia() {
if (desktopEntry) {
SessionService.launchDesktopEntry(desktopEntry, true);
if (appLauncher && currentApp) {
appLauncher.appLaunched(currentApp);
}
}
hideRequested();
}
function launchAction(action) {
if (desktopEntry) {
SessionService.launchDesktopAction(desktopEntry, action);
if (appLauncher && currentApp) {
appLauncher.appLaunched(currentApp);
}
}
hideRequested();
}
function activateSelected() {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
if (itemIndex === selectedMenuIndex) {
menuItems[i].action();
return;
}
itemIndex++;
}
}
}
property alias keyboardHandler: keyboardHandler
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
width: implicitWidth
height: implicitHeight
Rectangle {
id: menuContainer
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
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
}
Item {
id: keyboardHandler
anchors.fill: parent
focus: keyboardNavigation
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
selectNext();
event.accepted = true;
break;
case Qt.Key_Up:
selectPrevious();
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
activateSelected();
event.accepted = true;
break;
case Qt.Key_Escape:
case Qt.Key_Left:
hideRequested();
event.accepted = true;
break;
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Repeater {
model: menuItems
Item {
width: parent.width
height: modelData.type === "separator" ? 5 : 32
visible: !modelData.hidden
property int itemIndex: {
let count = 0;
for (let i = 0; i < index; i++) {
if (menuItems[i].type === "item" && !menuItems[i].hidden) {
count++;
}
}
return count;
}
Rectangle {
visible: modelData.type === "separator"
width: parent.width - Theme.spacingS * 2
height: parent.height
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: modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: {
if (keyboardNavigation && selectedMenuIndex === itemIndex) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
}
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
width: Theme.iconSize - 2
height: Theme.iconSize - 2
anchors.verticalCenter: parent.verticalCenter
DankIcon {
visible: modelData.icon !== undefined && modelData.icon !== ""
name: modelData.icon || ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
keyboardNavigation = false;
selectedMenuIndex = itemIndex;
}
onClicked: modelData.action()
}
}
}
}
}
}
}
}

View File

@@ -1,90 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Spotlight
Popup {
id: root
property var appLauncher: null
property var parentHandler: null
property var searchField: null
signal editAppRequested(var app)
function show(x, y, app, fromKeyboard) {
fromKeyboard = fromKeyboard || false;
menuContent.currentApp = app;
root.x = x + 4;
root.y = y + 4;
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
menuContent.keyboardNavigation = true;
if (parentHandler) {
parentHandler.enabled = false;
}
open();
}
onOpened: {
Qt.callLater(() => {
menuContent.keyboardHandler.forceActiveFocus();
});
}
function hide() {
if (parentHandler) {
parentHandler.enabled = true;
}
close();
}
width: menuContent.implicitWidth
height: menuContent.implicitHeight
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
onClosed: {
if (parentHandler) {
parentHandler.enabled = true;
}
if (searchField?.visible) {
Qt.callLater(() => {
searchField.forceActiveFocus();
});
}
}
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
}
}
contentItem: SpotlightContextMenuContent {
id: menuContent
appLauncher: root.appLauncher
onHideRequested: root.hide()
onEditAppRequested: app => root.editAppRequested(app)
}
}

View File

@@ -1,181 +0,0 @@
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
DankModal {
id: spotlightModal
layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal.contentWindow]
active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance
property bool openedFromOverview: false
property bool isClosing: false
function resetContent() {
if (!spotlightContent)
return;
if (spotlightContent.appLauncher)
spotlightContent.appLauncher.reset();
if (spotlightContent.fileSearchController)
spotlightContent.fileSearchController.reset();
if (spotlightContent.resetScroll)
spotlightContent.resetScroll();
if (spotlightContent.searchField)
spotlightContent.searchField.text = "";
spotlightContent.searchMode = "apps";
}
function show() {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus();
});
}
function showWithQuery(query) {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
if (spotlightContent?.searchField)
spotlightContent.searchField.text = query;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher) {
spotlightContent.appLauncher.ensureInitialized();
spotlightContent.appLauncher.searchQuery = query;
}
if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus();
});
}
function showWithEditApp(app) {
openedFromOverview = false;
isClosing = false;
resetContent();
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.openEditMode)
spotlightContent.openEditMode(app);
});
}
function hide() {
openedFromOverview = false;
isClosing = true;
spotlightOpen = false;
close();
}
onDialogClosed: {
isClosing = false;
resetContent();
}
function toggle() {
if (spotlightOpen) {
hide();
} else {
show();
}
}
shouldBeVisible: spotlightOpen
modalWidth: 500
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
animationScaleCollapsed: 0.96
animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
animationExitCurve: Theme.expressiveCurves.emphasized
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
}

View File

@@ -1,267 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: resultsContainer
property var appLauncher: null
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function resetScroll() {
resultsList.contentY = 0;
if (gridLoader.item) {
gridLoader.item.contentY = 0;
}
}
function getSelectedItemPosition() {
if (!appLauncher)
return {
x: 0,
y: 0
};
const selectedIndex = appLauncher.selectedIndex;
if (appLauncher.viewMode === "list") {
const itemY = selectedIndex * (resultsList.itemHeight + resultsList.itemSpacing) - resultsList.contentY;
return {
x: resultsList.width / 2,
y: itemY + resultsList.itemHeight / 2
};
} else if (gridLoader.item) {
const grid = gridLoader.item;
const row = Math.floor(selectedIndex / grid.actualColumns);
const col = selectedIndex % grid.actualColumns;
const itemX = col * grid.cellWidth + grid.leftMargin + grid.cellWidth / 2;
const itemY = row * grid.cellHeight - grid.contentY + grid.cellHeight / 2;
return {
x: itemX,
y: itemY
};
}
return {
x: 0,
y: 0
};
}
radius: Theme.cornerRadius
color: "transparent"
clip: true
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 32
z: 100
visible: {
if (!appLauncher)
return false;
const view = appLauncher.viewMode === "list" ? resultsList : (gridLoader.item || resultsList);
const isLastItem = appLauncher.viewMode === "list" ? view.currentIndex >= view.count - 1 : (gridLoader.item ? Math.floor(view.currentIndex / view.actualColumns) >= Math.floor((view.count - 1) / view.actualColumns) : false);
const hasOverflow = view.contentHeight > view.height;
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
return hasOverflow && (!isLastItem || !atBottom);
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
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;
const fadeHeight = 32;
const isLastItem = index === count - 1;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
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) => {
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
}
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) => {
resultsList.itemRightClicked(idx, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
}
}
Loader {
id: gridLoader
property real _lastWidth: 0
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: 1
visible: appLauncher && appLauncher.viewMode === "grid"
active: appLauncher && appLauncher.viewMode === "grid"
asynchronous: false
onLoaded: {
if (item) {
item.appLauncher = Qt.binding(() => resultsContainer.appLauncher);
}
}
onWidthChanged: {
if (visible && Math.abs(width - _lastWidth) > 1) {
_lastWidth = width;
active = false;
Qt.callLater(() => {
active = true;
});
}
}
sourceComponent: Component {
DankGridView {
id: resultsGrid
property var appLauncher: null
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
property int columns: appLauncher ? appLauncher.gridColumns : 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
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 real baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : width / columns
property real 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;
const fadeHeight = 32;
const isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
}
anchors.fill: parent
model: appLauncher ? appLauncher.model : null
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
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) => {
resultsContainer.itemRightClicked(index, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false;
}
delegate: AppLauncherGridDelegate {
gridView: resultsGrid
cellWidth: resultsGrid.cellWidth
cellHeight: resultsGrid.cellHeight
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) => {
resultsGrid.itemRightClicked(idx, modelData, mouseX, mouseY);
}
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
}
}
}
}
}