mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 09:18:43 +07:00
Replacement PlatformSaverLoader for Linux X11 systems (#9490)
This commit is contained in:
294
core/src/com/unciv/logic/files/FileChooser.kt
Normal file
294
core/src/com/unciv/logic/files/FileChooser.kt
Normal file
@ -0,0 +1,294 @@
|
||||
package com.unciv.logic.files
|
||||
|
||||
import com.badlogic.gdx.Files
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.graphics.g2d.Batch
|
||||
import com.badlogic.gdx.graphics.g2d.BitmapFont
|
||||
import com.badlogic.gdx.graphics.g2d.GlyphLayout
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.List
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Skin
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.badlogic.gdx.utils.Array as GdxArray
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.AutoScrollPane
|
||||
import com.unciv.ui.components.KeyboardBinding
|
||||
import com.unciv.ui.components.UncivTextField
|
||||
import com.unciv.ui.components.extensions.addSeparator
|
||||
import com.unciv.ui.components.extensions.isEnabled
|
||||
import com.unciv.ui.components.extensions.onChange
|
||||
import com.unciv.ui.components.extensions.onClick
|
||||
import com.unciv.ui.components.extensions.onDoubleClick
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.popups.Popup
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
|
||||
typealias ResultListener = (success: Boolean, file: FileHandle) -> Unit
|
||||
|
||||
/**
|
||||
* A file picker written in Gdx as using java.awt.JFileChooser or java.awt.FileDialog crashes on X11 desktops
|
||||
*
|
||||
* Based loosely on [olli-pekka's original][https://jvm-gaming.org/t/libgdx-scene2d-ui-filechooser-dialog/53228]
|
||||
*/
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate") // This has optional API features
|
||||
open class FileChooser(
|
||||
stageToShowOn: Stage,
|
||||
title: String?,
|
||||
startFile: FileHandle? = null,
|
||||
private val resultListener: ResultListener? = null
|
||||
) : Popup(stageToShowOn, false) {
|
||||
// config
|
||||
var filter = FileFilter { true }
|
||||
set(value) { field = value; resetList() }
|
||||
var directoryBrowsingEnabled = true
|
||||
set(value) { field = value; resetList() }
|
||||
var allowFolderSelect = false
|
||||
set(value) { field = value; resetList() }
|
||||
var showAbsolutePath = false
|
||||
set(value) { field = value; resetList() }
|
||||
var fileNameEnabled
|
||||
get() = fileNameCell.hasActor()
|
||||
set(value) {
|
||||
if (value) fileNameCell.setActor(fileNameWrapper)
|
||||
else fileNameCell.clearActor()
|
||||
}
|
||||
|
||||
// components
|
||||
private val fileNameInput = UncivTextField.create("Please enter a file name")
|
||||
private val fileNameLabel = "File name:".toLabel()
|
||||
private val fileNameWrapper = Table().apply {
|
||||
defaults().space(10f)
|
||||
add(fileNameLabel).growX().row()
|
||||
add(fileNameInput).growX().row()
|
||||
addSeparator(height = 1f)
|
||||
}
|
||||
private val fileNameCell: Cell<Actor?>
|
||||
private val dirTypeLabel = "".toLabel(Color.GRAY) // color forces a style clone
|
||||
private val pathLabel = "".toLabel(Color.GRAY, alignment = Align.left)
|
||||
private val pathLabelWrapper = Table().apply {
|
||||
touchable = Touchable.enabled
|
||||
defaults().space(10f)
|
||||
add(dirTypeLabel).pad(2f)
|
||||
add(pathLabel).left().growX()
|
||||
}
|
||||
private val fileList = FileList(skin)
|
||||
private val fileScroll = AutoScrollPane(fileList)
|
||||
private val okButton: TextButton
|
||||
|
||||
// operational
|
||||
private val maxHeight = stageToShowOn.height * 0.6f
|
||||
private val absoluteLocalPath = Gdx.files.local("").file().absoluteFile.canonicalPath
|
||||
private val absoluteExternalPath = if (Gdx.files.isExternalStorageAvailable)
|
||||
Gdx.files.external("").file().absoluteFile.canonicalPath
|
||||
else "/\\/\\/" // impossible placeholder
|
||||
private var currentDir: FileHandle? = null
|
||||
private var result: String? = null
|
||||
private val filterWithFolders = FileFilter {
|
||||
directoryBrowsingEnabled && it.isDirectory || filter.accept(it)
|
||||
}
|
||||
|
||||
private class FileListItem(val label: String, val file: FileHandle, val isFolder: Boolean) {
|
||||
constructor(file: FileHandle) : this(
|
||||
label = (if (file.isDirectory) " " else "") + file.name(),
|
||||
file,
|
||||
isFolder = file.isDirectory // cache, it's not trivially cheap
|
||||
)
|
||||
override fun toString() = label
|
||||
}
|
||||
|
||||
private class FileList(skin: Skin) : List<FileListItem>(skin) {
|
||||
val saveColor = Color()
|
||||
val folderColor = Color(1f, 0.86f, 0.5f, 1f) // #ffdb80, same Hue as Goldenrod but S=50 V=100
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
override fun drawItem(batch: Batch, font: BitmapFont, index: Int, item: FileListItem,
|
||||
x: Float, y: Float, width: Float): GlyphLayout {
|
||||
saveColor.set(font.color)
|
||||
font.setColor(if (item.isFolder) folderColor else Color.WHITE)
|
||||
val layout = super.drawItem(batch, font, index, item, x, y, width)
|
||||
font.setColor(saveColor)
|
||||
return layout
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
innerTable.top().left()
|
||||
|
||||
fileList.selection.setProgrammaticChangeEvents(false)
|
||||
fileNameInput.setTextFieldListener { textField, _ -> result = textField.text }
|
||||
|
||||
if (title != null) {
|
||||
addGoodSizedLabel(title).colspan(2).center().row()
|
||||
innerTable.addSeparator(height = 1f)
|
||||
}
|
||||
add(pathLabelWrapper).colspan(2).fillX().row()
|
||||
innerTable.addSeparator(Color.GRAY, height = 1f)
|
||||
add(fileScroll).colspan(2).fill().row()
|
||||
innerTable.addSeparator(height = 1f)
|
||||
fileNameCell = innerTable.add().colspan(2).growX()
|
||||
innerTable.row()
|
||||
|
||||
addCloseButton("Cancel", KeyboardBinding.Cancel) {
|
||||
reportResult(false)
|
||||
}
|
||||
okButton = addOKButton(Constants.OK, KeyboardBinding.Confirm) {
|
||||
reportResult(true)
|
||||
}.actor
|
||||
equalizeLastTwoButtonWidths()
|
||||
|
||||
fileList.onChange {
|
||||
val selected = fileList.selected ?: return@onChange
|
||||
if (!selected.file.isDirectory) {
|
||||
result = selected.file.name()
|
||||
fileNameInput.text = result
|
||||
}
|
||||
enableOKButton()
|
||||
}
|
||||
fileList.onDoubleClick(UncivSound.Silent) {
|
||||
val selected = fileList.selected ?: return@onDoubleClick
|
||||
if (selected.file.isDirectory)
|
||||
changeDirectory(selected.file)
|
||||
else {
|
||||
reportResult(true)
|
||||
close()
|
||||
}
|
||||
}
|
||||
pathLabelWrapper.onClick(UncivSound.Swap) { switchDomain() }
|
||||
|
||||
showListeners.add {
|
||||
if (currentDir == null) initialDirectory(startFile)
|
||||
stageToShowOn.scrollFocus = fileScroll
|
||||
stageToShowOn.keyboardFocus = fileNameInput
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMaxHeight() = maxHeight
|
||||
|
||||
private fun reportResult(success: Boolean) {
|
||||
resultListener?.invoke(success, getResult())
|
||||
}
|
||||
|
||||
private fun makeAbsolute(file: FileHandle): FileHandle {
|
||||
if (file.type() == Files.FileType.Absolute) return file
|
||||
return Gdx.files.absolute(file.file().absoluteFile.canonicalPath)
|
||||
}
|
||||
|
||||
private fun makeRelative(file: FileHandle): FileHandle {
|
||||
if (file.type() != Files.FileType.Absolute) return file
|
||||
val path = file.path()
|
||||
if (path.startsWith(absoluteLocalPath))
|
||||
return Gdx.files.local(path.removePrefix(absoluteLocalPath).removePrefix(File.separator))
|
||||
if (path.startsWith(absoluteExternalPath))
|
||||
return Gdx.files.external(path.removePrefix(absoluteExternalPath).removePrefix(File.separator))
|
||||
return file
|
||||
}
|
||||
|
||||
private fun initialDirectory(startFile: FileHandle?) {
|
||||
changeDirectory(makeAbsolute(when {
|
||||
startFile == null && Gdx.files.isExternalStorageAvailable ->
|
||||
Gdx.files.absolute(absoluteExternalPath)
|
||||
startFile == null ->
|
||||
Gdx.files.absolute(absoluteLocalPath)
|
||||
startFile.isDirectory -> startFile
|
||||
else -> startFile.parent()
|
||||
}))
|
||||
}
|
||||
|
||||
private fun switchDomain() {
|
||||
val current = currentDir?.path() ?: return
|
||||
changeDirectory(Gdx.files.absolute(when {
|
||||
!Gdx.files.isExternalStorageAvailable -> absoluteLocalPath
|
||||
current.startsWith(absoluteExternalPath) && !current.startsWith(absoluteLocalPath)
|
||||
-> absoluteLocalPath
|
||||
else -> absoluteExternalPath
|
||||
}))
|
||||
}
|
||||
|
||||
private fun changeDirectory(directory: FileHandle) {
|
||||
currentDir = directory
|
||||
|
||||
val relativeFile = if (showAbsolutePath) directory else makeRelative(directory)
|
||||
val (label, color) = when (relativeFile.type()) {
|
||||
Files.FileType.External -> "Ⓔ" to Color.CHARTREUSE
|
||||
Files.FileType.Local -> "Ⓛ" to Color.TAN
|
||||
else -> "" to Color.WHITE
|
||||
}
|
||||
dirTypeLabel.setText(label)
|
||||
dirTypeLabel.color.set(color)
|
||||
pathLabel.setText(relativeFile.path())
|
||||
|
||||
val list = directory.list(filterWithFolders)
|
||||
val items = GdxArray<FileListItem>(list.size)
|
||||
for (handle in list) {
|
||||
if (!directoryBrowsingEnabled && handle.isDirectory) continue
|
||||
if (handle.file().isHidden) continue
|
||||
items.add(FileListItem(handle))
|
||||
}
|
||||
items.sort(dirListComparator)
|
||||
if (directoryBrowsingEnabled && directory.file().parentFile != null) {
|
||||
items.insert(0, FileListItem(" ..", directory.parent(), true))
|
||||
}
|
||||
fileList.selected = null
|
||||
fileList.setItems(items)
|
||||
enableOKButton()
|
||||
}
|
||||
|
||||
private fun getResult() = makeRelative(
|
||||
if (result.isNullOrEmpty()) currentDir!! else currentDir!!.child(result)
|
||||
)
|
||||
|
||||
private fun resetList() {
|
||||
if (!hasParent()) return
|
||||
changeDirectory(currentDir!!)
|
||||
}
|
||||
|
||||
private fun enableOKButton() {
|
||||
fun getEnable(): Boolean {
|
||||
val file = fileList.selected?.file ?: return false
|
||||
if (!file.exists()) return false
|
||||
return (allowFolderSelect || !file.isDirectory)
|
||||
}
|
||||
okButton.isEnabled = getEnable()
|
||||
}
|
||||
|
||||
fun setOkButtonText(text: String) {
|
||||
okButton.setText(text.tr())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dirListComparator: Comparator<FileListItem> =
|
||||
Comparator { file1, file2 ->
|
||||
when {
|
||||
file1.file.isDirectory && !file2.file.isDirectory -> -1
|
||||
!file1.file.isDirectory && file2.file.isDirectory -> 1
|
||||
else -> file1.file.name().compareTo(file2.file.name())
|
||||
}
|
||||
}
|
||||
|
||||
fun createSaveDialog(stage: Stage, title: String?, path: FileHandle? = null, resultListener: ResultListener? = null) =
|
||||
FileChooser(stage, title, path, resultListener).apply {
|
||||
fileNameEnabled = true
|
||||
setOkButtonText("Save")
|
||||
}
|
||||
|
||||
fun createLoadDialog(stage: Stage, title: String?, path: FileHandle? = null, resultListener: ResultListener? = null) =
|
||||
FileChooser(stage, title, path, resultListener).apply {
|
||||
fileNameEnabled = false
|
||||
setOkButtonText("Load")
|
||||
}
|
||||
|
||||
fun createExtensionFilter(vararg extensions: String) = FileFilter {
|
||||
it.extension.lowercase() in extensions
|
||||
}
|
||||
}
|
||||
}
|
53
core/src/com/unciv/logic/files/LinuxX11SaverLoader.kt
Normal file
53
core/src/com/unciv/logic/files/LinuxX11SaverLoader.kt
Normal file
@ -0,0 +1,53 @@
|
||||
package com.unciv.logic.files
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.utils.Concurrency
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
class LinuxX11SaverLoader : PlatformSaverLoader {
|
||||
override fun saveGame(
|
||||
data: String,
|
||||
suggestedLocation: String,
|
||||
onSaved: (location: String) -> Unit,
|
||||
onError: (ex: Exception) -> Unit
|
||||
) {
|
||||
Concurrency.runOnGLThread {
|
||||
FileChooser.createSaveDialog(stage, "Save game", Gdx.files.absolute(suggestedLocation)) {
|
||||
success, file ->
|
||||
if (!success) return@createSaveDialog
|
||||
try {
|
||||
file.writeString(data, false, "UTF-8")
|
||||
onSaved(file.path())
|
||||
} catch (ex: Exception) {
|
||||
onError(ex)
|
||||
}
|
||||
}.open(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadGame(
|
||||
onLoaded: (data: String, location: String) -> Unit,
|
||||
onError: (Exception) -> Unit
|
||||
) {
|
||||
Concurrency.runOnGLThread {
|
||||
FileChooser.createLoadDialog(stage, "Load game") { success, file ->
|
||||
if (!success) return@createLoadDialog
|
||||
try {
|
||||
val data = file.readString("UTF-8")
|
||||
onLoaded(data, file.path())
|
||||
} catch (ex: Exception) {
|
||||
onError(ex)
|
||||
}
|
||||
}.open(true)
|
||||
}
|
||||
}
|
||||
|
||||
val stage get() = UncivGame.Current.screen!!.stage
|
||||
|
||||
companion object {
|
||||
fun isRequired() = System.getProperty("os.name", "") == "Linux" &&
|
||||
// System.getenv("XDG_SESSION_TYPE") == "x11" - below seems safer
|
||||
GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.javaClass.simpleName == "X11GraphicsDevice"
|
||||
}
|
||||
}
|
@ -20,4 +20,20 @@ interface PlatformSaverLoader {
|
||||
onLoaded: (data: String, location: String) -> Unit, // On-load-complete callback
|
||||
onError: (Exception) -> Unit = {} // On-load-error callback
|
||||
)
|
||||
|
||||
companion object {
|
||||
val None = object : PlatformSaverLoader {
|
||||
override fun saveGame(
|
||||
data: String,
|
||||
suggestedLocation: String,
|
||||
onSaved: (location: String) -> Unit,
|
||||
onError: (ex: Exception) -> Unit
|
||||
) {}
|
||||
|
||||
override fun loadGame(
|
||||
onLoaded: (data: String, location: String) -> Unit,
|
||||
onError: (Exception) -> Unit
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -320,7 +320,12 @@ class UncivFiles(
|
||||
/**
|
||||
* Platform dependent saver-loader to custom system locations
|
||||
*/
|
||||
lateinit var saverLoader: PlatformSaverLoader
|
||||
var saverLoader: PlatformSaverLoader = PlatformSaverLoader.None
|
||||
get() {
|
||||
if (field.javaClass.simpleName == "DesktopSaverLoader" && LinuxX11SaverLoader.isRequired())
|
||||
field = LinuxX11SaverLoader()
|
||||
return field
|
||||
}
|
||||
|
||||
/** Specialized function to access settings before Gdx is initialized.
|
||||
*
|
||||
|
@ -239,13 +239,11 @@ open class Popup(
|
||||
}
|
||||
|
||||
/** Overload of [addCloseButton] accepting a bindable key definition as [additionalKey] */
|
||||
fun addCloseButton(text: String, additionalKey: KeyboardBinding, action: () -> Unit) {
|
||||
fun addCloseButton(text: String, additionalKey: KeyboardBinding, action: () -> Unit) =
|
||||
addCloseButton(text, KeyboardBindings[additionalKey], action = action)
|
||||
}
|
||||
/** Overload of [addOKButton] accepting a bindable key definition as [additionalKey] */
|
||||
fun addOKButton(text: String, additionalKey: KeyboardBinding, style: TextButtonStyle? = null, action: () -> Unit) {
|
||||
fun addOKButton(text: String, additionalKey: KeyboardBinding, style: TextButtonStyle? = null, action: () -> Unit) =
|
||||
addOKButton(text, KeyboardBindings[additionalKey], style, action = action)
|
||||
}
|
||||
|
||||
/**
|
||||
* The last two additions ***must*** be buttons.
|
||||
|
Reference in New Issue
Block a user