1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-17 08:35:21 -04:00

keybinds: add toggle to switch to FloatingWindow and back

This commit is contained in:
bbedward
2026-06-16 23:03:06 -04:00
parent 0ca451483f
commit f26c0af39a
6 changed files with 575 additions and 314 deletions
+336
View File
@@ -0,0 +1,336 @@
import QtQml
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
FocusScope {
id: content
property real scrollStep: 60
property var activeFlickable: mainFlickable
property bool showFloatingToggle: true
property bool floating: false
property alias searchField: searchField
signal closeRequested
signal floatingToggleRequested
function scrollDown() {
if (!activeFlickable)
return;
let newY = activeFlickable.contentY + scrollStep;
newY = Math.min(newY, activeFlickable.contentHeight - activeFlickable.height);
activeFlickable.contentY = newY;
}
function scrollUp() {
if (!activeFlickable)
return;
let newY = activeFlickable.contentY - scrollStep;
newY = Math.max(0, newY);
activeFlickable.contentY = newY;
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
scrollDown();
event.accepted = true;
}
return;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
scrollUp();
event.accepted = true;
}
return;
case Qt.Key_Down:
scrollDown();
event.accepted = true;
return;
case Qt.Key_Up:
scrollUp();
event.accepted = true;
return;
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
width: parent.width
spacing: Theme.spacingM
StyledText {
Layout.alignment: Qt.AlignLeft
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
}
Item {
Layout.fillWidth: true
}
DankActionButton {
visible: content.showFloatingToggle
iconName: content.floating ? "close_fullscreen" : "open_in_new"
tooltipText: content.floating ? I18n.tr("Dock window") : I18n.tr("Open as window")
onClicked: content.floatingToggleRequested()
}
DankTextField {
id: searchField
Layout.alignment: Qt.AlignRight
leftIconName: "search"
keyForwardTargets: [content]
onTextEdited: searchDebounce.restart()
Keys.onEscapePressed: event => {
content.closeRequested();
event.accepted = true;
}
}
}
Timer {
id: searchDebounce
interval: 50
repeat: false
onTriggered: {
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
}
}
DankFlickable {
id: mainFlickable
width: parent.width
height: parent.height - parent.spacing - 40
contentWidth: rowLayout.implicitWidth
contentHeight: rowLayout.implicitHeight
clip: true
property var rawBinds: KeybindsService.cheatsheet.binds || {}
function generateCategories(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
const lowerQueryWords = query.split(/\s+/);
const processed = {};
for (const cat in rawBinds) {
const binds = rawBinds[cat];
const catLower = cat.toLowerCase();
const subcats = {};
let hasSubcats = false;
for (let i = 0; i < binds.length; i++) {
const bind = binds[i];
const keyLower = (bind.key || "").toLowerCase();
const descLower = (bind.desc || "").toLowerCase();
const actionLower = (bind.action || "").toLowerCase();
if (bind.hideOnOverlay)
continue;
let shouldContinue = false;
for (let j = 0; j < lowerQueryWords.length; j++) {
const word = lowerQueryWords[j];
if (!(word.length === 0 || keyLower.includes(word) || descLower.includes(word) || catLower.includes(word) || actionLower.includes(word))) {
shouldContinue = true;
break;
}
}
if (shouldContinue)
continue;
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat])
subcats[bind.subcat] = [];
subcats[bind.subcat].push(bind);
} else {
if (!subcats["_root"])
subcats["_root"] = [];
subcats["_root"].push(bind);
}
}
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,
subcatKeys: Object.keys(subcats)
};
}
return processed;
}
property var categories: generateCategories("")
function estimateCategoryHeight(catName) {
const catData = categories[catName];
if (!catData)
return 0;
let bindCount = 0;
for (const key of catData.subcatKeys) {
bindCount += catData.subcats[key]?.length || 0;
if (key !== "_root")
bindCount += 1;
}
return 40 + bindCount * 28;
}
property var categoryKeys: Object.keys(categories)
function distributeCategories(cols) {
const columns = [];
const heights = [];
for (let i = 0; i < cols; i++) {
columns.push([]);
heights.push(0);
}
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
for (const cat of sorted) {
let minIdx = 0;
for (let i = 1; i < cols; i++) {
if (heights[i] < heights[minIdx])
minIdx = i;
}
columns[minIdx].push(cat);
heights[minIdx] += estimateCategoryHeight(cat);
}
return columns;
}
Row {
id: rowLayout
width: mainFlickable.width
spacing: Theme.spacingM
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
property var columnCategories: mainFlickable.distributeCategories(numColumns)
Repeater {
model: rowLayout.numColumns
Column {
id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingXL
Repeater {
model: rowLayout.columnCategories[index] || []
Column {
id: categoryColumn
width: parent.width
spacing: Theme.spacingXS
property string catName: modelData
property var catData: mainFlickable.categories[catName]
StyledText {
text: categoryColumn.catName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item {
width: 1
height: Theme.spacingXS
}
Column {
width: parent.width
spacing: Theme.spacingM
Repeater {
model: categoryColumn.catData?.subcatKeys || []
Column {
width: parent.width
spacing: Theme.spacingXS
property string subcatName: modelData
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
StyledText {
visible: parent.subcatName !== "_root"
text: parent.subcatName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
color: Theme.primary
opacity: 0.7
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: parent.parent.subcatBinds
Item {
width: parent.width
height: 24
StyledRect {
id: keyBadge
width: Math.min(keyText.implicitWidth + 12, 160)
height: 22
radius: 4
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
color: Theme.secondary
text: (modelData.key || "").replace(/\+/g, " + ")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
width: Math.min(implicitWidth, 148)
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: 170
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}