mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-22 19:15:24 -04:00
keybinds: add toggle to switch to FloatingWindow and back
This commit is contained in:
@@ -465,6 +465,8 @@ Singleton {
|
|||||||
property bool launcherUseOverlayLayer: false
|
property bool launcherUseOverlayLayer: false
|
||||||
property string launcherStyle: "full"
|
property string launcherStyle: "full"
|
||||||
property bool spotlightBarShowModeChips: false
|
property bool spotlightBarShowModeChips: false
|
||||||
|
property bool keybindsFloatingWindow: false
|
||||||
|
onKeybindsFloatingWindowChanged: saveSettings()
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ var SPEC = {
|
|||||||
launcherUseOverlayLayer: { def: false },
|
launcherUseOverlayLayer: { def: false },
|
||||||
launcherStyle: { def: "full" },
|
launcherStyle: { def: "full" },
|
||||||
spotlightBarShowModeChips: { def: false },
|
spotlightBarShowModeChips: { def: false },
|
||||||
|
keybindsFloatingWindow: { def: false },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,334 +1,78 @@
|
|||||||
import QtQml
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:keybinds"
|
readonly property bool floating: SettingsData.keybindsFloatingWindow
|
||||||
useOverlayLayer: true
|
readonly property bool shouldBeVisible: floating ? (windowLoader.item ? windowLoader.item.visible : false) : (overlayLoader.item ? overlayLoader.item.shouldBeVisible : false)
|
||||||
property real scrollStep: 60
|
|
||||||
property var activeFlickable: null
|
|
||||||
property real _maxW: Math.min(root.screenWidth * 0.92, 1200)
|
|
||||||
property real _maxH: Math.min(root.screenHeight * 0.92, 900)
|
|
||||||
modalWidth: _maxW
|
|
||||||
modalHeight: _maxH
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
modalFocusScope.forceActiveFocus();
|
|
||||||
if (contentLoader.item?.searchField)
|
|
||||||
contentLoader.item.searchField.forceActiveFocus();
|
|
||||||
});
|
|
||||||
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
|
|
||||||
KeybindsService.loadCheatsheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollDown() {
|
function open() {
|
||||||
if (!root.activeFlickable)
|
if (floating) {
|
||||||
|
windowLoader.active = true;
|
||||||
|
windowLoader.item.show();
|
||||||
return;
|
return;
|
||||||
let newY = root.activeFlickable.contentY + scrollStep;
|
}
|
||||||
newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height);
|
overlayLoader.active = true;
|
||||||
root.activeFlickable.contentY = newY;
|
overlayLoader.item.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollUp() {
|
function close() {
|
||||||
if (!root.activeFlickable)
|
if (windowLoader.item)
|
||||||
|
windowLoader.item.hide();
|
||||||
|
if (overlayLoader.item)
|
||||||
|
overlayLoader.item.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible)
|
||||||
|
close();
|
||||||
|
else
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _switchFloating(toFloating) {
|
||||||
|
if (toFloating) {
|
||||||
|
if (overlayLoader.item)
|
||||||
|
overlayLoader.item.close();
|
||||||
|
SettingsData.keybindsFloatingWindow = true;
|
||||||
|
windowLoader.active = true;
|
||||||
|
windowLoader.item.show();
|
||||||
return;
|
return;
|
||||||
let newY = root.activeFlickable.contentY - root.scrollStep;
|
}
|
||||||
newY = Math.max(0, newY);
|
if (windowLoader.item)
|
||||||
root.activeFlickable.contentY = newY;
|
windowLoader.item.hide();
|
||||||
|
SettingsData.keybindsFloatingWindow = false;
|
||||||
|
overlayLoader.active = true;
|
||||||
|
overlayLoader.item.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
modalFocusScope.Keys.onPressed: event => {
|
Loader {
|
||||||
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
id: overlayLoader
|
||||||
scrollDown();
|
active: false
|
||||||
event.accepted = true;
|
asynchronous: false
|
||||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
|
||||||
scrollUp();
|
sourceComponent: KeybindsModalOverlay {
|
||||||
event.accepted = true;
|
onFloatingToggleRequested: root._switchFloating(true)
|
||||||
} else if (event.key === Qt.Key_Down) {
|
onDialogClosed: Qt.callLater(() => {
|
||||||
scrollDown();
|
if (!shouldBeVisible)
|
||||||
event.accepted = true;
|
overlayLoader.active = false;
|
||||||
} else if (event.key === Qt.Key_Up) {
|
})
|
||||||
scrollUp();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
Loader {
|
||||||
Item {
|
id: windowLoader
|
||||||
anchors.fill: parent
|
active: false
|
||||||
property alias searchField: searchField
|
asynchronous: false
|
||||||
|
|
||||||
Column {
|
sourceComponent: KeybindsModalWindow {
|
||||||
anchors.fill: parent
|
onFloatingToggleRequested: root._switchFloating(false)
|
||||||
anchors.margins: Theme.spacingL
|
onVisibleChanged: {
|
||||||
spacing: Theme.spacingL
|
if (!visible)
|
||||||
|
Qt.callLater(() => windowLoader.active = false);
|
||||||
RowLayout {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.alignment: Qt.AlignLeft
|
|
||||||
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: searchField
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
leftIconName: "search"
|
|
||||||
keyForwardTargets: [root.modalFocusScope]
|
|
||||||
onTextEdited: searchDebounce.restart()
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
root.close();
|
|
||||||
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
|
|
||||||
|
|
||||||
Component.onCompleted: root.activeFlickable = mainFlickable
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import QtQml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: overlay
|
||||||
|
|
||||||
|
signal floatingToggleRequested
|
||||||
|
|
||||||
|
layerNamespace: "dms:keybinds"
|
||||||
|
useOverlayLayer: true
|
||||||
|
property real _maxW: Math.min(overlay.screenWidth * 0.92, 1200)
|
||||||
|
property real _maxH: Math.min(overlay.screenHeight * 0.92, 900)
|
||||||
|
modalWidth: _maxW
|
||||||
|
modalHeight: _maxH
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
modalFocusScope.forceActiveFocus();
|
||||||
|
if (contentLoader.item?.searchField)
|
||||||
|
contentLoader.item.searchField.forceActiveFocus();
|
||||||
|
});
|
||||||
|
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
|
||||||
|
KeybindsService.loadCheatsheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
KeybindsContent {
|
||||||
|
showFloatingToggle: true
|
||||||
|
floating: false
|
||||||
|
onCloseRequested: overlay.close()
|
||||||
|
onFloatingToggleRequested: overlay.floatingToggleRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FloatingWindow {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
property bool disablePopupTransparency: true
|
||||||
|
property alias shouldBeVisible: win.visible
|
||||||
|
|
||||||
|
signal floatingToggleRequested
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
visible = !visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "keybindsModalWindow"
|
||||||
|
title: I18n.tr("Keybinds")
|
||||||
|
minimumSize: Qt.size(Math.min(560, Screen.width), Math.min(400, Screen.height))
|
||||||
|
implicitWidth: 1000
|
||||||
|
implicitHeight: screen ? Math.min(820, screen.height - 100) : 820
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible)
|
||||||
|
return;
|
||||||
|
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
|
||||||
|
KeybindsService.loadCheatsheet();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
keybindsContent.forceActiveFocus();
|
||||||
|
keybindsContent.searchField.forceActiveFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: win.visible = false
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
z: 10
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onPressed: windowControls.tryStartMove()
|
||||||
|
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "keyboard"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close_fullscreen"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
tooltipText: I18n.tr("Dock window")
|
||||||
|
onClicked: win.floatingToggleRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: windowControls.canMaximize
|
||||||
|
circular: false
|
||||||
|
iconName: win.maximized ? "fullscreen_exit" : "fullscreen"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
circular: false
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: win.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeybindsContent {
|
||||||
|
id: keybindsContent
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 48
|
||||||
|
showFloatingToggle: false
|
||||||
|
floating: true
|
||||||
|
onCloseRequested: win.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingWindowControls {
|
||||||
|
id: windowControls
|
||||||
|
targetWindow: win
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user