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", "font": "button",
"fontColor": "color", "fontColor": "color",
"downFontColor": "pressed", "downFontColor": "pressed",
"overFontColor": "highlight" "overFontColor": "highlight",
"disabledFontColor": "gray"
} }
}, },
"com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle": { "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. = Please don't use step 'Landmass' with map type 'Empty', create a new empty map instead. =
This map has errors: = This map has errors: =
The incompatible elements have been removed. = 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 ## Map/Tool names
My new map = My new map =
@ -1208,11 +1216,13 @@ Default Focus =
Please enter a new name for your city = Please enter a new name for your city =
Please select a tile for this building's [improvement] = 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 different string. =
Invalid input! Please enter a valid number. = Invalid input! Please enter a valid number. =
Please enter some text = Please enter some text =
Please enter a file name =
File name: =
# Technology UI # Technology UI

View File

@ -2,9 +2,17 @@ package com.unciv.ui.screens.mapeditorscreen
import com.badlogic.gdx.Application import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color 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.UncivGame
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapSize
import com.unciv.logic.map.MapSizeNew import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.TileMap 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.screens.basescreen.BaseScreen
import com.unciv.ui.components.KeyCharAndCode import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardPanningListener 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.basescreen.RecreateOnResize
import com.unciv.ui.screens.worldscreen.ZoomButtonPair import com.unciv.ui.screens.worldscreen.ZoomButtonPair
import com.unciv.utils.Concurrency import com.unciv.utils.Concurrency
import com.unciv.utils.Dispatcher import com.unciv.utils.Dispatcher
import com.unciv.utils.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -125,6 +136,34 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
fun getToolsWidth() = stage.width * 0.4f 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 { private fun newMapHolder(): EditorMapHolder {
ImageGetter.setNewRuleset(ruleset) ImageGetter.setNewRuleset(ruleset)
// setNewRuleset is missing some graphics - those "EmojiIcons"&co already rendered as font characters // 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() tileMap.setStartingLocationsTransients()
UncivGame.Current.translations.translationActiveMods = ruleset.mods UncivGame.Current.translations.translationActiveMods = ruleset.mods
val result = EditorMapHolder(this, tileMap) { val newHolder = EditorMapHolder(this, tileMap) {
tileClickHandler?.invoke(it) tileClickHandler?.invoke(it)
} }
for (oldPanningListener in stage.root.listeners.filterIsInstance<KeyboardPanningListener>()) for (oldPanningListener in stage.root.listeners.filterIsInstance<KeyboardPanningListener>())
stage.removeListener(oldPanningListener) // otherwise they accumulate stage.removeListener(oldPanningListener) // otherwise they accumulate
result.mapPanningSpeed = UncivGame.Current.settings.mapPanningSpeed newHolder.mapPanningSpeed = UncivGame.Current.settings.mapPanningSpeed
stage.addListener(KeyboardPanningListener(result, allowWASD = false)) stage.addListener(KeyboardPanningListener(newHolder, allowWASD = false))
if (Gdx.app.type == Application.ApplicationType.Desktop) if (Gdx.app.type == Application.ApplicationType.Desktop)
result.isAutoScrollEnabled = UncivGame.Current.settings.mapAutoScroll newHolder.isAutoScrollEnabled = UncivGame.Current.settings.mapAutoScroll
stage.root.addActorAt(0, result) addOverlayToMapHolder(newHolder.actor as Group) // That's the initially empty Group ZoomableScrollPane allocated
stage.scrollFocus = result
stage.root.addActorAt(0, newHolder)
stage.scrollFocus = newHolder
isDirty = true isDirty = true
modsTabNeedsRefresh = true modsTabNeedsRefresh = true
@ -157,15 +198,16 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
naturalWondersNeedRefresh = true naturalWondersNeedRefresh = true
if (UncivGame.Current.settings.showZoomButtons) { if (UncivGame.Current.settings.showZoomButtons) {
zoomController = ZoomButtonPair(result) zoomController = ZoomButtonPair(newHolder)
zoomController!!.setPosition(10f, 10f) zoomController!!.setPosition(10f, 10f)
stage.addActor(zoomController) stage.addActor(zoomController)
} }
return result return newHolder
} }
fun loadMap(map: TileMap, newRuleset: Ruleset? = null, selectPage: Int = 0) { fun loadMap(map: TileMap, newRuleset: Ruleset? = null, selectPage: Int = 0) {
clearOverlayImages()
mapHolder.remove() mapHolder.remove()
tileMap = map tileMap = map
ruleset = newRuleset ?: RulesetCache.getComplexRuleset(map.mapParameters) 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>) { fun applyRuleset(newRuleset: Ruleset, newBaseRuleset: String, mods: LinkedHashSet<String>) {
mapHolder.remove() recreateMapHolder {
tileMap.mapParameters.baseRuleset = newBaseRuleset tileMap.mapParameters.baseRuleset = newBaseRuleset
tileMap.mapParameters.mods = mods tileMap.mapParameters.mods = mods
tileMap.ruleset = newRuleset tileMap.ruleset = newRuleset
ruleset = newRuleset ruleset = newRuleset
}
mapHolder = newMapHolder() mapHolder = newMapHolder()
modsTabNeedsRefresh = false modsTabNeedsRefresh = false
} }
@ -251,4 +294,74 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
job.cancel() job.cancel()
jobs.clear() 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 package com.unciv.ui.screens.mapeditorscreen.tabs
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Table 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.files.MapSaver
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize
import com.unciv.models.translations.tr 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.KeyCharAndCode
import com.unciv.ui.components.TabbedPager 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.addSeparator
import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.keyShortcuts 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.toCheckBox
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton 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 import com.unciv.utils.Log
class MapEditorOptionsTab( class MapEditorOptionsTab(
@ -30,6 +37,9 @@ class MapEditorOptionsTab(
private val tileMatchGroup = ButtonGroup<CheckBox>() private val tileMatchGroup = ButtonGroup<CheckBox>()
private val copyMapButton = "Copy to clipboard".toTextButton() private val copyMapButton = "Copy to clipboard".toTextButton()
private val pasteMapButton = "Load copied data".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 seedToCopy = ""
private var tileMatchFuzziness = TileMatchFuzziness.CompleteMatch private var tileMatchFuzziness = TileMatchFuzziness.CompleteMatch
@ -41,6 +51,7 @@ class MapEditorOptionsTab(
BaseTerrain("Base terrain only"), BaseTerrain("Base terrain only"),
LandOrWater("Land or water only"), LandOrWater("Land or water only"),
} }
init { init {
top() top()
defaults().pad(10f) defaults().pad(10f)
@ -64,10 +75,47 @@ class MapEditorOptionsTab(
add("Map copy and paste".toLabel(Color.GOLD)).row() add("Map copy and paste".toLabel(Color.GOLD)).row()
copyMapButton.onActivation { copyHandler() } copyMapButton.onActivation { copyHandler() }
copyMapButton.keyShortcuts.add(KeyCharAndCode.ctrl('c')) copyMapButton.keyShortcuts.add(KeyCharAndCode.ctrl('c'))
add(copyMapButton).row()
pasteMapButton.onActivation { pasteHandler() } pasteMapButton.onActivation { pasteHandler() }
pasteMapButton.keyShortcuts.add(KeyCharAndCode.ctrl('v')) 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() { 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) { override fun activated(index: Int, caption: String, pager: TabbedPager) {
seedToCopy = editorScreen.tileMap.mapParameters.seed.toString() seedToCopy = editorScreen.tileMap.mapParameters.seed.toString()
seedLabel.setText("Current map RNG seed: [$seedToCopy]".tr()) 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) { override fun deactivated(index: Int, caption: String, pager: TabbedPager) {