Keyboard bindings for Main Menu Screen (#9680)

* Main Menu keyboard bindings

* Make keyboard binding tooltips dynamic so user changes need no UI rebuild
This commit is contained in:
SomeTroglodyte 2023-07-02 21:28:10 +02:00 committed by GitHub
parent c26837fdd7
commit 6726d2ce03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 45 deletions

View File

@ -35,16 +35,19 @@ import com.unciv.ui.screens.basescreen.BaseScreen
* @param animate Use show/hide animations
* @param forceContentSize Force virtual [content] width/height for alignment calculation
* - because Gdx auto layout reports wrong dimensions on scaled actors.
* @param contentRefresher Called just before showing the [content], to give the builder a chance to do last-minute updates.
* Return value is used as new `forceContentSize`.
*/
// region fields
class UncivTooltip <T: Actor>(
val target: Actor,
val content: T,
val targetAlign: Int = Align.topRight,
val tipAlign: Int = Align.topRight,
val offset: Vector2 = Vector2.Zero,
val animate: Boolean = true,
private val target: Actor,
private val content: T,
private val targetAlign: Int = Align.topRight,
private val tipAlign: Int = Align.topRight,
private val offset: Vector2 = Vector2.Zero,
private val animate: Boolean = true,
forceContentSize: Vector2? = null,
private val contentRefresher: (() -> Vector2?)? = null
) : InputListener() {
private val container: Container<T> = Container(content)
@ -57,8 +60,8 @@ class UncivTooltip <T: Actor>(
// touching buttons (exit fires, sometimes very late, with "to" actor being the label of the button)
private var touchDownSeen = false
private val contentWidth: Float
private val contentHeight: Float
private var contentWidth: Float
private var contentHeight: Float
init {
content.touchable = Touchable.disabled
@ -82,6 +85,13 @@ class UncivTooltip <T: Actor>(
container.remove()
}
if (contentRefresher != null) {
val forceContentSize = contentRefresher.invoke()
container.pack()
contentWidth = forceContentSize?.x ?: content.width
contentHeight = forceContentSize?.y ?: content.height
}
val pos = target.localToStageCoordinates(target.getEdgePoint(targetAlign)).add(offset)
container.run {
val originX = getOriginX(contentWidth, tipAlign)
@ -200,7 +210,7 @@ class UncivTooltip <T: Actor>(
companion object {
/** Duration of the fade/zoom-in/out animations */
const val tipAnimationDuration = 0.2f
private const val tipAnimationDuration = 0.2f
/**
* Add a [Label]-based Tooltip with a rounded-corner background to a [Table] or other [Group].
@ -221,7 +231,8 @@ class UncivTooltip <T: Actor>(
always: Boolean = false,
targetAlign: Int = Align.topRight,
tipAlign: Int = Align.top,
hideIcons: Boolean = false
hideIcons: Boolean = false,
dynamicTextProvider: (() -> String)? = null
) {
for (tip in listeners.filterIsInstance<UncivTooltip<*>>()) {
tip.hide(true)
@ -241,22 +252,37 @@ class UncivTooltip <T: Actor>(
val horizontalPad = if (text.length > 1) 10f else 6f
background.setPadding(4f+skewPadDescenders, horizontalPad, 8f-skewPadDescenders, horizontalPad)
val widthHeightRatio: Float
val multiRowSize = size * (1 + text.count { it == '\n' })
val labelWithBackground = Container(label).apply {
setBackground(background)
pack()
widthHeightRatio = width / height
isTransform = true // otherwise setScale is ignored
setScale(multiRowSize / height)
}
fun getMultiRowSize(text: String) = size * (1 + text.count { it == '\n' })
fun scaleContainerAndGetSize(text: String): Vector2 {
val multiRowSize = getMultiRowSize(text)
val widthHeightRatio = labelWithBackground.run {
pack()
setScale(1f)
val ratio = width / height
setScale(multiRowSize / height)
ratio
}
return Vector2(multiRowSize * widthHeightRatio, multiRowSize)
}
val contentRefresher: (() -> Vector2)? = if (dynamicTextProvider == null) null else { {
val newText = dynamicTextProvider()
label.setText(newText)
scaleContainerAndGetSize(newText)
} }
addListener(UncivTooltip(this,
labelWithBackground,
forceContentSize = Vector2(multiRowSize * widthHeightRatio, multiRowSize),
offset = Vector2(-multiRowSize/4, size/4),
forceContentSize = scaleContainerAndGetSize(text),
offset = Vector2(-getMultiRowSize(text)/4, size/4),
targetAlign = targetAlign,
tipAlign = tipAlign
tipAlign = tipAlign,
contentRefresher = contentRefresher
))
}
@ -287,6 +313,7 @@ class UncivTooltip <T: Actor>(
/**
* Add a [Label]-based Tooltip for a dynamic keyboard binding with a rounded-corner background to a [Table] or other [Group].
* Supports dynamic display of changes to the binding while the tip is attached to an actor, fetched the moment it is shown.
*
* Note this is automatically suppressed on devices without keyboard.
* Tip is positioned over top right corner, slightly overshooting the receiver widget.
@ -294,9 +321,10 @@ class UncivTooltip <T: Actor>(
* @param size _Vertical_ size of the entire Tooltip including background
*/
fun Actor.addTooltip(binding: KeyboardBinding, size: Float = 26f) {
val key = KeyboardBindings[binding]
if (key != KeyCharAndCode.UNKNOWN)
addTooltip(key.toString().tr(), size)
fun getText() = KeyboardBindings[binding].toString().tr()
addTooltip(getText(), size) {
getText()
}
}
}
}

View File

@ -34,6 +34,7 @@ data class KeyCharAndCode(val char: Char, val code: Int) {
//** debug helper, but also used for tooltips */
override fun toString(): String {
return when {
this == UNKNOWN -> "" // Makes tooltip code simpler. Sorry, debuggers.
char == Char.MIN_VALUE -> GdxKeyCodeFixes.toString(code)
this == ESC -> "ESC"
char < ' ' -> "Ctrl-" + (char.toCode() + 64).makeChar()

View File

@ -17,6 +17,16 @@ enum class KeyboardBinding(
/** Used by [KeyShortcutDispatcher.KeyShortcut] to mark an old-style shortcut with a hardcoded key */
None(Category.None, KeyCharAndCode.UNKNOWN),
// MainMenu
Resume(Category.MainMenu),
Quickstart(Category.MainMenu),
StartNewGame(Category.MainMenu, "Start new game", KeyCharAndCode('N')), // Not to be confused with NewGame (from World menu, Ctrl-N)
MainMenuLoad(Category.MainMenu, "Load game", KeyCharAndCode('L')),
Multiplayer(Category.MainMenu), // Name disambiguation maybe soon, not yet necessary
MapEditor(Category.MainMenu, "Map editor", KeyCharAndCode('E')),
ModManager(Category.MainMenu, "Mods", KeyCharAndCode('D')),
MainMenuOptions(Category.MainMenu, "Options", KeyCharAndCode('O')), // Separate binding from World where it's Ctrl-O default
// Worldscreen
Menu(Category.WorldScreen, KeyCharAndCode.TAB),
NextTurn(Category.WorldScreen),
@ -122,6 +132,7 @@ enum class KeyboardBinding(
enum class Category {
None,
MainMenu,
WorldScreen {
// Conflict checking within group plus keys assigned to UnitActions are a problem
override fun checkConflictsIn() = sequenceOf(this, MapPanning, UnitActions)

View File

@ -1,6 +1,5 @@
package com.unciv.ui.screens.mainmenuscreen
import com.badlogic.gdx.Input
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.ui.Stack
@ -24,14 +23,15 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
import com.unciv.ui.components.AutoScrollPane
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.tilegroups.TileGroupMap
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
@ -78,14 +78,13 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
/** Create one **Main Menu Button** including onClick/key binding
* @param text The text to display on the button
* @param icon The path of the icon to display on the button
* @param key Optional key binding (limited to Char subset of [KeyCharAndCode], which is OK for the main menu)
* @param binding keyboard binding
* @param function Action to invoke when the button is activated
*/
private fun getMenuButton(
text: String,
icon: String,
key: Char? = null,
keyVisualOnly: Boolean = false,
binding: KeyboardBinding,
function: () -> Unit
): Table {
val table = Table().pad(15f, 30f, 15f, 30f)
@ -98,17 +97,11 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
table.add(text.toLabel(fontSize = 30, alignment = Align.left)).expand().left().minWidth(200f)
table.touchable = Touchable.enabled
table.onActivation {
table.onActivation(binding = binding) {
stopBackgroundMapGeneration()
function()
}
if (key != null) {
if (!keyVisualOnly)
table.keyShortcuts.add(key)
table.addTooltip(key, 32f)
}
table.pack()
return table
}
@ -141,36 +134,36 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
val column2 = if (singleColumn) column1 else Table().apply { defaults().pad(10f).fillX() }
if (game.files.autosaveExists()) {
val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r')
val resumeTable = getMenuButton("Resume","OtherIcons/Resume", KeyboardBinding.Resume)
{ resumeGame() }
column1.add(resumeTable).row()
}
val quickstartTable = getMenuButton("Quickstart", "OtherIcons/Quickstart", 'q')
val quickstartTable = getMenuButton("Quickstart", "OtherIcons/Quickstart", KeyboardBinding.Quickstart)
{ quickstartNewGame() }
column1.add(quickstartTable).row()
val newGameButton = getMenuButton("Start new game", "OtherIcons/New", 'n')
val newGameButton = getMenuButton("Start new game", "OtherIcons/New", KeyboardBinding.StartNewGame)
{ game.pushScreen(NewGameScreen()) }
column1.add(newGameButton).row()
val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l')
val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", KeyboardBinding.MainMenuLoad)
{ game.pushScreen(LoadGameScreen()) }
column1.add(loadGameTable).row()
val multiplayerTable = getMenuButton("Multiplayer", "OtherIcons/Multiplayer", 'm')
val multiplayerTable = getMenuButton("Multiplayer", "OtherIcons/Multiplayer", KeyboardBinding.Multiplayer)
{ game.pushScreen(MultiplayerScreen()) }
column2.add(multiplayerTable).row()
val mapEditorScreenTable = getMenuButton("Map editor", "OtherIcons/MapEditor", 'e')
val mapEditorScreenTable = getMenuButton("Map editor", "OtherIcons/MapEditor", KeyboardBinding.MapEditor)
{ game.pushScreen(MapEditorScreen()) }
column2.add(mapEditorScreenTable).row()
val modsTable = getMenuButton("Mods", "OtherIcons/Mods", 'd')
val modsTable = getMenuButton("Mods", "OtherIcons/Mods", KeyboardBinding.ModManager)
{ game.pushScreen(ModManagementScreen()) }
column2.add(modsTable).row()
val optionsTable = getMenuButton("Options", "OtherIcons/Options", 'o')
val optionsTable = getMenuButton("Options", "OtherIcons/Options", KeyboardBinding.MainMenuOptions)
{ this.openOptionsPopup() }
column2.add(optionsTable).row()
@ -199,9 +192,10 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
.apply { actor.y -= 2.5f } // compensate font baseline (empirical)
.surroundWithCircle(64f, resizeActor = false)
helpButton.touchable = Touchable.enabled
// Passing the binding directly to onActivation gives you a size 26 tooltip...
helpButton.onActivation { openCivilopedia() }
helpButton.keyShortcuts.add(Input.Keys.F1)
helpButton.addTooltip(KeyCharAndCode(Input.Keys.F1), 30f)
helpButton.keyShortcuts.add(KeyboardBinding.Civilopedia)
helpButton.addTooltip(KeyboardBinding.Civilopedia, 30f)
helpButton.setPosition(30f, 30f)
stage.addActor(helpButton)
}