diff --git a/android/Images/OtherIcons/Keyboard.png b/android/Images/OtherIcons/Keyboard.png new file mode 100644 index 0000000000..26e93ba064 Binary files /dev/null and b/android/Images/OtherIcons/Keyboard.png differ diff --git a/android/assets/game.atlas b/android/assets/game.atlas index 6fe69b066e..4e52ea5351 100644 --- a/android/assets/game.atlas +++ b/android/assets/game.atlas @@ -13,126 +13,126 @@ CityStateIcons/Cultured index: -1 CityStateIcons/Maritime rotate: false - xy: 1831, 1823 + xy: 1831, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 CityStateIcons/Mercantile rotate: false - xy: 1939, 1823 + xy: 1939, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 CityStateIcons/Militaristic rotate: false - xy: 1939, 1715 + xy: 535, 1577 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 CityStateIcons/Religious rotate: false - xy: 1507, 1499 + xy: 1615, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 EmojiIcons/Culture rotate: false - xy: 436, 526 + xy: 436, 418 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Death rotate: false - xy: 436, 468 + xy: 436, 360 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Faith rotate: false - xy: 436, 410 + xy: 436, 302 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Food rotate: false - xy: 436, 352 + xy: 436, 244 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Gold rotate: false - xy: 436, 294 + xy: 436, 186 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Great Artist rotate: false - xy: 436, 236 + xy: 436, 128 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Great Engineer rotate: false - xy: 436, 178 + xy: 436, 70 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Great General rotate: false - xy: 436, 120 + xy: 436, 12 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Great Merchant rotate: false - xy: 436, 62 + xy: 494, 534 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Great Scientist rotate: false - xy: 436, 4 + xy: 494, 476 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Happiness rotate: false - xy: 494, 642 + xy: 494, 418 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Production rotate: false - xy: 494, 294 + xy: 494, 70 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Science rotate: false - xy: 494, 120 + xy: 552, 549 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 EmojiIcons/Turn rotate: false - xy: 494, 4 + xy: 552, 433 size: 50, 50 orig: 50, 50 offset: 0, 0 @@ -237,154 +237,154 @@ ImprovementIcons/Holy site index: -1 ImprovementIcons/Landmark rotate: false - xy: 1291, 1715 + xy: 1399, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Lumber mill rotate: false - xy: 1507, 1715 + xy: 1615, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Manufactory rotate: false - xy: 1615, 1715 + xy: 1723, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Mine rotate: false - xy: 535, 1577 + xy: 643, 1592 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Moai rotate: false - xy: 643, 1592 + xy: 751, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Offshore Platform rotate: false - xy: 1399, 1607 + xy: 1507, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Oil well rotate: false - xy: 1615, 1607 + xy: 1723, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Pasture rotate: false - xy: 1831, 1607 + xy: 1939, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Plantation rotate: false - xy: 514, 1361 + xy: 514, 1253 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Polder rotate: false - xy: 514, 1253 + xy: 514, 1145 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Quarry rotate: false - xy: 967, 1499 + xy: 1075, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Railroad rotate: false - xy: 1399, 1499 + xy: 1507, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Remove Fallout rotate: false - xy: 1723, 1499 + xy: 1831, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Remove Forest rotate: false - xy: 1831, 1499 + xy: 1939, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Remove Jungle rotate: false - xy: 1831, 1499 + xy: 1939, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Remove Marsh rotate: false - xy: 751, 1392 + xy: 859, 1392 size: 100, 99 orig: 100, 99 offset: 0, 0 index: -1 ImprovementIcons/Remove Railroad rotate: false - xy: 859, 1391 + xy: 967, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Remove Road rotate: false - xy: 967, 1391 + xy: 1075, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Repair rotate: false - xy: 1075, 1391 + xy: 1183, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Road rotate: false - xy: 1399, 1391 + xy: 1507, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Terrace farm rotate: false - xy: 328, 881 + xy: 328, 773 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ImprovementIcons/Trading post rotate: false - xy: 328, 665 + xy: 328, 557 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -426,14 +426,14 @@ StatIcons/Faith index: -1 NotificationIcons/PickConstruction rotate: false - xy: 406, 1313 + xy: 406, 1205 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 StatIcons/Production rotate: false - xy: 406, 1313 + xy: 406, 1205 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -447,14 +447,14 @@ NotificationIcons/PickPolicy index: -1 NotificationIcons/PickTech rotate: false - xy: 406, 1205 + xy: 520, 1469 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 StatIcons/Science rotate: false - xy: 406, 1205 + xy: 520, 1469 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -503,7 +503,7 @@ StatIcons/Movement index: -1 OtherIcons/BackArrow rotate: false - xy: 436, 642 + xy: 436, 534 size: 50, 50 orig: 50, 50 offset: 0, 0 @@ -573,7 +573,7 @@ OtherIcons/Cities index: -1 OtherIcons/CityState rotate: false - xy: 436, 584 + xy: 436, 476 size: 50, 50 orig: 50, 50 offset: 0, 0 @@ -643,154 +643,161 @@ OtherIcons/HexagonOutline index: -1 OtherIcons/Improvements rotate: false - xy: 494, 584 + xy: 494, 360 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 +OtherIcons/Keyboard + rotate: false + xy: 1291, 1715 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 OtherIcons/Link rotate: false - xy: 494, 468 + xy: 494, 244 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 OtherIcons/Load rotate: false - xy: 1399, 1823 + xy: 1399, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Loading rotate: false - xy: 1399, 1715 + xy: 1507, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 NotificationIcons/Loading rotate: false - xy: 1399, 1715 + xy: 1507, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 NotificationIcons/Working rotate: false - xy: 1399, 1715 + xy: 1507, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/LockSmall rotate: false - xy: 494, 410 + xy: 494, 186 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 OtherIcons/MapEditor rotate: false - xy: 1723, 1823 + xy: 1723, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/MenuIcon rotate: false - xy: 1831, 1715 + xy: 1939, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Mods rotate: false - xy: 751, 1607 + xy: 859, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Multiplayer rotate: false - xy: 859, 1607 + xy: 967, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/NationSwap rotate: false - xy: 967, 1607 + xy: 1075, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Nations rotate: false - xy: 494, 352 + xy: 494, 128 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 OtherIcons/New rotate: false - xy: 1075, 1607 + xy: 1183, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Notifications rotate: false - xy: 1291, 1607 + xy: 1399, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Options rotate: false - xy: 1723, 1607 + xy: 1831, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Pencil rotate: false - xy: 412, 1529 + xy: 406, 1421 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Pentagon rotate: false - xy: 406, 1421 + xy: 406, 1313 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Pillage rotate: false - xy: 520, 1469 + xy: 514, 1361 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Politics rotate: false - xy: 514, 1145 + xy: 643, 1484 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 NotificationIcons/WorldCongressVote rotate: false - xy: 514, 1145 + xy: 643, 1484 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Present rotate: false - xy: 859, 1499 + xy: 967, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -804,119 +811,119 @@ OtherIcons/Puppet index: -1 OtherIcons/Quest rotate: false - xy: 1075, 1499 + xy: 1183, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Question rotate: false - xy: 1183, 1499 + xy: 1291, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Quickstart rotate: false - xy: 1291, 1499 + xy: 1399, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Remove Heresy rotate: false - xy: 1939, 1499 + xy: 751, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Resources rotate: false - xy: 1183, 1391 + xy: 1291, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Resume rotate: false - xy: 1291, 1391 + xy: 1399, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Search rotate: false - xy: 1615, 1391 + xy: 1723, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/SecretOptions rotate: false - xy: 1723, 1391 + xy: 1831, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Settings rotate: false - xy: 1831, 1391 + xy: 1939, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Shield rotate: false - xy: 227, 1111 + xy: 220, 1003 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Sleep rotate: false - xy: 220, 787 + xy: 220, 679 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Speaker rotate: false - xy: 220, 679 + xy: 220, 571 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Spy rotate: false - xy: 220, 355 + xy: 220, 247 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Spy_White rotate: false - xy: 220, 247 + xy: 220, 139 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Star rotate: false - xy: 220, 145 + xy: 220, 37 size: 100, 94 orig: 100, 94 offset: 0, 0 index: -1 OtherIcons/Swap rotate: false - xy: 328, 989 + xy: 328, 881 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Timer rotate: false - xy: 328, 773 + xy: 328, 665 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -930,35 +937,35 @@ OtherIcons/Triangle index: -1 OtherIcons/Turn right rotate: false - xy: 328, 449 + xy: 328, 341 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Tyrannosaurus rotate: false - xy: 328, 341 + xy: 328, 233 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/WLTKD rotate: false - xy: 436, 700 + xy: 436, 592 size: 83, 65 orig: 83, 65 offset: 0, 0 index: -1 OtherIcons/Wait rotate: false - xy: 328, 125 + xy: 328, 17 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 OtherIcons/Wonders rotate: false - xy: 436, 773 + xy: 436, 665 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -1105,112 +1112,112 @@ ResourceIcons/Jewelry index: -1 ResourceIcons/Marble rotate: false - xy: 1723, 1715 + xy: 1831, 1823 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Oil rotate: false - xy: 1507, 1607 + xy: 1615, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Pearls rotate: false - xy: 1939, 1607 + xy: 412, 1529 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Porcelain rotate: false - xy: 751, 1499 + xy: 859, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Salt rotate: false - xy: 1507, 1391 + xy: 1615, 1391 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Sheep rotate: false - xy: 1939, 1391 + xy: 227, 1111 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Silk rotate: false - xy: 220, 1003 + xy: 220, 895 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Silver rotate: false - xy: 220, 895 + xy: 220, 787 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Spices rotate: false - xy: 220, 463 + xy: 220, 355 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Stone rotate: false - xy: 220, 37 + xy: 335, 1097 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Sugar rotate: false - xy: 335, 1097 + xy: 328, 989 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Truffles rotate: false - xy: 328, 557 + xy: 328, 449 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Uranium rotate: false - xy: 328, 233 + xy: 328, 125 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Whales rotate: false - xy: 328, 17 + xy: 436, 989 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Wheat rotate: false - xy: 436, 989 + xy: 436, 881 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 ResourceIcons/Wine rotate: false - xy: 436, 881 + xy: 436, 773 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -1259,49 +1266,49 @@ StatIcons/Happiness index: -1 StatIcons/InterceptRange rotate: false - xy: 494, 526 + xy: 494, 302 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 StatIcons/Malcontent rotate: false - xy: 1615, 1823 + xy: 1615, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 StatIcons/Population rotate: false - xy: 643, 1484 + xy: 751, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 TileIcons/Worked rotate: false - xy: 643, 1484 + xy: 751, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 StatIcons/Range rotate: false - xy: 494, 236 + xy: 494, 12 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 StatIcons/RangedStrength rotate: false - xy: 494, 178 + xy: 527, 607 size: 50, 50 orig: 50, 50 offset: 0, 0 index: -1 StatIcons/ReligiousStrength rotate: false - xy: 1615, 1499 + xy: 1723, 1499 size: 100, 100 orig: 100, 100 offset: 0, 0 @@ -1315,14 +1322,14 @@ StatIcons/Resistance index: -1 StatIcons/Specialist rotate: false - xy: 220, 571 + xy: 220, 463 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 StatIcons/Strength rotate: false - xy: 494, 62 + xy: 552, 491 size: 50, 50 orig: 50, 50 offset: 0, 0 @@ -1350,14 +1357,14 @@ TileIcons/CityCenter index: -1 TileIcons/Locked rotate: false - xy: 1507, 1823 + xy: 1507, 1715 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 TileIcons/NotWorked rotate: false - xy: 1183, 1607 + xy: 1291, 1607 size: 100, 100 orig: 100, 100 offset: 0, 0 diff --git a/android/assets/game.png b/android/assets/game.png index 47021a877a..4e0b685e09 100644 Binary files a/android/assets/game.png and b/android/assets/game.png differ diff --git a/android/assets/jsons/translations/German.properties b/android/assets/jsons/translations/German.properties index 0f89b5b9ef..55f68a6dd5 100644 --- a/android/assets/jsons/translations/German.properties +++ b/android/assets/jsons/translations/German.properties @@ -670,6 +670,7 @@ Display = Anzeige Gameplay = Spielmechanik Sound = Sound Advanced = Erweitert +Keys = Tastenzuordnung Locate mod errors = Mod-Probleme Debug = Nur für Eingeweihte @@ -2302,6 +2303,15 @@ You gave us units! = Ihr habt uns Einheiten geschenkt! We appreciate your gifts = Wir wissen eure Geschenke zu schätzen You returned captured units to us = Ihr habt uns gefangene Einheiten zurückgegeben + +#################### Lines from key bindings ####################### + +Next Turn = Nächste Runde +Next Turn Alternate = Nächste Runde Alternativ +Empire Overview = Reichsübersicht +Confirm Dialog = Dialog bestätigen +Cancel Dialog = Dialog ablehnen + #################### Lines from Buildings from Civ V - Vanilla #################### Palace = Palast @@ -6286,4 +6296,3 @@ Enemy military land units block tiles they are standing on. Enemy military naval City Blockade = Blockade einer Stadt One of your cities is under a naval blockade! When all adjacent water tiles of a coastal city are blocked - city loses harbor connection to all other cities, including capital. Make sure to de-blockade cities by deploying friendly military naval units to fight off invaders. = Eine deiner Städte steht unter einer Seeblockade! Wenn alle angrenzenden Wasserfelder einer Küstenstadt blockiert sind, verliert die Stadt die Hafenverbindung zu allen anderen Städten, einschließlich der Hauptstadt. Stelle sicher, dass du die Blockade aufhebst, indem du befreundete militärische Marineeinheiten einsetzt, um Eindringlinge abzuwehren. - diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index a714c435d3..ddc4c9180c 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -670,6 +670,7 @@ Display = Gameplay = Sound = Advanced = +Keys = Locate mod errors = Debug = diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index e101576b5e..98dedef15a 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -110,6 +110,16 @@ object GUI { return UncivGame.Current.worldScreen!!.selectedCiv } + private var keyboardAvailableCache: Boolean? = null + /** Tests availability of a physical keyboard */ + val keyboardAvailable: Boolean + get() { + // defer decision if Gdx.input not yet initialized + if (keyboardAvailableCache == null && Gdx.input != null) + keyboardAvailableCache = Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard) + return keyboardAvailableCache ?: false + } + } open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpecific { diff --git a/core/src/com/unciv/json/UncivJson.kt b/core/src/com/unciv/json/UncivJson.kt index 2fc6742400..5f9293aa4e 100644 --- a/core/src/com/unciv/json/UncivJson.kt +++ b/core/src/com/unciv/json/UncivJson.kt @@ -3,6 +3,9 @@ package com.unciv.json import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.SerializationException +import com.unciv.ui.components.KeyCharAndCode +import com.unciv.ui.components.KeyboardBindings import java.time.Duration @@ -13,8 +16,15 @@ fun json() = Json().apply { setIgnoreDeprecated(true) ignoreUnknownFields = true + // Default output type is JsonWriter.OutputType.minimal, which generates invalid Json - e.g. most quotes removed. + // To get better Json, use: + // setOutputType(JsonWriter.OutputType.json) + // Note an instance set to json can read minimal and vice versa + setSerializer(HashMapVector2.getSerializerClass(), HashMapVector2.createSerializer()) setSerializer(Duration::class.java, DurationSerializer()) + setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer()) + setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer()) } /** diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index ab623a91e6..8e54e7343e 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -9,6 +9,7 @@ import com.unciv.logic.multiplayer.FriendList import com.unciv.models.UncivSound import com.unciv.ui.components.FontFamilyData import com.unciv.ui.components.Fonts +import com.unciv.ui.components.KeyboardBindings import com.unciv.utils.ScreenOrientation import java.text.Collator import java.time.Duration @@ -103,6 +104,8 @@ class GameSettings { /** Maximum zoom-out of the map - performance heavy */ var maxWorldZoomOut = 2f + var keyBindings = KeyboardBindings() + /** used to migrate from older versions of the settings */ var version: Int? = null diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index 25404e6354..ab0896685d 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -21,6 +21,7 @@ import com.unciv.models.ruleset.unique.* import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.UnitType +import com.unciv.ui.components.KeyboardBinding import com.unciv.utils.debug import java.io.File import java.lang.reflect.Field @@ -86,7 +87,7 @@ object TranslationFileWriter { val linesToTranslate = mutableListOf() if (modFolder == null) { // base game - val templateFile = getFileHandle(modFolder, templateFileLocation) // read the template + val templateFile = getFileHandle(null, templateFileLocation) // read the template if (templateFile.exists()) linesToTranslate.addAll(templateFile.reader(TranslationFileReader.charset).readLines()) @@ -118,6 +119,10 @@ object TranslationFileWriter { for (diplomaticModifier in DiplomaticModifiers.values()) linesToTranslate += "${diplomaticModifier.text} = " + linesToTranslate += "\n\n#################### Lines from key bindings #######################\n" + for (binding in KeyboardBinding.values()) { + linesToTranslate += "${binding.label} = " + } for (baseRuleset in BaseRuleset.values()) { val generatedStringsFromBaseRuleset = diff --git a/core/src/com/unciv/ui/components/KeyCharAndCode.kt b/core/src/com/unciv/ui/components/KeyCharAndCode.kt index d3f0875ad5..069cc0fac3 100644 --- a/core/src/com/unciv/ui/components/KeyCharAndCode.kt +++ b/core/src/com/unciv/ui/components/KeyCharAndCode.kt @@ -1,7 +1,9 @@ package com.unciv.ui.components -import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input +import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.JsonValue + /* * For now, many combination keys cannot easily be expressed. @@ -44,9 +46,6 @@ data class KeyCharAndCode(val char: Char, val code: Int) { } companion object { - /** Tests presence of a physical keyboard - static here as convenience shortcut only */ - val keyboardAvailable = Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard) - // Convenience shortcuts for frequently used constants /** Android back, assigns ESC automatically as well */ val BACK = KeyCharAndCode(Input.Keys.BACK) @@ -83,74 +82,32 @@ data class KeyCharAndCode(val char: Char, val code: Int) { val code = Input.Keys.valueOf(char.uppercaseChar().toString()) return if (code == -1) KeyCharAndCode(char,0) else KeyCharAndCode(Char.MIN_VALUE, code) } - } -} - -data class KeyShortcut(val key: KeyCharAndCode, val priority: Int = 0) - - -open class KeyShortcutDispatcher { - private val shortcuts: MutableList Unit>> = mutableListOf() - - fun add(shortcut: KeyShortcut?, action: (() -> Unit)?): Unit { - if (action == null || shortcut == null) return - shortcuts.removeIf { it.first == shortcut } - shortcuts.add(Pair(shortcut, action)) - } - - fun add(key: KeyCharAndCode?, action: (() -> Unit)?): Unit { - if (key != null) - add(KeyShortcut(key), action) - } - - fun add(char: Char?, action: (() -> Unit)?): Unit { - if (char != null) - add(KeyCharAndCode(char), action) - } - - fun add(keyCode: Int?, action: (() -> Unit)?): Unit { - if (keyCode != null) - add(KeyCharAndCode(keyCode), action) - } - - fun remove(shortcut: KeyShortcut?): Unit { - shortcuts.removeIf { it.first == shortcut } - } - - fun remove(key: KeyCharAndCode?): Unit { - shortcuts.removeIf { it.first.key == key } - } - - fun remove(char: Char?): Unit { - shortcuts.removeIf { it.first.key.char == char } - } - - fun remove(keyCode: Int?): Unit { - shortcuts.removeIf { it.first.key.code == keyCode } - } - - open fun isActive(): Boolean = true - - - class Resolver(val key: KeyCharAndCode) { - private var priority = Int.MIN_VALUE - val trigerredActions: MutableList<() -> Unit> = mutableListOf() - - fun updateFor(dispatcher: KeyShortcutDispatcher) { - if (!dispatcher.isActive()) return - - for (shortcut in dispatcher.shortcuts) { - if (shortcut.first.key == key) { - if (shortcut.first.priority == priority) - trigerredActions.add(shortcut.second) - else if (shortcut.first.priority > priority) { - priority = shortcut.first.priority - trigerredActions.clear() - trigerredActions.add(shortcut.second) - } - } + fun parse(text: String): KeyCharAndCode = when { + text.length == 1 && text[0].isDefined() -> KeyCharAndCode(text[0]) + text.length == 3 && text[0] == '"' && text[2] == '"' -> KeyCharAndCode(text[1]) + text.length == 6 && text.startsWith("Ctrl-") -> ctrl(text[5]) + text == "ESC" -> ESC + text == "Backspace" -> KeyCharAndCode(Input.Keys.BACKSPACE) + text == "Del" -> DEL + Input.Keys.valueOf(text) != -1 -> KeyCharAndCode(Input.Keys.valueOf(text)) + else -> UNKNOWN } + } + + class Serializer : Json.Serializer { + override fun write(json: Json, key: KeyCharAndCode, knownType: Class<*>?) { + // Gdx Json is.... No comment. This `Any` is needed to resolve the ambiguity between + // public void writeValue (String name, @Null Object value, @Null Class knownType) + // and + // public void writeValue (@Null Object value, @Null Class knownType, @Null Class elementType) + // - we want the latter. And without the explicitly provided knownType it will _unpredictably_ use + // `{"class":"java.lang.String","value":"Space"}` instead of `"Space"`. + json.writeValue(key.toString() as Any, String::class.java, null) + } + + override fun read(json: Json, jsonData: JsonValue, type: Class<*>?): KeyCharAndCode { + return parse(jsonData.asString()) } } } diff --git a/core/src/com/unciv/ui/components/KeyShortcutDispatcher.kt b/core/src/com/unciv/ui/components/KeyShortcutDispatcher.kt new file mode 100644 index 0000000000..6b17944ebd --- /dev/null +++ b/core/src/com/unciv/ui/components/KeyShortcutDispatcher.kt @@ -0,0 +1,77 @@ +package com.unciv.ui.components + +open class KeyShortcutDispatcher { + data class KeyShortcut(val key: KeyCharAndCode, val priority: Int = 0) + + private data class ShortcutAction(val shortcut: KeyShortcut, val action: () -> Unit) + private val shortcuts: MutableList = mutableListOf() + + fun add(shortcut: KeyShortcut?, action: (() -> Unit)?) { + if (action == null || shortcut == null) return + shortcuts.removeIf { it.shortcut == shortcut } + shortcuts.add(ShortcutAction(shortcut, action)) + } + + fun add(binding: KeyboardBinding, action: (() -> Unit)?) { + add(KeyboardBindings[binding], action) + } + + fun add(key: KeyCharAndCode?, action: (() -> Unit)?) { + if (key != null) + add(KeyShortcut(key), action) + } + + fun add(char: Char?, action: (() -> Unit)?) { + if (char != null) + add(KeyCharAndCode(char), action) + } + + fun add(keyCode: Int?, action: (() -> Unit)?) { + if (keyCode != null) + add(KeyCharAndCode(keyCode), action) + } + + fun remove(shortcut: KeyShortcut?) { + shortcuts.removeIf { it.shortcut == shortcut } + } + + fun remove(binding: KeyboardBinding) { + remove(KeyboardBindings[binding]) + } + + fun remove(key: KeyCharAndCode?) { + shortcuts.removeIf { it.shortcut.key == key } + } + + fun remove(char: Char?) { + shortcuts.removeIf { it.shortcut.key.char == char } + } + + fun remove(keyCode: Int?) { + shortcuts.removeIf { it.shortcut.key.code == keyCode } + } + + open fun isActive(): Boolean = true + + + class Resolver(val key: KeyCharAndCode) { + private var priority = Int.MIN_VALUE + val triggeredActions: MutableList<() -> Unit> = mutableListOf() + + fun updateFor(dispatcher: KeyShortcutDispatcher) { + if (!dispatcher.isActive()) return + + for ((shortcut, action) in dispatcher.shortcuts) { + if (shortcut.key == key) { + if (shortcut.priority == priority) + triggeredActions.add(action) + else if (shortcut.priority > priority) { + priority = shortcut.priority + triggeredActions.clear() + triggeredActions.add(action) + } + } + } + } + } +} diff --git a/core/src/com/unciv/ui/components/KeyboardBinding.kt b/core/src/com/unciv/ui/components/KeyboardBinding.kt new file mode 100644 index 0000000000..64edf8d461 --- /dev/null +++ b/core/src/com/unciv/ui/components/KeyboardBinding.kt @@ -0,0 +1,29 @@ +package com.unciv.ui.components + +import com.badlogic.gdx.Input + +private val unCamelCaseRegex = Regex("([A-Z])([A-Z])([a-z])|([a-z])([A-Z])") +private fun unCamelCase(name: String) = unCamelCaseRegex.replace(name, """$1$4 $2$3$5""") + +enum class KeyboardBinding( + label: String? = null, + key: KeyCharAndCode? = null +) { + // Worldscreen + NextTurn, + NextTurnAlternate(key = KeyCharAndCode.SPACE), + Civilopedia(key = KeyCharAndCode(Input.Keys.F1)), + EmpireOverview, + // Popups + Confirm("Confirm Dialog", KeyCharAndCode('y')), + Cancel("Cancel Dialog", KeyCharAndCode('n')), + ; + + val label: String + val defaultKey: KeyCharAndCode + + init { + this.label = label ?: unCamelCase(name) + this.defaultKey = key ?: KeyCharAndCode(name[0]) + } +} diff --git a/core/src/com/unciv/ui/components/KeyboardBindings.kt b/core/src/com/unciv/ui/components/KeyboardBindings.kt new file mode 100644 index 0000000000..fe3b8caa83 --- /dev/null +++ b/core/src/com/unciv/ui/components/KeyboardBindings.kt @@ -0,0 +1,82 @@ +package com.unciv.ui.components + +import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.JsonValue +import com.unciv.GUI + +/** + * Manage user-configurable keyboard bindings + * + * A primary instance lives in [UncivGame.Current.settings][com.unciv.models.metadata.GameSettings] + * and is read/write accessible through the `KeyboardBindings[]` syntax. + **/ +class KeyboardBindings : HashMap() { + + /** this [put] overload helps the Json [Serializer] read method */ + private fun put(element: JsonValue) { + put(element.name, (element["value"] ?: element).asString()) + } + + /** Allows adding entries by [KeyboardBinding] as name / [KeyCharAndCode] as string representation */ + private fun put(name: String, value: String) { + val binding = KeyboardBinding.values().firstOrNull { it.name == name} ?: return + put(binding, value) + } + + /** Allows adding entries by [KeyCharAndCode] string representation, + * an empty [value] resets the binding to default */ + fun put(binding: KeyboardBinding, value: String) { + if (value.isEmpty()) { + remove(binding) + } else { + val key = KeyCharAndCode.parse(value) + if (key != KeyCharAndCode.UNKNOWN) + put(binding, key) + } + } + + /** + * Adds or replaces a binding or removes it if [value] is the default for [key] + * @param key the map key defining the binding + * @param value the keyboard key to assign + * @return the previously bound key if any + */ + // Note clearer parameter names gives "PARAMETER_NAME_CHANGED_ON_OVERRIDE" warnings + override fun put(key: KeyboardBinding, value: KeyCharAndCode): KeyCharAndCode? { + val result = super.get(key) + if (key.defaultKey == value) + remove(key) + else + super.put(key, value) + return result + } + + /** Indexed access will return default key for missing entries */ + override fun get(key: KeyboardBinding) = super.get(key) ?: key.defaultKey + + companion object { + // Convenience shortcuts allowing `KeyboardBindings[binding]` globally, accesses global settings + operator fun get(binding: KeyboardBinding) = default[binding] + operator fun set(binding: KeyboardBinding, key: KeyCharAndCode) { default[binding] = key } + val default get() = GUI.getSettings().keyBindings + } + + /** + * This class helps Gdx Json to read/write a readable, minimal serialization + * - without, KeyCharAndCode.Serializer.write will not be used properly + */ + class Serializer : Json.Serializer { + override fun write(json: Json, bindings: KeyboardBindings, knownType: Class<*>?) { + json.writeObjectStart() + for ((binding, key) in bindings) { + json.writeValue(binding.name, key, KeyCharAndCode::class.java) + } + json.writeObjectEnd() + } + + override fun read(json: Json, jsonData: JsonValue, type: Class<*>?) = KeyboardBindings().apply { + if (jsonData.isObject && jsonData.notEmpty()) + for (element in jsonData) put(element) + } + } +} diff --git a/core/src/com/unciv/ui/components/UncivTooltip.kt b/core/src/com/unciv/ui/components/UncivTooltip.kt index bd7cd0ee6f..8ea279e149 100644 --- a/core/src/com/unciv/ui/components/UncivTooltip.kt +++ b/core/src/com/unciv/ui/components/UncivTooltip.kt @@ -14,6 +14,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Tooltip import com.badlogic.gdx.utils.Align +import com.unciv.GUI import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.screens.basescreen.BaseScreen @@ -181,7 +182,7 @@ class UncivTooltip ( targetAlign: Int = Align.topRight, tipAlign: Int = Align.top ) { - if (!(always || KeyCharAndCode.keyboardAvailable) || text.isEmpty()) return + if (!(always || GUI.keyboardAvailable) || text.isEmpty()) return val label = text.toLabel(BaseScreen.skinStrings.skinConfig.baseColor, 38) label.setAlignment(Align.center) diff --git a/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt b/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt index b0630ebf4c..b5abe9b501 100644 --- a/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt @@ -32,8 +32,8 @@ import com.unciv.ui.audio.SoundPlayer import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.Fonts import com.unciv.ui.components.KeyCharAndCode -import com.unciv.ui.components.KeyShortcut import com.unciv.ui.components.KeyShortcutDispatcher +import com.unciv.ui.components.KeyboardBinding import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.ImageGetter import com.unciv.utils.concurrency.Concurrency @@ -98,16 +98,17 @@ fun Color.brighten(t: Float): Color = Color(this).lerp(Color.WHITE, t) * [activating][Actor.activate] the actor. However, other actions are possible too. */ class ActorKeyShortcutDispatcher internal constructor(val actor: Actor): KeyShortcutDispatcher() { - fun add(shortcut: KeyShortcut?): Unit = add(shortcut) { actor.activate() } - fun add(key: KeyCharAndCode?): Unit = add(key) { actor.activate() } - fun add(char: Char?): Unit = add(char) { actor.activate() } - fun add(keyCode: Int?): Unit = add(keyCode) { actor.activate() } + fun add(shortcut: KeyShortcut?) = add(shortcut) { actor.activate() } + fun add(binding: KeyboardBinding) = add(binding) { actor.activate() } + fun add(key: KeyCharAndCode?) = add(key) { actor.activate() } + fun add(char: Char?) = add(char) { actor.activate() } + fun add(keyCode: Int?) = add(keyCode) { actor.activate() } override fun isActive(): Boolean = actor.isActive() } -private class ActorAttachments { +private class ActorAttachments private constructor(actor: Actor) { companion object { fun getOrNull(actor: Actor): ActorAttachments? { return actor.userObject as ActorAttachments? @@ -127,11 +128,7 @@ private class ActorAttachments { private lateinit var activationActions: MutableList<() -> Unit> private var clickActivationListener: ClickListener? = null - val keyShortcuts: ActorKeyShortcutDispatcher - - private constructor(actor: Actor) { - keyShortcuts = ActorKeyShortcutDispatcher(actor) - } + val keyShortcuts = ActorKeyShortcutDispatcher(actor) fun activate() { if (this::activationActions.isInitialized) { @@ -175,7 +172,7 @@ fun Actor.removeActivationAction(action: (() -> Unit)?) { ActorAttachments.getOrNull(this)?.removeActivationAction(action) } -fun Actor.isActive(): Boolean = isVisible() && !(this is Disableable && (this as Disableable).isDisabled()) +fun Actor.isActive(): Boolean = isVisible && ((this as? Disableable)?.isDisabled != true) fun Actor.activate() { if (isActive()) @@ -188,10 +185,10 @@ val Actor.keyShortcuts get() = ActorAttachments.get(this).keyShortcuts fun Actor.onActivation(sound: UncivSound = UncivSound.Click, action: () -> Unit): Actor { - addActivationAction({ + addActivationAction { Concurrency.run("Sound") { SoundPlayer.play(sound) } action() - }) + } return this } @@ -202,7 +199,7 @@ enum class DispatcherVetoResult { Accept, Skip, SkipWithChildren } typealias DispatcherVetoer = (associatedActor: Actor?, keyDispatcher: KeyShortcutDispatcher?) -> DispatcherVetoResult /** - * Install shorcut dispatcher for this stage. It activates all actions associated with the + * Install shortcut dispatcher for this stage. It activates all actions associated with the * pressed key in [additionalShortcuts] (if specified) and all actors in the stage. It is * possible to temporarily disable or veto some shortcut dispatchers by passing an appropriate * [dispatcherVetoerCreator] function. This function may return a [DispatcherVetoer], which @@ -244,7 +241,7 @@ fun Stage.installShortcutDispatcher(additionalShortcuts: KeyShortcutDispatcher? private fun activate(key: KeyCharAndCode, dispatcherVetoer: DispatcherVetoer): Boolean { val shortcutResolver = KeyShortcutDispatcher.Resolver(key) - val pendingActors = ArrayDeque(getActors().toList()) + val pendingActors = ArrayDeque(actors.toList()) if (additionalShortcuts != null && dispatcherVetoer(null, additionalShortcuts) == DispatcherVetoResult.Accept) shortcutResolver.updateFor(additionalShortcuts) @@ -257,12 +254,12 @@ fun Stage.installShortcutDispatcher(additionalShortcuts: KeyShortcutDispatcher? if (shortcuts != null && vetoResult == DispatcherVetoResult.Accept) shortcutResolver.updateFor(shortcuts) if (actor is Group && vetoResult != DispatcherVetoResult.SkipWithChildren) - pendingActors.addAll(actor.getChildren()) + pendingActors.addAll(actor.children) } - for (action in shortcutResolver.trigerredActions) + for (action in shortcutResolver.triggeredActions) action() - return shortcutResolver.trigerredActions.any() + return shortcutResolver.triggeredActions.any() } }) } @@ -377,13 +374,13 @@ fun Actor.getAscendant(predicate: (Actor) -> Boolean): Actor? { /** The actors bounding box in stage coordinates */ val Actor.stageBoundingBox: Rectangle get() { - val bottomleft = localToStageCoordinates(Vector2(0f, 0f)) - val topright = localToStageCoordinates(Vector2(width, height)) + val bottomLeft = localToStageCoordinates(Vector2(0f, 0f)) + val topRight = localToStageCoordinates(Vector2(width, height)) return Rectangle( - bottomleft.x, - bottomleft.y, - topright.x - bottomleft.x, - topright.y - bottomleft.y + bottomLeft.x, + bottomLeft.y, + topRight.x - bottomLeft.x, + topRight.y - bottomLeft.y ) } diff --git a/core/src/com/unciv/ui/popups/ConfirmPopup.kt b/core/src/com/unciv/ui/popups/ConfirmPopup.kt index f55c6d18cb..1b2ddcfd62 100644 --- a/core/src/com/unciv/ui/popups/ConfirmPopup.kt +++ b/core/src/com/unciv/ui/popups/ConfirmPopup.kt @@ -4,15 +4,15 @@ import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle import com.badlogic.gdx.utils.Align import com.unciv.ui.screens.basescreen.BaseScreen -import com.unciv.ui.components.KeyCharAndCode +import com.unciv.ui.components.KeyboardBinding import com.unciv.ui.components.extensions.toLabel /** Variant of [Popup] pre-populated with one label, plus confirm and cancel buttons + * @param stageToShowOn Parent [Stage], see [Popup.stageToShowOn] * @param question The text for the label * @param confirmText The text for the "Confirm" button * @param isConfirmPositive If the action to be performed is positive or not (i.e. buy = positive, delete = negative), default false * @param action A lambda to execute when "Yes" is chosen - * @param screen The parent screen - see [Popup.screen]. Optional, defaults to the current [WorldScreen][com.unciv.ui.worldscreen.WorldScreen] * @param restoreDefault A lambda to execute when "No" is chosen */ open class ConfirmPopup( @@ -39,10 +39,11 @@ open class ConfirmPopup( init { promptLabel.setAlignment(Align.center) add(promptLabel).colspan(2).row() - addCloseButton("Cancel", KeyCharAndCode('n'), action = restoreDefault) + addCloseButton("Cancel", KeyboardBinding.Cancel, action = restoreDefault) val confirmStyleName = if (isConfirmPositive) "positive" else "negative" val confirmStyle = BaseScreen.skin.get(confirmStyleName, TextButtonStyle::class.java) - addOKButton(confirmText, KeyCharAndCode('y'), confirmStyle, action = action) + addOKButton(confirmText, KeyboardBinding.Confirm, confirmStyle, action = action) equalizeLastTwoButtonWidths() } + } diff --git a/core/src/com/unciv/ui/popups/Popup.kt b/core/src/com/unciv/ui/popups/Popup.kt index 676e4c4ead..4355964af7 100644 --- a/core/src/com/unciv/ui/popups/Popup.kt +++ b/core/src/com/unciv/ui/popups/Popup.kt @@ -17,6 +17,8 @@ import com.unciv.Constants import com.unciv.logic.event.EventBus import com.unciv.ui.components.AutoScrollPane import com.unciv.ui.components.KeyCharAndCode +import com.unciv.ui.components.KeyboardBinding +import com.unciv.ui.components.KeyboardBindings import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.center import com.unciv.ui.components.extensions.darken @@ -189,10 +191,19 @@ open class Popup( action() } } - cell.getActor().keyShortcuts.add(KeyCharAndCode.RETURN) + cell.actor.keyShortcuts.add(KeyCharAndCode.RETURN) return cell } + /** Overload of [addCloseButton] accepting a bindable key definition as [additionalKey] */ + fun addCloseButton(text: String, additionalKey: KeyboardBinding, action: () -> Unit) { + addCloseButton(text, KeyboardBindings[additionalKey], action = action) + } + /** Overload of [addOKButton] accepting a bindable key definition as [additionalKey] */ + fun addOKButton(text: String, additionalKey: KeyboardBinding, style: TextButtonStyle? = null, action: () -> Unit) { + addOKButton(text, KeyboardBindings[additionalKey], style, action = action) + } + /** * The last two additions ***must*** be buttons. * Make their width equal by setting minWidth of one cell to actor width of the other. @@ -242,7 +253,7 @@ val BaseScreen.popups private val Stage.popups: List get() = actors.filterIsInstance() -/** @return The currently active [Popup] or [null] if none. */ +/** @return The currently active [Popup] or `null` if none. */ val BaseScreen.activePopup: Popup? get() = popups.lastOrNull { it.isVisible } diff --git a/core/src/com/unciv/ui/popups/options/DisplayTab.kt b/core/src/com/unciv/ui/popups/options/DisplayTab.kt index a4f01ff8f8..2a02681d86 100644 --- a/core/src/com/unciv/ui/popups/options/DisplayTab.kt +++ b/core/src/com/unciv/ui/popups/options/DisplayTab.kt @@ -27,6 +27,7 @@ import com.unciv.ui.components.extensions.toTextButton import com.unciv.utils.Display import com.unciv.utils.ScreenMode + fun displayTab( optionsPopup: OptionsPopup, onChange: () -> Unit, @@ -237,7 +238,7 @@ private fun addSkinSelectBox(table: Table, settings: GameSettings, selectBoxMinW private fun addResetTutorials(table: Table, settings: GameSettings) { val resetTutorialsButton = "Reset tutorials".toTextButton() - resetTutorialsButton.onClick { + resetTutorialsButton.onClick { ConfirmPopup( table.stage, "Do you want to reset completed tutorials?", diff --git a/core/src/com/unciv/ui/popups/options/KeyBindingsTab.kt b/core/src/com/unciv/ui/popups/options/KeyBindingsTab.kt new file mode 100644 index 0000000000..b46704f4be --- /dev/null +++ b/core/src/com/unciv/ui/popups/options/KeyBindingsTab.kt @@ -0,0 +1,61 @@ +package com.unciv.ui.popups.options + +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextField +import com.unciv.ui.components.KeyboardBinding +import com.unciv.ui.components.TabbedPager +import com.unciv.ui.components.UncivTextField +import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.screens.civilopediascreen.MarkupRenderer + +class KeyBindingsTab( + optionsPopup: OptionsPopup, + labelWidth: Float +) : Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val keyBindings = optionsPopup.settings.keyBindings + private val keyFields = HashMap(KeyboardBinding.values().size) + private val disclaimer = MarkupRenderer.render(listOf( + FormattedLine("This is a work in progress.", color = "#b22222", centered = true), // FIREBRICK + FormattedLine(), + // FormattedLine("Do not pester the developers for missing entries!"), // little joke + FormattedLine("For discussion about missing entries, see the linked issue.", + link = "https://github.com/yairm210/Unciv/issues/8862"), + FormattedLine(separator = true), + ), labelWidth) + + init { + pad(10f) + defaults().pad(5f) + + for (binding in KeyboardBinding.values()) { + keyFields[binding] = UncivTextField.create(binding.defaultKey.toString()) + } + } + + private fun update() { + clear() + add(disclaimer).colspan(2).center().row() + + for (binding in KeyboardBinding.values()) { + add(binding.label.toLabel()) + keyFields[binding]!!.text = if (binding !in keyBindings) "" // show default = hint grayed + else keyBindings[binding].toString() + add(keyFields[binding]).row() + } + } + + fun save () { + for (binding in KeyboardBinding.values()) { + keyBindings.put(binding, keyFields[binding]!!.text) + } + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + update() + } + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + save() + } +} diff --git a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt index c0f7f27d7e..8a5742b416 100644 --- a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt +++ b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt @@ -12,7 +12,6 @@ import com.unciv.logic.multiplayer.storage.MultiplayerAuthException import com.unciv.models.UncivSound import com.unciv.models.metadata.GameSetting import com.unciv.models.metadata.GameSettings -import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.popups.Popup import com.unciv.ui.screens.basescreen.BaseScreen @@ -27,6 +26,7 @@ import com.unciv.ui.components.extensions.toGdxArray import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.popups.AuthPopup +import com.unciv.ui.popups.options.SettingsSelect.SelectItem import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread import java.time.Duration @@ -93,7 +93,9 @@ fun multiplayerTab( addSeparator(tab) - addMultiplayerServerOptions(tab, optionsPopup, listOf(curRefreshSelect, allRefreshSelect, turnCheckerSelect).filterNotNull()) + addMultiplayerServerOptions(tab, optionsPopup, + listOfNotNull(curRefreshSelect, allRefreshSelect, turnCheckerSelect) + ) return tab } @@ -118,13 +120,13 @@ private fun createNotificationSoundOptions(): List> = lis private fun buildUnitAttackSoundOptions(): List> { return RulesetCache.getSortedBaseRulesets() - .map(RulesetCache::get).filterNotNull() - .map(Ruleset::units).map { it.values } - .flatMap { it } - .filter { it.attackSound != null } - .filter { it.attackSound != "nuke" } // much too long for a notification + .asSequence() + .mapNotNull(RulesetCache::get) + .flatMap { it.units.values } + .filter { it.attackSound != null && it.attackSound != "nuke" } // much too long for a notification .distinctBy { it.attackSound } .map { SelectItem("[${it.name}] Attack Sound", UncivSound(it.attackSound!!)) } + .toList() } private fun addMultiplayerServerOptions( diff --git a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt index b4670d8e47..95a68d39c9 100644 --- a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt +++ b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt @@ -3,28 +3,14 @@ package com.unciv.ui.popups.options import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.ui.Label -import com.badlogic.gdx.scenes.scene2d.ui.SelectBox import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener -import com.badlogic.gdx.utils.Array import com.unciv.GUI import com.unciv.UncivGame -import com.unciv.logic.event.EventBus -import com.unciv.models.UncivSound import com.unciv.models.metadata.BaseRuleset -import com.unciv.models.metadata.GameSetting -import com.unciv.models.metadata.GameSettings -import com.unciv.models.metadata.SettingsPropertyChanged -import com.unciv.models.metadata.SettingsPropertyUncivSoundChanged import com.unciv.models.ruleset.RulesetCache -import com.unciv.models.translations.tr import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.extensions.center -import com.unciv.ui.components.extensions.onChange import com.unciv.ui.components.extensions.toCheckBox -import com.unciv.ui.components.extensions.toGdxArray -import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.Popup import com.unciv.ui.screens.basescreen.BaseScreen @@ -49,6 +35,7 @@ class OptionsPopup( val settings = screen.game.settings val tabs: TabbedPager val selectBoxMinWidth: Float + private var keyBindingsTab: KeyBindingsTab? = null //endregion @@ -112,6 +99,14 @@ class OptionsPopup( ImageGetter.getImage("OtherIcons/Settings"), 24f ) + if (GUI.keyboardAvailable) { + keyBindingsTab = KeyBindingsTab(this, tabMinWidth - 40f) // 40 = padding + tabs.addPage( + "Keys", keyBindingsTab, + ImageGetter.getImage("OtherIcons/Keyboard"), 24f + ) + } + if (RulesetCache.size > BaseRuleset.values().size) { val content = ModCheckTab(screen) tabs.addPage("Locate mod errors", content, ImageGetter.getImage("OtherIcons/Mods"), 24f) @@ -123,6 +118,7 @@ class OptionsPopup( addCloseButton { screen.game.musicController.onChange(null) center(screen.stage) + keyBindingsTab?.save() onClose() }.padBottom(10f) @@ -185,77 +181,3 @@ class OptionsPopup( } } - -class SelectItem(val label: String, val value: T) { - override fun toString(): String = label.tr() - override fun equals(other: Any?): Boolean = other is SelectItem<*> && value == other.value - override fun hashCode(): Int = value.hashCode() -} - -/** - * For creating a SelectBox that is automatically backed by a [GameSettings] property. - * - * **Warning:** [T] has to be the same type as the [GameSetting.kClass] of the [GameSetting] argument. - * - * This will also automatically send [SettingsPropertyChanged] events. - */ -open class SettingsSelect( - labelText: String, - items: Iterable>, - private val setting: GameSetting, - settings: GameSettings -) { - private val settingsProperty: KMutableProperty0 = setting.getProperty(settings) - private val label = createLabel(labelText) - private val refreshSelectBox = createSelectBox(items.toGdxArray(), settings) - val items by refreshSelectBox::items - - private fun createLabel(labelText: String): Label { - val selectLabel = labelText.toLabel() - selectLabel.wrap = true - return selectLabel - } - - private fun createSelectBox(initialItems: Array>, settings: GameSettings): SelectBox> { - val selectBox = SelectBox>(BaseScreen.skin) - selectBox.items = initialItems - - selectBox.selected = initialItems.firstOrNull() { it.value == settingsProperty.get() } ?: items.first() - selectBox.onChange { - val newValue = selectBox.selected.value - settingsProperty.set(newValue) - sendChangeEvent(newValue) - settings.save() - } - - return selectBox - } - - fun onChange(listener: (event: ChangeListener.ChangeEvent?) -> Unit) { - refreshSelectBox.onChange(listener) - } - - fun addTo(table: Table) { - table.add(label).growX().left() - table.add(refreshSelectBox).row() - } - - /** Maintains the currently selected item if possible, otherwise resets to the first item */ - fun replaceItems(options: Array>) { - val prev = refreshSelectBox.selected - refreshSelectBox.items = options - refreshSelectBox.selected = prev - } - - private fun sendChangeEvent(item: T) { - when (item) { - is UncivSound -> EventBus.send(object : SettingsPropertyUncivSoundChanged { - override val gameSetting = setting - override val value: UncivSound = settingsProperty.get() as UncivSound - }) - else -> EventBus.send(object : SettingsPropertyChanged { - override val gameSetting = setting - }) - } - } -} diff --git a/core/src/com/unciv/ui/popups/options/SettingsSelect.kt b/core/src/com/unciv/ui/popups/options/SettingsSelect.kt new file mode 100644 index 0000000000..b178330a27 --- /dev/null +++ b/core/src/com/unciv/ui/popups/options/SettingsSelect.kt @@ -0,0 +1,97 @@ +package com.unciv.ui.popups.options + +import com.badlogic.gdx.scenes.scene2d.ui.Label +import com.badlogic.gdx.scenes.scene2d.ui.SelectBox +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener +import com.badlogic.gdx.utils.Array +import com.unciv.logic.event.EventBus +import com.unciv.models.UncivSound +import com.unciv.models.metadata.GameSetting +import com.unciv.models.metadata.GameSettings +import com.unciv.models.metadata.SettingsPropertyChanged +import com.unciv.models.metadata.SettingsPropertyUncivSoundChanged +import com.unciv.models.translations.tr +import com.unciv.ui.components.extensions.onChange +import com.unciv.ui.components.extensions.toGdxArray +import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.screens.basescreen.BaseScreen +import kotlin.reflect.KMutableProperty0 + + +/** + * For creating a SelectBox that is automatically backed by a [GameSettings] property. + * + * **Warning:** [T] has to be the same type as the [GameSetting.kClass] of the [GameSetting] argument. + * + * This will also automatically send [SettingsPropertyChanged] events. + */ +open class SettingsSelect( + labelText: String, + items: Iterable>, + private val setting: GameSetting, + settings: GameSettings +) { + class SelectItem(val label: String, val value: T) { + override fun toString(): String = label.tr() + override fun equals(other: Any?): Boolean = other is SelectItem<*> && value == other.value + override fun hashCode(): Int = value.hashCode() + } + + private val settingsProperty: KMutableProperty0 = setting.getProperty(settings) + private val label = createLabel(labelText) + private val refreshSelectBox = createSelectBox(items.toGdxArray(), settings) + @Suppress("HasPlatformType") // Compiler problem + // Explicit type Array> as suggested crashes the compiler, except if one + // replaces `by x::` with `get() = x.` which according to docs should be entirely equivalent + val items by refreshSelectBox::items + + private fun createLabel(labelText: String): Label { + val selectLabel = labelText.toLabel() + selectLabel.wrap = true + return selectLabel + } + + private fun createSelectBox(initialItems: Array>, settings: GameSettings): SelectBox> { + val selectBox = SelectBox>(BaseScreen.skin) + selectBox.items = initialItems + + selectBox.selected = initialItems.firstOrNull { it.value == settingsProperty.get() } ?: items.first() + selectBox.onChange { + val newValue = selectBox.selected.value + settingsProperty.set(newValue) + sendChangeEvent(newValue) + settings.save() + } + + return selectBox + } + + fun onChange(listener: (event: ChangeListener.ChangeEvent?) -> Unit) { + refreshSelectBox.onChange(listener) + } + + fun addTo(table: Table) { + table.add(label).growX().left() + table.add(refreshSelectBox).row() + } + + /** Maintains the currently selected item if possible, otherwise resets to the first item */ + fun replaceItems(options: Array>) { + val prev = refreshSelectBox.selected + refreshSelectBox.items = options + refreshSelectBox.selected = prev + } + + private fun sendChangeEvent(item: T) { + when (item) { + is UncivSound -> EventBus.send(object : SettingsPropertyUncivSoundChanged { + override val gameSetting = setting + override val value: UncivSound = settingsProperty.get() as UncivSound + }) + else -> EventBus.send(object : SettingsPropertyChanged { + override val gameSetting = setting + }) + } + } +} diff --git a/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt b/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt index 9ef080c8f4..9eb08f39b9 100644 --- a/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt +++ b/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt @@ -37,7 +37,7 @@ abstract class BaseScreen : Screen { protected val tutorialController by lazy { TutorialController(this) } /** - * Keyboard shorcuts global to the screen. While this is public and can be modified, + * Keyboard shortcuts global to the screen. While this is public and can be modified, * you most likely should use [keyShortcuts][Actor.keyShortcuts] on appropriate [Actor] instead. */ val globalShortcuts = KeyShortcutDispatcher() diff --git a/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt b/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt index fd9f947c53..53f53016a0 100644 --- a/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt +++ b/core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt @@ -35,7 +35,7 @@ import kotlin.math.max * Special cases: * - Standalone [image][extraImage] from atlas or from ExtraImages * - A separator line ([separator]) - * - Automatic external links (no [text] but [link] begins with a URL protocol) + * - Automatic external links ([link] begins with a URL protocol) */ class FormattedLine ( /** Text to display. */ @@ -59,7 +59,7 @@ class FormattedLine ( val indent: Int = 0, /** Defines vertical padding between rows, defaults to 5f. */ val padding: Float = Float.NaN, - /** Sets text color, accepts Java names or 6/3-digit web colors (e.g. #FFA040). */ + /** Sets text color, accepts 6/3-digit web colors (e.g. #FFA040). */ val color: String = "", /** Renders a separator line instead of text. Can be combined only with [color] and [size] (line width, default 2) */ val separator: Boolean = false, diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index c8573a317a..76673fee84 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -27,6 +27,7 @@ import com.unciv.logic.trade.TradeEvaluation import com.unciv.models.TutorialTrigger import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.ui.components.KeyboardBinding import com.unciv.ui.components.KeyCharAndCode import com.unciv.ui.components.extensions.centerX import com.unciv.ui.components.extensions.darken @@ -219,9 +220,9 @@ class WorldScreen( } private fun addKeyboardPresses() { - // Space and N are assigned in createNextTurnButton - globalShortcuts.add(Input.Keys.F1) { game.pushScreen(CivilopediaScreen(gameInfo.ruleset)) } - globalShortcuts.add('E') { game.pushScreen(EmpireOverviewScreen(selectedCiv)) } // Empire overview last used page + // Space and N are assigned in NextTurnButton constructor + globalShortcuts.add(KeyboardBinding.Civilopedia) { game.pushScreen(CivilopediaScreen(gameInfo.ruleset)) } + globalShortcuts.add(KeyboardBinding.EmpireOverview) { game.pushScreen(EmpireOverviewScreen(selectedCiv)) } // Empire overview last used page /* * These try to be faithful to default Civ5 key bindings as found in several places online * Some are a little arbitrary, e.g. Economic info, Military info @@ -265,7 +266,7 @@ class WorldScreen( notificationsScroll.isVisible = uiEnabled minimapWrapper.isVisible = uiEnabled bottomUnitTable.isVisible = uiEnabled - battleTable.isVisible = uiEnabled && battleTable.update() != hide() + if (uiEnabled) battleTable.update() else battleTable.isVisible = false fogOfWarButton.isVisible = uiEnabled && viewingCiv.isSpectator() } } @@ -518,7 +519,7 @@ class WorldScreen( .map { it.otherCiv() } // we're now lazily enumerating over CivilizationInfo's we're at war with .flatMap { it.cities.asSequence() } // ... all *their* cities .filter { it.health == 1 } // ... those ripe for conquering - .flatMap { it.getCenterTile().getTilesInDistance(2).asSequence() } + .flatMap { it.getCenterTile().getTilesInDistance(2) } // ... all tiles around those in range of an average melee unit // -> and now we look for a unit that could do the conquering because it's ours // no matter whether civilian, air or ranged, tell user he needs melee diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt index 57b8cd8d4e..5706460db5 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt @@ -1,13 +1,13 @@ package com.unciv.ui.screens.worldscreen.status -import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import com.unciv.Constants import com.unciv.logic.civilization.managers.ReligionState import com.unciv.models.ruleset.BeliefType import com.unciv.models.translations.tr import com.unciv.ui.components.KeyCharAndCode -import com.unciv.ui.components.KeyShortcut +import com.unciv.ui.components.KeyShortcutDispatcher.KeyShortcut +import com.unciv.ui.components.KeyboardBinding import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.enable import com.unciv.ui.components.extensions.isEnabled @@ -35,8 +35,8 @@ class NextTurnButton : IconTextButton("", null, 30) { // label.setFontSize(30) labelCell.pad(10f) onActivation { nextTurnAction.action() } - keyShortcuts.add(Input.Keys.SPACE) - keyShortcuts.add('n') + keyShortcuts.add(KeyboardBinding.NextTurn) + keyShortcuts.add(KeyboardBinding.NextTurnAlternate) // Let unit actions override this for command "Wait". keyShortcuts.add(KeyShortcut(KeyCharAndCode('z'), -100)) } diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt index 48fcc81b6e..59f4ccee87 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt @@ -32,7 +32,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { private fun getUnitActionButton(unit: MapUnit, unitAction: UnitAction): Button { val icon = unitAction.getIcon() // If peripheral keyboard not detected, hotkeys will not be displayed - val key = if (KeyCharAndCode.keyboardAvailable) unitAction.type.key else KeyCharAndCode.UNKNOWN + val key = if (GUI.keyboardAvailable) unitAction.type.key else KeyCharAndCode.UNKNOWN val fontColor = if (unitAction.isCurrentAction) Color.YELLOW else Color.WHITE val actionButton = IconTextButton(unitAction.title, icon, fontColor = fontColor) diff --git a/docs/Credits.md b/docs/Credits.md index b98b0c1abf..1fa785249f 100644 --- a/docs/Credits.md +++ b/docs/Credits.md @@ -727,6 +727,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https: - Icon for Unique created by [vegeta1k95](https://github.com/vegeta1k95) - [Transform] created by letstalkaboutdune - [Swords](https://thenounproject.com/icon/swords-1580316/) created by Muhajir ila Robbi for Blockaded tile marker +- [Keyboard](https://thenounproject.com/icon/keyboard-2685534/) by Twenty Foo Studio for Options Keys ### Main menu