diff --git a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt index a208743fa5..5f5d5a5dfa 100644 --- a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt +++ b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt @@ -11,87 +11,11 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.TextureAtlas import com.badlogic.gdx.scenes.scene2d.* import com.badlogic.gdx.scenes.scene2d.ui.* -import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener -import com.badlogic.gdx.scenes.scene2d.utils.ClickListener import com.badlogic.gdx.scenes.scene2d.utils.Drawable import com.badlogic.gdx.utils.viewport.ExtendViewport import com.unciv.UncivGame import com.unciv.models.Tutorial -import com.unciv.models.UncivSound -import com.unciv.models.translations.tr import com.unciv.ui.tutorials.TutorialController -import java.util.HashMap -import kotlin.concurrent.thread -import kotlin.random.Random - -/* - * For now, combination keys cannot easily be expressed. - * Pressing Ctrl-Letter will arrive one event for Input.Keys.CONTROL_LEFT and one for the ASCII control code point - * so Ctrl-R can be handled using KeyCharAndCode('\u0012') - * Pressing Alt-Something likewise will fire once for Alt and once for the unmodified keys with no indication Alt is held - * (Exception: international keyboard AltGr-combos) - * An update supporting easy declarations for any modifier combos would need to use Gdx.input.isKeyPressed() - * Gdx seems to omit support for a modifier mask (e.g. Ctrl-Alt-Shift) so we would need to reinvent this - */ - -/** - * Represents a key for use in an InputListener keyTyped() handler - * - * Example: KeyCharAndCode('R'), KeyCharAndCode(Input.Keys.F1) - */ -data class KeyCharAndCode(val char: Char, val code: Int) { - // express keys with a Char value - constructor(char: Char): this(char.toLowerCase(), 0) - // express keys that only have a keyCode like F1 - constructor(code: Int): this(Char.MIN_VALUE, code) - // helper for use in InputListener keyTyped() - constructor(event: InputEvent?, character: Char) - : this ( - character.toLowerCase(), - if (character == Char.MIN_VALUE && event!=null) event.keyCode else 0 - ) - - @ExperimentalStdlibApi - override fun toString(): String { - // debug helper - return when { - char == Char.MIN_VALUE -> Input.Keys.toString(code) - char < ' ' -> "Ctrl-" + Char(char.toInt()+64) - else -> "\"$char\"" - } - } -} - -class KeyPressDispatcher: HashMap Unit)>() { - private var checkpoint: Set = setOf() - - // access by Char - operator fun get(char: Char) = this[KeyCharAndCode(char)] - operator fun set(char: Char, action: () -> Unit) { - this[KeyCharAndCode(char)] = action - } - operator fun contains(char: Char) = this.contains(KeyCharAndCode(char)) - fun remove(char: Char) = this.remove(KeyCharAndCode(char)) - - // access by Int keyCodes - operator fun get(code: Int) = this[KeyCharAndCode(code)] - operator fun set(code: Int, action: () -> Unit) { - this[KeyCharAndCode(code)] = action - } - operator fun contains(code: Int) = this.contains(KeyCharAndCode(code)) - fun remove(code: Int) = this.remove(KeyCharAndCode(code)) - - override fun clear() { - checkpoint = setOf() - super.clear() - } - fun setCheckpoint() { - checkpoint = keys.toSet() - } - fun revertToCheckPoint() { - keys.minus(checkpoint).forEach { remove(it) } - } -} open class CameraStageBaseScreen : Screen { @@ -195,176 +119,3 @@ open class CameraStageBaseScreen : Screen { } } - - -fun Button.disable(){ - touchable= Touchable.disabled - color= Color.GRAY -} -fun Button.enable() { - color = Color.WHITE - touchable = Touchable.enabled -} -var Button.isEnabled: Boolean - //Todo: Use in PromotionPickerScreen, TradeTable, WorldScreen.updateNextTurnButton - get() = touchable == Touchable.enabled - set(value) = if (value) enable() else disable() - -fun colorFromRGB(r: Int, g: Int, b: Int) = Color(r/255f, g/255f, b/255f, 1f) -fun colorFromRGB(rgb:List) = colorFromRGB(rgb[0],rgb[1],rgb[2]) - -fun Actor.centerX(parent:Actor){ x = parent.width/2 - width/2 } -fun Actor.centerY(parent:Actor){ y = parent.height/2- height/2} -fun Actor.center(parent:Actor){ centerX(parent); centerY(parent)} - -fun Actor.centerX(parent:Stage){ x = parent.width/2 - width/2 } -fun Actor.centerY(parent:Stage){ y = parent.height/2- height/2} -fun Actor.center(parent:Stage){ centerX(parent); centerY(parent)} - - -/** same as [onClick], but sends the [InputEvent] and coordinates along */ -fun Actor.onClickEvent(sound: UncivSound = UncivSound.Click, function: (event: InputEvent?, x: Float, y: Float) -> Unit) { - this.addListener(object : ClickListener() { - override fun clicked(event: InputEvent?, x: Float, y: Float) { - thread(name="Sound") { Sounds.play(sound) } - function(event, x, y) - } - }) -} - -// If there are other buttons that require special clicks then we'll have an onclick that will accept a string parameter, no worries -fun Actor.onClick(sound: UncivSound = UncivSound.Click, function: () -> Unit) { - onClickEvent(sound) { _, _, _ -> function() } -} - -fun Actor.onClick(function: () -> Unit): Actor { - onClick(UncivSound.Click, function) - return this -} - -fun Actor.onChange(function: () -> Unit): Actor { - this.addListener(object : ChangeListener() { - override fun changed(event: ChangeEvent?, actor: Actor?) { - function() - } - }) - return this -} - -fun Actor.surroundWithCircle(size: Float, resizeActor: Boolean = true, color: Color = Color.WHITE): IconCircleGroup { - return IconCircleGroup(size,this,resizeActor, color) -} - -fun Actor.addBorder(size:Float,color:Color,expandCell:Boolean=false):Table{ - val table = Table() - table.pad(size) - table.background = ImageGetter.getBackground(color) - val cell = table.add(this) - if (expandCell) cell.expand() - cell.fill() - table.pack() - return table -} - -fun Table.addSeparator(): Cell { - row() - val image = ImageGetter.getWhiteDot() - val cell = add(image).colspan(columns).height(2f).fill() - row() - return cell -} - -fun Table.addSeparatorVertical(): Cell { - val image = ImageGetter.getWhiteDot() - val cell = add(image).width(2f).fillY() - return cell -} - -fun Table.addCell(actor: T): Table { - add(actor) - return this -} - -/** - * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed - */ -fun ArrayList.withItem(item:T): ArrayList { - val newArrayList = ArrayList(this) - newArrayList.add(item) - return newArrayList -} - -/** - * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed - */ -fun HashSet.withItem(item:T): HashSet { - val newHashSet = HashSet(this) - newHashSet.add(item) - return newHashSet -} - -/** - * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed - */ -fun ArrayList.withoutItem(item:T): ArrayList { - val newArrayList = ArrayList(this) - newArrayList.remove(item) - return newArrayList -} - - -/** - * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed - */ -fun HashSet.withoutItem(item:T): HashSet { - val newHashSet = HashSet(this) - newHashSet.remove(item) - return newHashSet -} - -fun String.toTextButton() = TextButton(this.tr(), CameraStageBaseScreen.skin) - -/** also translates */ -fun String.toLabel() = Label(this.tr(),CameraStageBaseScreen.skin) -fun Int.toLabel() = this.toString().toLabel() - - - -// We don't want to use setFontSize and setFontColor because they set the font, -// which means we need to rebuild the font cache which means more memory allocation. -fun String.toLabel(fontColor:Color= Color.WHITE, fontSize:Int=18): Label { - var labelStyle = CameraStageBaseScreen.skin.get(Label.LabelStyle::class.java) - if(fontColor!= Color.WHITE || fontSize!=18) { // if we want the default we don't need to create another style - labelStyle = Label.LabelStyle(labelStyle) // clone this to another - labelStyle.fontColor = fontColor - if (fontSize != 18) labelStyle.font = Fonts.font - } - return Label(this.tr(), labelStyle).apply { setFontScale(fontSize/Fonts.ORIGINAL_FONT_SIZE) } -} - - -fun Label.setFontColor(color:Color): Label { style=Label.LabelStyle(style).apply { fontColor=color }; return this } - -fun Label.setFontSize(size:Int): Label { - style = Label.LabelStyle(style) - style.font = Fonts.font - style = style // because we need it to call the SetStyle function. Yuk, I know. - return this.apply { setFontScale(size/Fonts.ORIGINAL_FONT_SIZE) } // for chaining -} - - -fun List.randomWeighted(weights: List, random: Random = Random): T { - if (this.isEmpty()) throw NoSuchElementException("Empty list.") - if (this.size != weights.size) throw UnsupportedOperationException("Weights size does not match this list size.") - - val totalWeight = weights.sum() - val randDouble = random.nextDouble() - var sum = 0f - - for (i in weights.indices) { - sum += weights[i] / totalWeight - if (randDouble <= sum) - return this[i] - } - return this.last() -} diff --git a/core/src/com/unciv/ui/utils/ExtensionFunctions.kt b/core/src/com/unciv/ui/utils/ExtensionFunctions.kt new file mode 100644 index 0000000000..24564b85b6 --- /dev/null +++ b/core/src/com/unciv/ui/utils/ExtensionFunctions.kt @@ -0,0 +1,187 @@ +package com.unciv.ui.utils + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.InputEvent +import com.badlogic.gdx.scenes.scene2d.Stage +import com.badlogic.gdx.scenes.scene2d.Touchable +import com.badlogic.gdx.scenes.scene2d.ui.* +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener +import com.unciv.models.UncivSound +import com.unciv.models.translations.tr +import kotlin.concurrent.thread +import kotlin.random.Random + + +fun Button.disable(){ + touchable= Touchable.disabled + color= Color.GRAY +} +fun Button.enable() { + color = Color.WHITE + touchable = Touchable.enabled +} +var Button.isEnabled: Boolean + //Todo: Use in PromotionPickerScreen, TradeTable, WorldScreen.updateNextTurnButton + get() = touchable == Touchable.enabled + set(value) = if (value) enable() else disable() + +fun colorFromRGB(r: Int, g: Int, b: Int) = Color(r/255f, g/255f, b/255f, 1f) +fun colorFromRGB(rgb:List) = colorFromRGB(rgb[0],rgb[1],rgb[2]) + +fun Actor.centerX(parent: Actor){ x = parent.width/2 - width/2 } +fun Actor.centerY(parent: Actor){ y = parent.height/2- height/2} +fun Actor.center(parent: Actor){ centerX(parent); centerY(parent)} + +fun Actor.centerX(parent: Stage){ x = parent.width/2 - width/2 } +fun Actor.centerY(parent: Stage){ y = parent.height/2- height/2} +fun Actor.center(parent: Stage){ centerX(parent); centerY(parent)} + + +/** same as [onClick], but sends the [InputEvent] and coordinates along */ +fun Actor.onClickEvent(sound: UncivSound = UncivSound.Click, function: (event: InputEvent?, x: Float, y: Float) -> Unit) { + this.addListener(object : ClickListener() { + override fun clicked(event: InputEvent?, x: Float, y: Float) { + thread(name="Sound") { Sounds.play(sound) } + function(event, x, y) + } + }) +} + +// If there are other buttons that require special clicks then we'll have an onclick that will accept a string parameter, no worries +fun Actor.onClick(sound: UncivSound = UncivSound.Click, function: () -> Unit) { + onClickEvent(sound) { _, _, _ -> function() } +} + +fun Actor.onClick(function: () -> Unit): Actor { + onClick(UncivSound.Click, function) + return this +} + +fun Actor.onChange(function: () -> Unit): Actor { + this.addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + function() + } + }) + return this +} + +fun Actor.surroundWithCircle(size: Float, resizeActor: Boolean = true, color: Color = Color.WHITE): IconCircleGroup { + return IconCircleGroup(size,this,resizeActor, color) +} + +fun Actor.addBorder(size:Float, color: Color, expandCell:Boolean=false): Table { + val table = Table() + table.pad(size) + table.background = ImageGetter.getBackground(color) + val cell = table.add(this) + if (expandCell) cell.expand() + cell.fill() + table.pack() + return table +} + +fun Table.addSeparator(): Cell { + row() + val image = ImageGetter.getWhiteDot() + val cell = add(image).colspan(columns).height(2f).fill() + row() + return cell +} + +fun Table.addSeparatorVertical(): Cell { + val image = ImageGetter.getWhiteDot() + val cell = add(image).width(2f).fillY() + return cell +} + +fun Table.addCell(actor: T): Table { + add(actor) + return this +} + +/** + * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed + */ +fun ArrayList.withItem(item:T): ArrayList { + val newArrayList = ArrayList(this) + newArrayList.add(item) + return newArrayList +} + +/** + * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed + */ +fun HashSet.withItem(item:T): HashSet { + val newHashSet = HashSet(this) + newHashSet.add(item) + return newHashSet +} + +/** + * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed + */ +fun ArrayList.withoutItem(item:T): ArrayList { + val newArrayList = ArrayList(this) + newArrayList.remove(item) + return newArrayList +} + + +/** + * Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed + */ +fun HashSet.withoutItem(item:T): HashSet { + val newHashSet = HashSet(this) + newHashSet.remove(item) + return newHashSet +} + +fun String.toTextButton() = TextButton(this.tr(), CameraStageBaseScreen.skin) + +/** also translates */ +fun String.toLabel() = Label(this.tr(),CameraStageBaseScreen.skin) +fun Int.toLabel() = this.toString().toLabel() + + + +// We don't want to use setFontSize and setFontColor because they set the font, +// which means we need to rebuild the font cache which means more memory allocation. +fun String.toLabel(fontColor: Color = Color.WHITE, fontSize:Int=18): Label { + var labelStyle = CameraStageBaseScreen.skin.get(Label.LabelStyle::class.java) + if(fontColor!= Color.WHITE || fontSize!=18) { // if we want the default we don't need to create another style + labelStyle = Label.LabelStyle(labelStyle) // clone this to another + labelStyle.fontColor = fontColor + if (fontSize != 18) labelStyle.font = Fonts.font + } + return Label(this.tr(), labelStyle).apply { setFontScale(fontSize/Fonts.ORIGINAL_FONT_SIZE) } +} + + +fun Label.setFontColor(color: Color): Label { style= Label.LabelStyle(style).apply { fontColor=color }; return this } + +fun Label.setFontSize(size:Int): Label { + style = Label.LabelStyle(style) + style.font = Fonts.font + style = style // because we need it to call the SetStyle function. Yuk, I know. + return this.apply { setFontScale(size/ Fonts.ORIGINAL_FONT_SIZE) } // for chaining +} + + +fun List.randomWeighted(weights: List, random: Random = Random): T { + if (this.isEmpty()) throw NoSuchElementException("Empty list.") + if (this.size != weights.size) throw UnsupportedOperationException("Weights size does not match this list size.") + + val totalWeight = weights.sum() + val randDouble = random.nextDouble() + var sum = 0f + + for (i in weights.indices) { + sum += weights[i] / totalWeight + if (randDouble <= sum) + return this[i] + } + return this.last() +} diff --git a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt new file mode 100644 index 0000000000..8bb8532376 --- /dev/null +++ b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt @@ -0,0 +1,74 @@ +package com.unciv.ui.utils + +import com.badlogic.gdx.Input +import com.badlogic.gdx.scenes.scene2d.InputEvent +import java.util.HashMap + +/* + * For now, combination keys cannot easily be expressed. + * Pressing Ctrl-Letter will arrive one event for Input.Keys.CONTROL_LEFT and one for the ASCII control code point + * so Ctrl-R can be handled using KeyCharAndCode('\u0012') + * Pressing Alt-Something likewise will fire once for Alt and once for the unmodified keys with no indication Alt is held + * (Exception: international keyboard AltGr-combos) + * An update supporting easy declarations for any modifier combos would need to use Gdx.input.isKeyPressed() + * Gdx seems to omit support for a modifier mask (e.g. Ctrl-Alt-Shift) so we would need to reinvent this + */ + +/** + * Represents a key for use in an InputListener keyTyped() handler + * + * Example: KeyCharAndCode('R'), KeyCharAndCode(Input.Keys.F1) + */ +data class KeyCharAndCode(val char: Char, val code: Int) { + // express keys with a Char value + constructor(char: Char): this(char.toLowerCase(), 0) + // express keys that only have a keyCode like F1 + constructor(code: Int): this(Char.MIN_VALUE, code) + // helper for use in InputListener keyTyped() + constructor(event: InputEvent?, character: Char) + : this ( + character.toLowerCase(), + if (character == Char.MIN_VALUE && event!=null) event.keyCode else 0 + ) + + @ExperimentalStdlibApi + override fun toString(): String { + // debug helper + return when { + char == Char.MIN_VALUE -> Input.Keys.toString(code) + char < ' ' -> "Ctrl-" + Char(char.toInt()+64) + else -> "\"$char\"" + } + } +} + +class KeyPressDispatcher: HashMap Unit)>() { + private var checkpoint: Set = setOf() + + // access by Char + operator fun get(char: Char) = this[KeyCharAndCode(char)] + operator fun set(char: Char, action: () -> Unit) { + this[KeyCharAndCode(char)] = action + } + operator fun contains(char: Char) = this.contains(KeyCharAndCode(char)) + fun remove(char: Char) = this.remove(KeyCharAndCode(char)) + + // access by Int keyCodes + operator fun get(code: Int) = this[KeyCharAndCode(code)] + operator fun set(code: Int, action: () -> Unit) { + this[KeyCharAndCode(code)] = action + } + operator fun contains(code: Int) = this.contains(KeyCharAndCode(code)) + fun remove(code: Int) = this.remove(KeyCharAndCode(code)) + + override fun clear() { + checkpoint = setOf() + super.clear() + } + fun setCheckpoint() { + checkpoint = keys.toSet() + } + fun revertToCheckPoint() { + keys.minus(checkpoint).forEach { remove(it) } + } +}