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:
parent
441a9b26b2
commit
a89f51aa5d
|
@ -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")
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue