Initial port of launcher-gtk to kotlin. With this, manifold is completely gone from the codebase.

Might have introduced some new bugs + crashes, though.
This commit is contained in:
Johannes Frohnmeyer 2023-05-05 21:27:00 +02:00
parent 441a9b26b2
commit a89f51aa5d
Signed by: Johannes
GPG Key ID: E76429612C2929F4
72 changed files with 2272 additions and 2225 deletions

View File

@ -1,12 +0,0 @@
plugins {
id("inceptum.java")
id("jf.manifold")
}
project.extra["manifoldVersion"] = "2023.1.7"
dependencies {
val jfCommonsVersion: String by rootProject.extra
implementation("io.gitlab.jfronny:commons-manifold:$jfCommonsVersion")
}

View File

@ -1,7 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("inceptum.application")
id("com.github.johnrengelman.shadow")
id("jf.manifold")
kotlin("jvm") version "1.8.21"
}
application {

View File

@ -1,16 +0,0 @@
package extensions.manifold.rt.api.Array;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import java.lang.reflect.Array;
@Extension
public class ArrayExt {
public static int indexOf(@This Object array, Object elem) {
for (int i = 0, len = Array.getLength(array); i < len; i++) {
if (Array.get(array, i).equals(elem)) return i;
}
return -1;
}
}

View File

@ -1,14 +0,0 @@
package extensions.org.gnome.adw.ActionRow;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import org.gnome.adw.ActionRow;
import org.gnome.gtk.Label;
@Extension
public class ActionRowExt {
public static void fixSubtitle(@This ActionRow thiz) {
ILabel.theme((Label) thiz.firstChild.lastChild.prevSibling.lastChild, ILabel.Mode.SUBTITLE);
}
}

View File

@ -1,23 +0,0 @@
package extensions.org.gnome.gtk.Widget;
import manifold.ext.rt.api.Extension;
import manifold.ext.rt.api.This;
import org.gnome.gtk.Widget;
@Extension
public class WidgetExt {
public static void setMargin(@This Widget thiz, int margin) {
thiz.marginVertical = margin;
thiz.marginHorizontal = margin;
}
public static void setMarginVertical(@This Widget thiz, int margin) {
thiz.marginTop = margin;
thiz.marginBottom = margin;
}
public static void setMarginHorizontal(@This Widget thiz, int margin) {
thiz.marginStart = margin;
thiz.marginEnd = margin;
}
}

View File

@ -1,98 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.StringInputDialog;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
public enum GtkEnvBackend implements LauncherEnv.EnvBackend {
INSTANCE;
public Window dialogParent = null;
@Override
public void showError(String message, String title) {
Utils.LOGGER.error(message);
simpleDialog(message, title, MessageType.ERROR, ButtonsType.CLOSE, null, null);
}
@Override
public void showError(String message, Throwable t) {
simpleDialog(StringFormatter.toString(t), message, MessageType.ERROR, ButtonsType.CLOSE, null, null);
}
@Override
public void showInfo(String message, String title) {
Utils.LOGGER.info(message);
simpleDialog(message, title, MessageType.INFO, ButtonsType.CLOSE, null, null);
}
@Override
public void showOkCancel(String message, String title, Runnable ok, Runnable cancel, boolean defaultCancel) {
Utils.LOGGER.info(message);
simpleDialog(message, title, MessageType.QUESTION, ButtonsType.OK_CANCEL, ok, cancel);
}
@Override
public void getInput(String prompt, String details, String defaultValue, Consumer<String> ok, Runnable cancel) {
GtkMain.schedule(() -> {
DialogFlags flags = DialogFlags.DESTROY_WITH_PARENT;
if (dialogParent != null) flags = flags.or(DialogFlags.MODAL);
StringInputDialog dialog = new StringInputDialog(
dialogParent,
flags,
MessageType.QUESTION,
ButtonsType.OK_CANCEL,
details,
defaultValue
);
dialog.title = prompt;
dialog.onResponse(processResponses(dialog, () -> ok.accept(dialog.input), cancel));
dialog.show();
});
}
@Override
public void showLoginRefreshPrompt(MicrosoftAccount account) {
GtkMain.schedule(() -> {
new MicrosoftLoginDialog(dialogParent, account).show();
});
}
private void simpleDialog(String markup, String title, MessageType type, ButtonsType buttons, Runnable ok, Runnable cancel) {
GtkMain.schedule(() -> {
simpleDialog(dialogParent, markup, title, type, buttons, ok, cancel);
});
}
public static void simpleDialog(Window parent, String markup, String title, MessageType type, ButtonsType buttons, @Nullable Runnable ok, @Nullable Runnable cancel) {
MessageDialog dialog = new MessageDialog(parent, DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT), type, buttons, null);
dialog.title = title;
dialog.markup = markup;
dialog.onResponse(processResponses(dialog, ok, cancel));
dialog.show();
}
private static Dialog.Response processResponses(Dialog dialog, @Nullable Runnable ok, @Nullable Runnable cancel) {
return responseId -> {
switch (ResponseType.of(responseId)) {
case OK -> {
dialog.close();
if (ok != null) ok.run();
}
case CLOSE, CANCEL -> {
dialog.close();
if (cancel != null) cancel.run();
}
case DELETE_EVENT -> dialog.destroy();
default -> Utils.LOGGER.error("Unexpected response type: " + responseId);
}
};
}
}

View File

@ -1,71 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.gtk.window.MainWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager;
import org.gnome.gio.ApplicationFlags;
import org.gnome.glib.GLib;
import org.gnome.gtk.Application;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
public class GtkMain {
public static final String ID = "io.gitlab.jfronny.inceptum";
private static final Queue<Runnable> SCHEDULED = new ArrayDeque<>();
public static void schedule(Runnable task) {
SCHEDULED.add(Objects.requireNonNull(task));
}
public static void main(String[] args) throws IOException {
LauncherEnv.initialize(GtkEnvBackend.INSTANCE);
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION);
Utils.LOGGER.info("Loading from " + MetaHolder.BASE_PATH);
int statusCode = -1;
try {
statusCode = showGui(args);
} finally {
LauncherEnv.terminate();
System.exit(statusCode);
}
}
public static int showGui(String[] args) {
return setupApplication(args, app -> {
//TODO update check
AccountManager.loadAccounts();
GtkMenubar.create(app);
var window = new MainWindow(app);
window.show();
GtkEnvBackend.INSTANCE.dialogParent = window;
window.onCloseRequest(() -> {
GtkEnvBackend.INSTANCE.dialogParent = null;
app.quit();
return false;
});
});
}
public static int setupApplication(String[] args, Consumer<Application> onActivate) {
var app = new Application(ID, ApplicationFlags.FLAGS_NONE);
app.onActivate(() -> {
GLib.idleAdd(() -> {
Runnable r;
while ((r = SCHEDULED.poll()) != null) {
try {
r.run();
} catch (Throwable t) {
Utils.LOGGER.error("Could not run scheduled task", t);
}
}
return true;
});
onActivate.accept(app);
});
return app.run(args);
}
}

View File

@ -1,195 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.inceptum.common.MetaHolder;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.gtk.window.AboutWindow;
import io.gitlab.jfronny.inceptum.gtk.window.NewInstanceWindow;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog;
import io.gitlab.jfronny.inceptum.gtk.window.settings.launcher.LauncherSettingsWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
import io.gitlab.jfronny.inceptum.launcher.system.importer.Importers;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList;
import io.gitlab.jfronny.inceptum.launcher.system.launch.InstanceLauncher;
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType;
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps;
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import org.gnome.gtk.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
public class GtkMenubar {
public static MenuBuilder newMenu;
public static MenuBuilder accountsMenu;
public static MenuBuilder launchMenu;
public static void create(Application app) {
var menu = new MenuBuilder(app);
var file = menu.submenu("file");
newMenu = file.submenu("new");
generateNewMenu(app);
file.button("redownload", () -> {
ProcessState state = new ProcessState(3 + Steps.STEPS.size() * InstanceList.size(), "Initializing");
ProcessStateWatcherDialog.show(
GtkEnvBackend.INSTANCE.dialogParent,
"Reloading data",
"Could not execute refresh task",
state,
() -> {
state.incrementStep("Clearing cache directories");
JFiles.clearDirectory(MetaHolder.ASSETS_DIR);
JFiles.clearDirectory(MetaHolder.LIBRARIES_DIR, path -> !path.startsWith(MetaHolder.LIBRARIES_DIR.resolve("io/gitlab/jfronny")));
JFiles.clearDirectory(MetaHolder.NATIVES_DIR, path -> !path.startsWith(MetaHolder.NATIVES_DIR.resolve("forceload")));
JFiles.clearDirectory(MetaHolder.CACHE_DIR);
if (state.isCancelled) return;
state.incrementStep("Reloading instance list");
InstanceList.reset();
InstanceList.forEach(instance -> {
if (state.isCancelled) return;
Steps.reDownload(instance, state);
});
});
});
file.button("exit", app::quit);
launchMenu = menu.submenu("launch");
generateLaunchMenu(app);
accountsMenu = menu.submenu("account");
generateAccountsMenu(app);
var help = menu.submenu("help");
help.button("about", AboutWindow::createAndShow);
help.button("log", () -> {
//TODO
});
}
public static void generateNewMenu(Application app) {
Objects.requireNonNull(newMenu).clear();
newMenu.button("new", () -> new NewInstanceWindow(app).show());
newMenu.button("file", () -> {
FileChooserNative dialog = new FileChooserNative(
I18n.get("menu.file.new.file"),
GtkEnvBackend.INSTANCE.dialogParent,
FileChooserAction.OPEN,
"_" + I18n.get("select"),
"_" + I18n.get("cancel")
);
var filter = new FileFilter();
filter.addPattern("*.zip");
filter.addPattern("*.mrpack");
dialog.addFilter(filter);
dialog.onResponse(responseId -> {
if (responseId == ResponseType.ACCEPT.value) {
var file = dialog.file.path;
if (file == null) {
LauncherEnv.showError("The path returned by the file dialog is null", "Could not import");
return;
}
ProcessState state = new ProcessState(Importers.MAX_STEPS, "Initializing");
ProcessStateWatcherDialog.show(
GtkEnvBackend.INSTANCE.dialogParent,
I18n.get("menu.file.new.file"),
I18n.get("menu.file.new.file.error"),
state,
() -> Importers.importPack(Path.of(file), state)
);
}
});
dialog.show();
});
newMenu.button("url", () -> {
LauncherEnv.getInput(
I18n.get("menu.file.new.url"),
I18n.get("menu.file.new.url.details"),
(String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor),
s -> {
ProcessState state = new ProcessState(Importers.MAX_STEPS, "Initializing");
ProcessStateWatcherDialog.show(
GtkEnvBackend.INSTANCE.dialogParent,
I18n.get("menu.file.new.url"),
I18n.get("menu.file.new.url.error"),
state,
() -> Importers.importPack(s, state)
);
},
R::nop
);
});
}
public static void generateLaunchMenu(Application app) {
Objects.requireNonNull(launchMenu).clear();
try {
InstanceList.forEach(entry -> {
launchMenu.literalButton(entry.id() + ".launch", entry.toString(), () -> launch(entry, LaunchType.Client));
});
} catch (IOException e) {
Utils.LOGGER.error("Could not generate launch menu", e);
}
}
public static void launch(Instance instance, LaunchType launchType) {
if (instance.isSetupLocked) {
LauncherEnv.showError(I18n.get("instance.launch.locked.setup"), I18n.get("instance.launch.locked"));
} else if (instance.isRunningLocked) {
LauncherEnv.showOkCancel(
I18n.get("instance.launch.locked.running"),
I18n.get("instance.launch.locked"),
() -> forceLaunch(instance, launchType),
R::nop
);
} else forceLaunch(instance, launchType);
}
private static void forceLaunch(Instance instance, LaunchType launchType) {
ProcessState state = Steps.createProcessState();
ProcessStateWatcherDialog.show(
GtkEnvBackend.INSTANCE.dialogParent,
I18n.get("instance.launch.title"),
I18n.get("instance.launch.error"),
state,
() -> {
try {
Steps.reDownload(instance, state);
} catch (IOException e) {
Utils.LOGGER.error("Could not fetch instance, trying to start anyways", e);
}
if (state.isCancelled) return;
state.updateStep("Starting Game");
try {
if (launchType == LaunchType.Client) InstanceLauncher.launchClient(instance);
else InstanceLauncher.launch(instance, launchType, false, AccountManager.NULL_AUTH);
} catch (Throwable e) {
LauncherEnv.showError("Could not start instance", e);
}
}
);
}
public static void generateAccountsMenu(Application app) {
Objects.requireNonNull(accountsMenu).clear();
accountsMenu.button("new", () -> new MicrosoftLoginDialog(GtkEnvBackend.INSTANCE.dialogParent).show());
accountsMenu.button("manage", () -> {
var window = new LauncherSettingsWindow(app);
window.activePage = "settings.accounts";
window.show();
});
List<MicrosoftAccount> accounts = new ArrayList<>(AccountManager.accounts);
accounts.add(null);
accountsMenu.literalRadio("account", accounts.get(AccountManager.selectedIndex), accounts, (i, acc) -> {
if (acc == null) return I18n.get("account.none");
return acc.minecraftUsername;
}, AccountManager::switchAccount);
}
}

View File

@ -1,48 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import io.gitlab.jfronny.commons.LazySupplier;
import io.gitlab.jfronny.inceptum.gtk.GtkMain;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.gtk.*;
import org.jetbrains.annotations.PropertyKey;
import java.io.InputStream;
import java.util.function.Supplier;
public class ILabel extends Label {
private static Supplier<CssProvider> provider = new LazySupplier<>(() -> {
CssProvider provider = new CssProvider();
try (InputStream is = GtkMain.class.getClassLoader().getResourceAsStream("inceptum.css")) {
provider.loadFromData(is.readAllBytes());
} catch (Throwable t) {
throw new RuntimeException(t);
}
return provider;
});
public static void theme(Label label, Mode mode) {
switch (mode) {
case HEADING -> label.addCssClass("heading");
case SUBTITLE -> {
label.addCssClass("jf-subtitle");
label.styleContext.addProvider(provider.get(), Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}
case NORMAL -> {}
}
}
public ILabel(@PropertyKey(resourceBundle = I18n.BUNDLE) String str, Object... args) {
this(str, Mode.NORMAL, args);
}
public ILabel(@PropertyKey(resourceBundle = I18n.BUNDLE) String str, Mode mode, Object... args) {
super(I18n.get(str, args));
theme(this, mode);
}
public enum Mode {
NORMAL,
HEADING,
SUBTITLE
}
}

View File

@ -1,78 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import io.github.jwharm.javagi.util.ListIndexModel;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import org.gnome.gtk.*;
import org.pango.EllipsizeMode;
import org.pango.WrapMode;
import java.util.List;
public class InstanceGridEntryFactory extends SignalListItemFactory {
public InstanceGridEntryFactory(List<Instance> instanceList) {
super();
//TODO better design
onSetup(item -> {
var box = new Box(Orientation.VERTICAL, 5);
var thumbnail = new InstanceThumbnail();
box.append(thumbnail);
var label = new Label((String) null);
label.setSizeRequest(192, -1);
label.maxWidthChars = 20;
label.justify = Justification.CENTER;
label.halign = Align.START;
label.hexpand = true;
label.valign = Align.CENTER;
label.ellipsize = EllipsizeMode.MIDDLE;
label.lines = 3;
label.wrap = true;
label.wrapMode = WrapMode.WORD_CHAR;
label.marginTop = 10;
box.append(label);
// 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);
((ListItem) 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()));
ListItem li = (ListItem) item;
Box box = (Box) li.getChild();
InstanceThumbnail thumbnail = InstanceThumbnail.castFrom((Stack) box.firstChild);
Label label = (Label) thumbnail.nextSibling;
Instance instance = instanceList.get(((ListIndexModel.ListIndex) li.item).index);
thumbnail.bind(instance);
label.text = instance.toString();
//TODO right click menu + double click action
//TODO edit button document-edit-symbolic -> edit-delete-symbolic, edit-copy-symbolic
});
}
}

View File

@ -1,147 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import io.github.jwharm.javagi.base.Signal;
import io.github.jwharm.javagi.util.ListIndexModel;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.inceptum.common.MetaHolder;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.gtk.window.settings.instance.InstanceSettingsWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool;
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType;
import org.gnome.adw.ActionRow;
import org.gnome.gio.Menu;
import org.gnome.gtk.Stack;
import org.gnome.gtk.*;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
public class InstanceListEntryFactory extends SignalListItemFactory {
public InstanceListEntryFactory(Application app, List<Instance> instanceList) {
super();
onSetup(item -> {
ListItem li = (ListItem) item;
var thumbnail = new InstanceThumbnail();
thumbnail.name = "inceptum-thumbnail";
var launch = Button.newFromIconName("media-playback-start-symbolic");
launch.addCssClass("flat");
launch.name = "inceptum-launch";
launch.tooltipText = I18n.get("instance.launch");
launch.hasTooltip = true;
var menu = new MenuButton();
menu.addCssClass("flat");
menu.iconName = "view-more-symbolic";
menu.popover = PopoverMenu.newFromModel(new Menu());
var row = new ActionRow();
row.margin = 8;
row.name = "inceptum-row";
row.removeCssClass("activatable"); //TODO remove this workaround if a better way to support opening the menu is found
row.addPrefix(thumbnail);
row.addSuffix(launch);
row.addSuffix(menu);
row.fixSubtitle();
var rightClicked = new GestureClick();
rightClicked.button = 3;
rightClicked.onPressed((nPress, x, y) -> {
if (nPress == 1) menu.emitActivate();
});
row.addController(rightClicked);
li.setChild(row);
});
Map<String, Set<Signal<?>>> toDisconnect = new HashMap<>();
onBind(item -> {
Decomposed li = Decomposed.of((ListItem) item, instanceList);
if (li.instance.isLocked) {
li.item.activatable = false;
li.row.setSubtitle(li.instance.isRunningLocked
? I18n.get("instance.launch.locked.running")
: I18n.get("instance.launch.locked.setup"));
}
li.row.title = li.instance.toString();
li.thumbnail.bind(li.instance);
var menuBuilder = new MenuBuilder(li.popoverMenu, li.instance.id);
var launchSection = menuBuilder.literalSection("launch", null);
var kill = launchSection.literalButton("kill", I18n.get("instance.kill"), () -> {
//TODO test
LauncherEnv.showOkCancel(I18n.get("instance.kill.prompt"), I18n.get("instance.kill.details"), () -> {
if (!li.instance.kill()) LauncherEnv.showError(I18n.get("instance.kill.fail"), I18n.get("failed"));
}, R::nop);
});
kill.enabled = li.instance.isRunningLocked;
launchSection.literalButton("launch.client", I18n.get("instance.launch.client"),
() -> GtkMenubar.launch(li.instance, LaunchType.Client))
.iconName = "media-playback-start-symbolic";
launchSection.literalButton("launch.server", I18n.get("instance.launch.server"),
() -> GtkMenubar.launch(li.instance, LaunchType.Server))
.iconName = "network-server-symbolic";
var settingsSection = menuBuilder.literalSection("settings", null);
settingsSection.literalButton("settings", I18n.get("instance.settings"), () -> {
//TODO keep track of properties windows and don't allow opening two
new InstanceSettingsWindow(app, li.instance).show();
}).iconName = "document-edit-symbolic";
settingsSection.literalButton("directory", I18n.get("instance.directory"),
() -> Utils.openFile(li.instance.path.toFile()))
.iconName = "folder-symbolic";
settingsSection.literalButton("copy", I18n.get("instance.copy"), () -> {
LauncherEnv.getInput(I18n.get("instance.copy.prompt"), I18n.get("instance.copy.details"), InstanceNameTool.getNextValid(li.instance.name), s -> {
try {
JFiles.copyRecursive(li.instance.path, MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(s)));
} catch (IOException e) {
LauncherEnv.showError(I18n.get("instance.copy.fail"), e);
}
}, R::nop);
}).iconName = "edit-copy-symbolic";
settingsSection.literalButton("delete", I18n.get("instance.delete"), () -> {
LauncherEnv.showOkCancel(I18n.get("instance.delete.confirm"), I18n.get("instance.delete.confirm.title"), () -> {
try {
JFiles.deleteRecursive(li.instance.path);
} catch (IOException e) {
LauncherEnv.showError(I18n.get("instance.delete.fail"), e);
}
}, R::nop);
}).iconName = "edit-delete-symbolic";
Consumer<Signal<?>> dc = s -> toDisconnect.computeIfAbsent(li.instance.id, $ -> new HashSet<>()).add(s);
dc.accept(li.launch.onClicked(() -> GtkMenubar.launch(li.instance, LaunchType.Client)));
});
onUnbind(item -> {
Decomposed li = Decomposed.of((ListItem) item, instanceList);
li.popoverMenu.insertActionGroup(li.instance.id, null);
toDisconnect.get(li.instance.id).forEach(Signal::disconnect);
});
}
private record Decomposed(ListItem item, Instance instance, ActionRow row, InstanceThumbnail thumbnail, Button launch, PopoverMenu popoverMenu) {
public static Decomposed of(ListItem item, List<Instance> instanceList) {
Instance instance = instanceList.get(((ListIndexModel.ListIndex) item.item).index);
ActionRow row = (ActionRow) item.child;
Box prefixes = (Box) row.firstChild.firstChild;
Box suffixes = (Box) row.firstChild.lastChild;
InstanceThumbnail thumbnail = InstanceThumbnail.castFrom((Stack) prefixes.firstChild);
Button launch = (Button) suffixes.firstChild;
MenuButton menuButton = (MenuButton) launch.nextSibling;
PopoverMenu popoverMenu = (PopoverMenu) menuButton.popover;
return new Decomposed(item, instance, row, thumbnail, launch, popoverMenu);
}
}
}

View File

@ -1,48 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import org.gnome.gtk.*;
import java.lang.foreign.Addressable;
public class InstanceThumbnail extends Stack {
private static final String SPINNER = "spinner";
private static final String IMAGE = "image";
private static final String GENERIC = "generic";
private InstanceThumbnail(Addressable address) {
super(address);
}
public static InstanceThumbnail castFrom(Stack stack) {
return new InstanceThumbnail(stack.handle());
}
public InstanceThumbnail() {
super();
var spinner = new Spinner();
var image = new Image();
var generic = new Image();
spinner.name = SPINNER;
image.name = IMAGE;
generic.name = GENERIC;
generic.setFromIconName("media-playback-start-symbolic"); //TODO better default icon
addNamed(spinner, SPINNER);
addNamed(image, IMAGE);
addNamed(generic, GENERIC);
}
public void bind(Instance entry) {
var spinner = (Spinner) getChildByName(SPINNER);
var image = (Image) getChildByName(IMAGE); //TODO
var generic = (Image) getChildByName(GENERIC);
//TODO mark instance being played
if (entry.isSetupLocked) {
visibleChild = spinner;
} else if (false) { // if the instance has an image, load the image data and set it as the visible child
visibleChild = image;
} else {
visibleChild = generic;
}
}
}

View File

@ -1,87 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;
import java.util.function.*;
public class IRow extends Box {
public IRow(@PropertyKey(resourceBundle = I18n.BUNDLE) String title, @PropertyKey(resourceBundle = I18n.BUNDLE) @Nullable String subtitle, Object... args) {
super(Orientation.HORIZONTAL, 40);
margin = 8;
Widget head;
ILabel lab = new ILabel(title, args);
lab.halign = Align.START;
if (subtitle != null) {
Box headB = new Box(Orientation.VERTICAL, 0);
headB.append(lab);
ILabel lab1 = new ILabel(subtitle, ILabel.Mode.SUBTITLE, args);
lab1.halign = Align.START;
headB.append(lab1);
head = headB;
} else {
head = lab;
}
head.halign = Align.START;
head.valign = Align.CENTER;
append(head);
}
public Button setButton(@PropertyKey(resourceBundle = I18n.BUNDLE) String text, Button.Clicked action) {
Button btn = Button.newWithLabel(I18n.get(text));
packSmallEnd(btn);
btn.onClicked(action);
return btn;
}
public DropDown setDropdown(String[] options, int defaultIndex, IntConsumer changed) {
DropDown btn = new DropDown(new StringList(options), null);
packSmallEnd(btn);
btn.selected = defaultIndex;
btn.onNotify("selected", pspec -> {
changed.accept(btn.selected);
});
btn.expression = new PropertyExpression(StringObject.type, null, "string");
return btn;
}
public Switch setSwitch(boolean value, Consumer<Boolean> changed) {
Switch btn = new Switch();
packSmallEnd(btn);
btn.active = value;
btn.onStateSet(state -> {
changed.accept(state);
return false;
});
return btn;
}
public SpinButton setSpinButton(double value, double min, double max, double step, DoubleConsumer changed) {
SpinButton btn = SpinButton.newWithRange(min, max, step);
packSmallEnd(btn);
btn.value = value;
btn.onValueChanged(() -> changed.accept(btn.value));
return btn;
}
public Entry setEntry(String value, Consumer<String> changed) {
Entry entry = new Entry();
entry.text = value == null ? "" : value;
entry.hexpand = true;
entry.valign = Align.CENTER;
entry.halign = Align.FILL;
entry.onChanged(() -> changed.accept(entry.text));
append(entry);
return entry;
}
private void packSmallEnd(Widget widget) {
firstChild.hexpand = true;
widget.valign = Align.CENTER;
widget.halign = Align.END;
append(widget);
}
}

View File

@ -1,84 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab.SectionBuilder.Section;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;
import java.util.concurrent.atomic.AtomicInteger;
public class SettingsTab extends Box {
protected final Window window;
public SettingsTab(Window window) {
super(Orientation.VERTICAL, 8);
this.marginHorizontal = 24;
this.marginTop = 12;
this.window = window;
}
protected void section(@Nullable @PropertyKey(resourceBundle = I18n.BUNDLE) String title, SectionBuilder builder) {
if (title != null) append(new ILabel(title, ILabel.Mode.HEADING));
Frame frame = new Frame((String) null);
ListBox listBox = new ListBox();
listBox.selectionMode = SelectionMode.NONE;
listBox.showSeparators = true;
frame.child = listBox;
AtomicInteger count = new AtomicInteger(0);
builder.build(new Section() {
@Override
public IRow row(String title, @Nullable String subtitle, Object... args) {
IRow row = new IRow(title, subtitle, args);
row(row);
return row;
}
@Override
public ListBoxRow row(Widget row) {
listBox.append(row);
return listBox.getRowAtIndex(count.getAndIncrement());
}
@Override
public void remove(Widget row) {
listBox.remove(row);
count.decrementAndGet();
}
@Override
public void clear() {
for (int i = 0, len = count.getAndSet(0); i < len; i++) {
listBox.remove(listBox.getRowAtIndex(0));
}
}
});
append(frame);
}
public interface SectionBuilder {
void build(Section section);
interface Section {
IRow row(@PropertyKey(resourceBundle = I18n.BUNDLE) String title, @PropertyKey(resourceBundle = I18n.BUNDLE) @Nullable String subtitle, Object... args);
ListBoxRow row(Widget row);
void remove(Widget row);
void clear();
}
}
protected void showError(String message, Throwable t) {
GtkEnvBackend.simpleDialog(
window,
StringFormatter.toString(t),
message,
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
);
}
}

View File

@ -1,50 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.adw.HeaderBar;
import org.gnome.adw.*;
import org.gnome.gobject.BindingFlags;
import org.gnome.gtk.Application;
import org.gnome.gtk.Window;
import org.gnome.gtk.*;
import org.jetbrains.annotations.PropertyKey;
public class SettingsWindow extends Window {
protected final ViewStack stack;
public SettingsWindow(Application app) {
this.application = app;
this.stack = new ViewStack();
HeaderBar header = new HeaderBar();
ViewSwitcherTitle viewSwitcher = new ViewSwitcherTitle();
viewSwitcher.stack = stack;
header.titleWidget = viewSwitcher;
titlebar = header;
ScrolledWindow scroll = new ScrolledWindow();
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC);
scroll.child = stack;
scroll.vexpand = true;
ViewSwitcherBar bottomBar = new ViewSwitcherBar();
bottomBar.stack = stack;
viewSwitcher.bindProperty("title-visible", bottomBar, "reveal", BindingFlags.DEFAULT);
Box view = new Box(Orientation.VERTICAL, 0);
view.append(scroll);
view.append(bottomBar);
child = view;
setDefaultSize(720, 360);
}
public void addTab(SettingsTab tab, @PropertyKey(resourceBundle = I18n.BUNDLE) String title, String iconName) {
stack.addTitledWithIcon(tab, title, I18n.get(title), iconName);
}
public void setActivePage(@PropertyKey(resourceBundle = I18n.BUNDLE) String title) {
stack.visibleChildName = title;
}
}

View File

@ -1,10 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.menu;
import org.gnome.gio.MenuItem;
import org.gnome.gio.SimpleAction;
public class BuiltButtonItem extends BuiltMenuItem {
public BuiltButtonItem(SimpleAction action, MenuItem menuItem) {
super(action, menuItem);
}
}

View File

@ -1,29 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.menu;
import org.gnome.gio.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public abstract class BuiltMenuItem {
protected final SimpleAction action;
protected final MenuItem menuItem;
protected BuiltMenuItem(@NotNull SimpleAction action, @Nullable MenuItem menuItem) {
this.action = Objects.requireNonNull(action);
this.menuItem = menuItem;
}
public boolean getEnabled() {
return action.getEnabled();
}
public void setEnabled(boolean enabled) {
action.enabled = enabled;
}
public void setIconName(String iconName) {
Objects.requireNonNull(menuItem).icon = new ThemedIcon(iconName);
}
}

View File

@ -1,23 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.menu;
import org.gnome.gio.SimpleAction;
import org.gnome.glib.Variant;
import java.util.List;
public class BuiltRadioItem<T> extends BuiltMenuItem {
private final List<T> options;
public BuiltRadioItem(SimpleAction action, List<T> options) {
super(action, null);
this.options = options;
}
public void setSelected(T selected) {
action.setState(Variant.newInt32(options.indexOf(selected)));
}
public T getSelected() {
return options.get(action.getState().getInt32());
}
}

View File

@ -1,25 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.menu;
import org.gnome.gio.MenuItem;
import org.gnome.gio.SimpleAction;
import org.gnome.glib.Variant;
public class BuiltToggleItem extends BuiltMenuItem {
public BuiltToggleItem(SimpleAction action, MenuItem menuItem) {
super(action, menuItem);
}
public boolean getState() {
return action.getState().getBoolean();
}
public void setState(boolean state) {
action.state = Variant.newBoolean(state);
}
public boolean toggle() {
boolean toggled = !getState();
setState(toggled);
return toggled;
}
}

View File

@ -1,171 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.menu;
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.gio.*;
import org.gnome.glib.Variant;
import org.gnome.glib.VariantType;
import org.gnome.gtk.Application;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
public class MenuBuilder {
private static final Object LOCK = new Object();
private static Menu getRootMenu(Application app) {
synchronized (LOCK) {
var currentMenu = app.menubar;
if (currentMenu == null) {
var menu = new Menu();
app.menubar = menu;
return menu;
} else {
return (Menu) currentMenu;
}
}
}
private final ActionMap map;
private final Map<String, Action> refs = new LinkedHashMap<>();
private final Menu menu;
private final String prefix;
private final String groupName;
public MenuBuilder(Application app) {
this(app, getRootMenu(app), "");
}
public static MenuBuilder create(MenuButton target, String groupName) {
Menu menu = new Menu();
PopoverMenu pm = PopoverMenu.newFromModel(menu);
target.popover = pm;
return new MenuBuilder(pm, groupName);
}
public MenuBuilder(PopoverMenu menu, String groupName) {
this(insertMap(menu, groupName), (Menu) menu.menuModel, "", groupName);
}
private static ActionMap insertMap(PopoverMenu menu, String groupName) {
SimpleActionGroup ag = new SimpleActionGroup();
menu.insertActionGroup(groupName, ag);
return ag;
}
public MenuBuilder(Application app, Menu menu, String prefix) {
this(app, menu, prefix, "app.");
}
private MenuBuilder(ActionMap map, Menu menu, String prefix, String groupName) {
if (!Objects.requireNonNull(prefix).isEmpty && !prefix.endsWith(".")) prefix += ".";
if (!Objects.requireNonNull(groupName).isEmpty && !groupName.endsWith(".")) groupName += ".";
this.map = Objects.requireNonNull(map);
this.menu = Objects.requireNonNull(menu);
this.prefix = prefix;
this.groupName = groupName;
}
public BuiltButtonItem button(String name, ThrowingRunnable<?> onClick) {
return literalButton(name, I18n.get("menu." + prefix + name), onClick);
}
public BuiltButtonItem literalButton(String internalName, String label, ThrowingRunnable<?> onClick) {
internalName = prefix + internalName;
SimpleAction action = new SimpleAction(internalName, null);
addAction(internalName, action);
action.onActivate(variant -> {
try {
onClick.run();
} catch (Throwable e) {
Utils.LOGGER.error("Could not execute action", e);
}
});
MenuItem menuItem = new MenuItem(label, groupName + internalName);
menu.appendItem(menuItem);
action.enabled = true;
return new BuiltButtonItem(action, menuItem);
}
public BuiltToggleItem toggle(String name, boolean initial, Consumer<Boolean> onToggle) {
name = prefix + name;
SimpleAction action = SimpleAction.newStateful(name, null, Variant.newBoolean(initial));
addAction(name, action);
action.onActivate(variant -> {
boolean state = !action.getState().getBoolean();
action.state = Variant.newBoolean(state);
onToggle.accept(state);
});
MenuItem menuItem = new MenuItem(I18n.get("menu." + name), groupName + name);
menu.appendItem(menuItem);
return new BuiltToggleItem(action, menuItem);
}
public <T> BuiltRadioItem<T> radio(String name, T initial, List<T> options, Consumer<T> onCheck) {
return literalRadio(name, initial, options, (i, t) -> I18n.get("menu." + prefix + name, i), onCheck);
}
public <T> BuiltRadioItem<T> literalRadio(String name, T initial, List<T> options, BiFunction<Integer, T, String> stringifier, Consumer<T> onCheck) {
Objects.requireNonNull(options);
name = prefix + name;
SimpleAction action = SimpleAction.newStateful(name, new VariantType("i"), Variant.newInt32(options.indexOf(initial)));
addAction(name, action);
action.onActivate(variant -> {
action.state = variant;
onCheck.accept(options.get(variant.getInt32()));
});
int i = 0;
for (T option : options) {
menu.appendItem(new MenuItem(stringifier.apply(i, option), groupName + name + "(" + i + ")"));
i++;
}
return new BuiltRadioItem<>(action, options);
}
public MenuBuilder submenu(String name) {
return literalSubmenu(name, I18n.get("menu." + prefix + name));
}
public MenuBuilder literalSubmenu(String name, String label) {
name = prefix + name;
Menu submenu = new Menu();
menu.appendSubmenu(label, submenu);
return new MenuBuilder(map, submenu, name, groupName);
}
public MenuBuilder section(String name) {
return literalSection(name, I18n.get("section." + prefix + name));
}
public MenuBuilder literalSection(String name, @Nullable String label) {
name = prefix + name;
Menu section = new Menu();
menu.appendSection(label, section);
return new MenuBuilder(map, section, name, groupName);
}
public void clear() {
menu.removeAll();
refs.forEach((name, action) -> {
map.removeAction(name);
});
refs.clear();
}
private void addAction(String name, SimpleAction action) {
map.addAction(action);
refs.put(name, action);
}
public Menu getMenu() {
return menu;
}
public PopoverMenu asPopover() {
return PopoverMenu.newFromModel(menu);
}
}

View File

@ -1,18 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.util;
import org.jetbrains.annotations.PropertyKey;
import java.util.ResourceBundle;
public class I18n {
public static final String BUNDLE = "inceptum";
private static final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE);
public static String get(@PropertyKey(resourceBundle = BUNDLE) String key) {
return bundle.getString(key);
}
public static String get(@PropertyKey(resourceBundle = BUNDLE) String key, Object... args) {
return String.format(bundle.getString(key), args);
}
}

View File

@ -1,103 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.util;
import io.gitlab.jfronny.commons.LazySupplier;
import io.gitlab.jfronny.commons.OSUtils;
import io.gitlab.jfronny.inceptum.common.Utils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class Memory {
public static final long KB = 1024;
public static final long MB = KB * 1024;
public static final long GB = MB * 1024;
private static final MI impl = switch (OSUtils.TYPE) {
case LINUX -> new LinuxMI();
case WINDOWS -> new WindowsMI();
case MAC_OS -> new MacOsMI();
};
private static final LazySupplier<Long> totalMemory = new LazySupplier<>(impl::getTotalMemory);
public static long getMaxMBForInstance() {
return Math.max(totalMemory.get() / MB - 1024, 1024);
}
private interface MI {
long getTotalMemory();
}
private static class LinuxMI implements MI {
@Override
public long getTotalMemory() {
try (Stream<String> stream = Files.lines(Path.of("/proc/meminfo"))) {
var memTotal = stream
.filter(s -> s.startsWith("MemTotal:"))
.map(s -> s.substring("MemTotal:".length()))
.map(String::trim)
.findFirst();
if (memTotal.isPresent()) {
return parseDecimalMemorySizeToBinary(memTotal.get());
} else {
Utils.LOGGER.error("Could not find total memory");
return 32 * GB;
}
} catch (IOException e) {
Utils.LOGGER.error("Could not get total memory", e);
return 32 * GB;
}
}
// Taken from oshi
private static final Pattern BYTES_PATTERN = Pattern.compile("(\\d+) ?([kKMGT]?B?).*");
private static final Pattern WHITESPACES = Pattern.compile("\\s+");
private static long parseDecimalMemorySizeToBinary(String size) {
String[] mem = WHITESPACES.split(size);
if (mem.length < 2) {
// If no spaces, use regexp
Matcher matcher = BYTES_PATTERN.matcher(size.trim());
if (matcher.find() && matcher.groupCount() == 2) {
mem = new String[2];
mem[0] = matcher.group(1);
mem[1] = matcher.group(2);
}
}
long capacity = parseLongOrDefault(mem[0], 0L);
if (mem.length == 2 && mem[1].length() > 1) {
switch (mem[1].charAt(0)) {
case 'T' -> capacity <<= 40;
case 'G' -> capacity <<= 30;
case 'M' -> capacity <<= 20;
case 'K', 'k' -> capacity <<= 10;
default -> {}
}
}
return capacity;
}
private static long parseLongOrDefault(String s, long defaultLong) {
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
return defaultLong;
}
}
}
private static class WindowsMI implements MI {
@Override
public long getTotalMemory() {
return 32 * GB; // This is currently unsupported, but any implementations by Windows user using panama are welcome
}
}
private static class MacOsMI implements MI {
@Override
public long getTotalMemory() {
return 32 * GB; // This is currently unsupported, but any implementations by MacOS user using panama are welcome
}
}
}

View File

@ -1,28 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window;
import io.gitlab.jfronny.inceptum.common.BuildMetadata;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import org.gnome.gtk.AboutDialog;
import org.gnome.gtk.License;
public class AboutWindow extends AboutDialog {
public AboutWindow() {
programName = "Inceptum";
copyright = "Copyright (C) 2021-2023 JFronny";
version = BuildMetadata.VERSION;
licenseType = License.MIT_X11;
license = I18n.get("about.license");
websiteLabel = I18n.get("about.contact");
website = "https://jfronny.gitlab.io/contact.html";
if (!BuildMetadata.IS_PUBLIC) {
comments = I18n.get("about.unsupported-build");
}
int vm = Runtime.version().feature();
systemInformation = I18n.get(BuildMetadata.VM_VERSION == vm ? "about.jvm" : "about.jvm.unsupported", vm);
//TODO setLogo
}
public static void createAndShow() {
new AboutWindow().show();
}
}

View File

@ -1,170 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window;
import io.github.jwharm.javagi.util.ListIndexModel;
import io.gitlab.jfronny.inceptum.common.InceptumConfig;
import io.gitlab.jfronny.inceptum.common.Utils;
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.gtk.window.settings.launcher.LauncherSettingsWindow;
import io.gitlab.jfronny.inceptum.launcher.system.instance.*;
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType;
import org.gnome.adw.Clamp;
import org.gnome.adw.StatusPage;
import org.gnome.gio.Menu;
import org.gnome.glib.GLib;
import org.gnome.gtk.*;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
public class MainWindow extends ApplicationWindow {
private final Button listButton;
private final Button gridButton;
private final Stack stack;
private final StatusPage empty;
private final Clamp listContainer;
private final GridView gridView;
private final List<Instance> instanceList;
private final ListIndexModel instanceListIndex;
public MainWindow(Application app) {
super(app);
HeaderBar header = new HeaderBar();
MenuButton newButton = new MenuButton();
newButton.iconName = "list-add-symbolic";
newButton.menuModel = GtkMenubar.newMenu.menu;
MenuButton accountsButton = new MenuButton();
accountsButton.iconName = "avatar-default-symbolic";
accountsButton.menuModel = GtkMenubar.accountsMenu.menu;
listButton = Button.newFromIconName("view-list-symbolic");
listButton.onClicked(() -> {
InceptumConfig.listView = true;
InceptumConfig.saveConfig();
generateWindowBody();
});
gridButton = Button.newFromIconName("view-grid-symbolic");
gridButton.onClicked(() -> {
InceptumConfig.listView = false;
InceptumConfig.saveConfig();
generateWindowBody();
});
//TODO search button like boxes
MenuBuilder uiMenu = new MenuBuilder(app, new Menu(), "hamburger");
uiMenu.button("support", () -> Utils.openWebBrowser(new URI("https://git.frohnmeyer-wds.de/JfMods/Inceptum/issues")));
uiMenu.button("preferences", () -> new LauncherSettingsWindow(app).show());
uiMenu.button("about", AboutWindow::createAndShow);
MenuButton menuButton = new MenuButton();
menuButton.iconName = "open-menu-symbolic";
menuButton.menuModel = uiMenu.menu;
header.packStart(newButton);
header.packEnd(menuButton);
header.packEnd(gridButton);
header.packEnd(listButton);
header.packEnd(accountsButton);
instanceList = new ArrayList<>();
instanceListIndex = ListIndexModel.newInstance(instanceList.size());
var selection = new NoSelection(instanceListIndex);
ListView listView = new ListView(selection, new InstanceListEntryFactory(app, instanceList));
listView.addCssClass("rich-list");
listView.showSeparators = true;
listView.onActivate(position -> {
// Double click
GtkMenubar.launch(instanceList.get(position), LaunchType.Client);
});
Frame frame = new Frame((String) null);
frame.child = listView;
frame.marginHorizontal = 24;
frame.marginVertical = 12;
frame.valign = Align.START;
listContainer = new Clamp();
listContainer.maximumSize = 900;
listContainer.child = frame;
gridView = new GridView(selection, new InstanceGridEntryFactory(instanceList));
empty = new StatusPage();
empty.title = I18n.get("main.empty.title");
empty.description = I18n.get("main.empty.description");
//TODO empty.setIconName(new Str());
stack = new Stack();
stack.addChild(listContainer);
stack.addChild(gridView);
stack.addChild(empty);
ScrolledWindow scroll = new ScrolledWindow();
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC);
scroll.child = stack;
setDefaultSize(720, 360);
title = "Inceptum";
titlebar = header;
showMenubar = false;
child = 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 {
InstanceListWatcher isw = new InstanceListWatcher();
addTickCallback((widget, clock) -> {
try {
if (isw.poll()) generateWindowBody();
} catch (IOException e) {
Utils.LOGGER.error("Could not run update task", e);
}
return GLib.SOURCE_CONTINUE;
});
onCloseRequest(() -> {
try {
isw.close();
} catch (IOException ignored) {
}
return false;
});
}
private void generateWindowBody() {
if (listButton != null) listButton.visible = !InceptumConfig.listView;
if (gridButton != null) gridButton.visible = InceptumConfig.listView;
try {
// Unbind then clear
instanceListIndex.size = 0;
instanceList.clear();
// Add new entries
instanceList.addAll(InstanceList.ordered());
instanceListIndex.size = instanceList.size();
// Choose view for this amount of entries
if (InstanceList.isEmpty) stack.visibleChild = empty;
else if (InceptumConfig.listView) stack.visibleChild = listContainer;
else stack.visibleChild = gridView;
// This is called from a tick callback, so re-render
stack.queueResize();
stack.queueDraw();
} catch (IOException e) {
Utils.LOGGER.error("Could not generate window body", e);
}
}
}

View File

@ -1,16 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window;
import org.gnome.gtk.*;
public class NewInstanceWindow extends Assistant {
public NewInstanceWindow(Application app) {
this.application = app;
{
var initialPage = new Box(Orientation.VERTICAL, 8);
initialPage.append(new Label("Importing instances via this assistant is not yet supported, use the ImGUI"));
appendPage(initialPage);
setPageType(initialPage, AssistantPageType.INTRO);
}
}
}

View File

@ -1,81 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.launcher.api.account.*;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.URISyntaxException;
public class MicrosoftLoginDialog extends MessageDialog {
private static DialogFlags flags(boolean modal) {
DialogFlags flags = DialogFlags.DESTROY_WITH_PARENT;
if (modal) flags = flags.or(DialogFlags.MODAL);
return flags;
}
public MicrosoftLoginDialog(@Nullable Window parent) {
this(parent, null, null);
}
public MicrosoftLoginDialog(@Nullable Window parent, @Nullable MicrosoftAccount account) {
this(parent, account, null);
}
public MicrosoftLoginDialog(@Nullable Window parent, @Nullable Runnable onClose) {
this(parent, null, onClose);
}
public MicrosoftLoginDialog(@Nullable Window parent, @Nullable MicrosoftAccount account, @Nullable Runnable onClose) {
super(
parent,
flags(parent != null),
MessageType.QUESTION,
ButtonsType.CLOSE,
I18n.get("auth.description")
);
title = I18n.get("auth.title");
var server = new MicrosoftAuthServer(account);
try {
server.start();
} catch (Exception e) {
Utils.LOGGER.error("Could not start mc login server", e);
}
Runnable finalize = () -> {
server.close();
if (onClose != null) onClose.run();
};
onResponse(responseId -> {
switch (ResponseType.of(responseId)) {
case CLOSE, CANCEL -> {
finalize.run();
this.close();
}
case DELETE_EVENT -> {
finalize.run();
this.destroy();
}
default -> Utils.LOGGER.error("Unexpected response type: " + responseId);
}
});
var btn = Button.newWithLabel(I18n.get("auth.open-browser"));
((Box) messageArea).append(btn);
btn.onClicked(() -> {
try {
Utils.openWebBrowser(new URI(MicrosoftAuthAPI.MICROSOFT_LOGIN_URL));
} catch (URISyntaxException e) {
Utils.LOGGER.error("Could not open browser", e);
}
});
onCloseRequest(() -> {
finalize.run();
return false;
});
}
}

View File

@ -1,86 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend;
import io.gitlab.jfronny.inceptum.gtk.GtkMain;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import org.gnome.glib.GLib;
import org.gnome.gtk.*;
public class ProcessStateWatcherDialog extends MessageDialog {
private final ProcessState state;
private boolean finished = false;
private State cachedState = null;
public static ProcessStateWatcherDialog show(Window parent, String title, String errorMessage, ProcessState state, ThrowingRunnable<?> executor) {
ProcessStateWatcherDialog dialog = new ProcessStateWatcherDialog(parent, title, errorMessage, state, executor);
dialog.show();
return dialog;
}
public ProcessStateWatcherDialog(Window parent, String title, String errorMessage, ProcessState state, ThrowingRunnable<?> executor) {
//TODO alternate UI: Only show progress bar by default, but have a dropdown to a "console" with the actual steps
// this should make visualizing parallelized steps easier
super(parent, DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT), MessageType.INFO, ButtonsType.NONE, null);
this.state = state;
this.title = title;
addButton(I18n.get("cancel"), ResponseType.CANCEL.value);
onResponse(responseId -> {
switch (ResponseType.of(responseId)) {
case CLOSE, CANCEL -> {
state.cancel();
close();
}
case DELETE_EVENT -> destroy();
default -> Utils.LOGGER.error("Unexpected response type: " + responseId);
}
});
onCloseRequest(() -> {
if (finished) return false;
state.cancel();
return false;
});
var progress = new ProgressBar();
((Box) messageArea).append(progress);
addTickCallback((widget, clock) -> {
if (finished) return GLib.SOURCE_REMOVE;
var nc = new State(state);
if (!nc.equals(cachedState)) {
cachedState = nc;
setMarkup(cachedState.msg);
progress.fraction = Math.min(cachedState.progress, 1);
widget.queueDraw();
}
return GLib.SOURCE_CONTINUE;
});
new Thread(() -> {
try {
executor.run();
} catch (Throwable e) {
state.cancel();
Utils.LOGGER.error(errorMessage, e);
GtkEnvBackend.simpleDialog(
parent,
StringFormatter.toString(e),
errorMessage,
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
);
} finally {
finished = true;
GtkMain.schedule(this::close);
}
}).start();
}
record State(String msg, float progress) {
State(ProcessState source) {
this(source.currentStep, source.progress);
}
}
}

View File

@ -1,18 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog;
import org.gnome.gtk.*;
import org.jetbrains.annotations.Nullable;
public class StringInputDialog extends MessageDialog {
private final Entry entry = new Entry();
public StringInputDialog(@Nullable Window parent, DialogFlags flags, MessageType type, ButtonsType buttons, @Nullable String message, String value) {
super(parent, flags, type, buttons, message);
((Box) messageArea).append(entry);
entry.text = value;
}
public String getInput() {
return entry.text;
}
}

View File

@ -1,105 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend;
import io.gitlab.jfronny.inceptum.gtk.GtkMain;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog;
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporter;
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporters;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import org.gnome.gtk.*;
import java.nio.file.Path;
public class ExportTab extends SettingsTab {
private final Instance instance;
public ExportTab(Instance instance, InstanceSettingsWindow window) {
super(window);
this.instance = instance;
section(null, section -> {
{
var row = section.row("instance.settings.export.version", "instance.settings.export.version.subtitle");
row.setEntry(instance.meta.instanceVersion, s -> {
instance.meta.instanceVersion = s;
instance.writeMeta();
});
}
for (Exporter<?> exporter : Exporters.EXPORTERS) {
var row = section.row("instance.settings.export.title", "instance.settings.export.subtitle", exporter.name, exporter.fileExtension);
row.setButton("instance.settings.export", () -> {
FileChooserNative dialog = new FileChooserNative(
I18n.get("instance.settings.export.dialog.title", exporter.name),
window,
FileChooserAction.SAVE,
"_" + I18n.get("save"),
"_" + I18n.get("cancel")
);
var filter = new FileFilter();
filter.name = exporter.name + " Pack";
filter.addPattern("*." + exporter.fileExtension);
dialog.addFilter(filter);
dialog.currentName = exporter.getDefaultFileName(instance);
dialog.onResponse(responseId -> {
if (responseId == ResponseType.ACCEPT.value) {
var file = dialog.file.path;
if (file == null) {
GtkEnvBackend.simpleDialog(
window,
"The path returned by the file dialog is null",
"Could not export",
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
);
return;
}
export(exporter, Path.of(file));
}
});
dialog.show();
});
}
});
}
private void export(Exporter<?> exporter, Path path) {
ProcessState state = new ProcessState(Exporters.STEP_COUNT, "Initializing...");
ProcessStateWatcherDialog.show(
window,
I18n.get("instance.settings.export.dialog.title", exporter.name),
I18n.get("instance.settings.export.dialog.error", instance.name),
state,
() -> {
exporter.generate(state, instance, path);
GtkMain.schedule(() -> {
MessageDialog success = new MessageDialog(
window,
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
MessageType.INFO,
ButtonsType.NONE,
I18n.get("instance.settings.export.dialog.success", instance.name, path.toString())
);
success.title = I18n.get("instance.settings.export.dialog.success.title");
success.addButton(I18n.get("show"), ResponseType.OK.value);
success.addButton(I18n.get("ok"), ResponseType.CANCEL.value);
success.onResponse(responseId1 -> {
switch (ResponseType.of(responseId1)) {
case OK -> {
success.close();
Utils.openFile(path.toFile());
}
case CLOSE, CANCEL -> success.close();
case DELETE_EVENT -> success.destroy();
}
});
success.show();
});
}
);
}
}

View File

@ -1,232 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance;
import io.github.jwharm.javagi.base.GErrorException;
import io.gitlab.jfronny.commons.ArgumentsTokenizer;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab;
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
import io.gitlab.jfronny.inceptum.gtk.util.Memory;
import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi;
import io.gitlab.jfronny.inceptum.launcher.api.McApi;
import io.gitlab.jfronny.inceptum.launcher.model.fabric.FabricVersionLoaderInfo;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.launcher.model.mojang.VersionsList;
import io.gitlab.jfronny.inceptum.launcher.system.instance.*;
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser;
import org.gnome.gio.File;
import org.gnome.gobject.BindingFlags;
import org.gnome.gtk.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
public class GeneralTab extends SettingsTab {
private static final VersionsList VERSIONS = McApi.getVersions();
public GeneralTab(Instance instance, InstanceSettingsWindow window) {
super(window);
section(null, section -> {
var row = section.row("instance.settings.general.name", "instance.settings.general.name.placeholder");
Button apply = Button.newWithLabel(I18n.get("instance.settings.apply"));
Entry entry = row.setEntry(instance.name, s -> apply.sensitive = !s.equals(instance.name));
entry.placeholderText = I18n.get("instance.settings.general.name.placeholder");
apply.valign = Align.CENTER;
apply.onClicked(() -> {
try {
Path newPath = MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(entry.text));
Files.move(instance.path, newPath);
window.close();
new InstanceSettingsWindow(window.application, InstanceList.read(newPath)).show();
} catch (IOException e) {
showError("Could not rename", e);
}
});
apply.sensitive = false;
row.append(apply);
});
section("instance.settings.general.game", section -> {
{
var ref = new Object() {
Switch fabricEnabled = null;
Runnable versionChanged = null;
DropDown fabricVersion = null;
String defaultFabric = null;
String[] fabricVersions = null;
};
var gameRow = section.row("instance.settings.general.game.version", "instance.settings.general.game.version.subtitle");
String[] versions = VERSIONS.versions.stream()
.filter(s -> InceptumConfig.snapshots || s.type.equals("release"))
.map(s -> s.id)
.toArray(String[]::new);
int def = 0;
for (int i = 0; i < versions.length; i++) if (versions[i].equals(instance.gameVersion)) def = i;
gameRow.setDropdown(
versions,
def,
i -> {
instance.meta.gameVersion = instance.isFabric
? GameVersionParser.createVersionWithFabric(versions[i], instance.loaderVersion)
: versions[i];
instance.writeMeta();
ref.versionChanged.run();
}).enableSearch = true;
var fabricRow = section.row("instance.settings.general.game.fabric.enabled", "instance.settings.general.game.fabric.enabled.subtitle");
var loaderRow = section.row("instance.settings.general.game.fabric.version", "instance.settings.general.game.fabric.version.subtitle");
loaderRow.visible = instance.isFabric;
ref.fabricEnabled = fabricRow.setSwitch(instance.isFabric, bl -> {
if (bl) {
if (ref.fabricVersions != null && ref.fabricVersions.length != 0 && ref.defaultFabric != null) {
instance.meta.gameVersion = GameVersionParser.createVersionWithFabric(instance.gameVersion, ref.defaultFabric);
instance.writeMeta();
} else {
ref.fabricEnabled.active = false;
}
} else {
instance.meta.gameVersion = instance.gameVersion;
instance.writeMeta();
}
});
ref.fabricEnabled.bindProperty("active", loaderRow, "visible", BindingFlags.DEFAULT);
ref.versionChanged = () -> {
var ver = VERSIONS.versions.stream()
.filter(s -> s.id.equals(instance.gameVersion))
.findFirst()
.map(FabricMetaApi::getLoaderVersions)
.map(s -> s.toArray(FabricVersionLoaderInfo[]::new));
ref.defaultFabric = instance.isFabric ? instance.loaderVersion : ver
.map(Arrays::stream)
.map(a -> a.filter(s -> s.loader.stable))
.flatMap(Stream::findFirst)
.map(s -> s.loader.version)
.orElse(null);
ref.fabricVersions = ver.map(Arrays::stream)
.map(a -> a.map(s -> s.loader.version).toArray(String[]::new))
.orElse(null);
if (ref.fabricVersions == null || ref.fabricVersions.length == 0) {
ref.fabricEnabled.active = false;
} else if (ref.fabricVersion != null) ref.fabricVersion.model = new StringList(ref.fabricVersions);
};
ref.versionChanged.run();
ref.fabricVersion = loaderRow.setDropdown(ref.fabricVersions, ref.fabricVersions.indexOf(ref.defaultFabric), i -> {
instance.meta.gameVersion = i == -1 ? instance.gameVersion : GameVersionParser.createVersionWithFabric(instance.gameVersion, ref.fabricVersions[i]);
instance.writeMeta();
});
ref.fabricVersion.enableSearch = true;
}
{
var row = section.row("instance.settings.general.game.java", "instance.settings.general.game.java.subtitle");
var entry = row.setEntry(instance.meta.java, s -> {
instance.meta.java = s.isBlank() ? null : s;
instance.writeMeta();
});
var btn = Button.newFromIconName("folder-symbolic");
btn.valign = Align.CENTER;
btn.onClicked(() -> {
FileChooserNative dialog = new FileChooserNative(
I18n.get("instance.settings.general.game.java"),
window,
FileChooserAction.OPEN,
"_" + I18n.get("select"),
"_" + I18n.get("cancel")
);
if (instance.meta.java != null && Files.exists(Path.of(instance.meta.java))) {
try {
dialog.setFile(File.newForPath(instance.meta.java));
} catch (GErrorException e) {
Utils.LOGGER.error("Could not set starting point", e);
}
}
dialog.onResponse(responseId -> {
if (responseId == ResponseType.ACCEPT.value) {
var file = dialog.file.path;
if (file != null) entry.text = file;
}
});
dialog.show();
});
row.append(btn);
}
{
var row = section.row("instance.settings.general.game.memory.min", "instance.settings.general.game.memory.min.subtitle");
row.setSpinButton(instance.meta.minMem == null ? 512 : instance.meta.minMem / Memory.MB, 512, Memory.maxMBForInstance, 128, v -> {
instance.meta.minMem = (long) (v * Memory.MB);
if (instance.meta.minMem == Memory.GB / 2) instance.meta.minMem = null;
instance.writeMeta();
});
}
{
var row = section.row("instance.settings.general.game.memory.max", "instance.settings.general.game.memory.max.subtitle");
row.setSpinButton(instance.meta.maxMem == null ? 1024 : instance.meta.maxMem / Memory.MB, 1024, Memory.maxMBForInstance, 128, v -> {
instance.meta.maxMem = (long) (v * Memory.MB);
if (instance.meta.maxMem == Memory.GB) instance.meta.maxMem = null;
instance.writeMeta();
});
}
});
section("instance.settings.general.args", section -> {
if (instance.meta.arguments == null) instance.meta.arguments = new InstanceMeta.Arguments(List.of(), List.of(), List.of());
if (instance.meta.arguments.jvm == null) instance.meta.arguments = instance.meta.arguments.withJvm(List.of());
if (instance.meta.arguments.client == null) instance.meta.arguments = instance.meta.arguments.withClient(List.of());
if (instance.meta.arguments.server == null) instance.meta.arguments = instance.meta.arguments.withServer(List.of());
{
var row = section.row("instance.settings.general.args.jvm", "instance.settings.general.args.jvm.subtitle");
row.setEntry(ArgumentsTokenizer.join(instance.meta.arguments.jvm.toArray(String[]::new)), s -> {
instance.meta.arguments = instance.meta.arguments.withJvm(List.of(ArgumentsTokenizer.tokenize(s)));
instance.writeMeta();
});
}
{
var row = section.row("instance.settings.general.args.client", "instance.settings.general.args.client.subtitle");
row.setEntry(ArgumentsTokenizer.join(instance.meta.arguments.client.toArray(String[]::new)), s -> {
instance.meta.arguments = instance.meta.arguments.withClient(List.of(ArgumentsTokenizer.tokenize(s)));
instance.writeMeta();
});
}
{
var row = section.row("instance.settings.general.args.server", "instance.settings.general.args.server.subtitle");
row.setEntry(ArgumentsTokenizer.join(instance.meta.arguments.server.toArray(String[]::new)), s -> {
instance.meta.arguments = instance.meta.arguments.withServer(List.of(ArgumentsTokenizer.tokenize(s)));
instance.writeMeta();
});
}
});
section("instance.settings.general.manage", section -> {
{
var row = section.row("instance.delete", "instance.delete.subtitle");
row.setButton("instance.delete", () -> {
MessageDialog dialog = new MessageDialog(window, DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT), MessageType.WARNING, ButtonsType.OK_CANCEL, null);
dialog.markup = I18n.get("instance.delete.confirm");
dialog.title = I18n.get("instance.delete.confirm.title");
dialog.onResponse(responseId -> {
switch (ResponseType.of(responseId)) {
case OK -> {
try {
JFiles.deleteRecursive(instance.path);
dialog.close();
window.close();
} catch (IOException e) {
showError(I18n.get("instance.delete.fail"), e);
}
dialog.close();
}
case CLOSE, CANCEL -> dialog.close();
case DELETE_EVENT -> dialog.destroy();
}
});
dialog.show();
});
}
{
var row = section.row("instance.directory", "instance.directory.subtitle");
row.setButton("instance.directory", () -> Utils.openFile(instance.path.toFile()));
}
});
long timestamp = instance.meta.lastLaunched == null ? 0 : instance.meta.lastLaunched;
append(new ILabel("instance.settings.general.last-launched", ILabel.Mode.SUBTITLE, new Date(timestamp * 1000).toString()));
}
}

View File

@ -1,14 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsWindow;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import org.gnome.gtk.Application;
public class InstanceSettingsWindow extends SettingsWindow {
public InstanceSettingsWindow(Application app, Instance instance) {
super(app);
addTab(new GeneralTab(instance, this), "instance.settings.general", "preferences-other-symbolic");
addTab(new ModsTab(instance, this), "instance.settings.mods", "package-x-generic-symbolic");
addTab(new ExportTab(instance, this), "instance.settings.export", "send-to-symbolic");
}
}

View File

@ -1,13 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
public class ModsTab extends SettingsTab {
public ModsTab(Instance instance, InstanceSettingsWindow window) {
super(window);
append(new ILabel("instance.settings.mods.unsupported"));
//TODO implement this, somehow
}
}

View File

@ -1,58 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher;
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
import io.gitlab.jfronny.inceptum.gtk.control.ILabel;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab;
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog;
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
import org.gnome.gtk.*;
public class AccountsTab extends SettingsTab implements SettingsTab.SectionBuilder {
public AccountsTab(Window window) {
super(window);
section(null, this);
}
@Override
public void build(Section section) {
generateRows(section);
Button row = Button.newFromIconName("list-add-symbolic");
section.row(row);
row.onClicked(() -> new MicrosoftLoginDialog(window, () -> {
section.clear();
build(section);
GtkMenubar.generateAccountsMenu(window.application);
}).show());
}
private void generateRows(SectionBuilder.Section section) {
for (MicrosoftAccount account : AccountManager.getAccounts()) {
Box row = new Box(Orientation.HORIZONTAL, 40);
var ref = section.row(row);
row.margin = 8;
//TODO profile icon
Box head = new Box(Orientation.VERTICAL, 0);
head.hexpand = true;
head.halign = Align.START;
head.valign = Align.CENTER;
Label title = new Label(account.minecraftUsername);
title.halign = Align.START;
head.append(title);
Label subtitle = new Label(account.uuid);
ILabel.theme(subtitle, ILabel.Mode.SUBTITLE);
subtitle.halign = Align.START;
head.append(subtitle);
row.append(head);
Button remove = Button.newFromIconName("window-close-symbolic");
remove.valign = Align.CENTER;
remove.halign = Align.END;
remove.onClicked(() -> {
AccountManager.removeAccount(account);
section.remove(ref);
GtkMenubar.generateAccountsMenu(window.application);
});
row.append(remove);
}
}
}

View File

@ -1,36 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher;
import io.gitlab.jfronny.inceptum.common.InceptumConfig;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateChannel;
import io.gitlab.jfronny.inceptum.gtk.control.settings.IRow;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab;
import org.gnome.gtk.Window;
public class GeneralTab extends SettingsTab {
public GeneralTab(Window window) {
super(window);
section(null, section -> {
{
IRow row = section.row("settings.general.snapshots", "settings.general.snapshots.subtitle");
row.setSwitch(InceptumConfig.snapshots, b -> {
InceptumConfig.snapshots = b;
InceptumConfig.saveConfig();
});
}
{
IRow row = section.row("settings.general.update-channel", "settings.general.update-channel.subtitle");
row.setDropdown(new String[] {"Stable", "CI"}, InceptumConfig.channel == UpdateChannel.CI ? 1 : 0, state -> {
InceptumConfig.channel = state == 1 ? UpdateChannel.CI : UpdateChannel.Stable;
InceptumConfig.saveConfig();
});
}
{
IRow row = section.row("settings.general.author-name", "settings.general.author-name.subtitle");
row.setEntry(InceptumConfig.authorName, s -> {
InceptumConfig.authorName = s;
InceptumConfig.saveConfig();
});
}
});
}
}

View File

@ -1,12 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher;
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsWindow;
import org.gnome.gtk.Application;
public class LauncherSettingsWindow extends SettingsWindow {
public LauncherSettingsWindow(Application app) {
super(app);
addTab(new GeneralTab(this), "settings.general", "preferences-other-symbolic");
addTab(new AccountsTab(this), "settings.accounts", "system-users-symbolic");
}
}

View File

@ -0,0 +1,106 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.markup
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
import io.gitlab.jfronny.inceptum.gtk.window.dialog.StringInputDialog
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv.EnvBackend
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
import org.gnome.gtk.*
import java.util.function.Consumer
object GtkEnvBackend : EnvBackend {
@JvmField
var dialogParent: Window? = null
override fun showError(message: String, title: String) {
Utils.LOGGER.error(message)
simpleDialog(message, title, MessageType.ERROR, ButtonsType.CLOSE, null, null)
}
override fun showError(message: String, t: Throwable) {
simpleDialog(StringFormatter.toString(t), message, MessageType.ERROR, ButtonsType.CLOSE, null, null)
}
override fun showInfo(message: String, title: String) {
Utils.LOGGER.info(message)
simpleDialog(message, title, MessageType.INFO, ButtonsType.CLOSE, null, null)
}
override fun showOkCancel(message: String, title: String, ok: Runnable, cancel: Runnable, defaultCancel: Boolean) {
Utils.LOGGER.info(message)
simpleDialog(message, title, MessageType.QUESTION, ButtonsType.OK_CANCEL, ok, cancel)
}
override fun getInput(
prompt: String,
details: String,
defaultValue: String,
ok: Consumer<String>,
cancel: Runnable
) = schedule {
var flags = DialogFlags.DESTROY_WITH_PARENT
if (dialogParent != null) flags = flags.or(DialogFlags.MODAL)
val dialog = StringInputDialog(
dialogParent,
flags,
MessageType.QUESTION,
ButtonsType.OK_CANCEL,
details,
defaultValue
)
dialog.title = prompt
dialog.onResponse(processResponses(dialog, { ok.accept(dialog.input) }, cancel))
dialog.show()
}
override fun showLoginRefreshPrompt(account: MicrosoftAccount) =
schedule { MicrosoftLoginDialog(dialogParent, account).show() }
private fun simpleDialog(
markup: String,
title: String,
type: MessageType,
buttons: ButtonsType,
ok: Runnable?,
cancel: Runnable?
) = schedule { simpleDialog(dialogParent, markup, title, type, buttons, ok, cancel) }
@JvmStatic
fun simpleDialog(
parent: Window?,
markup: String,
title: String,
type: MessageType?,
buttons: ButtonsType?,
ok: Runnable?,
cancel: Runnable?
) {
val dialog =
MessageDialog(parent, DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT), type, buttons, null)
dialog.title = title
dialog.markup = markup
dialog.onResponse(processResponses(dialog, ok, cancel))
dialog.show()
}
private fun processResponses(dialog: Dialog, ok: Runnable?, cancel: Runnable?): Dialog.Response {
return Dialog.Response { responseId: Int ->
when (ResponseType.of(responseId)) {
ResponseType.OK -> {
dialog.close()
ok?.run()
}
ResponseType.CLOSE, ResponseType.CANCEL -> {
dialog.close()
cancel?.run()
}
ResponseType.DELETE_EVENT -> dialog.destroy()
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
}
}
}
}

View File

@ -0,0 +1,63 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.inceptum.common.BuildMetadata
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.window.MainWindow
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
import org.gnome.gio.ApplicationFlags
import org.gnome.glib.GLib
import org.gnome.gtk.*
import java.io.IOException
import java.util.*
import java.util.function.Consumer
import kotlin.system.exitProcess
object GtkMain {
const val ID = "io.gitlab.jfronny.inceptum"
@Throws(IOException::class)
@JvmStatic
fun main(args: Array<String>) {
LauncherEnv.initialize(GtkEnvBackend)
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION)
Utils.LOGGER.info("Loading from " + MetaHolder.BASE_PATH)
exitProcess(try {
showGui(args)
} catch (_: Throwable) {
-1
} finally {
LauncherEnv.terminate()
})
}
@JvmStatic
fun showGui(args: Array<String>): Int {
return setupApplication(args) {
//TODO update check
AccountManager.loadAccounts()
GtkMenubar.create(this)
val window = MainWindow(this)
window.show()
GtkEnvBackend.dialogParent = window
window.onCloseRequest {
GtkEnvBackend.dialogParent = null
this.quit()
false
}
}
}
@JvmStatic
fun setupApplication(args: Array<String>, onActivate: Application.() -> Unit): Int {
val app = Application(ID, ApplicationFlags.FLAGS_NONE)
app.onActivate {
GLib.idleAdd {
runScheduledTasks()
true
}
onActivate(app)
}
return app.run(args)
}
}

View File

@ -0,0 +1,209 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.commons.io.JFiles
import io.gitlab.jfronny.commons.ref.R
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.window.AboutWindow
import io.gitlab.jfronny.inceptum.gtk.window.NewInstanceWindow
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
import io.gitlab.jfronny.inceptum.gtk.window.settings.launcher.LauncherSettingsWindow
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
import io.gitlab.jfronny.inceptum.launcher.system.importer.Importers
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
import io.gitlab.jfronny.inceptum.launcher.system.launch.InstanceLauncher
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
import org.gnome.gtk.*
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.io.IOException
import java.nio.file.Path
import java.util.*
object GtkMenubar {
@JvmField
var newMenu: MenuBuilder? = null
@JvmField
var accountsMenu: MenuBuilder? = null
@JvmField
var launchMenu: MenuBuilder? = null
@JvmStatic
fun create(app: Application) {
val menu = MenuBuilder(app)
val file = menu.submenu("file")
newMenu = file.submenu("new")
generateNewMenu(app)
file.button("redownload") {
val state = ProcessState(3 + Steps.STEPS.size * InstanceList.size(), "Initializing")
ProcessStateWatcherDialog.show(
GtkEnvBackend.dialogParent,
"Reloading data",
"Could not execute refresh task",
state
) {
state.incrementStep("Clearing cache directories")
JFiles.clearDirectory(MetaHolder.ASSETS_DIR)
JFiles.clearDirectory(MetaHolder.LIBRARIES_DIR) { path: Path -> !path.startsWith(MetaHolder.LIBRARIES_DIR.resolve("io/gitlab/jfronny")) }
JFiles.clearDirectory(MetaHolder.NATIVES_DIR) { path: Path -> !path.startsWith(MetaHolder.NATIVES_DIR.resolve("forceload")) }
JFiles.clearDirectory(MetaHolder.CACHE_DIR)
if (state.isCancelled) return@show
state.incrementStep("Reloading instance list")
InstanceList.reset()
InstanceList.forEach<IOException> { instance: Instance? ->
if (state.isCancelled) return@forEach
Steps.reDownload(instance, state)
}
}
}
file.button("exit") { app.quit() }
launchMenu = menu.submenu("launch")
generateLaunchMenu(app)
accountsMenu = menu.submenu("account")
generateAccountsMenu(app)
val help = menu.submenu("help")
help.button("about") { AboutWindow.createAndShow() }
help.button("log") {
//TODO
}
}
@JvmStatic
fun generateNewMenu(app: Application) {
newMenu!!.clear()
newMenu!!.button("new") { NewInstanceWindow(app).show() }
newMenu!!.button("file") {
val dialog = FileChooserNative(
I18n["menu.file.new.file"],
GtkEnvBackend.dialogParent,
FileChooserAction.OPEN,
"_" + I18n["select"],
"_" + I18n["cancel"]
)
val filter = FileFilter()
filter.addPattern("*.zip")
filter.addPattern("*.mrpack")
dialog.addFilter(filter)
dialog.onResponse { responseId: Int ->
if (responseId == ResponseType.ACCEPT.value) {
val file = dialog.file!!.path
if (file == null) {
LauncherEnv.showError("The path returned by the file dialog is null", "Could not import")
return@onResponse
}
val state = ProcessState(Importers.MAX_STEPS, "Initializing")
ProcessStateWatcherDialog.show(
GtkEnvBackend.dialogParent,
I18n["menu.file.new.file"],
I18n["menu.file.new.file.error"],
state
) {
Importers.importPack(Path.of(file), state)
}
}
}
dialog.show()
}
newMenu!!.button("url") {
LauncherEnv.getInput(
I18n["menu.file.new.url"],
I18n["menu.file.new.url.details"],
Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor) as String,
{ s: String? ->
val state = ProcessState(Importers.MAX_STEPS, "Initializing")
ProcessStateWatcherDialog.show(
GtkEnvBackend.dialogParent,
I18n["menu.file.new.url"],
I18n["menu.file.new.url.error"],
state
) {
Importers.importPack(s, state)
}
}, {})
}
}
@JvmStatic
fun generateLaunchMenu(app: Application) {
launchMenu!!.clear()
try {
InstanceList.forEach<RuntimeException> { entry: Instance ->
launchMenu!!.literalButton(entry.id + ".launch", entry.toString()) {
launch(entry, LaunchType.Client)
}
}
} catch (e: IOException) {
Utils.LOGGER.error("Could not generate launch menu", e)
}
}
@JvmStatic
fun launch(instance: Instance, launchType: LaunchType) {
if (instance.isSetupLocked) {
LauncherEnv.showError(I18n["instance.launch.locked.setup"], I18n["instance.launch.locked"])
} else if (instance.isRunningLocked) {
LauncherEnv.showOkCancel(
I18n["instance.launch.locked.running"],
I18n["instance.launch.locked"]
) { forceLaunch(instance, launchType) }
} else forceLaunch(instance, launchType)
}
private fun forceLaunch(instance: Instance, launchType: LaunchType) {
val state = Steps.createProcessState()
ProcessStateWatcherDialog.show(
GtkEnvBackend.dialogParent,
I18n["instance.launch.title"],
I18n["instance.launch.error"],
state
) {
try {
Steps.reDownload(instance, state)
} catch (e: IOException) {
Utils.LOGGER.error("Could not fetch instance, trying to start anyways", e)
}
if (state.isCancelled) return@show
state.updateStep("Starting Game")
try {
if (launchType == LaunchType.Client) InstanceLauncher.launchClient(instance)
else InstanceLauncher.launch(
instance,
launchType,
false,
AccountManager.NULL_AUTH
)
} catch (e: Throwable) {
LauncherEnv.showError("Could not start instance", e)
}
}
}
@JvmStatic
fun generateAccountsMenu(app: Application) {
accountsMenu!!.clear()
accountsMenu!!.button("new") { MicrosoftLoginDialog(GtkEnvBackend.dialogParent).show() }
accountsMenu!!.button("manage") {
val window = LauncherSettingsWindow(app)
window.activePage = "settings.accounts"
window.show()
}
val accounts: MutableList<MicrosoftAccount?> = ArrayList(AccountManager.getAccounts())
accounts.add(null)
accountsMenu!!.literalRadio(
"account",
accounts[AccountManager.getSelectedIndex()],
accounts,
{ _, acc: MicrosoftAccount? ->
if (acc == null) return@literalRadio I18n["account.none"]
acc.minecraftUsername
}) { account: MicrosoftAccount? -> AccountManager.switchAccount(account) }
}
}

View File

@ -0,0 +1,22 @@
package io.gitlab.jfronny.inceptum.gtk
import io.gitlab.jfronny.inceptum.common.Utils
import java.util.*
import java.util.ArrayDeque
private val SCHEDULED: Queue<Runnable> = ArrayDeque()
fun schedule(task: Runnable) {
SCHEDULED.add(task)
}
fun runScheduledTasks() {
var r: Runnable?
while (SCHEDULED.poll().also { r = it } != null) {
try {
r!!.run()
} catch (t: Throwable) {
Utils.LOGGER.error("Could not run scheduled task", t)
}
}
}

View File

@ -0,0 +1,49 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.gitlab.jfronny.inceptum.gtk.GtkMain
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import org.gnome.gtk.CssProvider
import org.gnome.gtk.Gtk
import org.gnome.gtk.Label
import org.jetbrains.annotations.PropertyKey
class ILabel(str: @PropertyKey(resourceBundle = I18n.BUNDLE) String, mode: Mode, vararg args: Any?) :
Label(I18n.get(str, *args)) {
constructor(str: @PropertyKey(resourceBundle = I18n.BUNDLE) String, vararg args: Any?) : this(str, Mode.NORMAL, *args)
init {
theme(this, mode)
}
enum class Mode {
NORMAL,
HEADING,
SUBTITLE
}
companion object {
private val provider by lazy {
val provider = CssProvider()
try {
GtkMain::class.java.classLoader.getResourceAsStream("inceptum.css")!!
.use { provider.loadFromData(it.readAllBytes()) }
} catch (t: Throwable) {
throw RuntimeException(t)
}
provider
}
@JvmStatic
fun theme(label: Label, mode: Mode) {
when (mode) {
Mode.HEADING -> label.addCssClass("heading")
Mode.SUBTITLE -> {
label.addCssClass("jf-subtitle")
label.styleContext.addProvider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}
Mode.NORMAL -> {}
}
}
}
}

View File

@ -0,0 +1,72 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.github.jwharm.javagi.util.ListIndexModel.ListIndex
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import org.gnome.gtk.*
import org.pango.EllipsizeMode
import org.pango.WrapMode
class InstanceGridEntryFactory(instanceList: List<Instance>) : SignalListItemFactory() {
init {
//TODO better design
onSetup { item ->
val box = Box(Orientation.VERTICAL, 5)
val thumbnail = InstanceThumbnail()
box.append(thumbnail)
val label = Label(null as String?)
label.setSizeRequest(192, -1)
label.maxWidthChars = 20
label.justify = Justification.CENTER
label.halign = Align.START
label.hexpand = true
label.valign = Align.CENTER
label.ellipsize = EllipsizeMode.MIDDLE
label.lines = 3
label.wrap = true
label.wrapMode = WrapMode.WORD_CHAR
label.marginTop = 10
box.append(label)
// 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 as ListItem).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()));
val li = item as ListItem
val box = li.getChild() as Box
val thumbnail = InstanceThumbnail.castFrom(box.firstChild as Stack)
val label = thumbnail.nextSibling as Label
val instance = instanceList[(li.item as ListIndex).index]
thumbnail.bind(instance)
label.text = instance.toString()
}
}
}

View File

@ -0,0 +1,182 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.github.jwharm.javagi.base.Signal
import io.github.jwharm.javagi.util.ListIndexModel.ListIndex
import io.gitlab.jfronny.commons.io.JFiles
import io.gitlab.jfronny.commons.ref.R
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.fixSubtitle
import io.gitlab.jfronny.inceptum.gtk.util.margin
import io.gitlab.jfronny.inceptum.gtk.window.settings.instance.InstanceSettingsWindow
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
import org.gnome.adw.ActionRow
import org.gnome.gio.Menu
import org.gnome.gtk.*
import java.io.IOException
import java.util.function.Consumer
class InstanceListEntryFactory(app: Application?, instanceList: List<Instance>) : SignalListItemFactory() {
init {
onSetup {
val li = it as ListItem
val thumbnail = InstanceThumbnail()
thumbnail.name = "inceptum-thumbnail"
val launch = Button.newFromIconName("media-playback-start-symbolic")
launch.addCssClass("flat")
launch.name = "inceptum-launch"
launch.tooltipText = I18n["instance.launch"]
launch.hasTooltip = true
val menu = MenuButton()
menu.addCssClass("flat")
menu.iconName = "view-more-symbolic"
menu.setPopover(PopoverMenu.newFromModel(Menu()))
val row = ActionRow()
row.margin = 8
row.name = "inceptum-row"
row.removeCssClass("activatable") //TODO remove this workaround if a better way to support opening the menu is found
row.addPrefix(thumbnail)
row.addSuffix(launch)
row.addSuffix(menu)
row.fixSubtitle()
val rightClicked = GestureClick()
rightClicked.button = 3
rightClicked.onPressed { nPress, _, _ -> if (nPress == 1) menu.emitActivate() }
row.addController(rightClicked)
li.child = row
}
val toDisconnect: MutableMap<String, MutableSet<Signal<*>>> = HashMap()
onBind {
val li = Decomposed.of(it as ListItem, instanceList)
if (li.instance.isLocked) {
li.item.activatable = false
li.row.setSubtitle(
if (li.instance.isRunningLocked) I18n["instance.launch.locked.running"]
else I18n["instance.launch.locked.setup"]
)
}
li.row.title = li.instance.toString()
li.thumbnail.bind(li.instance)
val menuBuilder = MenuBuilder(li.popoverMenu, li.instance.id)
val launchSection = menuBuilder.literalSection("launch", null)
val kill = launchSection.literalButton("kill", I18n["instance.kill"]) {
//TODO test
LauncherEnv.showOkCancel(I18n["instance.kill.prompt"], I18n["instance.kill.details"]) {
if (!li.instance.kill()) LauncherEnv.showError(I18n["instance.kill.fail"], I18n["failed"])
}
}
kill.enabled = li.instance.isRunningLocked
launchSection.literalButton(
"launch.client", I18n["instance.launch.client"]
) { GtkMenubar.launch(li.instance, LaunchType.Client) }.iconName = "media-playback-start-symbolic"
launchSection.literalButton(
"launch.server", I18n["instance.launch.server"]
) { GtkMenubar.launch(li.instance, LaunchType.Server) }.iconName = "network-server-symbolic"
val settingsSection = menuBuilder.literalSection("settings", null)
settingsSection.literalButton("settings", I18n["instance.settings"]) {
//TODO keep track of properties windows and don't allow opening two
InstanceSettingsWindow(app, li.instance).show()
}.iconName = "document-edit-symbolic"
settingsSection.literalButton(
"directory", I18n["instance.directory"]
) { Utils.openFile(li.instance.path.toFile()) }.iconName = "folder-symbolic"
settingsSection.literalButton("copy", I18n["instance.copy"]) {
LauncherEnv.getInput(
I18n["instance.copy.prompt"],
I18n["instance.copy.details"],
InstanceNameTool.getNextValid(li.instance.name),
{ s: String? ->
try {
JFiles.copyRecursive(
li.instance.path,
MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(s))
)
} catch (e: IOException) {
LauncherEnv.showError(I18n["instance.copy.fail"], e)
}
}) { R.nop() }
}.iconName = "edit-copy-symbolic"
settingsSection.literalButton("delete", I18n["instance.delete"]) {
LauncherEnv.showOkCancel(
I18n["instance.delete.confirm"],
I18n["instance.delete.confirm.title"]
) {
try {
JFiles.deleteRecursive(li.instance.path)
} catch (e: IOException) {
LauncherEnv.showError(I18n["instance.delete.fail"], e)
}
}
}.iconName = "edit-delete-symbolic"
val dc = Consumer { s: Signal<*> ->
toDisconnect.computeIfAbsent(li.instance.id) { _ -> HashSet() }
.add(s)
}
dc.accept(li.launch.onClicked { GtkMenubar.launch(li.instance, LaunchType.Client) })
}
onUnbind {
val li = Decomposed.of(it as ListItem, instanceList)
li.popoverMenu.insertActionGroup(li.instance.id, null)
toDisconnect[li.instance.id]!!
.forEach(Consumer { obj: Signal<*> -> obj.disconnect() })
}
}
private class Decomposed(
item: ListItem,
instance: Instance,
row: ActionRow,
thumbnail: InstanceThumbnail,
launch: Button,
popoverMenu: PopoverMenu
) {
val item: ListItem
val instance: Instance
val row: ActionRow
val thumbnail: InstanceThumbnail
val launch: Button
val popoverMenu: PopoverMenu
init {
this.item = item
this.instance = instance
this.row = row
this.thumbnail = thumbnail
this.launch = launch
this.popoverMenu = popoverMenu
}
companion object {
fun of(item: ListItem, instanceList: List<Instance>): Decomposed {
val instance = instanceList[(item.item as ListIndex?)!!.index]
val row = item.child as ActionRow
val prefixes = row.firstChild!!.firstChild as Box
val suffixes = row.firstChild!!.lastChild as Box
val thumbnail = InstanceThumbnail.castFrom(prefixes.firstChild as Stack)
val launch = suffixes.firstChild as Button
val menuButton = launch.nextSibling as MenuButton
val popoverMenu = menuButton.popover as PopoverMenu
return Decomposed(item, instance, row, thumbnail, launch, popoverMenu)
}
}
}
}

View File

@ -0,0 +1,48 @@
package io.gitlab.jfronny.inceptum.gtk.control
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import org.gnome.gtk.Image
import org.gnome.gtk.Spinner
import org.gnome.gtk.Stack
import java.lang.foreign.Addressable
class InstanceThumbnail : Stack {
private constructor(address: Addressable) : super(address)
constructor() : super() {
val spinner = Spinner()
val image = Image()
val generic = Image()
spinner.name = SPINNER
image.name = IMAGE
generic.name = GENERIC
generic.setFromIconName("media-playback-start-symbolic") //TODO better default icon
addNamed(spinner, SPINNER)
addNamed(image, IMAGE)
addNamed(generic, GENERIC)
}
fun bind(entry: Instance) {
val spinner = getChildByName(SPINNER) as Spinner
val image = getChildByName(IMAGE) as Image //TODO
val generic = getChildByName(GENERIC) as Image
//TODO mark instance being played
visibleChild = if (entry.isSetupLocked) {
spinner
} else if (false) { // if the instance has an image, load the image data and set it as the visible child
image
} else {
generic
}
}
companion object {
private const val SPINNER = "spinner"
private const val IMAGE = "image"
private const val GENERIC = "generic"
@JvmStatic
fun castFrom(stack: Stack): InstanceThumbnail {
return InstanceThumbnail(stack.handle())
}
}
}

View File

@ -0,0 +1,89 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.margin
import org.gnome.gtk.*
import org.jetbrains.annotations.PropertyKey
import java.util.function.Consumer
import java.util.function.DoubleConsumer
import java.util.function.IntConsumer
class IRow(
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
vararg args: Any?
) : Box(Orientation.HORIZONTAL, 40) {
init {
margin = 8
val head: Widget
val lab = ILabel(title, *args)
lab.halign = Align.START
if (subtitle != null) {
val headB = Box(Orientation.VERTICAL, 0)
headB.append(lab)
val lab1 = ILabel(subtitle, ILabel.Mode.SUBTITLE, *args)
lab1.halign = Align.START
headB.append(lab1)
head = headB
} else {
head = lab
}
head.halign = Align.START
head.valign = Align.CENTER
append(head)
}
fun setButton(text: @PropertyKey(resourceBundle = I18n.BUNDLE) String, action: Button.Clicked?): Button {
return Button.newWithLabel(I18n[text]).apply {
packSmallEnd()
onClicked(action)
}
}
fun setDropdown(options: Array<String>, defaultIndex: Int, changed: IntConsumer): DropDown {
return DropDown(StringList(options), null).apply {
packSmallEnd()
selected = defaultIndex
onNotify("selected") { _ -> changed.accept(selected) }
expression = PropertyExpression(StringObject.getType(), null, "string")
}
}
fun setSwitch(value: Boolean, changed: Consumer<Boolean>): Switch {
return Switch().apply {
packSmallEnd()
active = value
onStateSet { state: Boolean ->
changed.accept(state)
false
}
}
}
fun setSpinButton(value: Double, min: Double, max: Double, step: Double, changed: DoubleConsumer): SpinButton {
return SpinButton.newWithRange(min, max, step).apply {
packSmallEnd()
this.value = value
onValueChanged { changed.accept(this.value) }
}
}
fun setEntry(value: String?, changed: Consumer<String>): Entry {
return Entry().apply {
text = value ?: ""
hexpand = true
valign = Align.CENTER
halign = Align.FILL
onChanged { changed.accept(text) }
append(this)
}
}
private fun Widget.packSmallEnd() {
firstChild!!.hexpand = true
valign = Align.CENTER
halign = Align.END
this@IRow.append(this)
}
}

View File

@ -0,0 +1,90 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.marginHorizontal
import org.gnome.gtk.*
import org.jetbrains.annotations.PropertyKey
import java.util.concurrent.atomic.AtomicInteger
open class SettingsTab(window: Window?) : Box(Orientation.VERTICAL, 8) {
@JvmField
protected val window: Window?
init {
marginHorizontal = 24
marginTop = 12
this.window = window
}
protected fun section(title: @PropertyKey(resourceBundle = I18n.BUNDLE) String?, builder: Section.() -> Unit) {
if (title != null) append(ILabel(title, ILabel.Mode.HEADING))
val frame = Frame(null as String?)
val listBox = ListBox()
listBox.selectionMode = SelectionMode.NONE
listBox.showSeparators = true
frame.child = listBox
val count = AtomicInteger(0)
builder(object : Section {
override fun row(title: String, subtitle: String?, vararg args: Any?, build: IRow.() -> Unit): IRow {
val row = IRow(title, subtitle, *args)
row(row)
build(row)
return row
}
override fun row(row: Widget): ListBoxRow {
listBox.append(row)
return listBox.getRowAtIndex(count.getAndIncrement())!!
}
override fun remove(row: Widget) {
listBox.remove(row)
count.decrementAndGet()
}
override fun clear() {
var i = 0
val len = count.getAndSet(0)
while (i < len) {
listBox.remove(listBox.getRowAtIndex(0))
i++
}
}
})
append(frame)
}
interface Section {
fun row(
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
vararg args: Any?
): IRow = row(title, subtitle, *args, build = {})
fun row(
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
vararg args: Any?,
build: IRow.() -> Unit
): IRow
fun row(row: Widget): ListBoxRow?
fun remove(row: Widget)
fun clear()
}
protected fun showError(message: String, t: Throwable) {
GtkEnvBackend.simpleDialog(
window,
StringFormatter.toString(t),
message,
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
)
}
}

View File

@ -0,0 +1,51 @@
package io.gitlab.jfronny.inceptum.gtk.control.settings
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import org.gnome.adw.HeaderBar
import org.gnome.adw.ViewStack
import org.gnome.adw.ViewSwitcherBar
import org.gnome.adw.ViewSwitcherTitle
import org.gnome.gobject.BindingFlags
import org.gnome.gtk.*
import org.jetbrains.annotations.PropertyKey
open class SettingsWindow(app: Application?) : Window() {
@JvmField
protected val stack: ViewStack
init {
application = app
stack = ViewStack()
val header = HeaderBar()
val viewSwitcher = ViewSwitcherTitle()
viewSwitcher.stack = stack
header.titleWidget = viewSwitcher
titlebar = header
val scroll = ScrolledWindow()
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC)
scroll.child = stack
scroll.vexpand = true
val bottomBar = ViewSwitcherBar()
bottomBar.stack = stack
viewSwitcher.bindProperty("title-visible", bottomBar, "reveal", BindingFlags.DEFAULT)
val view = Box(Orientation.VERTICAL, 0)
view.append(scroll)
view.append(bottomBar)
child = view
setDefaultSize(720, 360)
}
fun addTab(tab: SettingsTab?, title: @PropertyKey(resourceBundle = I18n.BUNDLE) String, iconName: String) {
stack.addTitledWithIcon(tab, title, I18n[title], iconName)
}
var activePage: String
get() = stack.visibleChildName!!
set(@PropertyKey(resourceBundle = I18n.BUNDLE) title) { stack.visibleChildName = title }
}

View File

@ -0,0 +1,6 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import org.gnome.gio.MenuItem
import org.gnome.gio.SimpleAction
class BuiltButtonItem(action: SimpleAction, menuItem: MenuItem?) : BuiltMenuItem(action, menuItem)

View File

@ -0,0 +1,24 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import org.gnome.gio.MenuItem
import org.gnome.gio.SimpleAction
import org.gnome.gio.ThemedIcon
abstract class BuiltMenuItem protected constructor(action: SimpleAction, @JvmField protected val menuItem: MenuItem?) {
@JvmField
protected val action: SimpleAction
init {
this.action = action
}
var enabled: Boolean
get() = action.enabled
set(enabled) {
action.enabled = enabled
}
var iconName: String?
set(iconName) { menuItem!!.setIcon(ThemedIcon(iconName)) }
get() = throw NotImplementedError()
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import org.gnome.gio.SimpleAction
import org.gnome.glib.Variant
class BuiltRadioItem<T>(action: SimpleAction, private val options: List<T>) : BuiltMenuItem(action, null) {
var selected: T
get() = options[action.getState()!!.getInt32()]
set(selected) {
action.setState(Variant.newInt32(options.indexOf(selected)))
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import org.gnome.gio.MenuItem
import org.gnome.gio.SimpleAction
import org.gnome.glib.Variant
class BuiltToggleItem(action: SimpleAction, menuItem: MenuItem?) : BuiltMenuItem(action, menuItem) {
var state: Boolean
get() = action.getState()!!.boolean
set(state) {
action.state = Variant.newBoolean(state)
}
fun toggle(): Boolean {
val toggled = !state
state = toggled
return toggled
}
}

View File

@ -0,0 +1,171 @@
package io.gitlab.jfronny.inceptum.gtk.menu
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import org.gnome.gio.*
import org.gnome.glib.Variant
import org.gnome.glib.VariantType
import org.gnome.gtk.Application
import org.gnome.gtk.MenuButton
import org.gnome.gtk.PopoverMenu
import java.util.*
import java.util.function.BiFunction
import java.util.function.Consumer
class MenuBuilder private constructor(map: ActionMap, menu: Menu, prefix: String, groupName: String) {
private val map: ActionMap
private val refs: MutableMap<String, Action> = LinkedHashMap()
val menu: Menu
private val prefix: String
private val groupName: String
constructor(menu: PopoverMenu, groupName: String) : this(
insertMap(menu, groupName),
menu.menuModel as Menu,
"",
groupName
)
@JvmOverloads
constructor(app: Application, menu: Menu = getRootMenu(app), prefix: String = "") : this(app, menu, prefix, "app.")
init {
fun String.suffix() = if (isNotEmpty() && !endsWith(".")) "$this." else this
this.map = map
this.menu = menu
this.prefix = prefix.suffix()
this.groupName = groupName.suffix()
}
fun button(name: String, onClick: ThrowingRunnable<*>): BuiltButtonItem {
return literalButton(name, I18n["menu.$prefix$name"], onClick)
}
fun literalButton(internalName: String, label: String?, onClick: ThrowingRunnable<*>): BuiltButtonItem {
var internalName = internalName
internalName = prefix + internalName
val action = SimpleAction(internalName, null)
addAction(internalName, action)
action.onActivate { _ ->
try {
onClick.run()
} catch (e: Throwable) {
Utils.LOGGER.error("Could not execute action", e)
}
}
val menuItem = MenuItem(label, groupName + internalName)
menu.appendItem(menuItem)
action.enabled = true
return BuiltButtonItem(action, menuItem)
}
fun toggle(name: String, initial: Boolean, onToggle: Consumer<Boolean?>): BuiltToggleItem {
var name = name
name = prefix + name
val action = SimpleAction.newStateful(name, null, Variant.newBoolean(initial))
addAction(name, action)
action.onActivate { _ ->
val state = !action.getState()!!.boolean
action.state = Variant.newBoolean(state)
onToggle.accept(state)
}
val menuItem = MenuItem(I18n["menu.$name"], groupName + name)
menu.appendItem(menuItem)
return BuiltToggleItem(action, menuItem)
}
fun <T> radio(name: String, initial: T, options: List<T>, onCheck: Consumer<T>): BuiltRadioItem<T> {
return literalRadio(name, initial, options, { i, _ -> I18n["menu.$prefix$name", i] }, onCheck)
}
fun <T> literalRadio(
name: String,
initial: T,
options: List<T>,
stringifier: BiFunction<Int, T, String?>,
onCheck: Consumer<T>
): BuiltRadioItem<T> {
var name = name
name = prefix + name
val action = SimpleAction.newStateful(name, VariantType("i"), Variant.newInt32(options.indexOf(initial)))
addAction(name, action)
action.onActivate { variant: Variant? ->
action.state = variant
onCheck.accept(options[variant!!.getInt32()])
}
for ((i, option) in options.withIndex()) {
menu.appendItem(MenuItem(stringifier.apply(i, option), "$groupName$name($i)"))
}
return BuiltRadioItem(action, options)
}
fun submenu(name: String): MenuBuilder {
return literalSubmenu(name, I18n["menu.$prefix$name"])
}
fun literalSubmenu(name: String, label: String?): MenuBuilder {
var name = name
name = prefix + name
val submenu = Menu()
menu.appendSubmenu(label, submenu)
return MenuBuilder(map, submenu, name, groupName)
}
fun section(name: String): MenuBuilder {
return literalSection(name, I18n["section.$prefix$name"])
}
fun literalSection(name: String, label: String?): MenuBuilder {
var name = name
name = prefix + name
val section = Menu()
menu.appendSection(label, section)
return MenuBuilder(map, section, name, groupName)
}
fun clear() {
menu.removeAll()
refs.forEach { (name, _) -> map.removeAction(name) }
refs.clear()
}
private fun addAction(name: String, action: SimpleAction) {
map.addAction(action)
refs[name] = action
}
fun asPopover(): PopoverMenu {
return PopoverMenu.newFromModel(menu)
}
companion object {
private val LOCK = Any()
private fun getRootMenu(app: Application): Menu {
synchronized(LOCK) {
val currentMenu = app.menubar
return if (currentMenu == null) {
val menu = Menu()
app.menubar = menu
menu
} else {
currentMenu as Menu
}
}
}
fun create(target: MenuButton, groupName: String): MenuBuilder {
val menu = Menu()
val pm = PopoverMenu.newFromModel(menu)
target.setPopover(pm)
return MenuBuilder(pm, groupName)
}
private fun insertMap(menu: PopoverMenu, groupName: String): ActionMap {
val ag = SimpleActionGroup()
menu.insertActionGroup(groupName, ag)
return ag
}
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.inceptum.gtk.util
import org.jetbrains.annotations.PropertyKey
import java.util.*
object I18n {
const val BUNDLE = "inceptum"
private val bundle = ResourceBundle.getBundle(BUNDLE)
@JvmStatic
operator fun get(key: @PropertyKey(resourceBundle = BUNDLE) String): String {
return bundle.getString(key)
}
@JvmStatic
operator fun get(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg args: Any?): String {
return String.format(bundle.getString(key), *args)
}
}

View File

@ -0,0 +1,95 @@
package io.gitlab.jfronny.inceptum.gtk.util
import io.gitlab.jfronny.commons.OSUtils
import io.gitlab.jfronny.inceptum.common.Utils
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Pattern
object Memory {
const val KB: Long = 1024
const val MB = KB * 1024
const val GB = MB * 1024
private val impl = when (OSUtils.TYPE) {
OSUtils.Type.LINUX -> LinuxMI()
OSUtils.Type.WINDOWS -> WindowsMI()
OSUtils.Type.MAC_OS -> MacOsMI()
}
private val totalMemory by lazy { impl.getTotalMemory() }
val maxMBForInstance: Long get() = (totalMemory / MB - 1024).coerceAtLeast(1024)
private interface MI {
fun getTotalMemory(): Long
}
private class LinuxMI : MI {
override fun getTotalMemory(): Long {
try {
Files.lines(Path.of("/proc/meminfo")).use { stream ->
val memTotal = stream
.filter { s: String -> s.startsWith("MemTotal:") }
.map { s: String -> s.substring("MemTotal:".length) }
.map { obj: String -> obj.trim { it <= ' ' } }
.findFirst()
return if (memTotal.isPresent()) {
parseDecimalMemorySizeToBinary(memTotal.get())
} else {
Utils.LOGGER.error("Could not find total memory")
32 * GB
}
}
} catch (e: IOException) {
Utils.LOGGER.error("Could not get total memory", e)
return 32 * GB
}
}
companion object {
// Taken from oshi
private val BYTES_PATTERN = Pattern.compile("(\\d+) ?([kKMGT]?B?).*")
private val WHITESPACES = Pattern.compile("\\s+")
private fun parseDecimalMemorySizeToBinary(size: String): Long {
var mem = WHITESPACES.split(size)
if (mem.size < 2) {
// If no spaces, use regexp
val matcher = BYTES_PATTERN.matcher(size.trim { it <= ' ' })
if (matcher.find() && matcher.groupCount() == 2) {
mem = arrayOfNulls(2)
mem[0] = matcher.group(1)
mem[1] = matcher.group(2)
}
}
var capacity = parseLongOrDefault(mem[0], 0L)
if (mem.size == 2 && mem[1]!!.length > 1) {
when (mem[1]!![0]) {
'T' -> capacity = capacity shl 40
'G' -> capacity = capacity shl 30
'M' -> capacity = capacity shl 20
'K', 'k' -> capacity = capacity shl 10
else -> {}
}
}
return capacity
}
private fun parseLongOrDefault(s: String, defaultLong: Long): Long = try {
s.toLong()
} catch (e: NumberFormatException) {
defaultLong
}
}
}
private class WindowsMI : MI {
override fun getTotalMemory(): Long {
return 32 * GB // This is currently unsupported, but any implementations by Windows user using panama are welcome
}
}
private class MacOsMI : MI {
override fun getTotalMemory(): Long {
return 32 * GB // This is currently unsupported, but any implementations by MacOS user using panama are welcome
}
}
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.inceptum.gtk.util
import java.util.stream.Stream
inline fun <reified T> Stream<T>.toTypedArray(): Array<T> = toArray { arrayOfNulls<T>(it) }

View File

@ -0,0 +1,35 @@
package io.gitlab.jfronny.inceptum.gtk.util
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import org.gnome.adw.ActionRow
import org.gnome.gtk.Label
import org.gnome.gtk.MenuButton
import org.gnome.gtk.MessageDialog
import org.gnome.gtk.Widget
var Widget.margin: Int
set(value) {
marginVertical = value
marginHorizontal = value
}
get() = throw NotImplementedError()
var Widget.marginVertical: Int
set(value) {
marginTop = value
marginBottom = value
}
get() = throw NotImplementedError()
var Widget.marginHorizontal: Int
set(value) {
marginStart = value
marginEnd = value
}
get() = throw NotImplementedError()
var MessageDialog.markup: String
set(value) { setMarkup(value) }
get() = throw NotImplementedError()
fun ActionRow.fixSubtitle() = ILabel.theme(firstChild!!.lastChild!!.prevSibling!!.lastChild as Label, ILabel.Mode.SUBTITLE)

View File

@ -0,0 +1,31 @@
package io.gitlab.jfronny.inceptum.gtk.window
import io.gitlab.jfronny.inceptum.common.BuildMetadata
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import org.gnome.gtk.AboutDialog
import org.gnome.gtk.License
class AboutWindow : AboutDialog() {
init {
programName = "Inceptum"
copyright = "Copyright (C) 2021-2023 JFronny"
version = BuildMetadata.VERSION
licenseType = License.MIT_X11
license = I18n["about.license"]
websiteLabel = I18n["about.contact"]
website = "https://jfronny.gitlab.io/contact.html"
if (!BuildMetadata.IS_PUBLIC) {
comments = I18n["about.unsupported-build"]
}
val vm = Runtime.version().feature()
systemInformation = I18n[if (BuildMetadata.VM_VERSION == vm) "about.jvm" else "about.jvm.unsupported", vm]
//TODO setLogo
}
companion object {
@JvmStatic
fun createAndShow() {
AboutWindow().show()
}
}
}

View File

@ -0,0 +1,170 @@
package io.gitlab.jfronny.inceptum.gtk.window
import io.github.jwharm.javagi.util.ListIndexModel
import io.gitlab.jfronny.inceptum.common.InceptumConfig
import io.gitlab.jfronny.inceptum.common.Utils
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.gtk.util.marginHorizontal
import io.gitlab.jfronny.inceptum.gtk.util.marginVertical
import io.gitlab.jfronny.inceptum.gtk.window.settings.launcher.LauncherSettingsWindow
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceListWatcher
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
import org.gnome.adw.Clamp
import org.gnome.adw.StatusPage
import org.gnome.gdk.FrameClock
import org.gnome.gio.*
import org.gnome.glib.GLib
import org.gnome.gtk.*
import org.gnome.gtk.Application
import java.io.IOException
import java.net.URI
class MainWindow(app: Application) : ApplicationWindow(app) {
private val listButton: Button
private val gridButton: Button
private val stack: Stack
private val empty: StatusPage
private val listContainer: Clamp
private val gridView: GridView
private val instanceList: MutableList<Instance>
private val instanceListIndex: ListIndexModel
init {
val header = HeaderBar()
val newButton = MenuButton()
newButton.iconName = "list-add-symbolic"
newButton.menuModel = GtkMenubar.newMenu!!.menu
val accountsButton = MenuButton()
accountsButton.iconName = "avatar-default-symbolic"
accountsButton.menuModel = GtkMenubar.accountsMenu!!.menu
listButton = Button.newFromIconName("view-list-symbolic")
listButton.onClicked {
InceptumConfig.listView = true
InceptumConfig.saveConfig()
generateWindowBody()
}
gridButton = Button.newFromIconName("view-grid-symbolic")
gridButton.onClicked {
InceptumConfig.listView = false
InceptumConfig.saveConfig()
generateWindowBody()
}
//TODO search button like boxes
val uiMenu = MenuBuilder(app, Menu(), "hamburger")
uiMenu.button("support") { Utils.openWebBrowser(URI("https://git.frohnmeyer-wds.de/JfMods/Inceptum/issues")) }
uiMenu.button("preferences") { LauncherSettingsWindow(app).show() }
uiMenu.button("about") { AboutWindow.createAndShow() }
val menuButton = MenuButton()
menuButton.iconName = "open-menu-symbolic"
menuButton.menuModel = uiMenu.menu
header.packStart(newButton)
header.packEnd(menuButton)
header.packEnd(gridButton)
header.packEnd(listButton)
header.packEnd(accountsButton)
instanceList = ArrayList()
instanceListIndex = ListIndexModel.newInstance(instanceList.size)
val selection = NoSelection(instanceListIndex)
val listView = ListView(selection, InstanceListEntryFactory(app, instanceList))
listView.addCssClass("rich-list")
listView.showSeparators = true
listView.onActivate { position: Int ->
// Double click
GtkMenubar.launch(instanceList[position], LaunchType.Client)
}
val frame = Frame(null as String?)
frame.child = listView
frame.marginHorizontal = 24
frame.marginVertical = 12
frame.valign = Align.START
listContainer = Clamp()
listContainer.maximumSize = 900
listContainer.child = frame
gridView = GridView(selection, InstanceGridEntryFactory(instanceList))
empty = StatusPage()
empty.title = I18n["main.empty.title"]
empty.description = I18n["main.empty.description"]
//TODO empty.setIconName(new Str());
stack = Stack()
stack.addChild(listContainer)
stack.addChild(gridView)
stack.addChild(empty)
val scroll = ScrolledWindow()
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC)
scroll.child = stack
setDefaultSize(720, 360)
title = "Inceptum"
titlebar = header
showMenubar = false
child = scroll
generateWindowBody()
//TODO DropTarget to add mods/instances
try {
setupDirWatcher()
} catch (e: IOException) {
Utils.LOGGER.error(
"Could not set up watch service, live updates of the instance dir will be unavailable",
e
)
}
}
@Throws(IOException::class)
private fun setupDirWatcher() {
val isw = InstanceListWatcher()
addTickCallback { _, _ ->
try {
if (isw.poll()) generateWindowBody()
} catch (e: IOException) {
Utils.LOGGER.error("Could not run update task", e)
}
GLib.SOURCE_CONTINUE
}
onCloseRequest {
try {
isw.close()
} catch (ignored: IOException) {}
false
}
}
private fun generateWindowBody() {
listButton.visible = !InceptumConfig.listView
gridButton.visible = InceptumConfig.listView
try {
// Unbind then clear
instanceListIndex.setSize(0)
instanceList.clear()
// Add new entries
instanceList.addAll(InstanceList.ordered())
instanceListIndex.setSize(instanceList.size)
// Choose view for this amount of entries
if (InstanceList.isEmpty()) stack.visibleChild = empty else if (InceptumConfig.listView) stack.visibleChild =
listContainer else stack.visibleChild = gridView
// This is called from a tick callback, so re-render
stack.queueResize()
stack.queueDraw()
} catch (e: IOException) {
Utils.LOGGER.error("Could not generate window body", e)
}
}
}

View File

@ -0,0 +1,15 @@
package io.gitlab.jfronny.inceptum.gtk.window
import org.gnome.gtk.*
class NewInstanceWindow(app: Application) : Assistant() {
init {
application = app
run {
val initialPage = Box(Orientation.VERTICAL, 8)
initialPage.append(Label("Importing instances via this assistant is not yet supported, use the ImGUI"))
appendPage(initialPage)
setPageType(initialPage, AssistantPageType.INTRO)
}
}
}

View File

@ -0,0 +1,74 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthAPI
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthServer
import org.gnome.gtk.*
import java.net.URI
import java.net.URISyntaxException
class MicrosoftLoginDialog @JvmOverloads constructor(
parent: Window?,
account: MicrosoftAccount? = null,
onClose: Runnable? = null
) : MessageDialog(
parent,
flags(parent != null),
MessageType.QUESTION,
ButtonsType.CLOSE,
I18n["auth.description"]
) {
constructor(parent: Window?, onClose: Runnable?) : this(parent, null, onClose)
init {
title = I18n["auth.title"]
val server = MicrosoftAuthServer(account)
try {
server.start()
} catch (e: Exception) {
Utils.LOGGER.error("Could not start mc login server", e)
}
val finalize = Runnable {
server.close()
onClose?.run()
}
onResponse { responseId: Int ->
when (ResponseType.of(responseId)) {
ResponseType.CLOSE, ResponseType.CANCEL -> {
finalize.run()
close()
}
ResponseType.DELETE_EVENT -> {
finalize.run()
destroy()
}
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
}
}
val btn = Button.newWithLabel(I18n["auth.open-browser"])
(messageArea as Box).append(btn)
btn.onClicked {
try {
Utils.openWebBrowser(URI(MicrosoftAuthAPI.MICROSOFT_LOGIN_URL))
} catch (e: URISyntaxException) {
Utils.LOGGER.error("Could not open browser", e)
}
}
onCloseRequest {
finalize.run()
false
}
}
companion object {
private fun flags(modal: Boolean): DialogFlags {
var flags = DialogFlags.DESTROY_WITH_PARENT
if (modal) flags = flags.or(DialogFlags.MODAL)
return flags
}
}
}

View File

@ -0,0 +1,107 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog
import io.gitlab.jfronny.commons.StringFormatter
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
import io.gitlab.jfronny.inceptum.gtk.GtkMain
import io.gitlab.jfronny.inceptum.gtk.schedule
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
import org.gnome.glib.GLib
import org.gnome.gtk.*
class ProcessStateWatcherDialog(
parent: Window?,
title: String,
errorMessage: String,
private val state: ProcessState,
executor: ThrowingRunnable<*>
) : MessageDialog(
parent,
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
MessageType.INFO,
ButtonsType.NONE,
null
) {
private var finished = false
private var cachedState: State? = null
init {
//TODO alternate UI: Only show progress bar by default, but have a dropdown to a "console" with the actual steps
// this should make visualizing parallelized steps easier
this.title = title
addButton(I18n["cancel"], ResponseType.CANCEL.value)
onResponse { responseId: Int ->
when (ResponseType.of(responseId)) {
ResponseType.CLOSE, ResponseType.CANCEL -> {
state.cancel()
close()
}
ResponseType.DELETE_EVENT -> destroy()
else -> Utils.LOGGER.error("Unexpected response type: $responseId")
}
}
onCloseRequest {
if (finished) return@onCloseRequest false
state.cancel()
false
}
val progress = ProgressBar()
(messageArea as Box).append(progress)
addTickCallback { widget, _ ->
if (finished) return@addTickCallback GLib.SOURCE_REMOVE
val nc = State(
state
)
if (nc != cachedState) {
cachedState = nc
setMarkup(cachedState!!.msg)
progress.fraction = cachedState!!.progress.coerceAtMost(1f).toDouble()
widget.queueDraw()
}
GLib.SOURCE_CONTINUE
}
Thread {
try {
executor.run()
} catch (e: Throwable) {
state.cancel()
Utils.LOGGER.error(errorMessage, e)
GtkEnvBackend.simpleDialog(
parent,
StringFormatter.toString(e),
errorMessage,
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
)
} finally {
finished = true
schedule { close() }
}
}.start()
}
@JvmRecord
internal data class State(val msg: String, val progress: Float) {
constructor(source: ProcessState) : this(source.currentStep, source.progress)
}
companion object {
@JvmStatic
fun show(
parent: Window?,
title: String,
errorMessage: String,
state: ProcessState,
executor: ThrowingRunnable<*>
): ProcessStateWatcherDialog {
val dialog = ProcessStateWatcherDialog(parent, title, errorMessage, state, executor)
dialog.show()
return dialog
}
}
}

View File

@ -0,0 +1,14 @@
package io.gitlab.jfronny.inceptum.gtk.window.dialog
import org.gnome.gtk.*
class StringInputDialog(parent: Window?, flags: DialogFlags, type: MessageType, buttons: ButtonsType, message: String, value: String) : MessageDialog(parent, flags, type, buttons, message) {
private val entry = Entry()
init {
(messageArea as Box).append(entry)
entry.text = value
}
val input: String get() = entry.text
}

View File

@ -0,0 +1,102 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
import io.gitlab.jfronny.inceptum.gtk.GtkMain
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import io.gitlab.jfronny.inceptum.gtk.schedule
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporter
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporters
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
import org.gnome.gtk.*
import java.nio.file.Path
class ExportTab(private val instance: Instance, window: InstanceSettingsWindow?) : SettingsTab(window) {
init {
section(null) {
row("instance.settings.export.version", "instance.settings.export.version.subtitle") {
setEntry(instance.meta.instanceVersion) {
instance.meta.instanceVersion = it
instance.writeMeta()
}
}
for (exporter in Exporters.EXPORTERS) {
row("instance.settings.export.title", "instance.settings.export.subtitle", exporter.name, exporter.fileExtension) {
setButton("instance.settings.export") {
val dialog = FileChooserNative(
I18n["instance.settings.export.dialog.title", exporter.name],
window,
FileChooserAction.SAVE,
"_" + I18n["save"],
"_" + I18n["cancel"]
)
val filter = FileFilter()
filter.name = exporter.name + " Pack"
filter.addPattern("*." + exporter.fileExtension)
dialog.addFilter(filter)
dialog.currentName = exporter.getDefaultFileName(instance)
dialog.onResponse { responseId: Int ->
if (responseId == ResponseType.ACCEPT.value) {
val file = dialog.file!!.path
if (file == null) {
GtkEnvBackend.simpleDialog(
window,
"The path returned by the file dialog is null",
"Could not export",
MessageType.ERROR,
ButtonsType.CLOSE,
null,
null
)
return@onResponse
}
export(exporter, Path.of(file))
}
}
dialog.show()
}
}
}
}
}
private fun export(exporter: Exporter<*>, path: Path) {
val state = ProcessState(Exporters.STEP_COUNT, "Initializing...")
ProcessStateWatcherDialog.show(
window,
I18n["instance.settings.export.dialog.title", exporter.name],
I18n["instance.settings.export.dialog.error", instance.name],
state
) {
exporter.generate(state, instance, path)
schedule {
val success = MessageDialog(
window,
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
MessageType.INFO,
ButtonsType.NONE,
I18n["instance.settings.export.dialog.success", instance.name, path.toString()]
)
success.title = I18n["instance.settings.export.dialog.success.title"]
success.addButton(I18n["show"], ResponseType.OK.value)
success.addButton(I18n["ok"], ResponseType.CANCEL.value)
success.onResponse { responseId1: Int ->
when (ResponseType.of(responseId1)) {
ResponseType.OK -> {
success.close()
Utils.openFile(path.toFile())
}
ResponseType.CLOSE, ResponseType.CANCEL -> success.close()
ResponseType.DELETE_EVENT -> success.destroy()
else -> {}
}
}
success.show()
}
}
}
}

View File

@ -0,0 +1,254 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
import io.github.jwharm.javagi.base.GErrorException
import io.gitlab.jfronny.commons.ArgumentsTokenizer
import io.gitlab.jfronny.commons.io.JFiles
import io.gitlab.jfronny.inceptum.common.InceptumConfig
import io.gitlab.jfronny.inceptum.common.MetaHolder
import io.gitlab.jfronny.inceptum.common.Utils
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import io.gitlab.jfronny.inceptum.gtk.util.I18n
import io.gitlab.jfronny.inceptum.gtk.util.Memory
import io.gitlab.jfronny.inceptum.gtk.util.markup
import io.gitlab.jfronny.inceptum.gtk.util.toTypedArray
import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi
import io.gitlab.jfronny.inceptum.launcher.api.McApi
import io.gitlab.jfronny.inceptum.launcher.model.fabric.FabricVersionLoaderInfo
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta
import io.gitlab.jfronny.inceptum.launcher.model.mojang.VersionsListInfo
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser
import org.gnome.gio.*
import org.gnome.gobject.BindingFlags
import org.gnome.gtk.*
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.stream.Stream
class GeneralTab(instance: Instance, window: InstanceSettingsWindow) : SettingsTab(window) {
companion object {
private val VERSIONS = McApi.getVersions()
}
init {
section(null) {
row("instance.settings.general.name", "instance.settings.general.name.placeholder") {
val apply = Button.newWithLabel(I18n["instance.settings.apply"])
val entry = setEntry(instance.name) { s: String -> apply.sensitive = s != instance.name }
entry.placeholderText = I18n["instance.settings.general.name.placeholder"]
apply.valign = Align.CENTER
apply.onClicked {
try {
val newPath = MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(entry.text))
Files.move(instance.path, newPath)
window.close()
InstanceSettingsWindow(window.application, InstanceList.read(newPath)).show()
} catch (e: IOException) {
showError("Could not rename", e)
}
}
apply.sensitive = false
append(apply)
}
}
section("instance.settings.general.game") {
run {
var fabricEnabled: Switch? = null
var versionChanged: (() -> Unit)? = null
var fabricVersion: DropDown? = null
var defaultFabric: String? = null
var fabricVersions: Array<String>? = null
val versions = VERSIONS.versions.stream()
.filter { InceptumConfig.snapshots || it.type == "release" }
.map { it.id }
.toTypedArray()
var def = 0
for (i in versions.indices) if (versions[i] == instance.gameVersion) def = i
row("instance.settings.general.game.version", "instance.settings.general.game.version.subtitle") {
setDropdown(versions, def) { i ->
instance.meta.gameVersion = if (instance.isFabric) GameVersionParser.createVersionWithFabric(
versions[i], instance.loaderVersion
) else versions[i]
instance.writeMeta()
versionChanged!!()
}.enableSearch = true
}
val fabricRow = row("instance.settings.general.game.fabric.enabled", "instance.settings.general.game.fabric.enabled.subtitle")
val loaderRow = row("instance.settings.general.game.fabric.version", "instance.settings.general.game.fabric.version.subtitle")
loaderRow.visible = instance.isFabric
fabricEnabled = fabricRow.setSwitch(instance.isFabric) { bl: Boolean ->
if (bl) {
if (fabricVersions != null && fabricVersions!!.isNotEmpty() && defaultFabric != null) {
instance.meta.gameVersion =
GameVersionParser.createVersionWithFabric(instance.gameVersion, defaultFabric)
instance.writeMeta()
} else {
fabricEnabled!!.active = false
}
} else {
instance.meta.gameVersion = instance.gameVersion
instance.writeMeta()
}
}
fabricEnabled.bindProperty("active", loaderRow, "visible", BindingFlags.DEFAULT)
versionChanged = {
val ver = VERSIONS.versions.stream()
.filter { it.id == instance.gameVersion }
.findFirst()
.map { FabricMetaApi.getLoaderVersions(it) }
.map { it.toTypedArray() }
defaultFabric = if (instance.isFabric) instance.loaderVersion else ver
.map { Arrays.stream(it) }
.map { it.filter { l -> l.loader.stable } }
.flatMap { it.findFirst() }
.map { it.loader.version }
.orElse(null)
fabricVersions = ver.map { Arrays.stream(it) }
.map{ it.map { l -> l.loader.version }.toTypedArray() }
.orElse(null)
if (fabricVersions == null || fabricVersions!!.isEmpty()) {
fabricEnabled.active = false
} else if (fabricVersion != null) fabricVersion!!.model = StringList(fabricVersions)
}
versionChanged()
fabricVersion =
loaderRow.setDropdown(fabricVersions!!, fabricVersions!!.indexOf(defaultFabric)) { i: Int ->
instance.meta.gameVersion =
if (i == -1) instance.gameVersion
else GameVersionParser.createVersionWithFabric(instance.gameVersion, fabricVersions!![i])
instance.writeMeta()
}
fabricVersion.enableSearch = true
}
row("instance.settings.general.game.java", "instance.settings.general.game.java.subtitle") {
val entry = setEntry(instance.meta.java) { s: String ->
instance.meta.java = s.ifBlank { null }
instance.writeMeta()
}
val btn = Button.newFromIconName("folder-symbolic")
btn.valign = Align.CENTER
btn.onClicked {
val dialog = FileChooserNative(
I18n["instance.settings.general.game.java"],
window,
FileChooserAction.OPEN,
"_" + I18n["select"],
"_" + I18n["cancel"]
)
if (instance.meta.java != null && Files.exists(Path.of(instance.meta.java))) {
try {
dialog.setFile(File.newForPath(instance.meta.java))
} catch (e: GErrorException) {
Utils.LOGGER.error("Could not set starting point", e)
}
}
dialog.onResponse { responseId: Int ->
if (responseId == ResponseType.ACCEPT.value) {
val file = dialog.file!!.path
if (file != null) entry.text = file
}
}
dialog.show()
}
append(btn)
}
row("instance.settings.general.game.memory.min", "instance.settings.general.game.memory.min.subtitle") {
setSpinButton(
(if (instance.meta.minMem == null) 512 else instance.meta.minMem / Memory.MB).toDouble(),
512.0,
Memory.maxMBForInstance.toDouble(),
128.0
) { v: Double ->
instance.meta.minMem = (v * Memory.MB).toLong()
if (instance.meta.minMem == Memory.GB / 2) instance.meta.minMem = null
instance.writeMeta()
}
}
row("instance.settings.general.game.memory.max", "instance.settings.general.game.memory.max.subtitle") {
setSpinButton(
(if (instance.meta.maxMem == null) 1024 else instance.meta.maxMem / Memory.MB).toDouble(),
1024.0,
Memory.maxMBForInstance.toDouble(),
128.0
) { v: Double ->
instance.meta.maxMem = (v * Memory.MB).toLong()
if (instance.meta.maxMem == Memory.GB) instance.meta.maxMem = null
instance.writeMeta()
}
}
}
section("instance.settings.general.args") {
if (instance.meta.arguments == null) instance.meta.arguments = InstanceMeta.Arguments(listOf(), listOf(), listOf())
if (instance.meta.arguments.jvm == null) instance.meta.arguments = instance.meta.arguments.withJvm(listOf())
if (instance.meta.arguments.client == null) instance.meta.arguments = instance.meta.arguments.withClient(listOf())
if (instance.meta.arguments.server == null) instance.meta.arguments = instance.meta.arguments.withServer(listOf())
row("instance.settings.general.args.jvm", "instance.settings.general.args.jvm.subtitle") {
setEntry(ArgumentsTokenizer.join(instance.meta.arguments.jvm.toTypedArray())) {
instance.meta.arguments = instance.meta.arguments.withJvm(listOf(*ArgumentsTokenizer.tokenize(it)))
instance.writeMeta()
}
}
row("instance.settings.general.args.client", "instance.settings.general.args.client.subtitle") {
setEntry(ArgumentsTokenizer.join(instance.meta.arguments.client.toTypedArray())) {
instance.meta.arguments = instance.meta.arguments.withClient(listOf(*ArgumentsTokenizer.tokenize(it)))
instance.writeMeta()
}
}
row("instance.settings.general.args.server", "instance.settings.general.args.server.subtitle") {
setEntry(ArgumentsTokenizer.join(instance.meta.arguments.server.toTypedArray())) {
instance.meta.arguments = instance.meta.arguments.withServer(listOf(*ArgumentsTokenizer.tokenize(it)))
instance.writeMeta()
}
}
}
section("instance.settings.general.manage") {
row("instance.delete", "instance.delete.subtitle") {
setButton("instance.delete") {
val dialog = MessageDialog(
window,
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
MessageType.WARNING,
ButtonsType.OK_CANCEL,
null
)
dialog.markup = I18n["instance.delete.confirm"]
dialog.title = I18n["instance.delete.confirm.title"]
dialog.onResponse { responseId: Int ->
when (ResponseType.of(responseId)) {
ResponseType.OK -> {
try {
JFiles.deleteRecursive(instance.path)
dialog.close()
window.close()
} catch (e: IOException) {
showError(I18n["instance.delete.fail"], e)
}
dialog.close()
}
ResponseType.CLOSE, ResponseType.CANCEL -> dialog.close()
ResponseType.DELETE_EVENT -> dialog.destroy()
else -> {}
}
}
dialog.show()
}
}
row("instance.directory", "instance.directory.subtitle") {
setButton("instance.directory") { Utils.openFile(instance.path.toFile()) }
}
}
val timestamp = if (instance.meta.lastLaunched == null) 0 else instance.meta.lastLaunched
append(ILabel("instance.settings.general.last-launched", ILabel.Mode.SUBTITLE, Date(timestamp * 1000).toString()))
}
}

View File

@ -0,0 +1,13 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsWindow
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
import org.gnome.gtk.Application
class InstanceSettingsWindow(app: Application?, instance: Instance) : SettingsWindow(app) {
init {
addTab(GeneralTab(instance, this), "instance.settings.general", "preferences-other-symbolic")
addTab(ModsTab(instance, this), "instance.settings.mods", "package-x-generic-symbolic")
addTab(ExportTab(instance, this), "instance.settings.export", "send-to-symbolic")
}
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
class ModsTab(instance: Instance?, window: InstanceSettingsWindow?) : SettingsTab(window) {
init {
append(ILabel("instance.settings.mods.unsupported"))
//TODO implement this, somehow
}
}

View File

@ -0,0 +1,60 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import io.gitlab.jfronny.inceptum.gtk.util.margin
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
import org.gnome.gtk.*
class AccountsTab(window: Window?) : SettingsTab(window) {
init {
section(null) {
build()
}
}
private fun Section.build() {
generateRows()
val row = Button.newFromIconName("list-add-symbolic")
row(row)
row.onClicked {
MicrosoftLoginDialog(window) {
clear()
build()
GtkMenubar.generateAccountsMenu(window!!.application!!)
}.show()
}
}
private fun Section.generateRows() {
for (account in AccountManager.getAccounts()) {
val row = Box(Orientation.HORIZONTAL, 40)
val ref = row(row)
row.margin = 8
//TODO profile icon
val head = Box(Orientation.VERTICAL, 0)
head.hexpand = true
head.halign = Align.START
head.valign = Align.CENTER
val title = Label(account.minecraftUsername)
title.halign = Align.START
head.append(title)
val subtitle = Label(account.uuid)
ILabel.theme(subtitle, ILabel.Mode.SUBTITLE)
subtitle.halign = Align.START
head.append(subtitle)
row.append(head)
val remove = Button.newFromIconName("window-close-symbolic")
remove.valign = Align.CENTER
remove.halign = Align.END
remove.onClicked {
AccountManager.removeAccount(account)
remove(ref)
GtkMenubar.generateAccountsMenu(window!!.application!!)
}
row.append(remove)
}
}
}

View File

@ -0,0 +1,31 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher
import io.gitlab.jfronny.inceptum.common.InceptumConfig
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateChannel
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsTab
import org.gnome.gtk.Window
class GeneralTab(window: Window?) : SettingsTab(window) {
init {
section(null) {
row("settings.general.snapshots", "settings.general.snapshots.subtitle") {
setSwitch(InceptumConfig.snapshots) { b: Boolean? ->
InceptumConfig.snapshots = b!!
InceptumConfig.saveConfig()
}
}
row("settings.general.update-channel", "settings.general.update-channel.subtitle") {
setDropdown(arrayOf("Stable", "CI"), if (InceptumConfig.channel == UpdateChannel.CI) 1 else 0) { state ->
InceptumConfig.channel = if (state == 1) UpdateChannel.CI else UpdateChannel.Stable
InceptumConfig.saveConfig()
}
}
row("settings.general.author-name", "settings.general.author-name.subtitle") {
setEntry(InceptumConfig.authorName) { s ->
InceptumConfig.authorName = s
InceptumConfig.saveConfig()
}
}
}
}
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.inceptum.gtk.window.settings.launcher
import io.gitlab.jfronny.inceptum.gtk.control.settings.SettingsWindow
import org.gnome.gtk.Application
class LauncherSettingsWindow(app: Application) : SettingsWindow(app) {
init {
addTab(GeneralTab(this), "settings.general", "preferences-other-symbolic")
addTab(AccountsTab(this), "settings.accounts", "system-users-symbolic")
}
}

View File

@ -62,7 +62,7 @@ public class GeneralTab extends Tab {
LauncherEnv.showError("Could not delete the instance", e);
}
window.close();
}, R::nop);
});
if (ImGui.checkbox("Custom Java", customJava)) {
if (customJava.get()) {
window.instance.meta().java = OSUtils.getJvmBinary();

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.inceptum.launcher;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.launcher.api.McApi;
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
@ -68,6 +69,10 @@ public class LauncherEnv {
backend.showInfo(message, title);
}
public static void showOkCancel(String message, String title, Runnable ok) {
showOkCancel(message, title, ok, R::nop);
}
public static void showOkCancel(String message, String title, Runnable ok, Runnable cancel) {
backend.showOkCancel(message, title, ok, cancel);
}

View File

@ -1,7 +1,6 @@
package io.gitlab.jfronny.inceptum.launcher.api.account;
import io.gitlab.jfronny.inceptum.launcher.api.account.GC_MicrosoftAccount;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.gson.compile.util.GList;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
@ -71,7 +70,7 @@ public class AccountManager {
public static void addAccount(MicrosoftAccount account) {
ACCOUNTS.add(account);
if (ACCOUNTS.size() > 1) {
LauncherEnv.showOkCancel("Account added successfully. Switch to it now?", "Success", () -> switchAccount(account), R::nop);
LauncherEnv.showOkCancel("Account added successfully. Switch to it now?", "Success", () -> switchAccount(account));
} else switchAccount(account);
saveAccounts();
}

View File

@ -1,6 +1,5 @@
package io.gitlab.jfronny.inceptum.launcher.api.account;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
import io.gitlab.jfronny.inceptum.common.Utils;
@ -200,7 +199,7 @@ public class MicrosoftAccount {
public boolean ensureAccessTokenValid() {
if (mustLogin) {
LauncherEnv.showOkCancel("You must login again in order to continue", "Login expired", () -> LauncherEnv.showLoginRefreshPrompt(this), R::nop);
LauncherEnv.showOkCancel("You must login again in order to continue", "Login expired", () -> LauncherEnv.showLoginRefreshPrompt(this));
return false;
} else return true;
}