1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/quickshell/Modules/Settings/KeybindsTab.qml

646 lines
26 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: keybindsTab
property var parentModal: null
property string selectedCategory: ""
property string searchQuery: ""
property string expandedKey: ""
property bool showingNewBind: false
property int _lastDataVersion: -1
property var _cachedCategories: []
property var _filteredBinds: []
property real _savedScrollY: 0
property bool _preserveScroll: false
property string _editingKey: ""
function _updateFiltered() {
const allBinds = KeybindsService.getFlatBinds();
if (!searchQuery && !selectedCategory) {
_filteredBinds = allBinds;
return;
}
const q = searchQuery.toLowerCase();
const isOverrideFilter = selectedCategory === "__overrides__";
const result = [];
for (let i = 0; i < allBinds.length; i++) {
const group = allBinds[i];
if (q) {
let keyMatch = false;
for (let k = 0; k < group.keys.length; k++) {
if (group.keys[k].key.toLowerCase().indexOf(q) !== -1) {
keyMatch = true;
break;
}
}
if (!keyMatch && group.desc.toLowerCase().indexOf(q) === -1 && group.action.toLowerCase().indexOf(q) === -1)
continue;
}
if (isOverrideFilter) {
let hasOverride = false;
for (let k = 0; k < group.keys.length; k++) {
if (group.keys[k].isOverride) {
hasOverride = true;
break;
}
}
if (!hasOverride)
continue;
} else if (selectedCategory && group.category !== selectedCategory) {
continue;
}
result.push(group);
}
_filteredBinds = result;
}
function _updateCategories() {
_cachedCategories = ["__overrides__"].concat(KeybindsService.getCategories());
}
function getCategoryLabel(cat) {
if (cat === "__overrides__")
return I18n.tr("Overrides");
return cat;
}
function toggleExpanded(action) {
expandedKey = expandedKey === action ? "" : action;
}
function startNewBind() {
showingNewBind = true;
expandedKey = "";
}
function cancelNewBind() {
showingNewBind = false;
}
function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData);
_editingKey = bindData.key;
expandedKey = bindData.action;
}
function _onSaveSuccess() {
if (showingNewBind) {
showingNewBind = false;
selectedCategory = "";
}
}
function scrollToTop() {
flickable.contentY = 0;
}
Timer {
id: searchDebounce
interval: 150
onTriggered: keybindsTab._updateFiltered()
}
Connections {
target: KeybindsService
function onBindsLoaded() {
const savedY = keybindsTab._savedScrollY;
const wasPreserving = keybindsTab._preserveScroll;
keybindsTab._lastDataVersion = KeybindsService._dataVersion;
keybindsTab._updateCategories();
keybindsTab._updateFiltered();
keybindsTab._preserveScroll = false;
if (wasPreserving)
Qt.callLater(() => flickable.contentY = savedY);
}
function onBindSaved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
}
function onBindSaveCompleted(success) {
if (success)
keybindsTab._onSaveSuccess();
}
function onBindRemoved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
}
}
function _ensureNiriProvider() {
if (!KeybindsService.available)
return;
const cachedProvider = KeybindsService.keybinds?.provider;
if (cachedProvider !== "niri" || KeybindsService._dataVersion === 0) {
KeybindsService.currentProvider = "niri";
KeybindsService.loadBinds();
return;
}
if (_lastDataVersion !== KeybindsService._dataVersion) {
_lastDataVersion = KeybindsService._dataVersion;
_updateCategories();
_updateFiltered();
}
}
Component.onCompleted: _ensureNiriProvider()
onVisibleChanged: {
if (!visible)
return;
Qt.callLater(scrollToTop);
_ensureNiriProvider();
}
DankFlickable {
id: flickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: contentColumn.implicitHeight
Column {
id: contentColumn
width: flickable.width
spacing: Theme.spacingL
topPadding: Theme.spacingXL
bottomPadding: Theme.spacingXL
StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: headerSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
id: headerSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Keyboard Shortcuts")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Click any shortcut to edit. Changes save to dms/binds.kdl")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
DankTextField {
id: searchField
width: parent.width - addButton.width - Theme.spacingM
height: 44
placeholderText: I18n.tr("Search keybinds...")
leftIconName: "search"
onTextChanged: {
keybindsTab.searchQuery = text;
searchDebounce.restart();
}
}
DankActionButton {
id: addButton
width: 44
height: 44
circular: false
iconName: "add"
iconSize: Theme.iconSize
iconColor: Theme.primary
anchors.verticalCenter: parent.verticalCenter
enabled: !keybindsTab.showingNewBind
opacity: enabled ? 1 : 0.5
onClicked: keybindsTab.startNewBind()
}
}
}
}
StyledRect {
id: warningBox
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: warningSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
readonly property var status: KeybindsService.dmsStatus
readonly property bool showError: !status.included && status.exists
readonly property bool showWarning: status.included && status.overriddenBy > 0
readonly property bool showSetup: !status.exists
color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent"
border.color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: 1
visible: (showError || showWarning || showSetup) && !KeybindsService.loading
Column {
id: warningSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: warningBox.showWarning ? "info" : "warning"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: {
if (warningBox.showSetup)
return I18n.tr("First Time Setup");
if (warningBox.showError)
return I18n.tr("Binds Include Missing");
if (warningBox.showWarning)
return I18n.tr("Possible Override Conflicts");
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
}
StyledText {
text: {
if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create dms/binds.kdl and add include to config.kdl.");
if (warningBox.showError)
return I18n.tr("dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.");
if (warningBox.showWarning) {
const count = warningBox.status.overriddenBy;
return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count);
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Rectangle {
id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: fixButtonText
text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...");
if (warningBox.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surface
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: !KeybindsService.fixing
onClicked: KeybindsService.fixDmsBindsInclude()
}
}
}
}
}
StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: categorySection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
id: categorySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Flow {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: allChip.implicitWidth + Theme.spacingL
height: 32
radius: 16
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest
StyledText {
id: allChip
text: I18n.tr("All")
font.pixelSize: Theme.fontSizeSmall
color: !keybindsTab.selectedCategory ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
keybindsTab.selectedCategory = "";
keybindsTab._updateFiltered();
}
}
}
Repeater {
model: keybindsTab._cachedCategories
delegate: Rectangle {
required property string modelData
required property int index
width: catText.implicitWidth + Theme.spacingL
height: 32
radius: 16
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest)
StyledText {
id: catText
text: keybindsTab.getCategoryLabel(modelData)
font.pixelSize: Theme.fontSizeSmall
color: keybindsTab.selectedCategory === modelData ? Theme.primaryText : (modelData === "__overrides__" ? Theme.primary : Theme.surfaceVariantText)
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
keybindsTab.selectedCategory = modelData;
keybindsTab._updateFiltered();
}
}
}
}
}
}
}
StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: newBindSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineVariant
border.width: 1
visible: keybindsTab.showingNewBind
Column {
id: newBindSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "add"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("New Keybind")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
KeybindItem {
width: parent.width
isNew: true
isExpanded: true
bindData: ({
keys: [
{
key: "",
source: "dms",
isOverride: true
}
],
action: "",
desc: ""
})
panelWindow: keybindsTab.parentModal
onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData)
onCancelEdit: keybindsTab.cancelNewBind()
}
}
}
StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2)
height: bindsListHeader.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
id: bindsListHeader
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "list"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: KeybindsService.loading ? I18n.tr("Shortcuts") : I18n.tr("Shortcuts") + " (" + keybindsTab._filteredBinds.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: KeybindsService.loading
DankIcon {
id: loadingIcon
name: "sync"
size: 20
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: KeybindsService.loading
}
}
StyledText {
text: I18n.tr("Loading keybinds...")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("No keybinds found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !KeybindsService.loading && keybindsTab._filteredBinds.length === 0
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: keybindsTab._filteredBinds
objectProp: "action"
}
delegate: Item {
required property var modelData
required property int index
width: parent.width
height: bindItem.height
KeybindItem {
id: bindItem
width: Math.min(650, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action
panelWindow: keybindsTab.parentModal
onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => {
KeybindsService.saveBind(originalKey, newData);
keybindsTab._editingKey = newData.key;
keybindsTab.expandedKey = modelData.action;
}
onRemoveBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
KeybindsService.removeBind(key);
keybindsTab._editingKey = remainingKey;
}
onIsExpandedChanged: {
if (!isExpanded || !keybindsTab._editingKey)
return;
const keyExists = keys.some(k => k.key === keybindsTab._editingKey);
if (keyExists) {
restoreKey = keybindsTab._editingKey;
keybindsTab._editingKey = "";
}
}
onKeysChanged: {
if (!isExpanded || !keybindsTab._editingKey)
return;
const keyExists = keys.some(k => k.key === keybindsTab._editingKey);
if (keyExists) {
restoreKey = keybindsTab._editingKey;
keybindsTab._editingKey = "";
}
}
Connections {
target: keybindsTab
function on_EditingKeyChanged() {
if (!bindItem.isExpanded || !keybindsTab._editingKey)
return;
const keyExists = bindItem.keys.some(k => k.key === keybindsTab._editingKey);
if (keyExists) {
bindItem.restoreKey = keybindsTab._editingKey;
keybindsTab._editingKey = "";
}
}
}
}
}
}
}
}
}
}