waffles: ctrl alt del menu

This commit is contained in:
end-4
2025-12-06 23:14:08 +01:00
parent 80a7804ade
commit 13968db31c
29 changed files with 578 additions and 90 deletions
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.models
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
Item {
id: root
required property LauncherSearchResult entry
property int iconSize: 24
implicitWidth: Math.max(iconSize, textIconLoader.implicitWidth)
implicitHeight: iconSize
Loader {
anchors.centerIn: parent
active: root.entry.iconType === LauncherSearchResult.IconType.System && root.entry.iconName !== ""
sourceComponent: WAppIcon {
implicitSize: root.iconSize
iconName: root.entry.iconName
tryCustomIcon: false
animated: false
}
}
Loader {
id: textIconLoader
anchors.centerIn: parent
active: root.entry.iconType === LauncherSearchResult.IconType.Text
sourceComponent: WText {
text: root.entry.iconName
font.pixelSize: root.iconSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Loader {
anchors.centerIn: parent
active: root.entry.iconType === LauncherSearchResult.IconType.Material || root.entry.iconType === LauncherSearchResult.IconType.None || root.entry.iconName === ""
sourceComponent: FluentIcon {
icon: root.entry.iconName ? WIcons.fluentFromMaterial(root.entry.iconName) : WIcons.guessIconForName(root.entry.name)
implicitSize: root.iconSize
animated: false
}
}
}
@@ -0,0 +1,40 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
BodyRectangle {
id: root
property alias context: searchResults.context
property string searchText: LauncherSearch.query
property alias currentIndex: searchResults.currentIndex
ColumnLayout {
anchors {
fill: parent
topMargin: 2
leftMargin: 24
rightMargin: 24
}
spacing: 12
TagStrip {
context: root.context
Layout.fillWidth: true
Layout.fillHeight: false
}
SearchResults {
id: searchResults
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
@@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.models
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
WChoiceButton {
id: root
required property LauncherSearchResult entry
property bool firstEntry: false
signal requestFocus()
checked: focus
animateChoiceHighlight: false
implicitWidth: contentLayout.implicitWidth + leftPadding + rightPadding
implicitHeight: contentLayout.implicitHeight + topPadding + bottomPadding
onClicked: {
execute();
}
function execute() {
GlobalStates.searchOpen = false;
root.entry.execute();
}
horizontalPadding: 0
verticalPadding: 0
contentItem: RowLayout {
id: contentLayout
spacing: 0
WButton {
id: launchButton
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPadding: 10
verticalPadding: 11
implicitHeight: root.firstEntry ? 62 : 36
implicitWidth: entryContentRow.implicitWidth + leftPadding + rightPadding
topRightRadius: 0
bottomRightRadius: 0
onClicked: root.click()
contentItem: Item {
RowLayout {
id: entryContentRow
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
spacing: 8
SearchEntryIcon {
entry: root.entry
iconSize: 24
}
EntryNameColumn {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
}
}
Rectangle {
id: separator
opacity: (root.hovered && !root.checked) ? 1 : 0
Layout.fillHeight: true
implicitWidth: 1
color: ColorUtils.transparentize(Looks.colors.fg, 0.75)
}
WButton {
visible: !root.checked
Layout.fillHeight: true
implicitWidth: 47
topLeftRadius: 0
bottomLeftRadius: 0
onClicked: root.requestFocus()
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: "chevron-right"
implicitSize: 14
}
}
}
}
component EntryNameColumn: ColumnLayout {
spacing: 4
WText {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: root.entry.name
font.pixelSize: Looks.font.pixelSize.large
maximumLineCount: 2
}
WText {
Layout.fillWidth: true
visible: root.firstEntry
text: root.entry.type
color: Looks.colors.accentUnfocused
}
}
MouseArea {
anchors.fill: parent
// hoverEnabled: true
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
}
}
@@ -0,0 +1,260 @@
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: pinned ? Translation.tr("Unpin from taskbar") : Translation.tr("Pin to taskbar"),
iconName: pinned ? "keep_off" : "keep",
iconType: LauncherSearchResult.IconType.Material,
execute: () => {
TaskbarApps.togglePin(appId);
}
})
] : []),
...(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: () => {
if (Config.options.launcher.pinnedApps.indexOf(appId) !== -1) {
Config.options.launcher.pinnedApps = Config.options.launcher.pinnedApps.filter(id => id !== appId)
} else {
Config.options.launcher.pinnedApps = Config.options.launcher.pinnedApps.concat([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 {}
}
}
@@ -0,0 +1,82 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.startMenu
RowLayout {
id: root
property StartMenuContext context
WPanelIconButton {
implicitWidth: 36
implicitHeight: 36
iconSize: 24
iconName: "arrow-left"
onClicked: LauncherSearch.query = ""
}
ListView {
id: tagListView
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Horizontal
spacing: 4
model: root.context.categories
clip: true
delegate: WBorderedButton {
id: tagButton
required property var modelData
border.width: 1
radius: height / 2
implicitWidth: tagButtonText.implicitWidth + 12 * 2
implicitHeight: 32
checked: {
if (modelData.prefix != "") {
return LauncherSearch.query.startsWith(modelData.prefix);
} else {
return !tagListView.model.some(i => (i.prefix != "" && LauncherSearch.query.startsWith(i.prefix)));
}
}
contentItem: Item {
WText {
id: tagButtonText
anchors.centerIn: parent
color: tagButton.fgColor
text: tagButton.modelData.name
font.pixelSize: Looks.font.pixelSize.large
}
}
onClicked: LauncherSearch.ensurePrefix(tagButton.modelData.prefix)
}
}
WPanelIconButton {
id: optionsButton
implicitWidth: 36
implicitHeight: 36
iconSize: 24
iconName: "more-horizontal"
onClicked: accountsMenu.open()
WMenu {
id: accountsMenu
x: -accountsMenu.implicitWidth + optionsButton.implicitWidth + 10
y: optionsButton.height
downDirection: true
Action {
icon.name: "people-settings"
text: Translation.tr("Manage accounts")
onTriggered: {
Quickshell.execDetached(["bash", "-c", Config.options.apps.manageUser])
GlobalStates.searchOpen = false;
}
}
}
}
}