Replacement PlatformSaverLoader for Linux X11 systems (#9490)

This commit is contained in:
SomeTroglodyte
2023-05-31 13:43:31 +02:00
committed by GitHub
parent ff9d5ff9c1
commit 9302036311
5 changed files with 371 additions and 5 deletions

View 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
}
}
}

View 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"
}
}

View File

@ -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
) {}
}
}
}

View File

@ -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.
*

View File

@ -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.