mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
500 lines
18 KiB
QML
500 lines
18 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell.Widgets
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Modals.DLauncherV2.Components
|
|
import "../../Common/LauncherKeybinds.js" as LauncherKeybinds
|
|
|
|
FocusScope {
|
|
id: root
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
property var parentModal: null
|
|
property alias searchField: searchInput
|
|
property var controllerOverride: null
|
|
readonly property var controller: controllerOverride ?? searchController
|
|
readonly property alias activeContextMenu: contextMenu
|
|
property var transientSurfaceTracker: null
|
|
property bool showResultsWithoutQuery: false
|
|
property bool suspendSearchUpdates: false
|
|
property real maxResultsHeight: 0
|
|
// Off for hosts that animate their own size, where this would only slide the bottom fade.
|
|
property bool resizeAnimated: true
|
|
property real resultsInset: LauncherMetrics.spotlightInset
|
|
|
|
property bool editMode: false
|
|
property var editingApp: null
|
|
property string editAppId: ""
|
|
|
|
readonly property bool _hasQuery: root.showResultsWithoutQuery || searchInput.text.length > 0 || root.controller.searchMode === "dmenu"
|
|
readonly property real _searchBarH: LauncherMetrics.pillHeight
|
|
readonly property real _searchAreaH: _searchBarH
|
|
readonly property alias searchAreaHeight: root._searchAreaH
|
|
readonly property real actionPanelHeight: actionPanel.height
|
|
readonly property real _statusH: Theme.listItemTwoLineHeight + Theme.spacingXL
|
|
readonly property bool _firstSectionShowsHeader: !!(root.controller.sections?.[0]?.title?.length > 0)
|
|
readonly property real _resultsTopGap: _firstSectionShowsHeader ? LauncherMetrics.resultsGap : root.resultsInset
|
|
readonly property real _rowsHeightBudget: _resultsTopGap + ((root.controller.searchMode === "dmenu" && root.controller.dmenuLines > 0) ? (root.controller.dmenuLines * (LauncherMetrics.rowHeight + LauncherMetrics.rowGap) + (_firstSectionShowsHeader ? LauncherMetrics.sectionBand : 0) + LauncherMetrics.fadeLength) : LauncherMetrics.maxResultsHeight)
|
|
readonly property real _maxResultsH: root.maxResultsHeight > 0 ? root.maxResultsHeight : Math.max(0, Math.min(_rowsHeightBudget + resultsList.bottomInset, (parentModal?.screenHeight ?? Theme.mediumBreakpoint) - (parentModal?.modalY ?? 0) - LauncherMetrics.pillHeight - actionPanel.height - Theme.spacingL))
|
|
readonly property real _resultsContentH: resultsList.contentHeight > 0 ? _resultsTopGap + resultsList.contentHeight + resultsList.bottomInset : _statusH
|
|
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _resultsTopGap + resultsList.fittedHeight + resultsList.bottomInset) : 0
|
|
readonly property int _resizeDuration: Theme.expressiveDurations.expressiveFastSpatial
|
|
readonly property real _frameClipRadius: Math.max(0, (parentModal?.frameBottomRadius ?? 0) - resultsInset)
|
|
readonly property real _editAvailableH: maxResultsHeight > 0 ? maxResultsHeight + _searchAreaH : Math.max(LauncherMetrics.maxResultsHeight, (parentModal?.screenHeight ?? Theme.mediumBreakpoint) - (parentModal?.modalY ?? 0) - Theme.spacingL)
|
|
readonly property real _editH: Math.min(editLoader.implicitHeight, _editAvailableH)
|
|
|
|
implicitHeight: editMode ? _editH + Theme.spacingM * 2 : _searchAreaH + mesgRow.height + resultsContainer.height + actionPanel.height
|
|
|
|
property bool _animateResize: false
|
|
|
|
Component.onCompleted: resizeAnimEnableTimer.restart()
|
|
|
|
Timer {
|
|
id: resizeAnimEnableTimer
|
|
interval: Theme.expressiveDurations.expressiveEffects
|
|
onTriggered: root._animateResize = true
|
|
}
|
|
|
|
function resetScroll() {
|
|
resultsList.resetScroll();
|
|
}
|
|
|
|
function resetSearch() {
|
|
if (root.controller.searchMode === "dmenu") {
|
|
root.controller.setSearchQuery("");
|
|
return;
|
|
}
|
|
root.controller.reset();
|
|
if (root.showResultsWithoutQuery)
|
|
root.controller.performSearch();
|
|
}
|
|
|
|
function closeTransientUi() {
|
|
transientSurfaceTracker?.closeAll?.();
|
|
actionPanel.hide();
|
|
root._clearEditState();
|
|
root.enabled = true;
|
|
}
|
|
|
|
function _clearEditState() {
|
|
editMode = false;
|
|
editingApp = null;
|
|
editAppId = "";
|
|
}
|
|
|
|
function openEditMode(app) {
|
|
if (!app)
|
|
return;
|
|
editingApp = app;
|
|
editAppId = app.id || app.execString || app.exec || "";
|
|
editMode = true;
|
|
}
|
|
|
|
function closeEditMode() {
|
|
root._clearEditState();
|
|
Qt.callLater(root._focusSearch);
|
|
}
|
|
|
|
function _focusSearch() {
|
|
searchInput.forceActiveFocus();
|
|
searchInput.cursorPosition = searchInput.text.length;
|
|
}
|
|
|
|
function _showContextMenu(item, sceneX, sceneY, fromKeyboard) {
|
|
if (!item || !contextMenu.hasContextMenuActions(item))
|
|
return;
|
|
const localPos = root.mapFromItem(null, sceneX, sceneY);
|
|
contextMenu.show(localPos.x, localPos.y, item, fromKeyboard);
|
|
}
|
|
|
|
function _handleKey(event) {
|
|
if (root.editMode) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.closeEditMode();
|
|
event.accepted = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const hasCtrl = event.modifiers & Qt.ControlModifier;
|
|
|
|
if (root.controller.searchMode === "dmenu") {
|
|
const kbN = root.controller.matchDmenuKeybind(event);
|
|
if (kbN > 0) {
|
|
root.controller.acceptDmenuViaKeybind(kbN);
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const launcherAction = LauncherKeybinds.actionForEvent(event, SettingsData.launcherKeybinds);
|
|
if (launcherAction) {
|
|
actionPanel.hide();
|
|
if (launcherAction.mode)
|
|
root.controller.setMode(launcherAction.mode);
|
|
else
|
|
_cycleCategory(launcherAction.id === "previousMode");
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case Qt.Key_Escape:
|
|
if (actionPanel.expanded) {
|
|
actionPanel.hide();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
if (root.controller.clearPluginFilter()) {
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
if (root.parentModal?.attemptUserCancel)
|
|
root.parentModal.attemptUserCancel();
|
|
else
|
|
root.parentModal?.hide();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Backspace:
|
|
if (searchInput.text.length === 0) {
|
|
if (root.controller.clearPluginFilter()) {
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
if (root.controller.autoSwitchedToFiles) {
|
|
root.controller.restorePreviousMode();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
}
|
|
event.accepted = false;
|
|
return;
|
|
case Qt.Key_Down:
|
|
root.controller.selectNext();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Up:
|
|
root.controller.selectPrevious();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Right:
|
|
case Qt.Key_Left:
|
|
if (root.controller.getCurrentSectionViewMode() === "list")
|
|
break;
|
|
if ((event.key === Qt.Key_Right) !== I18n.isRtl)
|
|
root.controller.selectRight();
|
|
else
|
|
root.controller.selectLeft();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_H:
|
|
case Qt.Key_L:
|
|
if (!hasCtrl || root.controller.getCurrentSectionViewMode() === "list")
|
|
break;
|
|
if ((event.key === Qt.Key_L) !== I18n.isRtl)
|
|
root.controller.selectRight();
|
|
else
|
|
root.controller.selectLeft();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_PageDown:
|
|
root.controller.selectPageDown(resultsList.pageRows);
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_PageUp:
|
|
root.controller.selectPageUp(resultsList.pageRows);
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_J:
|
|
if (hasCtrl) {
|
|
root.controller.selectNext();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
break;
|
|
case Qt.Key_K:
|
|
if (hasCtrl) {
|
|
root.controller.selectPrevious();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
break;
|
|
case Qt.Key_N:
|
|
case Qt.Key_P:
|
|
if (!hasCtrl)
|
|
break;
|
|
if (event.key === Qt.Key_N)
|
|
root.controller.selectNextSection();
|
|
else
|
|
root.controller.selectPreviousSection();
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Tab:
|
|
if (!hasCtrl && actionPanel.hasActions) {
|
|
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
|
}
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Backtab:
|
|
if (!hasCtrl && actionPanel.hasActions) {
|
|
actionPanel.expanded ? actionPanel.cycleAction(true) : actionPanel.show();
|
|
}
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Return:
|
|
case Qt.Key_Enter:
|
|
if (event.modifiers & Qt.ShiftModifier) {
|
|
if (root.controller.searchMode === "dmenu" && root.controller.dmenuMultiSelect)
|
|
root.controller.toggleDmenuMultiSelectHighlighted();
|
|
else
|
|
root.controller.pasteSelected();
|
|
} else if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
|
|
actionPanel.executeSelectedAction();
|
|
} else {
|
|
root.controller.executeSelected(!!(event.modifiers & Qt.ControlModifier));
|
|
}
|
|
event.accepted = true;
|
|
return;
|
|
case Qt.Key_Menu:
|
|
case Qt.Key_F10:
|
|
if (contextMenu.hasContextMenuActions(root.controller.selectedItem)) {
|
|
const scenePos = resultsList.getSelectedItemPosition();
|
|
_showContextMenu(root.controller.selectedItem, scenePos.x, scenePos.y, true);
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
event.accepted = false;
|
|
}
|
|
|
|
Controller {
|
|
id: searchController
|
|
active: !root.controllerOverride && (root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true)
|
|
viewModeContext: "spotlight"
|
|
forceLinearNavigation: false
|
|
}
|
|
|
|
LauncherContextMenu {
|
|
id: contextMenu
|
|
parent: root
|
|
controller: root.controller
|
|
searchField: searchInput
|
|
parentHandler: root
|
|
transientSurfaceTracker: root.transientSurfaceTracker
|
|
|
|
onEditAppRequested: app => root.openEditMode(app)
|
|
}
|
|
|
|
Connections {
|
|
target: root.parentModal
|
|
ignoreUnknownSignals: true
|
|
|
|
function onSpotlightOpenChanged() {
|
|
if (!root.parentModal?.spotlightOpen)
|
|
root.closeTransientUi();
|
|
}
|
|
|
|
function onContentVisibleChanged() {
|
|
if (!root.parentModal?.contentVisible) {
|
|
root.closeTransientUi();
|
|
return;
|
|
}
|
|
root._animateResize = false;
|
|
resizeAnimEnableTimer.restart();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.controller
|
|
|
|
function onSelectedItemChanged() {
|
|
if (actionPanel.expanded)
|
|
actionPanel.hide();
|
|
}
|
|
|
|
function onItemExecuted() {
|
|
root.parentModal?.hide();
|
|
CompositorService.closeNiriOverviewOnWindowFocus();
|
|
}
|
|
function onModeChanged(mode, userInitiated) {
|
|
if (!userInitiated || !SettingsData.rememberLastMode)
|
|
return;
|
|
SessionData.setLauncherLastMode(mode);
|
|
}
|
|
function onSearchQueryRequested(query) {
|
|
searchInput.text = query;
|
|
root._focusSearch();
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: searchBarItem
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: root._searchAreaH
|
|
visible: !root.editMode
|
|
|
|
LauncherSearchField {
|
|
id: searchInput
|
|
readonly property bool _isDmenu: root.controller.searchMode === "dmenu"
|
|
pluginName: _isDmenu ? root.controller.dmenuPrompt : root.controller.activePluginName
|
|
pluginIcon: _isDmenu ? root.controller.dmenuIcon : (root.controller.activePluginId ? root.controller.getPluginMetadata(root.controller.activePluginId).icon : "")
|
|
pluginIconVisible: _isDmenu ? !!root.controller.dmenuIcon : true
|
|
anchors.fill: parent
|
|
mode: root.controller.searchMode
|
|
showModes: SettingsData.spotlightBarShowModeChips || root._hasQuery
|
|
flat: true
|
|
onModeSelected: mode => root._selectMode(mode)
|
|
placeholderText: _isDmenu ? root.controller.dmenuPlaceholder : I18n.tr("Spotlight Search")
|
|
echoMode: (_isDmenu && root.controller.dmenuPassword) ? TextInput.Password : TextInput.Normal
|
|
hidePlaceholderOnFocus: false
|
|
ignoreUpDownKeys: true
|
|
ignoreTabKeys: true
|
|
keyForwardTargets: [searchKeyHandler]
|
|
|
|
onTextChanged: {
|
|
if (root.suspendSearchUpdates)
|
|
return;
|
|
actionPanel.hide();
|
|
if (text.length > 0) {
|
|
root.controller.setSearchQuery(text);
|
|
return;
|
|
}
|
|
root.resetSearch();
|
|
}
|
|
|
|
Item {
|
|
id: searchKeyHandler
|
|
Keys.onPressed: event => root._handleKey(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: mesgRow
|
|
readonly property bool _visible: root.controller.searchMode === "dmenu" && root.controller.dmenuMesg.length > 0
|
|
anchors.top: searchBarItem.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: root.resultsInset
|
|
anchors.rightMargin: root.resultsInset
|
|
height: _visible ? mesgText.implicitHeight + Theme.spacingS * 2 : 0
|
|
visible: _visible && !root.editMode
|
|
|
|
StyledText {
|
|
id: mesgText
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.topMargin: Theme.spacingS
|
|
text: root.controller.dmenuMesg
|
|
wrapMode: Text.WordWrap
|
|
color: Theme.onSurfaceVariant
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.top: mesgRow.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: Theme.outlineWidth
|
|
z: 1
|
|
color: Theme.outlineVariant
|
|
visible: !root.editMode && root._hasQuery && !(Theme.focusRingWidth > 0 && searchInput.getActiveFocus())
|
|
}
|
|
|
|
ClippingRectangle {
|
|
id: resultsContainer
|
|
anchors.top: mesgRow.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: root.resultsInset
|
|
anchors.rightMargin: root.resultsInset
|
|
color: "transparent"
|
|
bottomLeftRadius: actionPanel.height > 0 ? 0 : root._frameClipRadius
|
|
bottomRightRadius: bottomLeftRadius
|
|
height: root._resultsH
|
|
visible: !root.editMode
|
|
|
|
Behavior on height {
|
|
enabled: root.resizeAnimated && root._animateResize
|
|
NumberAnimation {
|
|
duration: root._resizeDuration
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.standard
|
|
}
|
|
}
|
|
|
|
ResultsList {
|
|
id: resultsList
|
|
focusReturnTarget: searchInput
|
|
keyForwardTargets: [searchKeyHandler]
|
|
readonly property real bottomInset: Theme.spacingS
|
|
availableHeight: Math.max(0, root._maxResultsH - root._resultsTopGap - bottomInset)
|
|
resizing: actionPanel.animating
|
|
anchors.fill: parent
|
|
anchors.topMargin: root._resultsTopGap
|
|
controller: root.controller
|
|
showEmptyState: root._hasQuery
|
|
transientSurfaceTracker: root.transientSurfaceTracker
|
|
|
|
onItemRightClicked: (index, item, sceneX, sceneY) => {
|
|
root._showContextMenu(item, sceneX, sceneY, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
ActionPanel {
|
|
id: actionPanel
|
|
anchors.top: resultsContainer.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
selectedItem: root.controller.selectedItem
|
|
controller: root.controller
|
|
visible: !root.editMode
|
|
}
|
|
|
|
Loader {
|
|
id: editLoader
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.margins: Theme.spacingM
|
|
height: root.editMode ? root._editH : 0
|
|
active: root.editMode
|
|
visible: active
|
|
focus: root.editMode
|
|
|
|
sourceComponent: AppEditView {
|
|
focus: true
|
|
editingApp: root.editingApp
|
|
editAppId: root.editAppId
|
|
onCloseRequested: root.closeEditMode()
|
|
}
|
|
|
|
onLoaded: item.loadOverride()
|
|
}
|
|
|
|
function _cycleCategory(reverse) {
|
|
const modes = searchInput.modes;
|
|
const idx = (searchInput.modeIndex + (reverse ? modes.length - 1 : 1)) % modes.length;
|
|
_selectMode(modes[idx].mode);
|
|
}
|
|
|
|
function _selectMode(mode) {
|
|
root.controller.setMode(mode, false);
|
|
if (root._hasQuery)
|
|
root.controller.setSearchQuery(searchInput.text);
|
|
root._focusSearch();
|
|
}
|
|
}
|