Keyboard bindings - collision check (#9580)

This commit is contained in:
SomeTroglodyte
2023-06-13 06:31:02 +02:00
committed by GitHub
parent 02d7325576
commit d666c44697
4 changed files with 65 additions and 5 deletions

View File

@ -398,7 +398,6 @@
{"text":"This is a work in progress.","color":"#b22222","starred":true}, {"text":"This is a work in progress.","color":"#b22222","starred":true},
{"text":"For technical reasons, only direct keys or Ctrl-Letter combinations can be used.","starred":true}, {"text":"For technical reasons, only direct keys or Ctrl-Letter combinations can be used.","starred":true},
{"text":"The Escape key is intentionally excluded from being reassigned.","starred":true}, {"text":"The Escape key is intentionally excluded from being reassigned.","starred":true},
{"text":"Currently, there are no checks to prevent conflicting assignments.","starred":true},
{}, {},
{"text":"Using the Keys page","header":3}, {"text":"Using the Keys page","header":3},
{"text":"Each binding has a button with an image looking like this:"}, {"text":"Each binding has a button with an image looking like this:"},
@ -407,6 +406,7 @@
{"text":"Double-click the image to reset the binding to default."}, {"text":"Double-click the image to reset the binding to default."},
{}, {},
{"text":"Bindings mapped to their default keys are displayed in gray, those reassigned by you in white."}, {"text":"Bindings mapped to their default keys are displayed in gray, those reassigned by you in white."},
{"text":"Conflicting assignments are marked red. Conflicts can exist across categories, like World Screen / Unit Actions. Note that at the moment, the game does not prevent saving conflicting assignments, though the result may be unexpected."},
{}, {},
{"text":"For discussion about missing entries, see the linked github issue.","link":"https://github.com/yairm210/Unciv/issues/8862"} {"text":"For discussion about missing entries, see the linked github issue.","link":"https://github.com/yairm210/Unciv/issues/8862"}
] ]

View File

@ -68,9 +68,16 @@ class KeyCapturingButton(
updateLabel() updateLabel()
} }
var markConflict = false
set(value) {
field = value
updateStyle()
}
private var savedFocus: Actor? = null private var savedFocus: Actor? = null
private val normalStyle: ImageTextButtonStyle private val normalStyle: ImageTextButtonStyle
private val defaultStyle: ImageTextButtonStyle private val defaultStyle: ImageTextButtonStyle
private val conflictStyle: ImageTextButtonStyle
init { init {
imageCell.size((style as KeyCapturingButtonStyle).imageSize) imageCell.size((style as KeyCapturingButtonStyle).imageSize)
@ -80,13 +87,23 @@ class KeyCapturingButton(
normalStyle = style normalStyle = style
defaultStyle = ImageTextButtonStyle(normalStyle) defaultStyle = ImageTextButtonStyle(normalStyle)
defaultStyle.fontColor = Color.GRAY.cpy() defaultStyle.fontColor = Color.GRAY.cpy()
conflictStyle = ImageTextButtonStyle(normalStyle)
conflictStyle.fontColor = Color.RED.cpy()
addListener(ButtonListener(this)) addListener(ButtonListener(this))
} }
private fun updateLabel() { private fun updateLabel() {
label.setText(if (current == KeyCharAndCode.UNKNOWN) "" else current.toString()) label.setText(if (current == KeyCharAndCode.UNKNOWN) "" else current.toString())
style = if (current == default) defaultStyle else normalStyle updateStyle()
} }
private fun updateStyle() {
style = when {
markConflict -> conflictStyle
current == default -> defaultStyle
else -> normalStyle
}
}
private fun handleKey(code: Int, control: Boolean) { private fun handleKey(code: Int, control: Boolean) {
current = if (control) KeyCharAndCode.ctrlFromCode(code) else KeyCharAndCode(code) current = if (control) KeyCharAndCode.ctrlFromCode(code) else KeyCharAndCode(code)
onKeyHit?.invoke(current) onKeyHit?.invoke(current)

View File

@ -12,6 +12,8 @@ enum class KeyboardBinding(
label: String? = null, label: String? = null,
key: KeyCharAndCode? = null key: KeyCharAndCode? = null
) { ) {
//region Enum Instances
/** Used by [KeyShortcutDispatcher.KeyShortcut] to mark an old-style shortcut with a hardcoded key */ /** Used by [KeyShortcutDispatcher.KeyShortcut] to mark an old-style shortcut with a hardcoded key */
None(Category.None, KeyCharAndCode.UNKNOWN), None(Category.None, KeyCharAndCode.UNKNOWN),
@ -89,10 +91,22 @@ enum class KeyboardBinding(
Confirm(Category.Popups, "Confirm Dialog", 'y'), Confirm(Category.Popups, "Confirm Dialog", 'y'),
Cancel(Category.Popups, "Cancel Dialog", 'n'), Cancel(Category.Popups, "Cancel Dialog", 'n'),
; ;
//endregion
enum class Category { enum class Category {
None, WorldScreen, UnitActions, Popups; None,
WorldScreen {
// Conflict checking within group plus keys assigned to UnitActions are a problem
override fun checkConflictsIn() = sequenceOf(this, UnitActions)
},
UnitActions {
// Conflict checking within group disabled, but any key assigned on WorldScreen is a problem
override fun checkConflictsIn() = sequenceOf(WorldScreen)
},
Popups
;
val label = unCamelCase(name) val label = unCamelCase(name)
open fun checkConflictsIn() = sequenceOf(this)
} }
val label: String val label: String
@ -104,12 +118,13 @@ enum class KeyboardBinding(
this.defaultKey = key ?: KeyCharAndCode(name[0]) this.defaultKey = key ?: KeyCharAndCode(name[0])
} }
// Helpers to make enum instance initializations shorter //region Helper constructors
constructor(category: Category, label: String, key: Char) : this(category, label, KeyCharAndCode(key)) constructor(category: Category, label: String, key: Char) : this(category, label, KeyCharAndCode(key))
constructor(category: Category, label: String, key: Int) : this(category, label, KeyCharAndCode(key)) constructor(category: Category, label: String, key: Int) : this(category, label, KeyCharAndCode(key))
constructor(category: Category, key: KeyCharAndCode) : this(category, null, key) constructor(category: Category, key: KeyCharAndCode) : this(category, null, key)
constructor(category: Category, key: Char) : this(category, KeyCharAndCode(key)) constructor(category: Category, key: Char) : this(category, KeyCharAndCode(key))
constructor(category: Category, key: Int) : this(category, KeyCharAndCode(key)) constructor(category: Category, key: Int) : this(category, KeyCharAndCode(key))
//endregion
/** Debug helper */ /** Debug helper */
override fun toString() = "$category.$name($defaultKey)" override fun toString() = "$category.$name($defaultKey)"

View File

@ -7,6 +7,7 @@ import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.ExpanderTab import com.unciv.ui.components.ExpanderTab
import com.unciv.ui.components.KeyCapturingButton import com.unciv.ui.components.KeyCapturingButton
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
@ -47,7 +48,7 @@ class KeyBindingsTab(
.map { (category, bindings) -> .map { (category, bindings) ->
category to bindings.asSequence() category to bindings.asSequence()
.sortedWith(compareBy(collator) { it.label.tr() }) .sortedWith(compareBy(collator) { it.label.tr() })
.map { it to KeyCapturingButton(it.defaultKey) } // associate would materialize a map .map { it to KeyCapturingButton(it.defaultKey) { onKeyHit() } } // associate would materialize a map
.toMap(LinkedHashMap()) .toMap(LinkedHashMap())
} }
.sortedBy { it.first.name.tr() } .sortedBy { it.first.name.tr() }
@ -81,6 +82,33 @@ class KeyBindingsTab(
} }
} }
private fun onKeyHit() {
for ((category, bindings) in groupedWidgets) {
val scope = category.checkConflictsIn()
if (scope.none()) continue
val usedKeys = mutableSetOf<KeyCharAndCode>()
val conflictingKeys = mutableSetOf<KeyCharAndCode>()
val widgetsInScope = scope
.mapNotNull { groupedWidgets[it] }
.flatMap { scopeMap -> scopeMap.map { it.key.category to it.value } }
for ((scopeCategory ,widget) in widgetsInScope) {
val key = widget.current
// We shall not use any key of a different category in scope,
// nor use a key within this category twice - if this category _is_ in scope.
if (key in usedKeys || scopeCategory != category)
conflictingKeys += key
else
usedKeys += key
}
for (widget in bindings.values) {
widget.markConflict = widget.current in conflictingKeys
}
}
}
fun save () { fun save () {
for ((binding, widget) in groupedWidgets.asSequence().flatMap { it.value.entries }) { for ((binding, widget) in groupedWidgets.asSequence().flatMap { it.value.entries }) {
keyBindings[binding] = widget.current keyBindings[binding] = widget.current