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

@ -77,10 +77,12 @@ if (project.hasProperty("github.token")) {
tasks.compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=enable")
}
}
tasks.compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=enable")
}
}

View File

@ -1,55 +0,0 @@
package link.infra.packwiz.installer.ui;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class CLIHandler implements IUserInterface {
@Override
public void handleException(Exception e) {
e.printStackTrace();
}
@Override
public void show(InputStateHandler h) {}
@Override
public void submitProgress(InstallProgress progress) {
StringBuilder sb = new StringBuilder();
if (progress.hasProgress) {
sb.append('(');
sb.append(progress.progress);
sb.append('/');
sb.append(progress.progressTotal);
sb.append(") ");
}
sb.append(progress.message);
System.out.println(sb.toString());
}
@Override
public void executeManager(Runnable task) {
task.run();
System.out.println("Finished successfully!");
}
@Override
public Future<Boolean> showOptions(List<IOptionDetails> options) {
for (IOptionDetails opt : options) {
opt.setOptionValue(true);
System.out.println("Warning: accepting option " + opt.getName() + " as option choosing is not implemented in the CLI");
}
CompletableFuture<Boolean> future = new CompletableFuture<>();
future.complete(false); // Can't be cancelled!
return future;
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
return future;
}
}

View File

@ -1,183 +0,0 @@
package link.infra.packwiz.installer.ui;
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
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.List;
import java.util.concurrent.CompletableFuture;
class ExceptionListWindow extends JDialog {
private static final long serialVersionUID = 1L;
private final JTextArea lblExceptionStacktrace;
/**
* Create the dialog.
*/
ExceptionListWindow(List<IExceptionDetails> eList, CompletableFuture<ExceptionListResult> future, int numTotal, boolean allowsIgnore, JFrame parentWindow) {
super(parentWindow, "Failed file downloads", true);
setBounds(100, 100, 540, 340);
setLocationRelativeTo(parentWindow);
getContentPane().setLayout(new BorderLayout());
{
JPanel errorPanel = new JPanel();
getContentPane().add(errorPanel, BorderLayout.NORTH);
{
JLabel lblWarning = new JLabel("One or more errors were encountered while installing the modpack!");
lblWarning.setIcon(UIManager.getIcon("OptionPane.warningIcon"));
errorPanel.add(lblWarning);
}
}
JPanel contentPanel = new JPanel();
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
getContentPane().add(contentPanel, BorderLayout.CENTER);
contentPanel.setLayout(new BorderLayout(0, 0));
{
JSplitPane splitPane = new JSplitPane();
splitPane.setResizeWeight(0.3);
contentPanel.add(splitPane);
{
lblExceptionStacktrace = new JTextArea("Select a file");
lblExceptionStacktrace.setBackground(UIManager.getColor("List.background"));
lblExceptionStacktrace.setOpaque(true);
lblExceptionStacktrace.setWrapStyleWord(true);
lblExceptionStacktrace.setLineWrap(true);
lblExceptionStacktrace.setEditable(false);
lblExceptionStacktrace.setFocusable(true);
lblExceptionStacktrace.setFont(UIManager.getFont("Label.font"));
lblExceptionStacktrace.setBorder(new EmptyBorder(5, 5, 5, 5));
JScrollPane scrollPane = new JScrollPane(lblExceptionStacktrace);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setRightComponent(scrollPane);
}
{
JList<String> list = new JList<>();
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setBorder(new EmptyBorder(5, 5, 5, 5));
ExceptionListModel listModel = new ExceptionListModel(eList);
list.setModel(listModel);
list.addListSelectionListener(e -> {
int i = list.getSelectedIndex();
if (i > -1) {
StringWriter sw = new StringWriter();
listModel.getExceptionAt(i).printStackTrace(new PrintWriter(sw));
lblExceptionStacktrace.setText(sw.toString());
// Scroll to the top
lblExceptionStacktrace.setCaretPosition(0);
} else {
lblExceptionStacktrace.setText("Select a file");
}
});
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setLeftComponent(scrollPane);
}
}
{
JPanel buttonPane = new JPanel();
getContentPane().add(buttonPane, BorderLayout.SOUTH);
buttonPane.setLayout(new BorderLayout(0, 0));
{
JPanel rightButtons = new JPanel();
buttonPane.add(rightButtons, BorderLayout.EAST);
{
JButton btnContinue = new JButton("Continue");
btnContinue.setToolTipText("Attempt to continue installing, excluding the failed downloads");
btnContinue.addActionListener(e -> {
future.complete(ExceptionListResult.CONTINUE);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnContinue);
}
{
JButton btnCancelLaunch = new JButton("Cancel launch");
btnCancelLaunch.setToolTipText("Stop launching the game");
btnCancelLaunch.addActionListener(e -> {
future.complete(ExceptionListResult.CANCEL);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnCancelLaunch);
}
{
JButton btnIgnoreUpdate = new JButton("Ignore update");
btnIgnoreUpdate.setEnabled(allowsIgnore);
btnIgnoreUpdate.setToolTipText("Start the game without attempting to update");
btnIgnoreUpdate.addActionListener(e -> {
future.complete(ExceptionListResult.IGNORE);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnIgnoreUpdate);
{
JLabel lblErrored = new JLabel(eList.size() + "/" + numTotal + " errored");
lblErrored.setHorizontalAlignment(SwingConstants.CENTER);
buttonPane.add(lblErrored, BorderLayout.CENTER);
}
{
JPanel leftButtons = new JPanel();
buttonPane.add(leftButtons, BorderLayout.WEST);
{
JButton btnReportIssue = new JButton("Report issue");
boolean supported = Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
btnReportIssue.setEnabled(supported);
if (supported) {
btnReportIssue.addActionListener(e -> {
try {
Desktop.getDesktop().browse(new URI("https://github.com/comp500/packwiz-installer/issues/new"));
} catch (IOException | URISyntaxException e1) {
// lol the button just won't work i guess
}
});
}
leftButtons.add(btnReportIssue);
}
}
}
}
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
future.complete(ExceptionListResult.CANCEL);
}
@Override
public void windowClosed(WindowEvent e) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(ExceptionListResult.CANCEL);
}
});
}
private static class ExceptionListModel extends AbstractListModel<String> {
private static final long serialVersionUID = 1L;
private final List<IExceptionDetails> details;
ExceptionListModel(List<IExceptionDetails> details) {
this.details = details;
}
public int getSize() {
return details.size();
}
public String getElementAt(int index) {
return details.get(index).getName();
}
Exception getExceptionAt(int index) {
return details.get(index).getException();
}
}
}

View File

@ -1,10 +0,0 @@
package link.infra.packwiz.installer.ui;
public interface IExceptionDetails {
Exception getException();
String getName();
enum ExceptionListResult {
CONTINUE, CANCEL, IGNORE
}
}

View File

@ -1,8 +0,0 @@
package link.infra.packwiz.installer.ui;
public interface IOptionDetails {
String getName();
boolean getOptionValue();
String getOptionDescription();
void setOptionValue(boolean value);
}

View File

@ -1,38 +0,0 @@
package link.infra.packwiz.installer.ui;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public interface IUserInterface {
void show(InputStateHandler handler);
void handleException(Exception e);
default void handleExceptionAndExit(Exception e) {
handleException(e);
System.exit(1);
}
default void setTitle(String title) {}
void submitProgress(InstallProgress progress);
void executeManager(Runnable task);
// Return true if the installation was cancelled!
Future<Boolean> showOptions(List<IOptionDetails> option);
Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore);
default void disableOptionsButton() {}
// Should not return CONTINUE
default Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
return future;
}
}

View File

@ -1,22 +0,0 @@
package link.infra.packwiz.installer.ui;
public class InputStateHandler {
private boolean optionsButtonPressed = false;
private boolean cancelButtonPressed = false;
synchronized void pressCancelButton() {
this.cancelButtonPressed = true;
}
synchronized void pressOptionsButton() {
this.optionsButtonPressed = true;
}
public synchronized boolean getCancelButton() {
return cancelButtonPressed;
}
public synchronized boolean getOptionsButton() {
return optionsButtonPressed;
}
}

View File

@ -1,22 +0,0 @@
package link.infra.packwiz.installer.ui;
public class InstallProgress {
public final String message;
public final boolean hasProgress;
public final int progress;
public final int progressTotal;
public InstallProgress(String message) {
this.message = message;
hasProgress = false;
progress = 0;
progressTotal = 0;
}
public InstallProgress(String message, int progress, int progressTotal) {
this.message = message;
hasProgress = true;
this.progress = progress;
this.progressTotal = progressTotal;
}
}

View File

@ -1,228 +0,0 @@
package link.infra.packwiz.installer.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
public class InstallWindow implements IUserInterface {
private JFrame frmPackwizlauncher;
private JLabel lblProgresslabel;
private JProgressBar progressBar;
private InputStateHandler inputStateHandler;
private String title = "Updating modpack...";
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
private AtomicBoolean aboutToCrash = new AtomicBoolean();
private JButton btnOptions;
@Override
public void show(InputStateHandler handler) {
this.inputStateHandler = handler;
EventQueue.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
initialize();
frmPackwizlauncher.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* Initialize the contents of the frame.
* @wbp.parser.entryPoint
*/
private void initialize() {
frmPackwizlauncher = new JFrame();
frmPackwizlauncher.setTitle(title);
frmPackwizlauncher.setBounds(100, 100, 493, 95);
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frmPackwizlauncher.setLocationRelativeTo(null);
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
panel.setLayout(new BorderLayout(0, 0));
progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
panel.add(progressBar, BorderLayout.CENTER);
lblProgresslabel = new JLabel("Loading...");
panel.add(lblProgresslabel, BorderLayout.SOUTH);
JPanel panel_1 = new JPanel();
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
GridBagLayout gbl_panel_1 = new GridBagLayout();
panel_1.setLayout(gbl_panel_1);
btnOptions = new JButton("Optional mods...");
btnOptions.addActionListener(e -> {
btnOptions.setText("Loading...");
btnOptions.setEnabled(false);
inputStateHandler.pressOptionsButton();
});
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
gbc_btnOptions.gridx = 0;
gbc_btnOptions.gridy = 0;
panel_1.add(btnOptions, gbc_btnOptions);
JButton btnCancel = new JButton("Cancel");
btnCancel.addActionListener(e -> {
btnCancel.setEnabled(false);
inputStateHandler.pressCancelButton();
});
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
gbc_btnCancel.gridx = 0;
gbc_btnCancel.gridy = 1;
panel_1.add(btnCancel, gbc_btnCancel);
}
@Override
public void handleException(Exception e) {
e.printStackTrace();
EventQueue.invokeLater(() -> {
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
});
}
@Override
public void handleExceptionAndExit(Exception e) {
e.printStackTrace();
// 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.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
System.exit(1);
});
// Pause forever, so it blocks while we wait for System.exit to take effect
try {
Thread.currentThread().join();
} catch (InterruptedException ex) {
// no u
}
}
@Override
public void setTitle(String title) {
this.title = title;
if (frmPackwizlauncher != null) {
EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title));
}
}
@Override
public void submitProgress(InstallProgress progress) {
StringBuilder sb = new 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?
System.out.println(sb.toString());
if (worker != null) {
worker.publishPublic(progress);
}
}
@Override
public void executeManager(Runnable task) {
EventQueue.invokeLater(() -> {
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
@Override
protected Void doInBackground() {
task.run();
return null;
}
@Override
protected void process(List<InstallProgress> chunks) {
// Only process last chunk
if (chunks.size() > 0) {
InstallProgress prog = chunks.get(chunks.size() - 1);
if (prog.hasProgress) {
progressBar.setIndeterminate(false);
progressBar.setValue(prog.progress);
progressBar.setMaximum(prog.progressTotal);
} else {
progressBar.setIndeterminate(true);
progressBar.setValue(0);
}
lblProgresslabel.setText(prog.message);
}
}
@Override
protected void done() {
if (aboutToCrash.get()) {
return;
}
// TODO: a better way to do this?
frmPackwizlauncher.dispose();
System.out.println("Finished successfully!");
System.exit(0);
}
};
worker.execute();
});
}
@Override
public Future<Boolean> showOptions(List<IOptionDetails> opts) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
OptionsSelectWindow dialog = new OptionsSelectWindow(opts, future, frmPackwizlauncher);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setVisible(true);
});
return future;
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
ExceptionListWindow dialog = new ExceptionListWindow(opts, future, numTotal, allowsIgnore, frmPackwizlauncher);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setVisible(true);
});
return future;
}
@Override
public void disableOptionsButton() {
if (btnOptions != null) {
btnOptions.setText("Optional mods...");
btnOptions.setEnabled(false);
}
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
Object[] buttons = {"Quit", "Ignore"};
int 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(result == 0 ? IExceptionDetails.ExceptionListResult.CANCEL : IExceptionDetails.ExceptionListResult.IGNORE);
});
return future;
}
}

View File

@ -1,33 +0,0 @@
package link.infra.packwiz.installer.ui;
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
class OptionTempHandler implements IOptionDetails {
private final IOptionDetails opt;
private boolean tempValue;
OptionTempHandler(IOptionDetails opt) {
this.opt = opt;
tempValue = opt.getOptionValue();
}
public String getName() {
return opt.getName();
}
public String getOptionDescription() {
return opt.getOptionDescription();
}
public boolean getOptionValue() {
return tempValue;
}
public void setOptionValue(boolean value) {
tempValue = value;
}
void finalise() {
opt.setOptionValue(tempValue);
}
}

View File

@ -1,205 +0,0 @@
package link.infra.packwiz.installer.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class OptionsSelectWindow extends JDialog implements ActionListener {
private static final long serialVersionUID = 1L;
private final JTextArea lblOptionDescription;
private final OptionTableModel tableModel;
private final CompletableFuture<Boolean> future;
/**
* Create the dialog.
*/
OptionsSelectWindow(List<IOptionDetails> optList, CompletableFuture<Boolean> future, JFrame parentWindow) {
super(parentWindow, "Select optional mods...", true);
tableModel = new OptionTableModel(optList);
this.future = future;
setBounds(100, 100, 450, 300);
setLocationRelativeTo(parentWindow);
getContentPane().setLayout(new BorderLayout());
JPanel contentPanel = new JPanel();
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
getContentPane().add(contentPanel, BorderLayout.CENTER);
contentPanel.setLayout(new BorderLayout(0, 0));
{
JSplitPane splitPane = new JSplitPane();
splitPane.setResizeWeight(0.5);
contentPanel.add(splitPane);
{
JTable table = new JTable();
table.setShowVerticalLines(false);
table.setShowHorizontalLines(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setShowGrid(false);
table.setModel(tableModel);
table.getColumnModel().getColumn(0).setResizable(false);
table.getColumnModel().getColumn(0).setPreferredWidth(15);
table.getColumnModel().getColumn(0).setMaxWidth(15);
table.getColumnModel().getColumn(1).setResizable(false);
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
int i = table.getSelectedRow();
if (i > -1) {
lblOptionDescription.setText(tableModel.getDescription(i));
} else {
lblOptionDescription.setText("Select an option...");
}
}
});
table.setTableHeader(null);
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getViewport().setBackground(UIManager.getColor("List.background"));
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setLeftComponent(scrollPane);
}
{
lblOptionDescription = new JTextArea("Select an option...");
lblOptionDescription.setBackground(UIManager.getColor("List.background"));
lblOptionDescription.setOpaque(true);
lblOptionDescription.setWrapStyleWord(true);
lblOptionDescription.setLineWrap(true);
lblOptionDescription.setEditable(false);
lblOptionDescription.setFocusable(false);
lblOptionDescription.setFont(UIManager.getFont("Label.font"));
lblOptionDescription.setBorder(new EmptyBorder(10, 10, 10, 10));
JScrollPane scrollPane = new JScrollPane(lblOptionDescription);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setRightComponent(scrollPane);
}
}
{
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
getContentPane().add(buttonPane, BorderLayout.SOUTH);
{
JButton okButton = new JButton("OK");
okButton.setActionCommand("OK");
okButton.addActionListener(this);
buttonPane.add(okButton);
getRootPane().setDefaultButton(okButton);
}
{
JButton cancelButton = new JButton("Cancel");
cancelButton.setActionCommand("Cancel");
cancelButton.addActionListener(this);
buttonPane.add(cancelButton);
}
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
future.complete(true);
}
@Override
public void windowClosed(WindowEvent e) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(true);
}
});
}
private static class OptionTableModel implements TableModel {
private List<OptionTempHandler> opts = new ArrayList<>();
OptionTableModel(List<IOptionDetails> givenOpts) {
for (IOptionDetails opt : givenOpts) {
opts.add(new OptionTempHandler(opt));
}
}
@Override
public int getRowCount() {
return opts.size();
}
@Override
public int getColumnCount() {
return 2;
}
private final String[] columnNames = {"Enabled", "Mod name"};
private final Class<?>[] columnTypes = {Boolean.class, String.class};
private final boolean[] columnEditables = {true, false};
@Override
public String getColumnName(int columnIndex) {
return columnNames[columnIndex];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return columnTypes[columnIndex];
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnEditables[columnIndex];
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
OptionTempHandler opt = opts.get(rowIndex);
return columnIndex == 0 ? opt.getOptionValue() : opt.getName();
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
if (columnIndex == 0) {
OptionTempHandler opt = opts.get(rowIndex);
opt.setOptionValue((boolean) aValue);
}
}
// Noop, the table model doesn't change!
@Override
public void addTableModelListener(TableModelListener l) {}
@Override
public void removeTableModelListener(TableModelListener l) {}
String getDescription(int rowIndex) {
return opts.get(rowIndex).getOptionDescription();
}
void finalise() {
for (OptionTempHandler opt : opts) {
opt.finalise();
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("OK")) {
tableModel.finalise();
future.complete(false);
dispose();
} else if (e.getActionCommand().equals("Cancel")) {
future.complete(true);
dispose();
}
}
}

View File

@ -1,13 +0,0 @@
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
public abstract class SwingWorkerButWithPublicPublish<T,V> extends SwingWorker<T,V> {
@SafeVarargs
public final void publishPublic(V... chunks) {
publish(chunks);
}
}

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