kcm_keys: fix layouting issues

kcm_keys has some layouting issues at very small window sizes:

- The column headers (Default/Custom shortcuts) always take up the full
  width. This works fine in most languages, but breaks for the usual
  culprits of volumnious verbiage, like German and Russian
- Somewhat similarly, the shortcuts themselves always take up their full
  single-line width. This works fine almost all the time, but can break
  if you have some four-modifier monstrosities defined (as we
  unfortunately do in e.g. emojier, for technical reasons). It's even
  worse if your language has some long key names like "Leertaste", or
  constraints from long column names.

In these cases we may end up with overlapping bits, and parts of the
text or even actual buttons that are off-screen.

In principle, we could fix this just by adding Layout.fillWidth in a
couple of strategic places and allowing things to elide/wrap as
necessary. But this doesn't always yield the best results because of the
layout structure.

kcm_keys has two columns here, one left-aligned and one right-aligned.
Visually, it does not make much sense to require that the header and the
contents have the same width, as it will yield unused space and
unnecessary elision.

So we switch from a RowLayout of ColumLayout to a ColumnLayout of
RowLayouts, and set fillWidth and elide as necessary.

This makes the contents always fit at all window sizes, and elides
things only if it's really necessary and we can't take unused space from
elsewhere. Some situations are still not the utmost of what is
potentially achievable, but we're talking absolute edge cases and
they're at most a little awkward, rather than obviously broken.

BUG: 525725
FIXED-IN: 6.8.0
This commit is contained in:
Christoph Wolk 2026-09-16 10:32:05 +02:00
parent 3ea0146c5d
commit 98f7a2d04f

View file

@ -118,145 +118,164 @@ QQC2.ItemDelegate {
active: false
visible: false
Layout.fillWidth: true
sourceComponent: RowLayout {
sourceComponent: ColumnLayout {
readonly property var originalIndex : {
const concatenatedIndex = kcm.filteredModel.mapToSource(dm.modelIndex(index))
return kcm.shortcutsModel.mapToSource(concatenatedIndex)
}
spacing: 0
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
spacing: 0
ColumnLayout {
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: parent.width * 0.5
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
Layout.fillWidth: true
level: 4
text: model.defaultShortcuts && model.defaultShortcuts.length !== 0 ?
i18ncp("@title:column %1 decides if singular or plural will be used", "Default shortcut",
"Default shortcuts", model.defaultShortcuts.length) :
i18nc("@label for shortcut with no default bindings", "No default shortcuts")
i18ncp("@title:column %1 decides if singular or plural will be used", "Default shortcut",
"Default shortcuts", model.defaultShortcuts.length) :
i18nc("@label for shortcut with no default bindings", "No default shortcuts")
textFormat: Text.PlainText
elide: Text.ElideRight
}
Kirigami.Separator {
Kirigami.Heading {
Layout.fillWidth: true
}
Repeater {
model: defaultShortcuts
QQC2.CheckBox {
Accessible.name: checked ? i18nc("@info:whatsthis accessible name", "Default shortcut %1 is enabled.", modelData) : i18nc("@info:whatsthis accessible name", "Default shortcut %1 is disabled.", modelData)
checked: activeShortcuts.indexOf(modelData) !== -1
text: modelData
onToggled: {
if (checked) {
kcm.requestKeySequence(this, originalIndex, modelData)
} else {
originalIndex.model.disableShortcut(originalIndex, modelData)
}
}
KCM.SettingHighlighter {
highlight: !checked
}
}
Layout.alignment: Qt.AlignRight
level: 4
text: i18nc("@title:column", "Custom shortcuts")
textFormat: Text.PlainText
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
}
}
ColumnLayout {
Layout.alignment: Qt.AlignTop | Qt.AlignRight
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
level: 4
Layout.alignment: Qt.AlignRight
text: i18nc("@title:column", "Custom shortcuts")
textFormat: Text.PlainText
}
Kirigami.Separator {
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ColumnLayout {
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
}
Repeater {
model: customShortcuts
RowLayout {
spacing: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight
KeySequenceItem {
id: keySequenceEditor
Layout.alignment: Qt.AlignRight
keySequence: modelData
showClearButton: false
patterns: ShortcutPattern.ModifierAndKey | ShortcutPattern.Modifier
multiKeyShortcutsAllowed: supportsMultipleKeys
checkForConflictsAgainst: ShortcutType.None
onCaptureFinished: {
kcm.requestKeySequence(this, originalIndex, keySequence, modelData)
spacing: Kirigami.Units.smallSpacing
Repeater {
model: defaultShortcuts
QQC2.CheckBox {
Layout.fillWidth: true
Accessible.name: checked ? i18nc("@info:whatsthis accessible name", "Default shortcut %1 is enabled.", modelData) : i18nc("@info:whatsthis accessible name", "Default shortcut %1 is disabled.", modelData)
checked: activeShortcuts.indexOf(modelData) !== -1
text: modelData
onToggled: {
if (checked) {
kcm.requestKeySequence(this, originalIndex, modelData)
} else {
originalIndex.model.disableShortcut(originalIndex, modelData)
}
}
KCM.SettingHighlighter {
highlight: true
highlight: !checked
}
Connections {
target: kcm
function onShortcutChangeRejected() {
if (keySequenceEditor.keySequence !== modelData) {
keySequenceEditor.keySequence = Qt.binding(() => modelData)
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Repeater {
model: customShortcuts
RowLayout {
spacing: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight
KeySequenceItem {
id: keySequenceEditor
Layout.alignment: Qt.AlignRight
keySequence: modelData
showClearButton: false
patterns: ShortcutPattern.ModifierAndKey | ShortcutPattern.Modifier
multiKeyShortcutsAllowed: supportsMultipleKeys
checkForConflictsAgainst: ShortcutType.None
onCaptureFinished: {
kcm.requestKeySequence(this, originalIndex, keySequence, modelData)
}
KCM.SettingHighlighter {
highlight: true
}
Connections {
target: kcm
function onShortcutChangeRejected() {
if (keySequenceEditor.keySequence !== modelData) {
keySequenceEditor.keySequence = Qt.binding(() => modelData)
}
}
}
}
}
QQC2.Button {
icon.name: "edit-delete"
text: i18nc("@action:button accessible", "Delete shortcut")
display: QQC2.AbstractButton.IconOnly
onClicked: originalIndex.model.disableShortcut(originalIndex, modelData)
QQC2.Button {
icon.name: "edit-delete"
text: i18nc("@action:button accessible", "Delete shortcut")
display: QQC2.AbstractButton.IconOnly
onClicked: originalIndex.model.disableShortcut(originalIndex, modelData)
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered || activeFocus
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
QQC2.Button {
text: i18nc("@action:button Add custom shortcut", "Add…")
Accessible.name: i18nc("@action:button accessible", "Add custom shortcut")
icon.name: "list-add-symbolic"
Layout.alignment: Qt.AlignRight
onClicked: {
this.visible = false
var newKeySequenceItem = newKeySequenceComponent.createObject(parent)
for (var i = 0; i < newKeySequenceItem.children.length; i++) {
if (newKeySequenceItem.children[i] instanceof KeySequenceItem) {
var keySequenceItem = newKeySequenceItem.children[i]
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered || activeFocus
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
newKeySequenceItem.finished.connect(() => {
newKeySequenceItem.destroy()
this.visible = true
})
keySequenceItem.startCapturing()
}
}
Component {
id: newKeySequenceComponent
RowLayout {
signal finished
QQC2.Button {
text: i18nc("@action:button Add custom shortcut", "Add…")
Accessible.name: i18nc("@action:button accessible", "Add custom shortcut")
icon.name: "list-add-symbolic"
Layout.alignment: Qt.AlignRight
spacing: Kirigami.Units.smallSpacing
KeySequenceItem {
showClearButton: false
patterns: ShortcutPattern.Modifier | ShortcutPattern.ModifierAndKey
multiKeyShortcutsAllowed: model.supportsMultipleKeys
checkForConflictsAgainst: ShortcutType.None
onCaptureFinished: {
kcm.requestKeySequence(this, originalIndex, keySequence)
parent.finished()
onClicked: {
this.visible = false
var newKeySequenceItem = newKeySequenceComponent.createObject(parent)
for (var i = 0; i < newKeySequenceItem.children.length; i++) {
if (newKeySequenceItem.children[i] instanceof KeySequenceItem) {
var keySequenceItem = newKeySequenceItem.children[i]
}
}
newKeySequenceItem.finished.connect(() => {
newKeySequenceItem.destroy()
this.visible = true
})
keySequenceItem.startCapturing()
}
QQC2.Button {
icon.name: "dialog-cancel"
text: i18nc("@action:button accessible", "Cancel capturing shortcut")
display: QQC2.AbstractButton.IconOnly
onClicked: parent.finished()
}
Component {
id: newKeySequenceComponent
RowLayout {
signal finished
Layout.alignment: Qt.AlignRight
spacing: Kirigami.Units.smallSpacing
KeySequenceItem {
showClearButton: false
patterns: ShortcutPattern.Modifier | ShortcutPattern.ModifierAndKey
multiKeyShortcutsAllowed: model.supportsMultipleKeys
checkForConflictsAgainst: ShortcutType.None
onCaptureFinished: {
kcm.requestKeySequence(this, originalIndex, keySequence)
parent.finished()
}
}
QQC2.Button {
icon.name: "dialog-cancel"
text: i18nc("@action:button accessible", "Cancel capturing shortcut")
display: QQC2.AbstractButton.IconOnly
onClicked: parent.finished()
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered || activeFocus
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered || activeFocus
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
}