forked from Shinonome/dots-hyprland
waffles: ctrl alt del menu
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user