Port UI to Kotlin

This commit is contained in:
comp500
2019-12-20 23:20:25 +00:00
parent 0770029dc6
commit bead683b7c
23 changed files with 696 additions and 817 deletions

View File

@@ -0,0 +1,47 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
class CLIHandler : IUserInterface {
override fun handleException(e: Exception) {
e.printStackTrace()
}
override fun show(handler: InputStateHandler) {}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
println(sb.toString())
}
override fun executeManager(task: () -> Unit) {
task()
println("Finished successfully!")
}
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
for (opt in options) {
opt.optionValue = true
// TODO: implement option choice in the CLI?
println("Warning: accepting option " + opt.name + " as option choosing is not implemented in the CLI")
}
return CompletableFuture<Boolean>().apply {
complete(false) // Can't be cancelled!
}
}
override fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
future.complete(ExceptionListResult.CANCEL)
return future
}
}

View File

@@ -0,0 +1,152 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.awt.BorderLayout
import java.awt.Desktop
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URI
import java.net.URISyntaxException
import java.util.concurrent.CompletableFuture
import javax.swing.*
import javax.swing.border.EmptyBorder
internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: CompletableFuture<ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
private val lblExceptionStacktrace: JTextArea
private class ExceptionListModel internal constructor(private val details: List<IExceptionDetails>) : AbstractListModel<String>() {
override fun getSize() = details.size
override fun getElementAt(index: Int) = details[index].name
fun getExceptionAt(index: Int) = details[index].exception
}
/**
* Create the dialog.
*/
init {
setBounds(100, 100, 540, 340)
setLocationRelativeTo(parentWindow)
contentPane.apply {
layout = BorderLayout()
// Error panel
add(JPanel().apply {
add(JLabel("One or more errors were encountered while installing the modpack!").apply {
icon = UIManager.getIcon("OptionPane.warningIcon")
})
}, BorderLayout.NORTH)
// Content panel
add(JPanel().apply {
border = EmptyBorder(5, 5, 5, 5)
layout = BorderLayout(0, 0)
add(JSplitPane().apply {
resizeWeight = 0.3
lblExceptionStacktrace = JTextArea("Select a file")
lblExceptionStacktrace.background = UIManager.getColor("List.background")
lblExceptionStacktrace.isOpaque = true
lblExceptionStacktrace.wrapStyleWord = true
lblExceptionStacktrace.lineWrap = true
lblExceptionStacktrace.isEditable = false
lblExceptionStacktrace.isFocusable = true
lblExceptionStacktrace.font = UIManager.getFont("Label.font")
lblExceptionStacktrace.border = EmptyBorder(5, 5, 5, 5)
rightComponent = JScrollPane(lblExceptionStacktrace)
leftComponent = JScrollPane(JList<String>().apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION
border = EmptyBorder(5, 5, 5, 5)
val listModel = ExceptionListModel(eList)
model = listModel
addListSelectionListener {
val i = selectedIndex
if (i > -1) {
val sw = StringWriter()
listModel.getExceptionAt(i).printStackTrace(PrintWriter(sw))
lblExceptionStacktrace.text = sw.toString()
// Scroll to the top
lblExceptionStacktrace.caretPosition = 0
} else {
lblExceptionStacktrace.text = "Select a file"
}
}
})
})
}, BorderLayout.CENTER)
// Button pane
add(JPanel().apply {
layout = BorderLayout(0, 0)
// Right buttons
add(JPanel().apply {
add(JButton("Continue").apply {
toolTipText = "Attempt to continue installing, excluding the failed downloads"
addActionListener {
future.complete(ExceptionListResult.CONTINUE)
this@ExceptionListWindow.dispose()
}
})
add(JButton("Cancel launch").apply {
toolTipText = "Stop launching the game"
addActionListener {
future.complete(ExceptionListResult.CANCEL)
this@ExceptionListWindow.dispose()
}
})
add(JButton("Ignore update").apply {
toolTipText = "Start the game without attempting to update"
isEnabled = allowsIgnore
addActionListener {
future.complete(ExceptionListResult.IGNORE)
this@ExceptionListWindow.dispose()
}
})
}, BorderLayout.EAST)
// Errored label
add(JLabel(eList.size.toString() + "/" + numTotal + " errored").apply {
horizontalAlignment = SwingConstants.CENTER
}, BorderLayout.CENTER)
// Left buttons
add(JPanel().apply {
add(JButton("Report issue").apply {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
addActionListener {
try {
Desktop.getDesktop().browse(URI("https://github.com/comp500/packwiz-installer/issues/new"))
} catch (e: IOException) {
// lol the button just won't work i guess
} catch (e: URISyntaxException) {}
}
} else {
isEnabled = false
}
})
}, BorderLayout.WEST)
}, BorderLayout.SOUTH)
}
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
future.complete(ExceptionListResult.CANCEL)
}
override fun windowClosed(e: WindowEvent) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(ExceptionListResult.CANCEL)
}
})
}
}

View File

@@ -0,0 +1,10 @@
package link.infra.packwiz.installer.ui
interface IExceptionDetails {
val exception: Exception
val name: String
enum class ExceptionListResult {
CONTINUE, CANCEL, IGNORE
}
}

View File

@@ -0,0 +1,7 @@
package link.infra.packwiz.installer.ui
interface IOptionDetails {
val name: String
var optionValue: Boolean
val optionDescription: String
}

View File

@@ -0,0 +1,34 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import kotlin.system.exitProcess
interface IUserInterface {
fun show(handler: InputStateHandler)
fun handleException(e: Exception)
@JvmDefault
fun handleExceptionAndExit(e: Exception) {
handleException(e)
exitProcess(1)
}
@JvmDefault
fun setTitle(title: String) {}
fun submitProgress(progress: InstallProgress)
fun executeManager(task: () -> Unit)
// Return true if the installation was cancelled!
fun showOptions(options: List<IOptionDetails>): Future<Boolean>
fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult>
@JvmDefault
fun disableOptionsButton() {}
// Should not return CONTINUE
@JvmDefault
fun showCancellationDialog(): Future<ExceptionListResult> {
return CompletableFuture<ExceptionListResult>().apply {
complete(ExceptionListResult.CANCEL)
}
}
}

View File

@@ -0,0 +1,21 @@
package link.infra.packwiz.installer.ui
class InputStateHandler {
// TODO: convert to coroutines/locks?
@get:Synchronized
var optionsButton = false
private set
@get:Synchronized
var cancelButton = false
private set
@Synchronized
fun pressCancelButton() {
cancelButton = true
}
@Synchronized
fun pressOptionsButton() {
optionsButton = true
}
}

View File

@@ -0,0 +1,12 @@
package link.infra.packwiz.installer.ui
data class InstallProgress(
val message: String,
val hasProgress: Boolean = false,
val progress: Int = 0,
val progressTotal: Int = 0
) {
constructor(message: String, progress: Int, progressTotal: Int) : this(message, true, progress, progressTotal)
constructor(message: String) : this(message, false)
}

View File

@@ -0,0 +1,219 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.awt.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.*
import javax.swing.border.EmptyBorder
import kotlin.system.exitProcess
class InstallWindow : IUserInterface {
private val frmPackwizlauncher: JFrame
private val lblProgresslabel: JLabel
private val progressBar: JProgressBar
private val btnOptions: JButton
private var inputStateHandler: InputStateHandler? = null
private var title = "Updating modpack..."
private var worker: SwingWorkerButWithPublicPublish<Unit, InstallProgress>? = null
private val aboutToCrash = AtomicBoolean()
// TODO: separate JFrame junk from IUserInterface junk?
init {
frmPackwizlauncher = JFrame().apply {
title = this@InstallWindow.title
setBounds(100, 100, 493, 95)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
setLocationRelativeTo(null)
// Progress bar and loading text
add(JPanel().apply {
border = EmptyBorder(10, 10, 10, 10)
layout = BorderLayout(0, 0)
progressBar = JProgressBar().apply {
isIndeterminate = true
}
add(progressBar, BorderLayout.CENTER)
lblProgresslabel = JLabel("Loading...")
add(lblProgresslabel, BorderLayout.SOUTH)
}, BorderLayout.CENTER)
// Buttons
add(JPanel().apply {
border = EmptyBorder(0, 5, 0, 5)
layout = GridBagLayout()
btnOptions = JButton("Optional mods...").apply {
alignmentX = Component.CENTER_ALIGNMENT
addActionListener {
text = "Loading..."
isEnabled = false
inputStateHandler?.pressOptionsButton()
}
}
add(btnOptions, GridBagConstraints().apply {
gridx = 0
gridy = 0
})
add(JButton("Cancel").apply {
addActionListener {
isEnabled = false
inputStateHandler?.pressCancelButton()
}
}, GridBagConstraints().apply {
gridx = 0
gridy = 1
})
}, BorderLayout.EAST)
}
}
override fun show(handler: InputStateHandler) {
inputStateHandler = handler
EventQueue.invokeLater {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
frmPackwizlauncher.isVisible = true
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun handleException(e: Exception) {
e.printStackTrace()
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"An error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
title, JOptionPane.ERROR_MESSAGE)
}
}
override fun handleExceptionAndExit(e: Exception) {
e.printStackTrace()
// TODO: Fix this mess
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
aboutToCrash.set(true)
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
title, JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
// Pause forever, so it blocks while we wait for System.exit to take effect
try {
Thread.currentThread().join()
} catch (ex: InterruptedException) { // no u
}
}
override fun setTitle(title: String) {
this.title = title
frmPackwizlauncher.let { frame ->
EventQueue.invokeLater { frame.title = title }
}
}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
// TODO: better logging library?
println(sb.toString())
worker?.publishPublic(progress)
}
override fun executeManager(task: Function0<Unit>) {
EventQueue.invokeLater {
// TODO: rewrite this stupidity to use channels??!!!
worker = object : SwingWorkerButWithPublicPublish<Unit, InstallProgress>() {
override fun doInBackground() {
task.invoke()
}
override fun process(chunks: List<InstallProgress>) {
// Only process last chunk
if (chunks.isNotEmpty()) {
val (message, hasProgress, progress, progressTotal) = chunks[chunks.size - 1]
if (hasProgress) {
progressBar.isIndeterminate = false
progressBar.value = progress
progressBar.maximum = progressTotal
} else {
progressBar.isIndeterminate = true
progressBar.value = 0
}
lblProgresslabel.text = message
}
}
override fun done() {
if (aboutToCrash.get()) {
return
}
// TODO: a better way to do this?
frmPackwizlauncher.dispose()
println("Finished successfully!")
exitProcess(0)
}
}.also {
it.execute()
}
}
}
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
val future = CompletableFuture<Boolean>()
EventQueue.invokeLater {
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
return future
}
override fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
EventQueue.invokeLater {
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
return future
}
override fun disableOptionsButton() {
btnOptions.apply {
text = "Optional mods..."
isEnabled = false
}
}
override fun showCancellationDialog(): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
EventQueue.invokeLater {
val buttons = arrayOf("Quit", "Ignore")
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
future.complete(if (result == 0) ExceptionListResult.CANCEL else ExceptionListResult.IGNORE)
}
return future
}
}

View File

@@ -0,0 +1,13 @@
package link.infra.packwiz.installer.ui
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {
override var optionValue = opt.optionValue
override val name get() = opt.name
override val optionDescription get() = opt.optionDescription
fun finalise() {
opt.optionValue = optionValue
}
}

View File

@@ -0,0 +1,166 @@
package link.infra.packwiz.installer.ui
import java.awt.BorderLayout
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.util.*
import java.util.concurrent.CompletableFuture
import javax.swing.*
import javax.swing.border.EmptyBorder
import javax.swing.event.TableModelListener
import javax.swing.table.TableModel
class OptionsSelectWindow internal constructor(optList: List<IOptionDetails>, future: CompletableFuture<Boolean>, parentWindow: JFrame?) : JDialog(parentWindow, "Select optional mods...", true), ActionListener {
private val lblOptionDescription: JTextArea
private val tableModel: OptionTableModel
private val future: CompletableFuture<Boolean>
private class OptionTableModel internal constructor(givenOpts: List<IOptionDetails>) : TableModel {
private val opts: List<OptionTempHandler>
init {
val mutOpts = ArrayList<OptionTempHandler>()
for (opt in givenOpts) {
mutOpts.add(OptionTempHandler(opt))
}
opts = mutOpts
}
override fun getRowCount() = opts.size
override fun getColumnCount() = 2
private val columnNames = arrayOf("Enabled", "Mod name")
private val columnTypes = arrayOf(Boolean::class.javaObjectType, String::class.java)
private val columnEditables = booleanArrayOf(true, false)
override fun getColumnName(columnIndex: Int) = columnNames[columnIndex]
override fun getColumnClass(columnIndex: Int) = columnTypes[columnIndex]
override fun isCellEditable(rowIndex: Int, columnIndex: Int) = columnEditables[columnIndex]
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
val opt = opts[rowIndex]
return if (columnIndex == 0) opt.optionValue else opt.name
}
override fun setValueAt(aValue: Any, rowIndex: Int, columnIndex: Int) {
if (columnIndex == 0) {
val opt = opts[rowIndex]
opt.optionValue = aValue as Boolean
}
}
// Noop, the table model doesn't change!
override fun addTableModelListener(l: TableModelListener) {}
override fun removeTableModelListener(l: TableModelListener) {}
fun getDescription(rowIndex: Int) = opts[rowIndex].optionDescription
fun finalise() {
for (opt in opts) {
opt.finalise()
}
}
}
override fun actionPerformed(e: ActionEvent) {
if (e.actionCommand == "OK") {
tableModel.finalise()
future.complete(false)
dispose()
} else if (e.actionCommand == "Cancel") {
future.complete(true)
dispose()
}
}
/**
* Create the dialog.
*/
init {
tableModel = OptionTableModel(optList)
this.future = future
setBounds(100, 100, 450, 300)
setLocationRelativeTo(parentWindow)
contentPane.apply {
layout = BorderLayout()
add(JPanel().apply {
border = EmptyBorder(5, 5, 5, 5)
layout = BorderLayout(0, 0)
add(JSplitPane().apply {
resizeWeight = 0.5
lblOptionDescription = JTextArea("Select an option...").apply {
background = UIManager.getColor("List.background")
isOpaque = true
wrapStyleWord = true
lineWrap = true
isEditable = false
isFocusable = false
font = UIManager.getFont("Label.font")
border = EmptyBorder(10, 10, 10, 10)
}
leftComponent = JScrollPane(JTable().apply {
showVerticalLines = false
showHorizontalLines = false
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
setShowGrid(false)
model = tableModel
columnModel.getColumn(0).resizable = false
columnModel.getColumn(0).preferredWidth = 15
columnModel.getColumn(0).maxWidth = 15
columnModel.getColumn(1).resizable = false
selectionModel.addListSelectionListener {
val i = selectedRow
if (i > -1) {
lblOptionDescription.text = tableModel.getDescription(i)
} else {
lblOptionDescription.text = "Select an option..."
}
}
tableHeader = null
}).apply {
viewport.background = UIManager.getColor("List.background")
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
}
rightComponent = JScrollPane(lblOptionDescription)
})
add(JPanel().apply {
layout = FlowLayout(FlowLayout.RIGHT)
add(JButton("OK").apply {
actionCommand = "OK"
addActionListener(this@OptionsSelectWindow)
this@OptionsSelectWindow.rootPane.defaultButton = this
})
add(JButton("Cancel").apply {
actionCommand = "Cancel"
addActionListener(this@OptionsSelectWindow)
})
}, BorderLayout.SOUTH)
}, BorderLayout.CENTER)
}
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
future.complete(true)
}
override fun windowClosed(e: WindowEvent) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(true)
}
})
}
}

View File

@@ -0,0 +1,13 @@
package link.infra.packwiz.installer.ui
import javax.swing.SwingWorker
// Q: AAA WHAT HAVE YOU DONE THIS IS DISGUSTING
// A: it just makes things easier, so i can easily have one interface for CLI/GUI
// if someone has a better way to do this please PR it
abstract class SwingWorkerButWithPublicPublish<T, V> : SwingWorker<T, V>() {
@SafeVarargs
fun publishPublic(vararg chunks: V) {
publish(*chunks)
}
}