Files
bbedward 5a72cd9616 aqueous: move IPC integration to QML, delete test fixtures (since we
dont have a QML test structure even setup), delete in-repo docs
2026-09-07 09:08:46 -04:00

1015 lines
43 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
Item {
id: keybindsTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var parentModal: null
property string selectedCategory: ""
property string searchQuery: ""
property string requestedSearchQuery: ""
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: ""
property var editDraft: null
property var reviewSnapshot: null
property bool reviewingEdit: false
property bool editBusy: false
property string editError: ""
property int _editRequest: 0
property bool _editAlive: true
readonly property bool hasEditDraft: editDraft !== null
readonly property bool editInvalidated: hasEditDraft && (editDraft.provider !== KeybindsService.currentProvider || !KeybindsService.bindEditSession || editDraft.session !== KeybindsService.bindEditSession)
onEditInvalidatedChanged: {
if (!editInvalidated)
return;
_editRequest++;
editBusy = false;
reviewingEdit = true;
reviewSnapshot = null;
editError = KeybindsService.bindEditError("invalidated");
}
Component.onDestruction: {
_editAlive = false;
_editRequest++;
}
function beginEdit(binding, key) {
if (hasEditDraft || editBusy || KeybindsService.bindMutationBusy) {
ToastService.showInfo(I18n.tr("Save or discard the current edit before editing another shortcut.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings"));
return false;
}
try {
editDraft = KeybindsService.captureBindEdit(binding, key);
editError = "";
reviewSnapshot = null;
reviewingEdit = false;
return true;
} catch (e) {
ToastService.showError(I18n.tr("Failed to load keybinds", "Aqueous shortcut editor could not load the current bindings"), AqueousService.errorMessage(String(e)), String(e));
return false;
}
}
function updateEditDraft(originalKey, data) {
if (!editDraft || editDraft.operation !== "set" || editBusy || editInvalidated)
return;
editDraft = KeybindsService.updateBindEdit(editDraft, originalKey, data);
}
function submitEdit() {
if (!editDraft || editBusy || reviewingEdit || editInvalidated || KeybindsService.bindMutationBusy)
return;
editBusy = true;
editError = "";
const token = ++_editRequest;
const complete = result => {
if (!keybindsTab._editAlive || token !== keybindsTab._editRequest)
return;
keybindsTab.editBusy = false;
if (result.success) {
const key = keybindsTab.editDraft.operation === "set" ? keybindsTab.editDraft.data.key : "";
const action = keybindsTab.editDraft.data.action;
keybindsTab.editDraft = null;
keybindsTab.reviewSnapshot = null;
keybindsTab.reviewingEdit = false;
keybindsTab._editingKey = key;
keybindsTab.expandedKey = action;
return;
}
keybindsTab.reviewSnapshot = result.snapshot || null;
keybindsTab.reviewingEdit = result.code !== "load_failed" && result.code !== "busy";
keybindsTab.editError = KeybindsService.bindEditError(result.code);
};
switch (editDraft.operation) {
case "set":
KeybindsService.saveBind(editDraft.originalKey, editDraft.data, editDraft, complete);
return;
case "remove":
KeybindsService.removeBind(editDraft.originalKey, editDraft, complete);
return;
case "reset":
KeybindsService.resetBind(editDraft.originalKey, editDraft, complete);
return;
}
}
function reloadEdit() {
if (!editDraft || editBusy || editInvalidated)
return;
editBusy = true;
reviewingEdit = true;
const token = ++_editRequest;
KeybindsService.loadBindReview((snapshot, message) => {
if (!keybindsTab._editAlive || token !== keybindsTab._editRequest)
return;
keybindsTab.editBusy = false;
keybindsTab.reviewSnapshot = snapshot;
keybindsTab.editError = message;
});
}
function acceptReview() {
if (!editDraft || !reviewSnapshot || editBusy || editInvalidated)
return;
const result = KeybindsService.reconcileBindEdit(editDraft, reviewSnapshot);
if (!result.draft) {
editError = KeybindsService.bindEditError(result.code);
return;
}
editDraft = result.draft;
reviewingEdit = false;
editError = "";
if (editDraft.operation !== "set")
confirmEditRemoval();
}
function confirmEditRemoval() {
if (!editDraft)
return;
if (editDraft.operation === "reset") {
confirmResetBind(editDraft.originalKey, "");
return;
}
confirmRemoveBind(editDraft.originalKey, "");
}
function discardEdit() {
if (editBusy || KeybindsService.bindMutationBusy)
return;
_editRequest++;
editDraft = null;
reviewSnapshot = null;
reviewingEdit = false;
editError = "";
expandedKey = "";
showingNewBind = false;
_editingKey = "";
KeybindsService.loadBinds(false);
}
function _updateFiltered() {
let allBinds = KeybindsService.getFlatBinds();
if (keybindsTab.editDraft?.action) {
const binding = keybindsTab.editDraft.binding;
const found = allBinds.some(bind => bind.action === binding.action);
allBinds = allBinds.map(bind => bind.action === binding.action ? binding : bind);
if (!found)
allBinds.push(binding);
}
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) {
if (KeybindsService.requiresBindReview && !keybindsTab.hasEditDraft) {
const binding = KeybindsService.getFlatBinds().find(bind => bind.action === action);
if (!binding || !keybindsTab.beginEdit(binding, binding.keys[0]?.key || ""))
return;
} else if (keybindsTab.hasEditDraft && action !== keybindsTab.editDraft.action) {
ToastService.showInfo(I18n.tr("Save or discard the current edit before editing another shortcut.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings"));
return;
}
expandedKey = expandedKey === action ? "" : action;
}
function startNewBind() {
if (KeybindsService.readOnly) {
KeybindsService.showHyprlandReadOnlyWarning();
return;
}
if (KeybindsService.requiresBindReview) {
if (!keybindsTab.beginEdit({
action: "",
desc: ""
}, ""))
return;
newBindItem.resetEdits();
}
showingNewBind = true;
expandedKey = "";
}
function cancelNewBind() {
if (keybindsTab.hasEditDraft) {
keybindsTab.discardEdit();
return;
}
showingNewBind = false;
}
function saveNewBind(bindData) {
saveBind("", bindData);
}
function saveBind(originalKey, bindData) {
if (KeybindsService.requiresBindReview) {
keybindsTab.updateEditDraft(originalKey, bindData);
keybindsTab.submitEdit();
return;
}
KeybindsService.saveBind(originalKey, bindData);
_editingKey = bindData.key;
expandedKey = bindData.action;
}
function confirmRemoveBind(key, remainingKey) {
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "remove") : null;
if (KeybindsService.requiresBindReview && !draft)
return;
const baselineDraft = keybindsTab.editDraft;
removeBindConfirm.showWithOptions({
title: I18n.tr("Remove Shortcut?"),
message: KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.").arg(key) : I18n.tr("Remove the shortcut %1?").arg(key),
confirmText: I18n.tr("Remove"),
confirmColor: Theme.primary,
onConfirm: () => {
if (draft) {
if (keybindsTab.editDraft !== baselineDraft)
return;
keybindsTab.editDraft = draft;
keybindsTab.submitEdit();
return;
}
KeybindsService.removeBind(key);
keybindsTab._editingKey = remainingKey;
}
});
}
function confirmResetBind(key, remainingKey) {
const draft = KeybindsService.requiresBindReview ? prepareRemoval(key, "reset") : null;
if (KeybindsService.requiresBindReview && !draft)
return;
const baselineDraft = keybindsTab.editDraft;
removeBindConfirm.showWithOptions({
title: I18n.tr("Reset to default"),
message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key),
confirmText: I18n.tr("Reset"),
confirmColor: Theme.primary,
onConfirm: () => {
if (draft) {
if (keybindsTab.editDraft !== baselineDraft)
return;
keybindsTab.editDraft = draft;
keybindsTab.submitEdit();
return;
}
KeybindsService.resetBind(key);
keybindsTab._editingKey = remainingKey;
}
});
}
function prepareRemoval(key, operation) {
if (!hasEditDraft) {
const binding = KeybindsService.getFlatBinds().find(bind => bind.keys.some(entry => entry.key === key));
if (!binding || !beginEdit(binding, key))
return null;
}
if (editBusy || reviewingEdit || editInvalidated)
return null;
return KeybindsService.updateBindEdit(editDraft, key, null, operation);
}
function _onSaveSuccess() {
if (showingNewBind) {
showingNewBind = false;
selectedCategory = "";
}
}
function scrollToTop() {
flickable.contentY = 0;
}
function _scrollToExpandedItem() {
for (let i = 0; i < bindsRepeater.count; i++) {
const item = bindsRepeater.itemAt(i);
if (item && item.modelData.action === expandedKey) {
const itemY = item.mapToItem(flickable.contentItem, 0, 0).y;
const itemH = item.height;
const viewH = flickable.height;
if (itemY >= flickable.contentY && itemY + itemH <= flickable.contentY + viewH)
return;
flickable.contentY = Math.max(0, Math.min(itemY - viewH / 4, flickable.contentHeight - viewH));
return;
}
}
flickable.contentY = _savedScrollY;
}
Timer {
id: searchDebounce
interval: 150
onTriggered: keybindsTab._updateFiltered()
}
ConfirmModal {
id: removeBindConfirm
}
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) {
if (keybindsTab.expandedKey)
Qt.callLater(keybindsTab._scrollToExpandedItem);
else
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 _ensureCurrentProvider() {
if (!KeybindsService.available)
return;
if (KeybindsService.requiresBindReview) {
if (!keybindsTab.hasEditDraft && !KeybindsService.bindMutationBusy)
KeybindsService.loadBinds(false);
return;
}
const cachedProvider = KeybindsService.keybinds?.provider;
const targetProvider = KeybindsService.currentProvider;
if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) {
KeybindsService.loadBinds();
return;
}
if (_lastDataVersion !== KeybindsService._dataVersion) {
_lastDataVersion = KeybindsService._dataVersion;
_updateCategories();
_updateFiltered();
}
}
function _applyRequestedSearch() {
if (!requestedSearchQuery)
return;
const query = requestedSearchQuery;
selectedCategory = "";
searchField.text = query;
searchQuery = query;
_updateFiltered();
if (parentModal?.keybindSearchQuery === query)
parentModal.keybindSearchQuery = "";
Qt.callLater(scrollToTop);
}
Component.onCompleted: {
_ensureCurrentProvider();
Qt.callLater(_applyRequestedSearch);
}
onRequestedSearchQueryChanged: Qt.callLater(_applyRequestedSearch)
onVisibleChanged: {
if (!visible)
return;
_ensureCurrentProvider();
Qt.callLater(() => {
_applyRequestedSearch();
scrollToTop();
});
}
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.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
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
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
readonly property string bindsFile: KeybindsService.requiresBindReview ? "aqueous-config" : KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
text: KeybindsService.requiresBindReview ? I18n.tr("Click any shortcut to edit Aqueous configuration", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings") : KeybindsService.readOnly ? I18n.tr("Hyprland conf mode is read-only in Settings") : I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
DankTextField {
id: searchField
width: parent.width - addButton.width - Theme.spacingM
placeholderText: I18n.tr("Search keybinds...")
leftIconName: "search"
onTextChanged: {
keybindsTab.searchQuery = text;
searchDebounce.restart();
}
}
DankActionButton {
id: addButton
width: searchField.height
height: searchField.height
circular: false
iconName: "add"
iconSize: Theme.iconSize
iconColor: Theme.primary
anchors.verticalCenter: parent.verticalCenter
enabled: !keybindsTab.showingNewBind && !KeybindsService.readOnly
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 showLegacy: KeybindsService.readOnly
readonly property bool showWarning: !showLegacy && status.included && status.overriddenBy > 0
readonly property bool showSetup: !showLegacy && !status.included
color: (showLegacy || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : Theme.withAlpha(Theme.primary, 0)
border.color: (showLegacy || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.primary, 0)
border.width: 1
visible: (showLegacy || 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.showLegacy)
return I18n.tr("Hyprland conf mode");
if (warningBox.showSetup)
return I18n.tr("First Time Setup");
if (warningBox.showWarning)
return I18n.tr("Possible Override Conflicts");
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: {
if (warningBox.showLegacy)
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings.");
if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/binds");
if (warningBox.showWarning) {
const count = warningBox.status.overriddenBy;
return I18n.ntr("%1 DMS bind may be overridden by config binds that come after the include.", "%1 DMS binds may be overridden by config binds that come after the include.", count).arg(count);
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankButton {
id: fixButton
visible: !warningBox.showLegacy && warningBox.showSetup
text: KeybindsService.fixing ? I18n.tr("Setting up...") : I18n.tr("Setup")
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !KeybindsService.fixing
anchors.verticalCenter: parent.verticalCenter
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.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Column {
id: categorySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Flow {
width: parent.width
spacing: Theme.spacingS
Rectangle {
readonly property real chipHeight: allChip.implicitHeight + Theme.spacingM
width: allChip.implicitWidth + Theme.spacingL
height: chipHeight
radius: chipHeight / 2
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.floatingWindowFieldColor
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
readonly property real chipHeight: catText.implicitHeight + Theme.spacingM
width: catText.implicitWidth + Theme.spacingL
height: chipHeight
radius: chipHeight / 2
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.floatingWindowFieldColor)
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();
}
}
}
}
}
}
}
Column {
width: Math.min(650, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
visible: keybindsTab.hasEditDraft && (keybindsTab.reviewingEdit || keybindsTab.editError !== "" || keybindsTab.editDraft.operation !== "set")
spacing: Theme.spacingM
StyledText {
width: parent.width
visible: keybindsTab.reviewingEdit || keybindsTab.editError !== ""
text: keybindsTab.editError || I18n.tr("Review the current bindings before saving this edit.", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings")
color: Theme.error
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
visible: keybindsTab.reviewingEdit && !!keybindsTab.reviewSnapshot
text: KeybindsService.describeBindReview(keybindsTab.editDraft, keybindsTab.reviewSnapshot)
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
Flow {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: I18n.tr("Refresh")
enabled: !keybindsTab.editBusy && !keybindsTab.editInvalidated
onClicked: keybindsTab.reloadEdit()
}
DankButton {
text: I18n.tr("Accept reviewed changes", "Aqueous keyboard shortcut editor, retaining an unsaved edit while reviewing current bindings")
visible: keybindsTab.reviewingEdit && !!keybindsTab.reviewSnapshot
enabled: !keybindsTab.editBusy && !keybindsTab.editInvalidated
onClicked: keybindsTab.acceptReview()
}
DankButton {
text: I18n.tr("Discard")
enabled: !keybindsTab.editBusy && !KeybindsService.bindMutationBusy
onClicked: keybindsTab.discardEdit()
}
DankButton {
text: I18n.tr("Remove")
visible: keybindsTab.hasEditDraft && keybindsTab.editDraft.operation !== "set"
enabled: !keybindsTab.editBusy && !keybindsTab.reviewingEdit && !keybindsTab.editInvalidated
onClicked: keybindsTab.confirmEditRemoval()
}
}
}
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.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
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 {
id: newBindItem
width: parent.width
isNew: true
isExpanded: true
bindData: ({
keys: [
{
key: "",
source: "dms",
isOverride: true
}
],
action: "",
desc: ""
})
panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly
retainedEdit: keybindsTab.hasEditDraft && keybindsTab.showingNewBind ? keybindsTab.editDraft : null
saveBlocked: retainEdits && (keybindsTab.reviewingEdit || keybindsTab.editDraft.operation !== "set")
enabled: !retainEdits || (!keybindsTab.editBusy && !keybindsTab.editInvalidated && !KeybindsService.bindMutationBusy)
onEditChanged: {
if (retainEdits)
keybindsTab.updateEditDraft("", {
key: editKey,
action: editAction,
desc: editDesc
});
}
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.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
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: {
if (KeybindsService.loading)
return I18n.tr("Shortcuts");
const count = keybindsTab._filteredBinds.length;
return count === 1 ? I18n.tr("Shortcut (%1)").arg(count) : I18n.tr("Shortcuts (%1)").arg(count);
}
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
smoothTransform: KeybindsService.loading
RotationAnimator 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 {
id: bindsRepeater
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
retainedEdit: keybindsTab.hasEditDraft && keybindsTab.editDraft.action === modelData.action ? keybindsTab.editDraft : null
saveBlocked: retainEdits && (keybindsTab.reviewingEdit || keybindsTab.editDraft.operation !== "set")
enabled: !retainEdits || (!keybindsTab.editBusy && !keybindsTab.editInvalidated && !KeybindsService.bindMutationBusy)
onEditChanged: {
if (KeybindsService.requiresBindReview && isExpanded && hasChanges && !keybindsTab.hasEditDraft)
keybindsTab.beginEdit(bindData, addingNewKey ? "" : _originalKey);
if (retainEdits)
keybindsTab.updateEditDraft(addingNewKey ? "" : _originalKey, {
key: editKey,
action: editAction,
desc: editDesc
});
}
onCancelEdit: keybindsTab.discardEdit()
panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly
onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => {
keybindsTab.saveBind(originalKey, newData);
}
onRemoveBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
keybindsTab.confirmRemoveBind(key, remainingKey);
}
onResetBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
keybindsTab.confirmResetBind(key, 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 = "";
}
}
}
}
}
}
}
}
}
}