Enhance list entries

This commit is contained in:
Johannes Frohnmeyer 2022-09-30 18:07:18 +02:00
parent 62dd7da114
commit b87889f5b4
Signed by: Johannes
GPG Key ID: E76429612C2929F4
13 changed files with 353 additions and 63 deletions

View File

@ -19,7 +19,7 @@ public class LaunchCommand extends BaseInstanceCommand {
public LaunchCommand() {
super("Launches an instance of the game (client by default). Non-blocking (batch commands will continue if this is ran)",
"[game arguments...]",
List.of("run", "launch", "start"),
List.of("run", "instance.launch", "start"),
List.of(
new LaunchCommand("Explicitly launch a client", "client", false, false),
new LaunchCommand("Launch a server", "server", true, false,

View File

@ -44,21 +44,24 @@ public enum GtkEnvBackend implements LauncherEnv.EnvBackend { //TODO test
@Override
public void getInput(String prompt, String details, String defaultValue, Consumer<String> ok, Runnable cancel) {
//TODO spacing
Dialog dialog = new Dialog();
if (dialogParent != null) dialog.setTransientFor(dialogParent);
dialog.setModal(GTK.TRUE);
if (dialogParent != null) dialog.setDestroyWithParent(GTK.TRUE);
dialog.setTitle(new Str(prompt));
Box box = dialog.getContentArea();
box.append(new Label(new Str(details)));
Entry entry = new Entry();
Editable entryEditable = new Editable(entry.cast());
entryEditable.setText(new Str(defaultValue));
box.append(entry);
dialog.addButton(I18n.str("ok"), ResponseType.OK);
dialog.addButton(I18n.str("cancel"), ResponseType.CANCEL);
dialog.onResponse(processResponses(dialog, () -> ok.accept(entryEditable.getText().toString()), cancel));
dialog.show();
//TODO run on main thread
GtkMain.schedule(() -> {
Dialog dialog = new Dialog();
if (dialogParent != null) dialog.setTransientFor(dialogParent);
dialog.setModal(GTK.TRUE);
if (dialogParent != null) dialog.setDestroyWithParent(GTK.TRUE);
dialog.setTitle(new Str(prompt));
Box box = dialog.getContentArea();
box.append(new Label(new Str(details)));
Entry entry = new Entry();
Editable entryEditable = new Editable(entry.cast());
entryEditable.setText(new Str(defaultValue));
box.append(entry);
dialog.addButton(I18n.str("ok"), ResponseType.OK);
dialog.addButton(I18n.str("cancel"), ResponseType.CANCEL);
dialog.onResponse(processResponses(dialog, () -> ok.accept(entryEditable.getText().toString()), cancel));
dialog.show();
});
}
@Override
@ -67,11 +70,13 @@ public enum GtkEnvBackend implements LauncherEnv.EnvBackend { //TODO test
}
private void simpleDialog(String markup, String title, int type, int buttons, Runnable ok, Runnable cancel) {
MessageDialog dialog = new MessageDialog(dialogParent, DialogFlags.MODAL | DialogFlags.DESTROY_WITH_PARENT, type, buttons, null);
dialog.setTitle(new Str(title));
dialog.setMarkup(new Str(markup));
dialog.onResponse(processResponses(dialog, ok, cancel));
dialog.show();
GtkMain.schedule(() -> {
MessageDialog dialog = new MessageDialog(dialogParent, DialogFlags.MODAL | DialogFlags.DESTROY_WITH_PARENT, type, buttons, null);
dialog.setTitle(new Str(title));
dialog.setMarkup(new Str(markup));
dialog.onResponse(processResponses(dialog, ok, cancel));
dialog.show();
});
}
private Dialog.OnResponse processResponses(Dialog dialog, @Nullable Runnable ok, @Nullable Runnable cancel) {

View File

@ -2,6 +2,7 @@ package io.gitlab.jfronny.inceptum.gtk;
import ch.bailu.gtk.GTK;
import ch.bailu.gtk.gio.ApplicationFlags;
import ch.bailu.gtk.glib.Glib;
import ch.bailu.gtk.gtk.Application;
import ch.bailu.gtk.type.Str;
import ch.bailu.gtk.type.Strs;
@ -10,9 +11,11 @@ import io.gitlab.jfronny.inceptum.gtk.window.MainWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import java.io.IOException;
import java.util.*;
public class GtkMain {
public static final Str ID = new Str("io.gitlab.jfronny.inceptum");
private static final Queue<Runnable> SCHEDULED = new ArrayDeque<>();
public static void main(String[] args) throws IOException {
LauncherEnv.initialize(GtkEnvBackend.INSTANCE);
@ -28,12 +31,27 @@ public class GtkMain {
}
}
public static void schedule(Runnable task) {
SCHEDULED.add(Objects.requireNonNull(task));
}
public static int showGui(String[] args) throws IOException {
var app = new Application(ID, ApplicationFlags.FLAGS_NONE);
app.onActivate(() -> {
GtkMenubar.create(app);
var window = new MainWindow(app);
window.show();
Glib.idleAdd(user_data -> {
Runnable r;
while ((r = SCHEDULED.poll()) != null) {
try {
r.run();
} catch (Throwable t) {
Utils.LOGGER.error("Could not run scheduled task", t);
}
}
return GTK.TRUE;
}, null);
GtkEnvBackend.INSTANCE.dialogParent = window;
window.onCloseRequest(() -> {
GtkEnvBackend.INSTANCE.dialogParent = null;

View File

@ -46,32 +46,35 @@ public class GtkMenubar {
launchMenu.clear();
try {
InstanceList.forEach(entry -> {
launchMenu.literalButton(entry.id(), entry.toString(), () -> {
Runnable launch = () -> {
try {
Steps.reDownload(entry.path(), Steps.createProcessState());
} catch (IOException e) {
Utils.LOGGER.error("Could not redownload instance, trying to start anyways", e);
}
InstanceLauncher.launchClient(entry.path(), entry.meta());
};
if (InstanceLock.isSetupLocked(entry.path())) {
LauncherEnv.showError(I18n.get("launch.locked.setup"), I18n.get("launch.locked"));
return;
}
if (InstanceLock.isRunningLocked(entry.path())) {
LauncherEnv.showOkCancel(I18n.get("launch.locked.running"), I18n.get("launch.locked"), () -> {
new Thread(launch).start(); //TODO loom
}, R::nop);
}
new Thread(launch).start();
});
launchMenu.literalButton(entry.id(), entry.toString(), () -> launch(entry));
});
} catch (IOException e) {
Utils.LOGGER.error("Could not generate launch menu", e);
}
}
public static void launch(InstanceList.Entry instance) {
//TODO show popup during launch w/ cancel option (lock main UI)
Runnable launch = () -> {
try {
Steps.reDownload(instance.path(), Steps.createProcessState());
} catch (IOException e) {
Utils.LOGGER.error("Could not redownload instance, trying to start anyways", e);
}
InstanceLauncher.launchClient(instance.path(), instance.meta());
};
if (InstanceLock.isSetupLocked(instance.path())) {
LauncherEnv.showError(I18n.get("instance.launch.locked.setup"), I18n.get("instance.launch.locked"));
return;
}
if (InstanceLock.isRunningLocked(instance.path())) {
LauncherEnv.showOkCancel(I18n.get("instance.launch.locked.running"), I18n.get("instance.launch.locked"), () -> {
new Thread(launch).start(); //TODO loom
}, R::nop);
}
new Thread(launch).start();
}
public static void generateAccountsMenu() {
Objects.requireNonNull(accountsMenu);
accountsMenu.clear();

View File

@ -5,6 +5,7 @@ import ch.bailu.gtk.gio.ApplicationFlags;
import ch.bailu.gtk.gtk.*;
import ch.bailu.gtk.type.Str;
import ch.bailu.gtk.type.Strs;
import io.gitlab.jfronny.inceptum.common.R;
public class TestStart {
public static void main(String[] args) {
@ -30,6 +31,7 @@ public class TestStart {
private static void test() {
GtkEnvBackend backend = GtkEnvBackend.INSTANCE;
backend.getInput("Ae", "IoU\naee", "Def", s -> {}, R::nop);
// backend.showInfo("Some message", "Title");
// backend.showError("Yes!", "AAee");
// backend.showError("Nes!", new ArrayIndexOutOfBoundsException("Top 500 cheese"));

View File

@ -0,0 +1,54 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import ch.bailu.gtk.GTK;
import ch.bailu.gtk.bridge.ListIndex;
import ch.bailu.gtk.gtk.*;
import ch.bailu.gtk.type.Str;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.launcher.util.InstanceList;
import java.util.List;
public class InstanceGridEntryFactory extends SignalListItemFactory {
public InstanceGridEntryFactory(List<InstanceList.Entry> instanceList) {
super();
//TODO better design
onSetup(item -> {
var box = new Box(Orientation.HORIZONTAL, 5);
Label label = new Label(Str.NULL);
// label.setXalign(0);
label.setWidthChars(20);
label.setMarginEnd(10);
box.append(label);
Button launch = new Button();
launch.setIconName(new Str("computer-symbolic"));
launch.setTooltipText(I18n.str("instance.launch"));
launch.setHasTooltip(GTK.TRUE);
box.append(launch);
Button openDir = new Button();
openDir.setIconName(new Str("folder-symbolic"));
openDir.setTooltipText(I18n.str("instance.directory"));
openDir.setHasTooltip(GTK.TRUE);
box.append(openDir);
item.setChild(box);
//TODO server launch with network-server-symbolic
//TODO kill current instance
});
onBind(item -> {
Label label = new Label(item.getChild().getFirstChild().cast());
Button launch = new Button(label.getNextSibling().cast());
Button openDir = new Button(launch.getNextSibling().cast());
InstanceList.Entry instance = instanceList.get(ListIndex.toIndex(item));
label.setText(new Str(instance.toString()));
launch.onClicked(() -> GtkMenubar.launch(instance));
openDir.onClicked(() -> Utils.openFile(instance.path().toFile()));
//TODO edit button document-edit-symbolic -> edit-delete-symbolic, edit-copy-symbolic
});
}
}

View File

@ -0,0 +1,69 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import ch.bailu.gtk.GTK;
import ch.bailu.gtk.bridge.ListIndex;
import ch.bailu.gtk.gtk.*;
import ch.bailu.gtk.type.Str;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.launcher.util.InstanceList;
import java.util.List;
public class InstanceListEntryFactory extends SignalListItemFactory {
public InstanceListEntryFactory(List<InstanceList.Entry> instanceList) {
super();
onSetup(item -> {
var thumbnail = new InstanceThumbnail();
var label = new Label(Str.NULL);
label.setHexpand(GTK.TRUE);
var launch = new Button();
launch.setIconName(new Str("computer-symbolic"));
launch.setTooltipText(I18n.str("instance.launch"));
launch.setHasTooltip(GTK.TRUE);
var openDir = new Button();
openDir.setIconName(new Str("folder-symbolic"));
openDir.setTooltipText(I18n.str("instance.directory"));
openDir.setHasTooltip(GTK.TRUE);
var row = new Box(Orientation.HORIZONTAL, 8);
row.append(thumbnail);
row.append(label);
row.append(launch);
row.append(openDir);
item.setChild(row);
//TODO server launch with network-server-symbolic
//TODO kill current instance
});
onBind(item -> {
InstanceList.Entry instance = instanceList.get(ListIndex.toIndex(item));
Box row = new Box(item.getChild().cast());
InstanceThumbnail thumbnail = new InstanceThumbnail(row.getFirstChild().cast());
Label label = new Label(thumbnail.getNextSibling().cast());
Button launch = new Button(label.getNextSibling().cast());
Button openDir = new Button(launch.getNextSibling().cast());
thumbnail.bind(instance);
label.setText(new Str(instance.toString()));
launch.onClicked(() -> GtkMenubar.launch(instance));
openDir.onClicked(() -> Utils.openFile(instance.path().toFile()));
//TODO why the hell does this crash the VM?
// var controller = new EventControllerLegacy();
// controller.onEvent(event -> {
// if (event.getEventType() == EventType.BUTTON_RELEASE) {
// Utils.LOGGER.info("Button " + new ButtonEvent(event.cast()).getButton());
// }
// return GTK.FALSE;
// });
// row.addController(controller);
//TODO edit button document-edit-symbolic -> edit-delete-symbolic, edit-copy-symbolic
});
}
}

View File

@ -0,0 +1,42 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import ch.bailu.gtk.gtk.*;
import ch.bailu.gtk.type.CPointer;
import ch.bailu.gtk.type.Str;
import io.gitlab.jfronny.inceptum.launcher.util.InstanceList;
import io.gitlab.jfronny.inceptum.launcher.util.InstanceLock;
public class InstanceThumbnail extends Stack {
private static final Str SPINNER = new Str("spinner");
private static final Str IMAGE = new Str("image");
private static final Str GENERIC = new Str("generic");
public InstanceThumbnail(CPointer pointer) {
super(pointer);
}
public InstanceThumbnail() {
super();
var spinner = new Spinner();
var image = new Image();
var generic = new Image();
generic.setFromIconName(new Str("media-playback-start-symbolic")); //TODO better default icon
addNamed(spinner, SPINNER);
addNamed(image, IMAGE);
addNamed(generic, GENERIC);
}
public void bind(InstanceList.Entry entry) {
var spinner = new Spinner(getChildByName(SPINNER).cast());
var image = new Image(getChildByName(IMAGE).cast()); //TODO
var generic = new Image(getChildByName(GENERIC).cast());
//TODO mark instance being played
if (InstanceLock.isSetupLocked(entry.path())) {
setVisibleChild(spinner);
} else if (false) { // if the instance has an image, load the image data and set it as the visible child
setVisibleChild(image);
} else {
setVisibleChild(generic);
}
}
}

View File

@ -1,29 +1,47 @@
package io.gitlab.jfronny.inceptum.gtk.window;
import ch.bailu.gtk.GTK;
import ch.bailu.gtk.adw.StatusPage;
import ch.bailu.gtk.bridge.ListIndex;
import ch.bailu.gtk.gio.Menu;
import ch.bailu.gtk.glib.Glib;
import ch.bailu.gtk.gtk.*;
import ch.bailu.gtk.type.Str;
import io.gitlab.jfronny.inceptum.common.InceptumConfig;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
import io.gitlab.jfronny.inceptum.gtk.control.InstanceGridEntryFactory;
import io.gitlab.jfronny.inceptum.gtk.control.InstanceListEntryFactory;
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.launcher.util.InstanceList;
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import static java.nio.file.StandardWatchEventKinds.*;
public class MainWindow extends ApplicationWindow {
private final Button listButton;
private final Button gridButton;
private final Stack stack;
private final Box empty;
private final ListView listView;
private final GridView gridView;
private final List<InstanceList.Entry> instanceList;
private final ListIndex instanceListIndex;
public MainWindow(Application app) {
super(app);
var header = new HeaderBar();
var newButton = new Button();
HeaderBar header = new HeaderBar();
Button newButton = new Button();
newButton.setIconName(new Str("list-add-symbolic"));
newButton.onClicked(NewInstanceWindow::createAndShow);
var accountsButton = new MenuButton();
MenuButton accountsButton = new MenuButton();
accountsButton.setIconName(new Str("avatar-default-symbolic"));
accountsButton.setPopover(GtkMenubar.accountsMenu.asPopover());
@ -43,11 +61,13 @@ public class MainWindow extends ApplicationWindow {
generateWindowBody();
});
var uiMenu = new MenuBuilder(app, new Menu(), "hamburger");
//TODO search button like boxes
MenuBuilder uiMenu = new MenuBuilder(app, new Menu(), "hamburger");
uiMenu.button("support", () -> Utils.openWebBrowser(new URI("https://gitlab.com/jfmods/inceptum/-/issues")));
uiMenu.button("preferences", () -> {}); //TODO preferences UI inspired by boxes
uiMenu.button("about", AboutWindow::createAndShow);
var menuButton = new MenuButton();
MenuButton menuButton = new MenuButton();
menuButton.setIconName(new Str("open-menu-symbolic"));
menuButton.setPopover(uiMenu.asPopover());
@ -58,18 +78,87 @@ public class MainWindow extends ApplicationWindow {
header.packEnd(listButton);
header.packEnd(accountsButton);
instanceList = new ArrayList<>();
instanceListIndex = new ListIndex();
listView = new ListView(instanceListIndex.inSelectionModel(), new InstanceListEntryFactory(instanceList));
gridView = new GridView(instanceListIndex.inSelectionModel(), new InstanceGridEntryFactory(instanceList));
empty = new Box(Orientation.VERTICAL, 8);
empty.setHalign(Align.CENTER);
empty.setValign(Align.CENTER);
var emptyTitle = new Label(Str.NULL);
emptyTitle.setMarkup(I18n.str("main.empty.title"));
emptyTitle.setHalign(Align.CENTER);
var emptyDescription = new Label(I18n.str("main.empty.description"));
emptyDescription.setHalign(Align.CENTER);
empty.append(emptyTitle);
empty.append(emptyDescription);
//TODO empty.setIconName(new Str());
stack = new Stack();
stack.addChild(listView);
stack.addChild(gridView);
ScrolledWindow scroll = new ScrolledWindow();
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC);
scroll.setChild(stack);
setDefaultSize(360, 720);
setTitle(new Str("Inceptum"));
setTitlebar(header);
setShowMenubar(GTK.FALSE);
setChild(new TextView());
setChild(scroll);
generateWindowBody();
//TODO DropTarget to add mods/instances
try {
setupDirWatcher();
} catch (IOException e) {
Utils.LOGGER.error("Could not set up watch service, live updates of the instance dir will be unavailable", e);
}
}
private void setupDirWatcher() throws IOException { //TODO test (including after lock state change)
WatchService ws = FileSystems.getDefault().newWatchService();
MetaHolder.INSTANCE_DIR.register(ws, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
int source = Glib.idleAdd(user_data -> {
//TODO watch instance dirs for locks
WatchKey key = ws.poll();
boolean instancesChanged = false;
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context() instanceof Path p) {
p = MetaHolder.INSTANCE_DIR.resolve(p);
instancesChanged |= Files.exists(p.resolve("instance.json"));
}
}
}
if (instancesChanged) generateWindowBody();
return GTK.TRUE;
}, null);
onCloseRequest(() -> {
try {
ws.close();
} catch (IOException ignored) {
}
Glib.sourceRemove(source);
return GTK.FALSE;
});
}
private void generateWindowBody() {
if (listButton != null) listButton.setVisible(GTK.is(!InceptumConfig.listView));
if (gridButton != null) gridButton.setVisible(GTK.is(InceptumConfig.listView));
//TODO create list/grid view
try {
instanceList.clear();
InstanceList.forEach(instanceList::add);
instanceListIndex.setSize(instanceList.size());
if (InstanceList.isEmpty()) stack.setVisibleChild(empty);
else if (InceptumConfig.listView) stack.setVisibleChild(listView);
else stack.setVisibleChild(gridView);
} catch (IOException e) {
Utils.LOGGER.error("Could not generate window body", e);
}
}
}

View File

@ -21,9 +21,13 @@ cancel=Cancel
menu.hamburger.support=Support
menu.hamburger.preferences=Preferences
menu.hamburger.about=About
launch.locked=Can't launch right now
launch.locked.setup=This instance is currently setting up.\
instance.launch.locked=Can't launch right now
instance.launch.locked.setup=This instance is currently setting up.\
Please wait until that is finished before playing.
launch.locked.running=This instance is currently running.\
instance.launch.locked.running=This instance is currently running.\
Click OK to start a second instance of the game.\
Please be aware that doing so is unsupported and WILL cause issues!
instance.launch=Launch
main.empty.title=# Welcome to Inceptum
main.empty.description=To get started, create (or import) a new instance using the + button
instance.directory=Open Directory

View File

@ -20,10 +20,14 @@ cancel=Abbrechen
menu.hamburger.support=Unterstützung
menu.hamburger.preferences=Einstellungen
menu.hamburger.about=Über
launch.locked=Instanz kann momentan nicht gestartet werden
launch.locked.setup=Diese Instanz wird noch eingerichtet.\
instance.launch.locked=Instanz kann momentan nicht gestartet werden
instance.launch.locked.setup=Diese Instanz wird noch eingerichtet.\
Bitte warte, bis dieser Prozess abgeschlossen ist,\
bevor du sie startest.
launch.locked.running=Diese Instanz läuft bereits.\
instance.launch.locked.running=Diese Instanz läuft bereits.\
Bestätigen sie den Start mit OK, um sie ein zweites mal zu starten.\
Bitte seien sie sich bewusst, dass dies zu Problemen führen wird und NICHT UNTERSÜTZT ist.
instance.launch=Starten
main.empty.title=# Willkommen bei Inceptum
main.empty.description=Importiere oder erstelle um anzufangen eine Instanz mit dem +
instance.directory=Verzeichnis öffnen

View File

@ -128,10 +128,11 @@ class ModsDirScannerImpl implements ModsDirScanner {
WatchKey key = service.poll();
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context() instanceof Path p)
toScan.add(p);
if (event.context() instanceof Path p) {
toScan.add(modsDir.resolve(p));
}
}
key.reset();
if (!key.reset()) Utils.LOGGER.warn("Could not reset config watch key");
}
JFiles.listTo(modsDir, path -> {
if (!descriptions.containsKey(path))

View File

@ -25,10 +25,9 @@ public class InstanceList {
});
}
public static void forEachLaunchable(Consumer<Entry> target) throws IOException {
forEach(entry -> {
if (!InstanceLock.isLocked(entry.path)) target.accept(entry);
});
public static boolean isEmpty() throws IOException {
if (!Files.exists(MetaHolder.INSTANCE_DIR)) return true;
return JFiles.list(MetaHolder.INSTANCE_DIR, Files::isDirectory).isEmpty();
}
public static Entry read(Path instancePath) throws IOException {