Files
illogical-impulse/dots/.config/quickshell/ii/modules/waffle/startMenu/searchPage/SearchResults.qml
T
2025-12-07 00:04:24 +01:00

257 lines
9.3 KiB
QML

pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import qs.modules.common.functions
import qs.modules.common.models
import qs.modules.waffle.startMenu
import Quickshell
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick
RowLayout {
id: root
property int maxResultsPerCategory: 4
property StartMenuContext context
property int currentIndex: context.currentIndex
onCurrentIndexChanged: {
forceCurrentIndex(currentIndex);
}
function focusFirstItem() {
forceCurrentIndex(0);
}
function forceCurrentIndex(index) {
context.currentIndex = index;
// Somehow this hack is needed
if (index === 0) {
resultList.incrementCurrentIndex();
resultList.decrementCurrentIndex();
} else {
resultList.decrementCurrentIndex();
resultList.incrementCurrentIndex();
}
}
Connections {
target: context
function onAccepted() {
resultList.currentItem?.execute();
}
}
ResultList {
id: resultList
Layout.fillHeight: true
Layout.fillWidth: true
}
ResultPreview {
Layout.preferredWidth: 386
Layout.leftMargin: 1
Layout.rightMargin: 1
entry: resultList.model[resultList.currentIndex] ?? searchResultComp.createObject()
}
component ResultList: WListView {
id: resultListView
section {
criteria: ViewSection.FullString
property: "category" // This is "type" with tweaks to make it match more closely
labelPositioning: ViewSection.InlineLabels
delegate: Item {
id: sectionButton
required property string section
implicitHeight: sectionChoiceButton.implicitHeight + resultListView.spacing
width: ListView.view?.width
WChoiceButton {
id: sectionChoiceButton
anchors {
left: parent.left
right: parent.right
top: parent.top
}
implicitHeight: 38
contentItem: WText {
text: sectionButton.section
font.pixelSize: Looks.font.pixelSize.large
font.weight: Looks.font.weight.strong
}
onClicked: {
root.context.selectCategory(sectionButton.section);
}
}
}
}
clip: true
spacing: 4
currentIndex: root.currentIndex
// We can't use a ScriptModel here because it would mess up sections
model: {
const allResults = LauncherSearch.results;
// Find categories
var categories = new Set();
for (let i = 0; i < allResults.length; i++) {
categories.add(allResults[i].type);
}
// Collect max 4 per category
var categorizedResults = [];
categories.forEach(category => {
let count = 0;
for (let i = 0; i < allResults.length; i++) {
if (allResults[i].type === category) {
const entry = allResults[i];
const tweakedEntry = searchResultComp.createObject(null, Object.assign({}, entry));
tweakedEntry.category = categorizedResults.length === 0 ? Translation.tr("Best match") : entry.type
categorizedResults.push(tweakedEntry); // Section header
count++;
if (count >= root.maxResultsPerCategory) {
break;
}
}
}
});
// print(JSON.stringify(categorizedResults, null, 2));
return categorizedResults;
}
onModelChanged: {
root.focusFirstItem();
}
delegate: SearchResultButton {
required property int index
required property var modelData
entry: modelData
firstEntry: index === 0
width: ListView.view?.width
checked: resultListView.currentIndex === index
onRequestFocus: {
root.forceCurrentIndex(index);
}
}
}
component ResultPreview: Rectangle {
id: resultPreview
property LauncherSearchResult entry // LauncherSearchResult
Layout.fillHeight: true
color: Looks.colors.bg1
radius: Looks.radius.large
ColumnLayout {
anchors.fill: parent
anchors.margins: 22
spacing: 13
ColumnLayout {
id: mainInfoColumn
Layout.alignment: Qt.AlignHCenter
SearchEntryIcon {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 10
Layout.bottomMargin: 12
entry: resultPreview.entry
iconSize: 64
}
WText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
text: resultPreview.entry?.name || ""
font.pixelSize: Looks.font.pixelSize.xlarger
}
WText {
Layout.alignment: Qt.AlignHCenter
text: resultPreview.entry?.type || ""
color: Looks.colors.accentUnfocused
font.pixelSize: Looks.font.pixelSize.normal
}
}
Rectangle {
id: resultSeparator
implicitHeight: 2
Layout.topMargin: 16
Layout.fillWidth: true
color: Looks.colors.bg2Hover
}
WListView {
id: actionsColumn
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
spacing: 2
model: {
const isAppEntry = resultPreview.entry.type === Translation.tr("App");
const appId = isAppEntry ? resultPreview.entry.id : "";
const pinned = isAppEntry ? (Config.options.dock.pinnedApps.includes(appId)) : false;
const startPinned = isAppEntry ? (Config.options.launcher.pinnedApps.includes(appId)) : false;
var result = [
searchResultComp.createObject(null, {
name: resultPreview.entry.verb,
iconName: isAppEntry ? "open_in_new" : "keyboard_return",
iconType: LauncherSearchResult.IconType.Material,
execute: () => {
resultPreview.entry.execute();
}
}),
...(isAppEntry ? [
searchResultComp.createObject(null, {
name: startPinned ? Translation.tr("Unpin from Start") : Translation.tr("Pin to Start"),
iconName: startPinned ? "keep_off" : "keep",
iconType: LauncherSearchResult.IconType.Material,
execute: () => {
LauncherApps.togglePin(appId);
}
})
] : []),
...(isAppEntry ? [
searchResultComp.createObject(null, {
name: pinned ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar"),
iconName: pinned ? "keep_off" : "keep",
iconType: LauncherSearchResult.IconType.Material,
execute: () => {
TaskbarApps.togglePin(appId);
}
})
] : []),
];
result = result.concat(resultPreview.entry.actions);
return result;
}
delegate: WButton {
id: actionButton
required property var modelData
width: ListView.view?.width
icon.name: modelData.iconName
text: modelData.name
onClicked: modelData.execute();
contentItem: RowLayout {
spacing: 11
SearchEntryIcon {
entry: actionButton.modelData
iconSize: 16
}
WText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
text: actionButton.text
}
}
}
}
}
}
Component {
id: searchResultComp
LauncherSearchResult {}
}
}