mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 01:08:25 +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
|
onLoaded: (data: String, location: String) -> Unit, // On-load-complete callback
|
||||||
onError: (Exception) -> Unit = {} // On-load-error 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
|
* 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.
|
/** 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] */
|
/** 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)
|
addCloseButton(text, KeyboardBindings[additionalKey], action = action)
|
||||||
}
|
|
||||||
/** Overload of [addOKButton] accepting a bindable key definition as [additionalKey] */
|
/** 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)
|
addOKButton(text, KeyboardBindings[additionalKey], style, action = action)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last two additions ***must*** be buttons.
|
* The last two additions ***must*** be buttons.
|
||||||
|
Reference in New Issue
Block a user