diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 94cae0802a..2f4c24f2dd 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -812,23 +812,21 @@ Screen orientation = Landscape (fixed) = Portrait (fixed) = Auto (sensor adjusted) = - -Enable display cutout (requires restart) = - -Max zoom out = - +Enable using display cutout areas = +Hide system status and navigation bars = Font family = Font size multiplier = Default Font = +Max zoom out = +Enable Easter Eggs = +Enlarge selected notifications = + Generate translation files = Translation files are generated successfully. = Fastlane files are generated successfully. = Update Mod categories = -Enable Easter Eggs = -Enlarge selected notifications = - ## Keys tab Keys = Please see the Tutorial. = diff --git a/android/src/com/unciv/app/AndroidDisplay.kt b/android/src/com/unciv/app/AndroidDisplay.kt index 8f8224ab37..0d140bcb1d 100644 --- a/android/src/com/unciv/app/AndroidDisplay.kt +++ b/android/src/com/unciv/app/AndroidDisplay.kt @@ -2,41 +2,19 @@ package com.unciv.app import android.app.Activity import android.content.pm.ActivityInfo -import android.database.ContentObserver import android.os.Build -import android.os.Handler -import android.provider.Settings import android.view.Display import android.view.Display.Mode +import android.view.View import android.view.WindowManager import androidx.annotation.RequiresApi import com.unciv.models.metadata.GameSettings import com.unciv.models.translations.tr -import com.unciv.utils.Log import com.unciv.utils.PlatformDisplay import com.unciv.utils.ScreenMode import com.unciv.utils.ScreenOrientation -class AndroidScreenMode( - private val modeId: Int) : ScreenMode { - private var name: String = "Default" - - @RequiresApi(Build.VERSION_CODES.M) - constructor(mode: Mode) : this(mode.modeId) { - name = "${mode.physicalWidth}x${mode.physicalHeight} (${mode.refreshRate.toInt()}HZ)" - } - - override fun getId(): Int { - return modeId - } - - override fun toString(): String { - return name.tr() - } - -} - class AndroidDisplay(private val activity: Activity) : PlatformDisplay { private var display: Display? = null @@ -45,6 +23,7 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { init { // Fetch current display + @Suppress("DEPRECATION") // M..P should use the deprecated API display = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> activity.display Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> activity.windowManager.defaultDisplay @@ -52,7 +31,7 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { } // Add default mode - displayModes[0] = AndroidScreenMode(0) + displayModes[AndroidScreenMode.defaultId] = AndroidScreenMode.default // Add other supported modes if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) @@ -71,14 +50,31 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { } override fun setScreenMode(id: Int, settings: GameSettings) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - activity.runOnUiThread { - val params = activity.window.attributes - params.preferredDisplayModeId = id - activity.window.attributes = params - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + activity.runOnUiThread { + val params = activity.window.attributes + params.preferredDisplayModeId = id + activity.window.attributes = params } + } + override fun hasSystemUiVisibility() = true + + override fun setSystemUiVisibility(hide: Boolean) { + activity.runOnUiThread { + setSystemUiVisibilityFromUiThread(hide) + } + } + internal fun setSystemUiVisibilityFromUiThread(hide: Boolean) { + @Suppress("DEPRECATION") // Avoids @RequiresApi(Build.VERSION_CODES.R) + activity.window.decorView.systemUiVisibility = + if (hide) + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + else + View.SYSTEM_UI_FLAG_LAYOUT_STABLE } override fun hasCutout(): Boolean { @@ -95,15 +91,19 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { } override fun setCutout(enabled: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val params = activity.window.attributes - params.layoutInDisplayCutoutMode = when { - enabled -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } - activity.window.attributes = params + activity.runOnUiThread { + setCutoutFromUiThread(enabled) } } + internal fun setCutoutFromUiThread(enabled: Boolean) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return + val params = activity.window.attributes + params.layoutInDisplayCutoutMode = when { + enabled -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + activity.window.attributes = params // This is the only line to actually need to be running on the Ui Thread + } /* @@ -121,7 +121,6 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { } override fun setOrientation(orientation: ScreenOrientation) { - val mode = when (orientation) { ScreenOrientation.Landscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE ScreenOrientation.Portrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT @@ -133,4 +132,25 @@ class AndroidDisplay(private val activity: Activity) : PlatformDisplay { activity.requestedOrientation = mode } + class AndroidScreenMode private constructor( + private val modeId: Int, + private val name: String + ) : ScreenMode { + @RequiresApi(Build.VERSION_CODES.M) + constructor(mode: Mode) : this(mode.modeId, "${mode.physicalWidth}x${mode.physicalHeight} (${mode.refreshRate.toInt()}Hz)") + + override fun getId(): Int { + return modeId + } + + override fun toString(): String { + return name.tr() + } + + companion object { + const val defaultId = 0 + val default: AndroidScreenMode + get() = AndroidScreenMode(defaultId, "Default") + } + } } diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 7ebaa3faee..eb8097ec3e 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -23,7 +23,8 @@ open class AndroidLauncher : AndroidApplication() { Log.backend = AndroidLogBackend(this) // Setup Android display - Display.platform = AndroidDisplay(this) + val displayImpl = AndroidDisplay(this) + Display.platform = displayImpl // Setup Android fonts Fonts.fontImplementation = AndroidFont() @@ -32,18 +33,19 @@ open class AndroidLauncher : AndroidApplication() { UncivFiles.saverLoader = AndroidSaverLoader(this) UncivFiles.preferExternalStorage = true + val config = AndroidApplicationConfiguration().apply { useImmersiveMode = false } + val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path) + + // Setup orientation, immersive mode and display cutout + displayImpl.setOrientation(settings.displayOrientation) + displayImpl.setCutoutFromUiThread(settings.androidCutout) + displayImpl.setSystemUiVisibilityFromUiThread(settings.androidHideSystemUi) + // Create notification channels for Multiplayer notificator MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) copyMods() - val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true } - val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path) - - // Setup orientation and display cutout - Display.setOrientation(settings.displayOrientation) - Display.setCutout(settings.androidCutout) - game = AndroidGame(this) initialize(game, config) diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 54149b13ab..0dcfa5ea66 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -100,6 +100,7 @@ class GameSettings { var showAutosaves: Boolean = false var androidCutout: Boolean = false + var androidHideSystemUi = true var multiplayer = GameSettingsMultiplayer() diff --git a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt index 2513948bac..346c67076a 100644 --- a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt +++ b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt @@ -22,6 +22,7 @@ import com.unciv.models.metadata.ScreenSize import com.unciv.models.translations.TranslationFileWriter import com.unciv.models.translations.tr import com.unciv.ui.components.UncivTooltip.Companion.addTooltip +import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.setFontColor import com.unciv.ui.components.extensions.toCheckBox @@ -57,34 +58,48 @@ fun advancedTab( val settings = optionsPopup.settings addAutosaveTurnsSelectBox(this, settings) + addSeparator(Color.GRAY) - if (Display.hasOrientation()) { + if (Display.hasOrientation()) addOrientationSelectBox(this, optionsPopup) - } - if (Display.hasCutout()) { + if (Display.hasCutout()) addCutoutCheckbox(this, optionsPopup) - } - addMaxZoomSlider(this, settings) + if (Display.hasSystemUiVisibility()) + addHideSystemUiCheckbox(this, optionsPopup) addFontFamilySelect(this, settings, optionsPopup.selectBoxMinWidth, onFontChange) - addFontSizeMultiplier(this, settings, onFontChange) + addSeparator(Color.GRAY) - addTranslationGeneration(this, optionsPopup) - - addSetUserId(this, settings) + addMaxZoomSlider(this, settings) addEasterEggsCheckBox(this, settings) addEnlargeNotificationsCheckBox(this, settings) + addSeparator(Color.GRAY) + + addSetUserId(this, settings) + + addTranslationGeneration(this, optionsPopup) } private fun addCutoutCheckbox(table: Table, optionsPopup: OptionsPopup) { - optionsPopup.addCheckbox(table, "Enable display cutout (requires restart)", optionsPopup.settings.androidCutout) + optionsPopup.addCheckbox(table, "Enable using display cutout areas", optionsPopup.settings.androidCutout) { optionsPopup.settings.androidCutout = it + Display.setCutout(it) + optionsPopup.reopenAfterDiplayLayoutChange() + } +} + +private fun addHideSystemUiCheckbox(table: Table, optionsPopup: OptionsPopup) { + optionsPopup.addCheckbox(table, "Hide system status and navigation bars", optionsPopup.settings.androidHideSystemUi) + { + optionsPopup.settings.androidHideSystemUi = it + Display.setSystemUiVisibility(hide = it) + optionsPopup.reopenAfterDiplayLayoutChange() } } @@ -101,6 +116,7 @@ private fun addOrientationSelectBox(table: Table, optionsPopup: OptionsPopup) { val orientation = selectBox.selected settings.displayOrientation = orientation Display.setOrientation(orientation) + optionsPopup.reopenAfterDiplayLayoutChange() } table.add(selectBox).minWidth(optionsPopup.selectBoxMinWidth).pad(10f).row() @@ -202,7 +218,7 @@ private fun addFontSizeMultiplier( settings: GameSettings, onFontChange: () -> Unit ) { - table.add("Font size multiplier".toLabel()).left().fillX() + table.add("Font size multiplier".toLabel()).left().fillX().padTop(5f) val fontSizeSlider = UncivSlider( 0.7f, 1.5f, 0.05f, @@ -214,11 +230,11 @@ private fun addFontSizeMultiplier( if (!fontSizeSlider.isDragging) onFontChange() } - table.add(fontSizeSlider).pad(5f).row() + table.add(fontSizeSlider).pad(5f).padTop(10f).row() } private fun addMaxZoomSlider(table: Table, settings: GameSettings) { - table.add("Max zoom out".tr()).left().fillX() + table.add("Max zoom out".tr()).left().fillX().padTop(5f) val maxZoomSlider = UncivSlider( 2f, 6f, 1f, initial = settings.maxWorldZoomOut @@ -227,7 +243,7 @@ private fun addMaxZoomSlider(table: Table, settings: GameSettings) { if (GUI.isWorldLoaded()) GUI.getMap().reloadMaxZoom() } - table.add(maxZoomSlider).pad(5f).row() + table.add(maxZoomSlider).pad(5f).padTop(10f).row() } private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) { diff --git a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt index bdc196ed45..bbb2839e29 100644 --- a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt +++ b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt @@ -13,11 +13,14 @@ import com.unciv.ui.components.extensions.toCheckBox import com.unciv.ui.components.widgets.TabbedPager import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.Popup +import com.unciv.ui.popups.hasOpenPopups import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.basescreen.RecreateOnResize import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.utils.Concurrency import com.unciv.utils.withGLContext +import kotlinx.coroutines.delay import kotlin.reflect.KMutableProperty0 /** @@ -160,6 +163,22 @@ class OptionsPopup( } } + /** Call if an option change might trigger a Screen.resize + * + * Does nothing if any Popup (which can only be this one) is still open after a short delay and context yield. + * Reason: A resize might relaunch the parent screen ([MainMenuScreen] is [RecreateOnResize]) and thus close this Popup. + */ + fun reopenAfterDiplayLayoutChange() { + Concurrency.run("Reload from options") { + delay(100) + withGLContext { + val screen = UncivGame.Current.screen ?: return@withGLContext + if (screen.hasOpenPopups()) return@withGLContext // e.g. Orientation auto to fixed while auto is already the new orientation + screen.openOptionsPopup(tabs.activePage) + } + } + } + fun addCheckbox(table: Table, text: String, initialState: Boolean, updateWorld: Boolean = false, newRow: Boolean = true, action: ((Boolean) -> Unit)) { val checkbox = text.toCheckBox(initialState) { action(it) diff --git a/core/src/com/unciv/utils/Display.kt b/core/src/com/unciv/utils/Display.kt index 5e0daa4074..9bed181da7 100644 --- a/core/src/com/unciv/utils/Display.kt +++ b/core/src/com/unciv/utils/Display.kt @@ -19,31 +19,35 @@ interface ScreenMode { } interface PlatformDisplay { - fun setScreenMode(id: Int, settings: GameSettings) {} - fun getScreenModes(): Map { return hashMapOf() } + fun getScreenModes(): Map = hashMapOf() - fun hasCutout(): Boolean { return false } + fun hasCutout(): Boolean = false fun setCutout(enabled: Boolean) {} - fun hasOrientation(): Boolean { return false } + fun hasOrientation(): Boolean = false fun setOrientation(orientation: ScreenOrientation) {} fun hasUserSelectableSize(id: Int): Boolean = false + + fun hasSystemUiVisibility(): Boolean = false + fun setSystemUiVisibility(hide: Boolean) {} } object Display { - lateinit var platform: PlatformDisplay - fun hasOrientation(): Boolean { return platform.hasOrientation() } + fun hasOrientation() = platform.hasOrientation() fun setOrientation(orientation: ScreenOrientation) { platform.setOrientation(orientation) } - fun hasCutout(): Boolean { return platform.hasCutout() } + fun hasCutout() = platform.hasCutout() fun setCutout(enabled: Boolean) { platform.setCutout(enabled) } - fun getScreenModes(): Map { return platform.getScreenModes() } + fun getScreenModes() = platform.getScreenModes() fun setScreenMode(id: Int, settings: GameSettings) { platform.setScreenMode(id, settings) } fun hasUserSelectableSize(id: Int) = platform.hasUserSelectableSize(id) + + fun hasSystemUiVisibility() = platform.hasSystemUiVisibility() + fun setSystemUiVisibility(hide: Boolean) = platform.setSystemUiVisibility(hide) }