Allow image overlay and changing world wrap in map editor (#9493)

This commit is contained in:
SomeTroglodyte 2023-06-03 21:43:35 +02:00 committed by GitHub
parent 9bbd3b416e
commit 42b35bce4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 225 additions and 21 deletions

View File

@ -208,7 +208,8 @@
"font": "button",
"fontColor": "color",
"downFontColor": "pressed",
"overFontColor": "highlight"
"overFontColor": "highlight",
"disabledFontColor": "gray"
}
},
"com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle": {

View File

@ -514,6 +514,14 @@ River generation failed! =
Please don't use step 'Landmass' with map type 'Empty', create a new empty map instead. =
This map has errors: =
The incompatible elements have been removed. =
Current map: World Wrap =
Overlay image =
Click to choose a file =
Choose an image =
Overlay transparency: =
Invalid overlay image =
World wrap is incompatible with an overlay and was deactivated. =
An overlay image is incompatible with world wrap and was deactivated. =
## Map/Tool names
My new map =
@ -1208,11 +1216,13 @@ Default Focus =
Please enter a new name for your city =
Please select a tile for this building's [improvement] =
# Ask for text or numbers popup UI
# Specialized Popups - Ask for text or numbers, file picker
Invalid input! Please enter a different string. =
Invalid input! Please enter a valid number. =
Please enter some text =
Please enter a file name =
File name: =
# Technology UI

View File

@ -2,9 +2,17 @@ package com.unciv.ui.screens.mapeditorscreen
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable
import com.unciv.UncivGame
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize
import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.TileMap
@ -20,10 +28,13 @@ import com.unciv.ui.components.tilegroups.TileGroup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardPanningListener
import com.unciv.ui.images.ImageWithCustomSize
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.RecreateOnResize
import com.unciv.ui.screens.worldscreen.ZoomButtonPair
import com.unciv.utils.Concurrency
import com.unciv.utils.Dispatcher
import com.unciv.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -125,6 +136,34 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
fun getToolsWidth() = stage.width * 0.4f
fun setWorldWrap(newValue: Boolean) {
if (newValue == tileMap.mapParameters.worldWrap) return
setWorldWrapFixOddWidth(newValue)
if (newValue && overlayFile != null) {
overlayFile = null
ToastPopup("An overlay image is incompatible with world wrap and was deactivated.", stage, 4000)
tabs.options.update()
}
recreateMapHolder()
}
private fun setWorldWrapFixOddWidth(newValue: Boolean) = tileMap.mapParameters.run {
// Turning *off* WW and finding an odd width means it must have been rounded
// down by the TileMap constructor - fix so we can turn it back on later
if (worldWrap && mapSize.width % 2 != 0 && shape == MapShape.rectangular)
mapSize.width--
worldWrap = newValue
}
private fun recreateMapHolder(actionWhileRemoved: ()->Unit = {}) {
val savedScale = mapHolder.scaleX
clearOverlayImages()
mapHolder.remove()
actionWhileRemoved()
mapHolder = newMapHolder()
mapHolder.zoom(savedScale)
}
private fun newMapHolder(): EditorMapHolder {
ImageGetter.setNewRuleset(ruleset)
// setNewRuleset is missing some graphics - those "EmojiIcons"&co already rendered as font characters
@ -138,18 +177,20 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
tileMap.setStartingLocationsTransients()
UncivGame.Current.translations.translationActiveMods = ruleset.mods
val result = EditorMapHolder(this, tileMap) {
val newHolder = EditorMapHolder(this, tileMap) {
tileClickHandler?.invoke(it)
}
for (oldPanningListener in stage.root.listeners.filterIsInstance<KeyboardPanningListener>())
stage.removeListener(oldPanningListener) // otherwise they accumulate
result.mapPanningSpeed = UncivGame.Current.settings.mapPanningSpeed
stage.addListener(KeyboardPanningListener(result, allowWASD = false))
newHolder.mapPanningSpeed = UncivGame.Current.settings.mapPanningSpeed
stage.addListener(KeyboardPanningListener(newHolder, allowWASD = false))
if (Gdx.app.type == Application.ApplicationType.Desktop)
result.isAutoScrollEnabled = UncivGame.Current.settings.mapAutoScroll
newHolder.isAutoScrollEnabled = UncivGame.Current.settings.mapAutoScroll
stage.root.addActorAt(0, result)
stage.scrollFocus = result
addOverlayToMapHolder(newHolder.actor as Group) // That's the initially empty Group ZoomableScrollPane allocated
stage.root.addActorAt(0, newHolder)
stage.scrollFocus = newHolder
isDirty = true
modsTabNeedsRefresh = true
@ -157,15 +198,16 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
naturalWondersNeedRefresh = true
if (UncivGame.Current.settings.showZoomButtons) {
zoomController = ZoomButtonPair(result)
zoomController = ZoomButtonPair(newHolder)
zoomController!!.setPosition(10f, 10f)
stage.addActor(zoomController)
}
return result
return newHolder
}
fun loadMap(map: TileMap, newRuleset: Ruleset? = null, selectPage: Int = 0) {
clearOverlayImages()
mapHolder.remove()
tileMap = map
ruleset = newRuleset ?: RulesetCache.getComplexRuleset(map.mapParameters)
@ -181,11 +223,12 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
}
fun applyRuleset(newRuleset: Ruleset, newBaseRuleset: String, mods: LinkedHashSet<String>) {
mapHolder.remove()
tileMap.mapParameters.baseRuleset = newBaseRuleset
tileMap.mapParameters.mods = mods
tileMap.ruleset = newRuleset
ruleset = newRuleset
recreateMapHolder {
tileMap.mapParameters.baseRuleset = newBaseRuleset
tileMap.mapParameters.mods = mods
tileMap.ruleset = newRuleset
ruleset = newRuleset
}
mapHolder = newMapHolder()
modsTabNeedsRefresh = false
}
@ -251,4 +294,74 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
job.cancel()
jobs.clear()
}
//region Overlay Image
// To support world wrap with an overlay, one could maybe do up to tree versions of the same
// Image tiled side by side (therefore "clearOverlayImages"), they _could_ use the same Texture
// instance - that part works. But how to position and clip them properly escapes me - better
// coders are welcome to try. To work around, we simply turn world wrap off when an overlay is
// loaded, and allow to freely turn WW on and off. After all, the distinction becomes relevant
// *only* when a game is started, units move, and tile neighbors get a meaning.
private var imageOverlay: Image? = null
internal var overlayFile: FileHandle? = null
set(value) {
field = value
overlayFileChanged(value)
}
internal var overlayAlpha = 0.33f
set(value) {
field = value
overlayAlphaChanged(value)
}
private fun clearOverlayImages() {
val oldImage = imageOverlay ?: return
imageOverlay = null
oldImage.remove()
(oldImage.drawable as? TextureRegionDrawable)?.region?.texture?.dispose()
}
private fun overlayFileChanged(value: FileHandle?) {
clearOverlayImages()
if (value == null) return
if (tileMap.mapParameters.worldWrap) {
setWorldWrapFixOddWidth(false)
ToastPopup("World wrap is incompatible with an overlay and was deactivated.", stage, 4000)
tabs.options.update()
}
recreateMapHolder()
}
private fun overlayAlphaChanged(value: Float) {
imageOverlay?.color?.a = value
}
private fun addOverlayToMapHolder(newHolderContent: Group) {
clearOverlayImages()
if (overlayFile == null) return
try {
val texture = Texture(overlayFile)
texture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear)
imageOverlay = ImageWithCustomSize(TextureRegion(texture))
} catch (ex: Throwable) {
Log.error("Invalid overlay image", ex)
overlayFile = null
ToastPopup("Invalid overlay image", stage, 3000)
tabs.options.update()
return
}
imageOverlay?.apply {
touchable = Touchable.disabled
setFillParent(true)
color.a = overlayAlpha
newHolderContent.addActor(this)
}
}
//endregion
}

View File

@ -1,17 +1,21 @@
package com.unciv.ui.screens.mapeditorscreen.tabs
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.logic.files.FileChooser
import com.unciv.logic.files.MapSaver
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize
import com.unciv.models.translations.tr
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.keyShortcuts
@ -20,6 +24,9 @@ import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.popups.ToastPopup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
import com.unciv.utils.Log
class MapEditorOptionsTab(
@ -30,6 +37,9 @@ class MapEditorOptionsTab(
private val tileMatchGroup = ButtonGroup<CheckBox>()
private val copyMapButton = "Copy to clipboard".toTextButton()
private val pasteMapButton = "Load copied data".toTextButton()
private val worldWrapCheckBox: CheckBox
private val overlayFileButton= TextButton(null, BaseScreen.skin)
private val overlayAlphaSlider: UncivSlider
private var seedToCopy = ""
private var tileMatchFuzziness = TileMatchFuzziness.CompleteMatch
@ -41,6 +51,7 @@ class MapEditorOptionsTab(
BaseTerrain("Base terrain only"),
LandOrWater("Land or water only"),
}
init {
top()
defaults().pad(10f)
@ -64,10 +75,47 @@ class MapEditorOptionsTab(
add("Map copy and paste".toLabel(Color.GOLD)).row()
copyMapButton.onActivation { copyHandler() }
copyMapButton.keyShortcuts.add(KeyCharAndCode.ctrl('c'))
add(copyMapButton).row()
pasteMapButton.onActivation { pasteHandler() }
pasteMapButton.keyShortcuts.add(KeyCharAndCode.ctrl('v'))
add(pasteMapButton).row()
add(Table().apply {
add(copyMapButton).padRight(15f)
add(pasteMapButton)
}).row()
addSeparator(Color.GRAY)
worldWrapCheckBox = "Current map: World Wrap".toCheckBox(editorScreen.tileMap.mapParameters.worldWrap) {
editorScreen.setWorldWrap(it)
}
add(worldWrapCheckBox).growX().row()
addSeparator(Color.GRAY)
add("Overlay image".toLabel(Color.GOLD)).row()
overlayFileButton.style = TextButton.TextButtonStyle(overlayFileButton.style)
showOverlayFileName()
overlayFileButton.onClick {
// TODO - to allow accessing files *outside the app scope* on Android, switch to
// [UncivFiles.saverLoader] and teach PlatformSaverLoader to deliver a stream or
// ByteArray or PixMap instead of doing a text file load using system/JVM default encoding..
// Then we'd need to make a *managed* PixMap-based Texture out of that, because only
// managed will survive GL context loss automatically. Cespenar says "could get messy".
FileChooser.createLoadDialog(stage, "Choose an image", editorScreen.overlayFile) {
success: Boolean, file: FileHandle ->
if (!success) return@createLoadDialog
editorScreen.overlayFile = file
showOverlayFileName()
}.apply {
filter = FileChooser.createExtensionFilter("png", "jpg", "jpeg")
}.open()
}
add(overlayFileButton).fillX().row()
overlayAlphaSlider = UncivSlider(0f, 1f, 0.05f, initial = editorScreen.overlayAlpha) {
editorScreen.overlayAlpha = it
}
add(Table().apply {
add("Overlay opacity:".toLabel(alignment = Align.left)).left()
add(overlayAlphaSlider).right()
}).row()
}
private fun copyHandler() {
@ -85,10 +133,42 @@ class MapEditorOptionsTab(
}
}
private fun showOverlayFileName() = overlayFileButton.run {
if (editorScreen.overlayFile == null) {
setText("Click to choose a file")
style.fontColor.a = 0.5f
} else {
setText(editorScreen.overlayFile!!.path())
style.fontColor.a = 1f
}
}
/** Check whether we can flip world wrap without ruining geometry */
private fun canChangeWorldWrap(): Boolean {
val params = editorScreen.tileMap.mapParameters
// Can't change for hexagonal at all, as non-ww must always have an odd number of columns and ww nust have an even number of columns
if (params.shape != MapShape.rectangular) return false
// Too small?
if (params.mapSize.radius < MapSize.Tiny.radius) return false
// Even-width rectangular have no problems, but that has not necessarily been saved in mapSize!
if (params.mapSize.width % 2 == 0) return true
// The recorded width may have been reduced to even by the TileMap constructor.
// In such a case we allow turning WW off, and editorScreen.setWorldWrap will fix the width.
return (params.worldWrap)
}
fun update() {
pasteMapButton.isEnabled = Gdx.app.clipboard.hasContents()
worldWrapCheckBox.isChecked = editorScreen.tileMap.mapParameters.worldWrap
worldWrapCheckBox.isDisabled = !canChangeWorldWrap()
showOverlayFileName()
}
override fun activated(index: Int, caption: String, pager: TabbedPager) {
seedToCopy = editorScreen.tileMap.mapParameters.seed.toString()
seedLabel.setText("Current map RNG seed: [$seedToCopy]".tr())
pasteMapButton.isEnabled = Gdx.app.clipboard.hasContents()
update()
overlayAlphaSlider.value = editorScreen.overlayAlpha
}
override fun deactivated(index: Int, caption: String, pager: TabbedPager) {