package com.github.otbproject.otbproject.gui;
import com.github.otbproject.otbproject.App;
import com.github.otbproject.otbproject.bot.Control;
import com.github.otbproject.otbproject.config.Configs;
import com.github.otbproject.otbproject.config.WebConfig;
import com.github.otbproject.otbproject.fs.FSUtil;
import com.github.otbproject.otbproject.util.ThreadUtil;
import com.github.otbproject.otbproject.util.version.AppVersion;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.FlowPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListenerAdapter;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class GuiApplication extends Application {
private static GuiController controller;
/**
* Variable set to {@code true} when {@code GuiApplication} has
* loaded and dereferrencing {@code controller} will not produce
* a {@link NullPointerException}
*/
private static volatile boolean ready = false;
/**
* Not volatile to allow faster access than to {@code ready}
* for most of the execution, as this value is not volatile
*/
private static boolean cheapReady = false;
static boolean notReady() {
return !cheapReady && !ready;
}
static final Lock READY_LOCK = new ReentrantLock(true);
static final BlockingQueue<Runnable> NOT_READY_QUEUE = new LinkedBlockingQueue<>();
private static final ExecutorService DESKTOP_SERVICE = ThreadUtil.newSingleThreadExecutor("Desktop Daemon");
/**
* The main entry point for all JavaFX applications.
* The start method is called after the init method has returned,
* and after the system is ready for the application to begin running.
* <p>
* NOTE: This method is called on the JavaFX Application Thread.
* </p>
*
* @param primaryStage the primary stage for this application, onto which
* the application scene can be set. The primary stage will be embedded in
* the browser if the application was launched as an applet.
* Applications may create other stages, if needed, but they will not be
* primary stages and will not be embedded in the browser.
*/
@Override
public void start(Stage primaryStage) throws Exception {
Thread.currentThread().setUncaughtExceptionHandler(ThreadUtil.UNCAUGHT_EXCEPTION_HANDLER);
Font.loadFont(getClass().getClassLoader().getResourceAsStream("assets/fonts/UbuntuMono-R.ttf"), 12);
Font.loadFont(getClass().getClassLoader().getResourceAsStream("assets/fonts/Ubuntu-R.ttf"), 12);
FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource("console.fxml"));
Parent start = loader.load();
primaryStage.setScene(new Scene(start, 1200, 515));
primaryStage.setResizable(false);
primaryStage.setTitle("OTB");
primaryStage.getIcons().add(new Image("file:" + FSUtil.assetsDir() + File.separator + FSUtil.Assets.LOGO));
// Create tailer
CustomTailerListenerAdapter listenerAdapter = new CustomTailerListenerAdapter();
File logFile = new File(FSUtil.logsDir() + File.separator + "console.log");
Tailer tailer = Tailer.create(logFile, listenerAdapter, 250);
// Set on-close action
primaryStage.setOnCloseRequest(event -> {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Confirm Close");
alert.setHeaderText("WARNING: \"Close Window\" DOES NOT STOP THE BOT.");
alert.setContentText("Closing this window without exiting may make it difficult to stop the bot.\nPress \"Exit\" to stop the bot and exit.\nPress \"Cancel\" to keep the window open.");
DialogPane dialogPane = alert.getDialogPane();
GuiUtils.setDialogPaneStyle(dialogPane);
ButtonType buttonTypeCloseNoExit = new ButtonType("Close Window", ButtonBar.ButtonData.LEFT);
ButtonType buttonTypeExit = new ButtonType("Exit", ButtonBar.ButtonData.FINISH);
ButtonType buttonTypeCancel = new ButtonType("Cancel", ButtonBar.ButtonData.FINISH);
alert.getButtonTypes().setAll(buttonTypeCloseNoExit, buttonTypeExit, buttonTypeCancel);
GuiUtils.setDefaultButton(alert, buttonTypeExit);
alert.initStyle(StageStyle.UNDECORATED);
alert.showAndWait().ifPresent(buttonType -> {
if (buttonType == buttonTypeCloseNoExit) {
primaryStage.hide();
tailer.stop();
listenerAdapter.stop();
} else if (buttonType == buttonTypeExit) {
Control.shutdownAndExit();
System.exit(0);
}
});
event.consume();
});
controller = loader.<GuiController>getController();
setUpMenus();
controller.cliOutput.appendText("> ");
controller.commandsInput.setEditable(false);
controller.commandsOutput.appendText("Type \"help\" for a list of commands.\nThe PID of the bot is probably "
+ App.PID + " if you are using an Oracle JVM, but it may be different,"
+ " especially if you are using a different JVM. Be careful stopping the bot using this PID.");
controller.readHistory();
primaryStage.show();
// Notify waiting GUI Runnables that ready
READY_LOCK.lock();
try {
cheapReady = true;
ready = true;
NOT_READY_QUEUE.forEach(Runnable::run);
NOT_READY_QUEUE.clear();
} finally {
READY_LOCK.unlock();
}
}
public static void start(String[] args) {
launch(args);
}
public static void setInputInactive() {
GuiUtils.runSafe(() -> {
controller.commandsInput.setEditable(false);
controller.commandsInput.setPromptText("Command executing, please wait...");
});
}
public static void setInputActive() {
GuiUtils.runSafe(() -> {
controller.commandsInput.setEditable(true);
controller.commandsInput.setPromptText("Enter command here...");
});
}
public static void addInfo(String text) {
GuiUtils.runSafe(() -> controller.commandsOutput.appendText("\n\n" + text));
}
public static void clearLog() {
GuiUtils.runSafe(controller.logOutput::clear);
}
public static void clearInfo() {
GuiUtils.runSafe(controller.commandsOutput::clear);
}
public static void clearCliOutput() {
GuiUtils.runSafe(controller.cliOutput::clear);
}
public static void clearHistory() {
GuiUtils.runSafe(() -> {
controller.history.clear();
controller.historyPointer = 0;
controller.writeHistory();
});
}
static class CustomTailerListenerAdapter extends TailerListenerAdapter {
private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
private final List<String> buffer = new ArrayList<>();
private final ScheduledFuture<?> scheduledFuture;
public CustomTailerListenerAdapter() {
scheduledFuture = Executors.newSingleThreadScheduledExecutor(ThreadUtil.newThreadFactory("GUI-console-daemon"))
.scheduleWithFixedDelay(this::addToConsole, 0, 100, TimeUnit.MILLISECONDS);
}
void stop() {
scheduledFuture.cancel(true);
}
private void addToConsole() {
// Ensure it does not attempt to append text before the GUI is ready
if (notReady()) {
return;
}
queue.drainTo(buffer);
if (!buffer.isEmpty()) {
String text = buffer.stream().collect(Collectors.joining("\n", "", "\n"));
GuiUtils.runSafe(() -> controller.logOutput.appendText(text));
buffer.clear();
}
}
@Override
public void handle(String line) {
try {
queue.put(line);
} catch (InterruptedException e) {
App.logger.catching(e);
Thread.currentThread().interrupt();
}
}
}
private void setUpMenus() {
controller.openBaseDir.setOnAction(event -> {
DESKTOP_SERVICE.execute(() -> {
try {
Desktop.getDesktop().open(new File(FSUtil.getBaseDir()));
} catch (IOException e) {
App.logger.catching(e);
}
});
event.consume();
});
controller.quit.setOnAction(event -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Confirm Quit");
alert.setHeaderText("Are you sure you want to quit?");
DialogPane dialogPane = alert.getDialogPane();
GuiUtils.setDialogPaneStyle(dialogPane);
ButtonType buttonTypeYes = new ButtonType("Yes", ButtonBar.ButtonData.FINISH);
ButtonType buttonTypeNo = new ButtonType("No", ButtonBar.ButtonData.FINISH);
alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo);
GuiUtils.setDefaultButton(alert, buttonTypeYes);
alert.initStyle(StageStyle.UNDECORATED);
alert.showAndWait().ifPresent(buttonType -> {
if (buttonType == buttonTypeYes) {
Control.shutdownAndExit();
}
});
event.consume();
});
controller.botStart.setOnAction(event -> {
try {
addInfo(Control.startup() ? "Started bot" : "Did not start bot - bot already running");
} catch (Control.StartupException e) {
App.logger.catching(e);
addInfo("Failed to start bot");
}
event.consume();
});
controller.botStop.setOnAction(event -> {
addInfo(Control.shutdown(true) ? "Bot stopped" : "Did not stop bot - bot not running");
event.consume();
});
controller.botRestart.setOnAction(event -> {
addInfo(Control.restart() ? "Restarted bot" : "Failed to restart bot");
event.consume();
});
controller.webOpen.setOnAction(event -> {
openWebInterfaceInBrowser();
event.consume();
});
}
private void openWebInterfaceInBrowser() {
this.getHostServices().showDocument("http://127.0.0.1:" + Configs.getWebConfig().get(WebConfig::getPortNumber));
}
public static void newReleaseAlert() {
GuiUtils.runSafe(() -> {
String url = "https://github.com/OTBProject/OTBProject/releases/latest";
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("New Release Available");
alert.setHeaderText("New Release Available: OTB Version " + AppVersion.latest());
alert.setContentText("Version " + AppVersion.latest() + " of OTB is now available!" +
"\n\nPress \"Get New Release\" or go to" +
"\n" + url +
"\nto get the new release." +
"\n\nPressing \"Don't Ask Again\" will prevent notifications " +
"\nfor all future releases of OTB.");
DialogPane dialogPane = alert.getDialogPane();
GuiUtils.setDialogPaneStyle(dialogPane);
ButtonType buttonTypeDontAskAgain = new ButtonType("Don't Ask Again", ButtonBar.ButtonData.LEFT);
ButtonType buttonTypeGetRelease = new ButtonType("Get New Release", ButtonBar.ButtonData.FINISH);
ButtonType buttonTypeIgnoreOnce = new ButtonType("Ignore Once", ButtonBar.ButtonData.FINISH);
alert.getButtonTypes().setAll(buttonTypeDontAskAgain, buttonTypeGetRelease, buttonTypeIgnoreOnce);
GuiUtils.setDefaultButton(alert, buttonTypeGetRelease);
alert.initStyle(StageStyle.UNDECORATED);
alert.showAndWait().ifPresent(buttonType -> {
if (buttonType == buttonTypeDontAskAgain) {
Configs.getGeneralConfig().edit(config -> config.setUpdateChecking(false));
} else if (buttonType == buttonTypeGetRelease) {
DESKTOP_SERVICE.execute(() -> {
try {
Desktop.getDesktop().browse(new URI(url));
} catch (IOException | URISyntaxException e) {
App.logger.catching(e);
}
});
}
});
});
}
public static void errorAlert(String title, String header) {
GuiUtils.runSafe(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(header);
String url = "https://github.com/OTBProject/OTBProject/issues";
FlowPane outer = new FlowPane();
FlowPane inner = new FlowPane();
Hyperlink link = new Hyperlink("here");
link.setOnAction((evt) -> DESKTOP_SERVICE.execute(() -> {
try {
Desktop.getDesktop().open(new File(FSUtil.logsDir()));
} catch (IOException e) {
App.logger.catching(e);
}
}));
outer.getChildren().addAll(new Label("Please report this problem to the developers at"), new Label('"' + url + '"'), inner,
new Label("(in the \"logs\" folder in your installation directory)"));
inner.getChildren().addAll(new Label("and give them the log file \"app.log\" found"), link);
alert.getDialogPane().contentProperty().set(outer);
showErrorAlert(alert, url);
});
}
public static void fatalErrorAlert(String fileName) {
GuiUtils.runSafe(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Fatal Error");
alert.setHeaderText("OTB experienced a fatal error the last time it ran");
String url = "https://github.com/OTBProject/OTBProject/issues";
alert.setContentText("Please report this problem to the developers at\n\"" + url + "\"\nand provide them with the file: " + fileName);
showErrorAlert(alert, url);
});
}
public static void multipleFatalErrorAlert(java.util.List<String> fileNames) {
GuiUtils.runSafe(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Fatal Error");
alert.setHeaderText("OTB somehow experienced multiple fatal errors the last time it ran");
String url = "https://github.com/OTBProject/OTBProject/issues";
alert.setContentText("Please report this problem to the developers at\n\"" + url + "\"\nand provide them with the files:\n"
+ fileNames.stream().collect(Collectors.joining("\n")));
showErrorAlert(alert, url);
});
}
private static void showErrorAlert(Alert alert, String url) {
DialogPane dialogPane = alert.getDialogPane();
GuiUtils.setDialogPaneStyle(dialogPane);
ButtonType buttonTypeReportIssue = new ButtonType("Report Problem", ButtonBar.ButtonData.FINISH);
ButtonType buttonTypeClose = new ButtonType("OK", ButtonBar.ButtonData.FINISH);
alert.getButtonTypes().setAll(buttonTypeReportIssue, buttonTypeClose);
GuiUtils.setDefaultButton(alert, buttonTypeReportIssue);
alert.initStyle(StageStyle.UNDECORATED);
alert.showAndWait().ifPresent(buttonType -> {
if (buttonType == buttonTypeReportIssue && Desktop.isDesktopSupported()) {
DESKTOP_SERVICE.execute(() -> {
try {
Desktop.getDesktop().browse(new URI(url));
} catch (IOException | URISyntaxException e) {
App.logger.catching(e);
}
});
}
});
}
}