From 987f67d9cd73fe5dd02c739dd937e56385577ef6 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:52:15 +0100 Subject: [PATCH] [code quality] Reorg, clean up and comment a few things (#10527) * UncivGame is a pure class file again, GUI split off * Purify GameSettings step 1 - non-multiplayer nested classes * Purify GameSettings step 2 - multiplayer nested classes * Purify GameParameters - BaseRuleset to own file * Rework WindowState to centralize minimum/maximum treatment * Rename MultiplayerTurnNotifierDesktop to UncivWindowListener * Clarifications on what the WindowListener actually does (and now the attention-getting does something on non-Windows too) --- .../unciv/app/MultiplayerTurnCheckWorker.kt | 4 +- core/src/com/unciv/GUI.kt | 83 ++++++ core/src/com/unciv/UncivGame.kt | 76 ------ .../com/unciv/models/metadata/BaseRuleset.kt | 7 + .../unciv/models/metadata/GameParameters.kt | 7 - .../com/unciv/models/metadata/GameSettings.kt | 255 +++++++++++------- .../unciv/models/metadata/SettingsEvents.kt | 1 + .../translations/TranslationFileWriter.kt | 2 +- .../unciv/ui/popups/options/AdvancedTab.kt | 2 +- .../com/unciv/ui/popups/options/DisplayTab.kt | 2 +- .../unciv/ui/popups/options/MultiplayerTab.kt | 2 +- .../unciv/ui/popups/options/SettingsSelect.kt | 2 +- .../mainmenu/WorldScreenMusicPopup.kt | 2 +- .../com/unciv/app/desktop/DesktopDisplay.kt | 3 +- .../src/com/unciv/app/desktop/DesktopGame.kt | 6 +- .../com/unciv/app/desktop/DesktopLauncher.kt | 21 +- ...ifierDesktop.kt => UncivWindowListener.kt} | 60 +++-- .../src/com/unciv/dev/FasterUIDevelopment.kt | 3 +- 18 files changed, 316 insertions(+), 222 deletions(-) create mode 100644 core/src/com/unciv/GUI.kt create mode 100644 core/src/com/unciv/models/metadata/BaseRuleset.kt rename desktop/src/com/unciv/app/desktop/{MultiplayerTurnNotifierDesktop.kt => UncivWindowListener.kt} (50%) diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 584a2b7c4b..c75a550a13 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -30,8 +30,7 @@ import com.unciv.logic.GameInfo import com.unciv.logic.files.UncivFiles import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.OnlineMultiplayerServer -import com.unciv.models.metadata.GameSettingsMultiplayer -import kotlinx.coroutines.runBlocking +import com.unciv.models.metadata.GameSettings.GameSettingsMultiplayer import java.io.FileNotFoundException import java.io.PrintWriter import java.io.StringWriter @@ -39,6 +38,7 @@ import java.io.Writer import java.time.Duration import java.util.GregorianCalendar import java.util.concurrent.TimeUnit +import kotlinx.coroutines.runBlocking class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParameters) diff --git a/core/src/com/unciv/GUI.kt b/core/src/com/unciv/GUI.kt new file mode 100644 index 0000000000..018c64e696 --- /dev/null +++ b/core/src/com/unciv/GUI.kt @@ -0,0 +1,83 @@ +package com.unciv + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.unciv.logic.civilization.Civilization +import com.unciv.models.metadata.GameSettings +import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.clearUndoCheckpoints +import com.unciv.ui.screens.worldscreen.WorldMapHolder +import com.unciv.ui.screens.worldscreen.WorldScreen +import com.unciv.ui.screens.worldscreen.unit.UnitTable + +object GUI { + + fun setUpdateWorldOnNextRender() { + UncivGame.Current.worldScreen?.shouldUpdate = true + } + + fun pushScreen(screen: BaseScreen) { + UncivGame.Current.pushScreen(screen) + } + + fun resetToWorldScreen() { + UncivGame.Current.resetToWorldScreen() + } + + fun getSettings(): GameSettings { + return UncivGame.Current.settings + } + + fun isWorldLoaded(): Boolean { + return UncivGame.Current.worldScreen != null + } + + fun isMyTurn(): Boolean { + if (!UncivGame.isCurrentInitialized() || !isWorldLoaded()) return false + return UncivGame.Current.worldScreen!!.isPlayersTurn + } + + fun isAllowedChangeState(): Boolean { + return UncivGame.Current.worldScreen!!.canChangeState + } + + fun getWorldScreen(): WorldScreen { + return UncivGame.Current.worldScreen!! + } + + fun getWorldScreenIfActive(): WorldScreen? { + return UncivGame.Current.getWorldScreenIfActive() + } + + fun getMap(): WorldMapHolder { + return UncivGame.Current.worldScreen!!.mapHolder + } + + fun getUnitTable(): UnitTable { + return UncivGame.Current.worldScreen!!.bottomUnitTable + } + + fun getViewingPlayer(): Civilization { + return UncivGame.Current.worldScreen!!.viewingCiv + } + + fun getSelectedPlayer(): Civilization { + return UncivGame.Current.worldScreen!!.selectedCiv + } + + /** Disable Undo (as in: forget the way back, but allow future undo checkpoints) */ + fun clearUndoCheckpoints() { + UncivGame.Current.worldScreen?.clearUndoCheckpoints() + } + + 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 + } + +} diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index e5e5eaf20f..d4f16009d8 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -11,7 +11,6 @@ import com.badlogic.gdx.utils.Align import com.unciv.logic.GameInfo import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.UncivShowableException -import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.PlayerType import com.unciv.logic.files.UncivFiles import com.unciv.logic.multiplayer.OnlineMultiplayer @@ -38,10 +37,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.savescreens.LoadGameScreen import com.unciv.ui.screens.worldscreen.PlayerReadyScreen -import com.unciv.ui.screens.worldscreen.UndoHandler.Companion.clearUndoCheckpoints -import com.unciv.ui.screens.worldscreen.WorldMapHolder import com.unciv.ui.screens.worldscreen.WorldScreen -import com.unciv.ui.screens.worldscreen.unit.UnitTable import com.unciv.utils.Concurrency import com.unciv.utils.DebugUtils import com.unciv.utils.Display @@ -57,78 +53,6 @@ import java.util.UUID import kotlinx.coroutines.CancellationException import kotlin.system.exitProcess -object GUI { - - fun setUpdateWorldOnNextRender() { - UncivGame.Current.worldScreen?.shouldUpdate = true - } - - fun pushScreen(screen: BaseScreen) { - UncivGame.Current.pushScreen(screen) - } - - fun resetToWorldScreen() { - UncivGame.Current.resetToWorldScreen() - } - - fun getSettings(): GameSettings { - return UncivGame.Current.settings - } - - fun isWorldLoaded(): Boolean { - return UncivGame.Current.worldScreen != null - } - - fun isMyTurn(): Boolean { - if (!UncivGame.isCurrentInitialized() || !isWorldLoaded()) return false - return UncivGame.Current.worldScreen!!.isPlayersTurn - } - - fun isAllowedChangeState(): Boolean { - return UncivGame.Current.worldScreen!!.canChangeState - } - - fun getWorldScreen(): WorldScreen { - return UncivGame.Current.worldScreen!! - } - - fun getWorldScreenIfActive(): WorldScreen? { - return UncivGame.Current.getWorldScreenIfActive() - } - - fun getMap(): WorldMapHolder { - return UncivGame.Current.worldScreen!!.mapHolder - } - - fun getUnitTable(): UnitTable { - return UncivGame.Current.worldScreen!!.bottomUnitTable - } - - fun getViewingPlayer(): Civilization { - return UncivGame.Current.worldScreen!!.viewingCiv - } - - fun getSelectedPlayer(): Civilization { - return UncivGame.Current.worldScreen!!.selectedCiv - } - - /** Disable Undo (as in: forget the way back, but allow future undo checkpoints) */ - fun clearUndoCheckpoints() { - UncivGame.Current.worldScreen?.clearUndoCheckpoints() - } - - 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 { var deepLinkedMultiplayerGame: String? = null diff --git a/core/src/com/unciv/models/metadata/BaseRuleset.kt b/core/src/com/unciv/models/metadata/BaseRuleset.kt new file mode 100644 index 0000000000..96b06531aa --- /dev/null +++ b/core/src/com/unciv/models/metadata/BaseRuleset.kt @@ -0,0 +1,7 @@ +package com.unciv.models.metadata + +@Suppress("EnumEntryName") // These merit unusual names +enum class BaseRuleset(val fullName:String) { + Civ_V_Vanilla("Civ V - Vanilla"), + Civ_V_GnK("Civ V - Gods & Kings"), +} diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index 237c14d6b2..124df8ea43 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -4,13 +4,6 @@ import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.PlayerType import com.unciv.models.ruleset.Speed - -@Suppress("EnumEntryName") // These merit unusual names -enum class BaseRuleset(val fullName:String) { - Civ_V_Vanilla("Civ V - Vanilla"), - Civ_V_GnK("Civ V - Gods & Kings"), -} - class GameParameters : IsPartOfGameInfoSerialization { // Default values are the default new game var difficulty = "Prince" var speed = Speed.DEFAULT diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 0dcfa5ea66..3f6bca9a0e 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -19,20 +19,6 @@ import java.util.Locale import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty0 -data class WindowState (val width: Int = 900, val height: Int = 600) - -enum class ScreenSize( - @Suppress("unused") // Actual width determined by screen aspect ratio, this as comment only - val virtualWidth: Float, - val virtualHeight: Float -) { - Tiny(750f,500f), - Small(900f,600f), - Medium(1050f,700f), - Large(1200f,800f), - Huge(1500f,1000f) -} - class GameSettings { /** Allows panning the map by moving the pointer to the screen edges */ @@ -132,7 +118,6 @@ class GameSettings { var enlargeSelectedNotification = true /** Whether the Nation Picker shows icons only or the horizontal "civBlocks" with leader/nation name */ - enum class NationPickerListMode { Icons, List } var nationPickerListMode = NationPickerListMode.List /** Size of automatic display of UnitSet art in Civilopedia - 0 to disable */ @@ -148,14 +133,17 @@ class GameSettings { } } + //region + fun save() { refreshWindowSize() UncivGame.Current.files.setGeneralSettings(this) } + fun refreshWindowSize() { if (isFreshlyCreated || Gdx.app.type != ApplicationType.Desktop) return if (!Display.hasUserSelectableSize(screenMode)) return - windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height) + windowState = WindowState.current() } fun addCompletedTutorialTask(tutorialTask: String): Boolean { @@ -189,99 +177,170 @@ class GameSettings { fun getCollatorFromLocale(): Collator { return Collator.getInstance(getCurrentLocale()) } -} -enum class LocaleCode(var language: String, var country: String) { - Arabic("ar", "IQ"), - Belarusian("be", "BY"), - BrazilianPortuguese("pt", "BR"), - Bulgarian("bg", "BG"), - Catalan("ca", "ES"), - Croatian("hr", "HR"), - Czech("cs", "CZ"), - Danish("da", "DK"), - Dutch("nl", "NL"), - English("en", "US"), - Estonian("et", "EE"), - Finnish("fi", "FI"), - French("fr", "FR"), - German("de", "DE"), - Greek("el", "GR"), - Hindi("hi", "IN"), - Hungarian("hu", "HU"), - Indonesian("in", "ID"), - Italian("it", "IT"), - Japanese("ja", "JP"), - Korean("ko", "KR"), - Latvian("lv", "LV"), - Lithuanian("lt", "LT"), - Malay("ms", "MY"), - Norwegian("no", "NO"), - NorwegianNynorsk("nn", "NO"), - PersianPinglishDIN("fa", "IR"), // These might just fall back to default - PersianPinglishUN("fa", "IR"), - Polish("pl", "PL"), - Portuguese("pt", "PT"), - Romanian("ro", "RO"), - Russian("ru", "RU"), - Serbian("sr", "RS"), - SimplifiedChinese("zh", "CN"), - Slovak("sk", "SK"), - Spanish("es", "ES"), - Swedish("sv", "SE"), - Thai("th", "TH"), - TraditionalChinese("zh", "TW"), - Turkish("tr", "TR"), - Ukrainian("uk", "UA"), - Vietnamese("vi", "VN"), - Afrikaans("af", "ZA") -} + //endregion + //region -class GameSettingsMultiplayer { - var userId = "" - var passwords = mutableMapOf() - @Suppress("unused") // @GGuenni knows what he intended with this field - var userName: String = "" - var server = Constants.uncivXyzServer - var friendList: MutableList = mutableListOf() - var turnCheckerEnabled = true - var turnCheckerPersistentNotificationEnabled = true - var turnCheckerDelay: Duration = Duration.ofMinutes(5) - var statusButtonInSinglePlayer = false - var currentGameRefreshDelay: Duration = Duration.ofSeconds(10) - var allGameRefreshDelay: Duration = Duration.ofMinutes(5) - var currentGameTurnNotificationSound: UncivSound = UncivSound.Silent - var otherGameTurnNotificationSound: UncivSound = UncivSound.Silent - var hideDropboxWarning = false + /** + * Knowledge on Window "state", limited. + * - Size: Saved + * - Iconified, Maximized: Not saved + * - Position / Multimonitor display choice: Not saved + * + * Note: Useful on desktop only, on Android we do not explicitly support `Activity.isInMultiWindowMode` returning true. + * (On Android Display.hasUserSelectableSize will return false, and AndroidLauncher & co ignore it) + * + * Open to future enhancement - but: + * retrieving a valid position from our upstream libraries while the window is maximized or iconified has proven tricky so far. + */ + data class WindowState(val width: Int = 900, val height: Int = 600) { + constructor(bounds: java.awt.Rectangle) : this(bounds.width, bounds.height) - fun getAuthHeader(): String { - val serverPassword = passwords[server] ?: "" - val preEncodedAuthValue = "$userId:$serverPassword" - return "Basic ${Base64Coder.encodeString(preEncodedAuthValue)}" + companion object { + /** Our choice of minimum window width */ + const val minimumWidth = 120 + /** Our choice of minimum window height */ + const val minimumHeight = 80 + + fun current() = WindowState(Gdx.graphics.width, Gdx.graphics.height) + } + + /** + * Constrains the dimensions of `this` [WindowState] to be within [minimumWidth] x [minimumHeight] to [maximumWidth] x [maximumHeight]. + * @param maximumWidth defaults to unlimited + * @param maximumHeight defaults to unlimited + * @return `this` unchanged if it is within valid limits, otherwise a new WindowState that is. + */ + fun coerceIn(maximumWidth: Int = Int.MAX_VALUE, maximumHeight: Int = Int.MAX_VALUE): WindowState { + if (width in minimumWidth..maximumWidth && height in minimumHeight..maximumHeight) + return this + return WindowState( + width.coerceIn(minimumWidth, maximumWidth), + height.coerceIn(minimumHeight, maximumHeight) + ) + } + + /** + * Constrains the dimensions of `this` [WindowState] to be within [minimumWidth] x [minimumHeight] to `maximumWidth` x `maximumHeight`. + * @param maximumWindowBounds provides maximum sizes + * @return `this` unchanged if it is within valid limits, otherwise a new WindowState that is. + * @see coerceIn + */ + fun coerceIn(maximumWindowBounds: java.awt.Rectangle) = + coerceIn(maximumWindowBounds.width, maximumWindowBounds.height) } -} -@Suppress("SuspiciousCallableReferenceInLambda") // By @Azzurite, safe as long as that warning below is followed -enum class GameSetting( - val kClass: KClass<*>, - private val propertyGetter: (GameSettings) -> KMutableProperty0<*> -) { -// Uncomment these once they are refactored to send events on change + enum class ScreenSize( + @Suppress("unused") // Actual width determined by screen aspect ratio, this as comment only + val virtualWidth: Float, + val virtualHeight: Float + ) { + Tiny(750f,500f), + Small(900f,600f), + Medium(1050f,700f), + Large(1200f,800f), + Huge(1500f,1000f) + } + + enum class NationPickerListMode { Icons, List } + + enum class LocaleCode(var language: String, var country: String) { + Arabic("ar", "IQ"), + Belarusian("be", "BY"), + BrazilianPortuguese("pt", "BR"), + Bulgarian("bg", "BG"), + Catalan("ca", "ES"), + Croatian("hr", "HR"), + Czech("cs", "CZ"), + Danish("da", "DK"), + Dutch("nl", "NL"), + English("en", "US"), + Estonian("et", "EE"), + Finnish("fi", "FI"), + French("fr", "FR"), + German("de", "DE"), + Greek("el", "GR"), + Hindi("hi", "IN"), + Hungarian("hu", "HU"), + Indonesian("in", "ID"), + Italian("it", "IT"), + Japanese("ja", "JP"), + Korean("ko", "KR"), + Latvian("lv", "LV"), + Lithuanian("lt", "LT"), + Malay("ms", "MY"), + Norwegian("no", "NO"), + NorwegianNynorsk("nn", "NO"), + PersianPinglishDIN("fa", "IR"), // These might just fall back to default + PersianPinglishUN("fa", "IR"), + Polish("pl", "PL"), + Portuguese("pt", "PT"), + Romanian("ro", "RO"), + Russian("ru", "RU"), + Serbian("sr", "RS"), + SimplifiedChinese("zh", "CN"), + Slovak("sk", "SK"), + Spanish("es", "ES"), + Swedish("sv", "SE"), + Thai("th", "TH"), + TraditionalChinese("zh", "TW"), + Turkish("tr", "TR"), + Ukrainian("uk", "UA"), + Vietnamese("vi", "VN"), + Afrikaans("af", "ZA") + } + + //endregion + //region Multiplayer-specific + + class GameSettingsMultiplayer { + var userId = "" + var passwords = mutableMapOf() + @Suppress("unused") // @GGuenni knows what he intended with this field + var userName: String = "" + var server = Constants.uncivXyzServer + var friendList: MutableList = mutableListOf() + var turnCheckerEnabled = true + var turnCheckerPersistentNotificationEnabled = true + var turnCheckerDelay: Duration = Duration.ofMinutes(5) + var statusButtonInSinglePlayer = false + var currentGameRefreshDelay: Duration = Duration.ofSeconds(10) + var allGameRefreshDelay: Duration = Duration.ofMinutes(5) + var currentGameTurnNotificationSound: UncivSound = UncivSound.Silent + var otherGameTurnNotificationSound: UncivSound = UncivSound.Silent + var hideDropboxWarning = false + + fun getAuthHeader(): String { + val serverPassword = passwords[server] ?: "" + val preEncodedAuthValue = "$userId:$serverPassword" + return "Basic ${Base64Coder.encodeString(preEncodedAuthValue)}" + } + } + + @Suppress("SuspiciousCallableReferenceInLambda") // By @Azzurite, safe as long as that warning below is followed + enum class GameSetting( + val kClass: KClass<*>, + private val propertyGetter: (GameSettings) -> KMutableProperty0<*> + ) { + // Uncomment these once they are refactored to send events on change // MULTIPLAYER_USER_ID(String::class, { it.multiplayer::userId }), // MULTIPLAYER_SERVER(String::class, { it.multiplayer::server }), // MULTIPLAYER_STATUSBUTTON_IN_SINGLEPLAYER(Boolean::class, { it.multiplayer::statusButtonInSinglePlayer }), // MULTIPLAYER_TURN_CHECKER_ENABLED(Boolean::class, { it.multiplayer::turnCheckerEnabled }), // MULTIPLAYER_TURN_CHECKER_PERSISTENT_NOTIFICATION_ENABLED(Boolean::class, { it.multiplayer::turnCheckerPersistentNotificationEnabled }), // MULTIPLAYER_HIDE_DROPBOX_WARNING(Boolean::class, { it.multiplayer::hideDropboxWarning }), - MULTIPLAYER_TURN_CHECKER_DELAY(Duration::class, { it.multiplayer::turnCheckerDelay }), - MULTIPLAYER_CURRENT_GAME_REFRESH_DELAY(Duration::class, { it.multiplayer::currentGameRefreshDelay }), - MULTIPLAYER_ALL_GAME_REFRESH_DELAY(Duration::class, { it.multiplayer::allGameRefreshDelay }), - MULTIPLAYER_CURRENT_GAME_TURN_NOTIFICATION_SOUND(UncivSound::class, { it.multiplayer::currentGameTurnNotificationSound }), - MULTIPLAYER_OTHER_GAME_TURN_NOTIFICATION_SOUND(UncivSound::class, { it.multiplayer::otherGameTurnNotificationSound }); + MULTIPLAYER_TURN_CHECKER_DELAY(Duration::class, { it.multiplayer::turnCheckerDelay }), + MULTIPLAYER_CURRENT_GAME_REFRESH_DELAY(Duration::class, { it.multiplayer::currentGameRefreshDelay }), + MULTIPLAYER_ALL_GAME_REFRESH_DELAY(Duration::class, { it.multiplayer::allGameRefreshDelay }), + MULTIPLAYER_CURRENT_GAME_TURN_NOTIFICATION_SOUND(UncivSound::class, { it.multiplayer::currentGameTurnNotificationSound }), + MULTIPLAYER_OTHER_GAME_TURN_NOTIFICATION_SOUND(UncivSound::class, { it.multiplayer::otherGameTurnNotificationSound }); - /** **Warning:** It is the obligation of the caller to select the same type [T] that the [kClass] of this property has */ - fun getProperty(settings: GameSettings): KMutableProperty0 { - @Suppress("UNCHECKED_CAST") - return propertyGetter(settings) as KMutableProperty0 + /** **Warning:** It is the obligation of the caller to select the same type [T] that the [kClass] of this property has */ + fun getProperty(settings: GameSettings): KMutableProperty0 { + @Suppress("UNCHECKED_CAST") + return propertyGetter(settings) as KMutableProperty0 + } } + + //endregion } diff --git a/core/src/com/unciv/models/metadata/SettingsEvents.kt b/core/src/com/unciv/models/metadata/SettingsEvents.kt index 1972fd63d8..cfbd668a62 100644 --- a/core/src/com/unciv/models/metadata/SettingsEvents.kt +++ b/core/src/com/unciv/models/metadata/SettingsEvents.kt @@ -2,6 +2,7 @@ package com.unciv.models.metadata import com.unciv.logic.event.Event import com.unciv.models.UncivSound +import com.unciv.models.metadata.GameSettings.GameSetting /** **Warning:** this event is in the process of completion and **not** used for all settings yet! **Only the settings in [GameSetting] get events sent!** */ interface SettingsPropertyChanged : Event { diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index 0d82684418..e6571aa9c6 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -7,7 +7,7 @@ import com.unciv.json.json import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.models.SpyAction import com.unciv.models.metadata.BaseRuleset -import com.unciv.models.metadata.LocaleCode +import com.unciv.models.metadata.GameSettings.LocaleCode import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.GlobalUniques diff --git a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt index 346c67076a..15d6b6f4a1 100644 --- a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt +++ b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt @@ -18,7 +18,7 @@ import com.unciv.GUI import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.ModCategories -import com.unciv.models.metadata.ScreenSize +import com.unciv.models.metadata.GameSettings.ScreenSize import com.unciv.models.translations.TranslationFileWriter import com.unciv.models.translations.tr import com.unciv.ui.components.UncivTooltip.Companion.addTooltip diff --git a/core/src/com/unciv/ui/popups/options/DisplayTab.kt b/core/src/com/unciv/ui/popups/options/DisplayTab.kt index 74f4c1e406..6f4d21bb42 100644 --- a/core/src/com/unciv/ui/popups/options/DisplayTab.kt +++ b/core/src/com/unciv/ui/popups/options/DisplayTab.kt @@ -8,7 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Array import com.unciv.GUI import com.unciv.models.metadata.GameSettings -import com.unciv.models.metadata.ScreenSize +import com.unciv.models.metadata.GameSettings.ScreenSize import com.unciv.models.skins.SkinCache import com.unciv.models.tilesets.TileSetCache import com.unciv.models.translations.tr diff --git a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt index 2765fec6dd..4842b44c05 100644 --- a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt +++ b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt @@ -9,8 +9,8 @@ import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached 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.metadata.GameSettings.GameSetting import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.components.UncivTextField import com.unciv.ui.components.extensions.addSeparator diff --git a/core/src/com/unciv/ui/popups/options/SettingsSelect.kt b/core/src/com/unciv/ui/popups/options/SettingsSelect.kt index e9e2e80292..5e87ea78c4 100644 --- a/core/src/com/unciv/ui/popups/options/SettingsSelect.kt +++ b/core/src/com/unciv/ui/popups/options/SettingsSelect.kt @@ -7,8 +7,8 @@ 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.GameSettings.GameSetting import com.unciv.models.metadata.SettingsPropertyChanged import com.unciv.models.metadata.SettingsPropertyUncivSoundChanged import com.unciv.models.translations.tr diff --git a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMusicPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMusicPopup.kt index c0f55a7766..eb434be60b 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMusicPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMusicPopup.kt @@ -7,7 +7,7 @@ import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings -import com.unciv.models.metadata.ScreenSize +import com.unciv.models.metadata.GameSettings.ScreenSize import com.unciv.ui.audio.MusicController import com.unciv.ui.components.extensions.setSize import com.unciv.ui.components.fonts.Fonts diff --git a/desktop/src/com/unciv/app/desktop/DesktopDisplay.kt b/desktop/src/com/unciv/app/desktop/DesktopDisplay.kt index 27cc6c0301..a7927f347b 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopDisplay.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopDisplay.kt @@ -58,8 +58,7 @@ enum class DesktopScreenMode : ScreenMode { val maximumWindowBounds = getMaximumWindowBounds() // Make sure an inappropriate saved size doesn't make the window unusable - val width = settings.windowState.width.coerceIn(120, maximumWindowBounds.width) - val height = settings.windowState.height.coerceIn(80, maximumWindowBounds.height) + val (width, height) = settings.windowState.coerceIn(maximumWindowBounds) // Kludge - see also DesktopLauncher - without, moving the window might revert to the size stored in config (Lwjgl3Application::class.java).getDeclaredField("config").run { diff --git a/desktop/src/com/unciv/app/desktop/DesktopGame.kt b/desktop/src/com/unciv/app/desktop/DesktopGame.kt index 1a107a14f5..8a88b2b75b 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopGame.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopGame.kt @@ -7,10 +7,10 @@ class DesktopGame(config: Lwjgl3ApplicationConfiguration) : UncivGame() { private val audio = HardenGdxAudio() private var discordUpdater = DiscordUpdater() - private val turnNotifier = MultiplayerTurnNotifierDesktop() + private val windowListener = UncivWindowListener() init { - config.setWindowListener(turnNotifier) + config.setWindowListener(windowListener) discordUpdater.setOnUpdate { @@ -41,7 +41,7 @@ class DesktopGame(config: Lwjgl3ApplicationConfiguration) : UncivGame() { } override fun notifyTurnStarted() { - turnNotifier.turnStarted() + windowListener.turnStarted() } override fun dispose() { diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 0cb4a9049c..bc11454f3e 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -8,13 +8,12 @@ import com.unciv.app.desktop.DesktopScreenMode.Companion.getMaximumWindowBounds import com.unciv.json.json import com.unciv.logic.files.SETTINGS_FILE_NAME import com.unciv.logic.files.UncivFiles -import com.unciv.models.metadata.ScreenSize -import com.unciv.models.metadata.WindowState +import com.unciv.models.metadata.GameSettings.ScreenSize +import com.unciv.models.metadata.GameSettings.WindowState import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.utils.Display import com.unciv.utils.Log -import kotlin.math.max internal object DesktopLauncher { @@ -49,27 +48,27 @@ internal object DesktopLauncher { config.setWindowIcon("ExtraImages/Icon.png") config.setTitle("Unciv") config.setHdpiMode(HdpiMode.Logical) - config.setWindowSizeLimits(120, 80, -1, -1) + config.setWindowSizeLimits(WindowState.minimumWidth, WindowState.minimumHeight, -1, -1) // We don't need the initial Audio created in Lwjgl3Application, HardenGdxAudio will replace it anyway. // Note that means config.setAudioConfig() would be ignored too, those would need to go into the HardenedGdxAudio constructor. config.disableAudio(true) + // LibGDX not yet configured, use regular java class + val maximumWindowBounds = getMaximumWindowBounds() + val settings = UncivFiles.getSettingsForPlatformLaunchers() if (settings.isFreshlyCreated) { settings.screenSize = ScreenSize.Large // By default we guess that Desktops have larger screens - // LibGDX not yet configured, use regular java class - val maximumWindowBounds = getMaximumWindowBounds() - settings.windowState = WindowState( - width = maximumWindowBounds.width, - height = maximumWindowBounds.height - ) + settings.windowState = WindowState(maximumWindowBounds) FileHandle(SETTINGS_FILE_NAME).writeString(json().toJson(settings), false, Charsets.UTF_8.name()) // so when we later open the game we get fullscreen } // Kludge! This is a workaround - the matching call in DesktopDisplay doesn't "take" quite permanently, // the window might revert to the "config" values when the user moves the window - worse if they // minimize/restore. And the config default is 640x480 unless we set something here. - config.setWindowedMode(max(settings.windowState.width, 100), max(settings.windowState.height, 100)) + val (width, height) = settings.windowState.coerceIn(maximumWindowBounds) + config.setWindowedMode(width, height) + config.setInitialBackgroundColor(BaseScreen.clearColor) if (!isRunFromJAR) { diff --git a/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt b/desktop/src/com/unciv/app/desktop/UncivWindowListener.kt similarity index 50% rename from desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt rename to desktop/src/com/unciv/app/desktop/UncivWindowListener.kt index 9ea2e05793..e9ed83ba9c 100644 --- a/desktop/src/com/unciv/app/desktop/MultiplayerTurnNotifierDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/UncivWindowListener.kt @@ -10,23 +10,21 @@ import com.sun.jna.platform.win32.WinUser import com.unciv.utils.Log import org.lwjgl.glfw.GLFWNativeWin32 -class MultiplayerTurnNotifierDesktop: Lwjgl3WindowAdapter() { - companion object { - val user32: User32? = try { - if (System.getProperty("os.name")?.contains("Windows") == true) { - Native.load(User32::class.java) - } else { - null - } - } catch (e: UnsatisfiedLinkError) { - Log.error("Error while initializing turn notifier", e) - null - } - } +/** + * This is a Lwjgl3WindowListener serving the following purposes: + * + * - Catch and store our Lwjgl3Window instance + * - Track whether the Window has focus + * - "Flash" the Window to alert "your turn" in multiplayer + */ +class UncivWindowListener : Lwjgl3WindowAdapter() { + private var window: Lwjgl3Window? = null private var hasFocus: Boolean = true override fun created(window: Lwjgl3Window?) { + // In DesktopDisplay we use `(Gdx.graphics as? Lwjgl3Graphics)?.window`, so this entire listener could be made redundant. + // But we might need other uses in the future, so leave it as is. this.window = window } @@ -39,27 +37,57 @@ class MultiplayerTurnNotifierDesktop: Lwjgl3WindowAdapter() { } + /** Asks the operating system to request the player's attention */ fun turnStarted() { flashWindow() } /** - * See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-flashwindowex + * Requests user attention + * + * Excerpt from the [GLFW.glfwRequestWindowAttention](https://www.glfw.org/docs/latest/window_guide.html#window_attention) JavaDoc: + * - This function must only be called from the main thread. + * - **macOS:** Attention is requested to the application as a whole, not the specific window. + * + * On Windows, the OS-specific API is called directly instead. + * - See [FlashWindowEx](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-flashwindowex) * - * We should've used FlashWindow instead of FlashWindowEx, but for some reason the former has no binding in Java's User32 */ private fun flashWindow() { try { - if (user32 == null || window == null || hasFocus) return + if (window == null || hasFocus) return + if (user32 == null) + // Use Cross-Platform implementation + return window!!.flash() + + // Windows-specific implementation: val flashwinfo = WinUser.FLASHWINFO() val hwnd = GLFWNativeWin32.glfwGetWin32Window(window!!.windowHandle) flashwinfo.hWnd = WinNT.HANDLE(Pointer.createConstant(hwnd)) flashwinfo.dwFlags = 3 // FLASHW_ALL flashwinfo.uCount = 3 + // FlashWindow (no binding in Java's User32) instead of FlashWindowEx would flash just once user32.FlashWindowEx(flashwinfo) } catch (e: Throwable) { /** try to ignore even if we get an [Error], just log it */ Log.error("Error while notifying the user of their turn", e) } } + + private companion object { + /** Marshals JNA access to the Windows User32 API through [com.sun.jna.platform.win32.User32] + * + * (which, by the way, says "Incomplete implementation to support demos.") + */ + val user32: User32? = try { + if (System.getProperty("os.name")?.contains("Windows") == true) { + Native.load(User32::class.java) + } else { + null + } + } catch (e: UnsatisfiedLinkError) { + Log.error("Error while initializing turn notifier", e) + null + } + } } diff --git a/tests/src/com/unciv/dev/FasterUIDevelopment.kt b/tests/src/com/unciv/dev/FasterUIDevelopment.kt index 1db01b0ca7..f6e429de46 100644 --- a/tests/src/com/unciv/dev/FasterUIDevelopment.kt +++ b/tests/src/com/unciv/dev/FasterUIDevelopment.kt @@ -62,7 +62,8 @@ object FasterUIDevelopment { val settings = UncivFiles.getSettingsForPlatformLaunchers() if (!settings.isFreshlyCreated) { - config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80)) + val (width, height) = settings.windowState.coerceIn() + config.setWindowedMode(width, height) } Lwjgl3Application(UIDevGame(), config)