1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml
2026-03-31 11:04:18 -04:00

499 lines
19 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var controller: null
property int gridColumns: controller?.gridColumns ?? 4
property var _visualRows: []
property var _flatIndexToRowMap: ({})
property var _cumulativeHeights: []
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
function _rebuildVisualModel() {
var sections = root.controller?.sections ?? [];
var rows = [];
var indexMap = {};
var cumHeights = [];
var cumY = 0;
for (var s = 0; s < sections.length; s++) {
var section = sections[s];
var sectionId = section.id;
cumHeights.push(cumY);
rows.push({
_rowId: "h_" + sectionId,
type: "header",
section: section,
sectionId: sectionId,
height: 32
});
cumY += 32;
if (section.collapsed)
continue;
var versionTrigger = root.controller?.viewModeVersion ?? 0;
void (versionTrigger);
var mode = root.controller?.getSectionViewMode(sectionId) ?? "list";
var items = section.items ?? [];
var flatStartIndex = section.flatStartIndex ?? 0;
if (mode === "list") {
for (var i = 0; i < items.length; i++) {
var flatIdx = flatStartIndex + i;
indexMap[flatIdx] = rows.length;
cumHeights.push(cumY);
rows.push({
_rowId: items[i].id,
type: "list_item",
item: items[i],
flatIndex: flatIdx,
sectionId: sectionId,
height: 52
});
cumY += 52;
}
} else {
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var numRows = Math.ceil(items.length / cols);
for (var r = 0; r < numRows; r++) {
var rowItems = [];
for (var c = 0; c < cols; c++) {
var idx = r * cols + c;
if (idx >= items.length)
break;
var fi = flatStartIndex + idx;
indexMap[fi] = rows.length;
rowItems.push({
item: items[idx],
flatIndex: fi
});
}
cumHeights.push(cumY);
rows.push({
_rowId: "gr_" + sectionId + "_" + r,
type: "grid_row",
items: rowItems,
sectionId: sectionId,
viewMode: mode,
cols: cols,
height: cellHeight
});
cumY += cellHeight;
}
}
}
root._flatIndexToRowMap = indexMap;
root._cumulativeHeights = cumHeights;
root._visualRows = rows;
}
onGridColumnsChanged: Qt.callLater(_rebuildVisualModel)
onWidthChanged: Qt.callLater(_rebuildVisualModel)
Connections {
target: root.controller
function onSectionsChanged() {
Qt.callLater(root._rebuildVisualModel);
}
function onViewModeVersionChanged() {
Qt.callLater(root._rebuildVisualModel);
}
function onSearchModeChanged() {
root._visualRows = [];
root._cumulativeHeights = [];
root._flatIndexToRowMap = {};
}
}
function resetScroll() {
mainListView.contentY = mainListView.originY;
}
function ensureVisible(index) {
if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
return;
var entry = controller.flatModel[index];
if (!entry || entry.isHeader)
return;
var rowIndex = _flatIndexToRowMap[index];
if (rowIndex === undefined)
return;
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
var rowY = _cumulativeHeights[rowIndex];
var scrollY = mainListView.contentY - mainListView.originY;
if (rowY < scrollY + stickyHeader.height) {
mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
}
}
}
function getSelectedItemPosition() {
var fallback = mapToItem(null, width / 2, height / 2);
if (!controller?.flatModel || controller.selectedFlatIndex < 0)
return fallback;
var entry = controller.flatModel[controller.selectedFlatIndex];
if (!entry || entry.isHeader)
return fallback;
var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex];
if (rowIndex === undefined)
return fallback;
var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0;
var row = _visualRows[rowIndex];
if (!row)
return fallback;
var itemX = width / 2;
var itemH = row.height;
if (row.type === "grid_row") {
var rowItems = row.items;
for (var i = 0; i < rowItems.length; i++) {
if (rowItems[i].flatIndex === controller.selectedFlatIndex) {
var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols);
itemX = i * cellWidth + cellWidth / 2;
break;
}
}
}
var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2;
var clampedY = Math.max(40, Math.min(height - 40, visualY));
return mapToItem(null, itemX, clampedY);
}
Connections {
target: root.controller
function onSelectedFlatIndexChanged() {
if (root.controller?.keyboardNavigationActive) {
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
}
}
}
DankListView {
id: mainListView
anchors.fill: parent
clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
model: ScriptModel {
values: root._visualRows
objectProp: "_rowId"
}
add: null
remove: null
displaced: null
move: null
delegate: Item {
id: delegateRoot
required property var modelData
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
}
ResultItem {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "list_item"
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
}
}
Row {
id: gridRowContent
anchors.fill: parent
visible: delegateRoot.modelData?.type === "grid_row"
Repeater {
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
Item {
id: gridCellDelegate
required property var modelData
required property int index
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
width: cellWidth
height: delegateRoot.height
GridItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
TileItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
}
}
}
}
Rectangle {
id: bottomShadow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 24
z: 100
visible: {
if (BlurService.enabled)
return false;
if (mainListView.contentHeight <= mainListView.height)
return false;
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
if (atBottom)
return false;
var flatModel = root.controller?.flatModel;
if (!flatModel || flatModel.length === 0)
return false;
var lastItemIdx = -1;
for (var i = flatModel.length - 1; i >= 0; i--) {
if (!flatModel[i].isHeader) {
lastItemIdx = i;
break;
}
}
if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
return false;
return true;
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
Rectangle {
id: stickyHeader
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 32
z: 101
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: stickyHeaderSection !== null
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
readonly property var stickyHeaderSection: {
var scrollY = mainListView.contentY - mainListView.originY;
if (scrollY <= 0)
return null;
var rows = root._visualRows;
var heights = root._cumulativeHeights;
if (rows.length === 0 || heights.length === 0)
return null;
var lo = 0;
var hi = rows.length - 1;
while (lo < hi) {
var mid = (lo + hi + 1) >> 1;
if (mid < heights.length && heights[mid] <= scrollY)
lo = mid;
else
hi = mid - 1;
}
for (var i = lo; i >= 0; i--) {
if (rows[i].type === "header")
return rows[i].section;
}
return null;
}
SectionHeader {
width: parent.width
section: stickyHeader.stickyHeaderSection
controller: root.controller
viewMode: {
void (stickyHeader.versionTrigger);
return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
}
canChangeViewMode: {
void (stickyHeader.versionTrigger);
return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
}
canCollapse: {
void (stickyHeader.versionTrigger);
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
}
isSticky: true
}
}
Item {
anchors.centerIn: parent
visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
width: emptyColumn.implicitWidth
height: emptyColumn.implicitHeight
Column {
id: emptyColumn
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: getEmptyIcon()
size: 48
color: Theme.outlineButton
function getEmptyIcon() {
var mode = root.controller?.searchMode ?? "all";
switch (mode) {
case "files":
var fileType = root.controller?.fileSearchType ?? "all";
switch (fileType) {
case "dir":
return "folder_open";
case "file":
return "insert_drive_file";
default:
return "folder_open";
}
case "plugins":
return "extension";
case "apps":
return "apps";
default:
return "search_off";
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: getEmptyText()
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
function getEmptyText() {
var mode = root.controller?.searchMode ?? "all";
var hasQuery = root.controller?.searchQuery?.length > 0;
switch (mode) {
case "files":
if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
if (!hasQuery)
return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2)
return I18n.tr("Type at least 2 characters");
var fileType = root.controller?.fileSearchType ?? "all";
switch (fileType) {
case "dir":
return I18n.tr("No folders found");
case "file":
return I18n.tr("No files found");
default:
return I18n.tr("No results found");
}
case "plugins":
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
case "apps":
return I18n.tr("No apps found");
default:
return I18n.tr("No results found");
}
}
}
}
}
}