package com.mattc.autotyper.gui.fx;
import static javafx.scene.input.KeyCombination.ModifierValue.DOWN;
import static javafx.scene.input.KeyCombination.ModifierValue.UP;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.mattc.autotyper.AppVersion;
import com.mattc.autotyper.Autotyper;
import com.mattc.autotyper.Ref;
import com.mattc.autotyper.Strings;
import com.mattc.autotyper.Strings.Resources;
import com.mattc.autotyper.gui.ConfirmFileDialog;
import com.mattc.autotyper.gui.GuiAccessor;
import com.mattc.autotyper.gui.LocationHandler;
import com.mattc.autotyper.meta.Outcome;
import com.mattc.autotyper.minify.Minifier;
import com.mattc.autotyper.robot.Keyboard;
import com.mattc.autotyper.robot.KeyboardMethodology;
import com.mattc.autotyper.util.Console;
import com.mattc.autotyper.util.OS;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
public class FXAutotyperWindow extends Application {
/**
* Meta Key for Rank in MetaToggleGroup
*/
private static final String META_RANK = "RANK";
private static FXAutotyperWindow INSTANCE;
private final Preferences prefs = Preferences.userNodeForPackage(FXAutotyperWindow.class);
private final EvictingQueue<String> locations = EvictingQueue.create(50);
private Keyboard keys;
private Stage primaryStage;
private final ObjectProperty<Path> fileProperty = new SimpleObjectProperty<>();
private final IntegerProperty inputDelayProperty = new SimpleIntegerProperty(40);
private final IntegerProperty waitTimeProperty = new SimpleIntegerProperty(5000);
private final BooleanProperty minifyProperty = new SimpleBooleanProperty(false);
private boolean doConfirm;
private volatile boolean doSave = false;
private int curRank;
@Override
public void start(final Stage primaryStage) throws Exception {
try {
if (INSTANCE != null) throw new IllegalStateException("Multiple Instances of FXAutotyperWindow!");
Thread.currentThread().setName("FX_GUI");
FXAutotyperWindow.INSTANCE = this;
this.primaryStage = primaryStage;
// Load Preferences and obtain proper Keyboard
loadPrefs();
obtainKeyboard();
addIcons(primaryStage);
// Initialize Parent Nodes
final BorderPane root = new BorderPane();
final StackPane gridStack = new StackPane();
final GridPane grid = new GridPane();
grid.setGridLinesVisible(false);
grid.setHgap(5);
grid.setVgap(5);
gridStack.getChildren().add(grid);
// Initialize Button Bar
final StackPane buttonStack = new StackPane();
final HBox buttonBox = new HBox(10);
final MetaToggleGroup btnGroup = new MetaToggleGroup();
final Button startBtn = new Button("Start");
final RadioButton fileBtn = new RadioButton("File");
final RadioButton urlBtn = new RadioButton("URL");
final RadioButton pasteBtn = new RadioButton("Paste");
final RadioButton autoBtn = new RadioButton("Auto");
final CheckBox minifyBtn = new CheckBox("Minify Lua Code?");
minifyBtn.setTooltip(new TimedTooltip("Speed up autotyping by reducing character count.\nMay not be effective on *all* files.\n\nWill fail on non-Lua code.", 200));
minifyBtn.selectedProperty().bindBidirectional(minifyProperty);
MetaToggleGroup.addTogglesToGroup(btnGroup, fileBtn, Strings.GHOST_TEXT_FSELECT, urlBtn, Strings.GHOST_TEXT_USELECT, pasteBtn, Strings.GHOST_TEXT_PSELECT, autoBtn, Strings.GHOST_TEXT_ASELECT);
btnGroup.putProperty(fileBtn, META_RANK, 1);
btnGroup.putProperty(urlBtn, META_RANK, 2);
btnGroup.putProperty(pasteBtn, META_RANK, 3);
btnGroup.putProperty(autoBtn, META_RANK, 4);
btnGroup.setSelectedForProperty(META_RANK, this.curRank);
buttonStack.setAlignment(Pos.BASELINE_LEFT);
StackPane.setAlignment(startBtn, Pos.BASELINE_RIGHT);
startBtn.setPrefSize(50, 20);
buttonBox.getChildren().addAll(fileBtn, urlBtn, pasteBtn, autoBtn, minifyBtn);
buttonStack.setId("button-box");
buttonStack.setPadding(new Insets(15, 25, 15, 25));
buttonStack.getChildren().addAll(buttonBox, startBtn);
startBtn.setDefaultButton(true);
// Initialize the Grid
final HBox locBox = new HBox(5);
final Label locLabel = new Label("Location:");
final AutoCompleteTextField locField = new AutoCompleteTextField(Lists.newArrayList(locations));
final InteractiveBox wBox = new InteractiveBox("Wait %t seconds before typing.", Pos.CENTER);
final InteractiveBox iBox = new InteractiveBox("Wait %t milliseconds between keystrokes.", Pos.CENTER);
final InteractiveBox cBox = new InteractiveBox("I %B want to see a preview before typing.", Pos.CENTER);
final TextField wField = wBox.getInteractiveChild(0, TextField.class);
final TextField iField = iBox.getInteractiveChild(0, TextField.class);
final ToggleButton cBtn = cBox.getInteractiveChild(0, ToggleButton.class);
wField.setPrefColumnCount(2);
wField.setAlignment(Pos.CENTER);
wField.setText(Integer.toString(this.waitTimeProperty.get() / 1000));
iField.setPrefColumnCount(2);
iField.setAlignment(Pos.CENTER);
iField.setText(Integer.toString(this.inputDelayProperty.get()));
locField.setPrefColumnCount(32);
locField.setPromptText(btnGroup.getMetaStringForSelected());
cBtn.setSelected(this.doConfirm);
locField.setTooltip(new TimedTooltip("Location from which data can be autotyped from.", 200));
locBox.getChildren().addAll(locLabel, locField);
FXGuiUtils.setMaxCharCount(wField, 2);
FXGuiUtils.setMaxCharCount(iField, 3);
FXGuiUtils.setToggleTextSwitch(cBtn, "do", "do not");
btnGroup.selectedToggleProperty().addListener((obs, oldValue, newValue) -> {
locField.setPromptText(btnGroup.getMetaString(newValue));
FXAutotyperWindow.this.curRank = btnGroup.getMetaForSelected(META_RANK, Integer.class);
});
wField.textProperty().addListener((obs, oldValue, newValue) -> {
if (newValue.trim().isEmpty()) {
FXAutotyperWindow.this.waitTimeProperty.set(1000);
} else if (!isValid(newValue)) {
wField.setText(oldValue);
} else {
FXAutotyperWindow.this.waitTimeProperty.set(Integer.parseInt(newValue) * 1000);
}
});
iField.textProperty().addListener((obs, ov, nv) -> {
if (nv.trim().isEmpty()) {
FXAutotyperWindow.this.inputDelayProperty.set(com.mattc.autotyper.Parameters.MIN_DELAY);
} else if (!isValid(nv)) {
iField.setText(ov);
} else {
int value = Integer.parseInt(nv);
FXAutotyperWindow.this.inputDelayProperty.set(Math.max(value, com.mattc.autotyper.Parameters.MIN_DELAY));
}
});
// When we lose foucs, check value to display at least the minimum delay
iField.focusedProperty().addListener((obs, ov, nv) -> {
if (nv || !isValid(iField.getText()))
return;
int val = Integer.parseInt(iField.getText());
if (val < com.mattc.autotyper.Parameters.MIN_DELAY) {
iField.setText(String.valueOf(com.mattc.autotyper.Parameters.MIN_DELAY));
inputDelayProperty.set(com.mattc.autotyper.Parameters.MIN_DELAY);
}
});
cBtn.selectedProperty().addListener((obs, oldValue, newValue) -> FXAutotyperWindow.this.doConfirm = newValue);
startBtn.setOnAction(new EventHandler<ActionEvent>() {
Task<Boolean> typerTask;
@Override
public void handle(ActionEvent event) {
final String text = locField.getText().trim();
if (text.length() == 0) {
showError("Some Text Must be Entered!");
} else if ((typerTask != null) && typerTask.isRunning()) {
showError("Cannot run two simultaneous jobs! Please wait for the other to terminate...");
} else {
typerTask = makeTask();
btnGroup.getSelectedToggle();
LocationHandler handler;
Outcome outcome;
if (fileBtn.isSelected()) {
handler = LocationHandler.FILE;
} else if (urlBtn.isSelected()) {
handler = LocationHandler.URL;
} else if (pasteBtn.isSelected()) {
handler = LocationHandler.PASTEBIN;
} else {
try {
handler = LocationHandler.detect(text);
} catch (final Exception e1) {
Console.debug(e1);
showError(String.format("Could Not Auto-Detect Location:%n%s", e1.getMessage()));
return;
}
}
// Isolate Useful URI
final String textNoTag;
if (text.startsWith(handler.tag()))
textNoTag = text.substring(text.indexOf(':') + 1).trim();
else
textNoTag = text;
outcome = handler.canHandle(textNoTag);
if (outcome.isFailure()) {
showError(outcome.reason);
} else {
Path file = handler.handle(textNoTag);
try {
if (minifyProperty.get()) {
boolean success = false;
try {
file = Minifier.minifyFileToCopy(file);
success = true;
} catch (Exception e) {
Console.exception(e);
}
if (!success && !promptContinue("Minification Failed! Continue without minification?"))
return;
}
if (FXAutotyperWindow.this.doConfirm) if (!approve(file)) return;
fileProperty.set(file);
int seconds = (FXAutotyperWindow.this.waitTimeProperty.get() / 1000);
ButtonType[] types = new ButtonType[]{ButtonType.YES, ButtonType.NO};
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Start Autotyping in " + seconds + " seconds?\n\nAll windows will be hidden until the task is complete...", types);
alert.initOwner(FXAutotyperWindow.this.primaryStage);
alert.initModality(Modality.APPLICATION_MODAL);
alert.initStyle(StageStyle.DECORATED);
alert.getDialogPane().setPrefSize(420, 200);
alert.setOnHidden((e) -> {
if (alert.getResult() == ButtonType.YES) {
Platform.runLater(typerTask);
if (saveToHistory(textNoTag, handler.tag()))
locField.addData(handler.tag() + textNoTag);
}
});
alert.show();
} catch (final Exception e1) {
Console.exception(e1);
}
}
}
}
private void prestart() {
setInputDisabled(true);
primaryStage.hide();
}
private void onSuccess(WorkerStateEvent e) {
setInputDisabled(false);
primaryStage.show();
showMessage("Autotyping Complete!");
}
private void onError(WorkerStateEvent e) {
setInputDisabled(false);
primaryStage.show();
Preconditions.checkNotNull(e.getSource());
Throwable ex = e.getSource().getException();
if (ex != null) {
Console.exception(ex);
showError("Autotyping Failed!: " + ex.getMessage());
} else {
Console.bigWarning("Autotyping Failed: Unknown Reason! Source: " + e.getSource());
showError("Autotyping Failed!: Unknown Reason!");
}
}
private void setInputDisabled(boolean state) {
locField.setDisable(state);
wField.setDisable(state);
iField.setDisable(state);
cBtn.setDisable(state);
}
private Task<Boolean> makeTask() {
Task<Boolean> task = new FXAutoTypingTask(keys, fileProperty, waitTimeProperty, inputDelayProperty, this::prestart);
task.setOnSucceeded(this::onSuccess);
task.setOnFailed(this::onError);
return task;
}
});
// Put it all together
// I have to admit all those constant numbers just look reallllly annoying.
grid.add(wBox, 0, 0, 1, 1);
grid.add(iBox, 0, 1, 1, 1);
grid.add(cBox, 0, 2, 1, 1);
grid.add(locBox, 0, 3, 1, 1);
grid.setAlignment(Pos.CENTER);
root.setTop(getMenuBar());
root.setBottom(buttonStack);
root.setCenter(gridStack);
final Scene scene;
if (OS.get() == OS.WINDOWS) {
scene = new Scene(root, 450, 250);
} else {
scene = new Scene(root, 600, 250);
}
scene.getStylesheets().add(Resources.getCSS("AutotyperWindow").url().toExternalForm());
primaryStage.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
if (!locField.contains(locField.sceneToLocal(event.getSceneX(), event.getSceneY()))) {
locField.hidePopup();
}
});
this.doSave = true;
locField.installXYChangeListeners(primaryStage);
primaryStage.setTitle(Ref.APP_NAME + " " + Ref.VERSION);
primaryStage.setResizable(true);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
Alert alert = FXGuiUtils.buildException(e);
alert.showAndWait();
Console.exception(e);
}
}
@Override
public void stop() {
if (this.doSave) {
savePrefs(this.waitTimeProperty.get(), this.inputDelayProperty.get(), this.curRank, this.locations);
}
this.keys.destroy();
}
// FXAutotyperWindow Specific Image Loading. We only expect 4
private void addIcons(Stage stage) {
Resources.setAppIcons(stage);
}
private HBox getMenuBar() {
final Image infoIcon = new Image(Resources.getImage("about_icon.png").stream(), 30, 30, false, true);
final Image copyIcon = new Image(Resources.getImage("copyright_icon.png").stream(), 30, 30, false, true);
final HBox bar = new HBox(5);
bar.setId("menu-bar");
bar.setPadding(new Insets(5, 5, 8, 5));
final Button infoBtn = new Button("", new ImageView(infoIcon));
final Button copyBtn = new Button("", new ImageView(copyIcon));
infoBtn.setTooltip(new TimedTooltip("Get Info (Opens in Browser)", 200));
copyBtn.setTooltip(new TimedTooltip("See Copyright Statement", 200));
final KeyCodeCombination infoAcc = new KeyCodeCombination(KeyCode.I, UP, DOWN, UP, UP, UP);
final KeyCodeCombination copyAcc = new KeyCodeCombination(KeyCode.C, UP, DOWN, UP, UP, UP);
infoBtn.setId("glass-button");
copyBtn.setId("glass-button");
infoBtn.sceneProperty().addListener((obs, ov, nv) -> {
if (nv != null)
nv.getAccelerators().put(infoAcc, FXAutotyperWindow.this::showGithubPage);
if (ov != null)
ov.getAccelerators().remove(infoAcc);
});
copyBtn.sceneProperty().addListener((obs, ov, nv) -> {
if (nv != null)
nv.getAccelerators().put(copyAcc, FXAutotyperWindow.this::showCopyrightInfo);
if (ov != null)
ov.getAccelerators().remove(copyAcc);
});
infoBtn.setOnAction((e) -> getHostServices().showDocument(Strings.GITHUB_URL));
copyBtn.setOnAction((e) -> Autotyper.printCopyrightStatement(true));
bar.getChildren().addAll(infoBtn, copyBtn);
return bar;
}
private void showGithubPage() {
getHostServices().showDocument(Strings.GITHUB_URL);
}
private void showCopyrightInfo() {
Autotyper.printCopyrightStatement(true);
}
private void savePrefs(int waitTime, int delay, int selected, EvictingQueue<String> locations) {
Console.empty();
Console.info("SAVING AS VERSION: " + Ref.VERSION);
this.prefs.put(Strings.PREFS_GUI_VERSION, Ref.VERSION);
this.prefs.putInt(Strings.PREFS_GUI_WAIT, waitTime);
this.prefs.putInt(Strings.PREFS_GUI_INPUTDELAY, delay);
this.prefs.putInt(Strings.PREFS_GUI_SELECTED, selected);
this.prefs.putBoolean(Strings.PREFS_GUI_MINIFY, minifyProperty.get());
this.prefs.putBoolean(Strings.PREFS_GUI_CONFIRM, this.doConfirm);
Console.info("\tWAIT TIME: " + waitTime);
Console.info("\tIN. DELAY: " + delay);
Console.info("\tSEL. BTN.: " + selected);
Console.info("\tMINIFY? : " + minifyProperty.get());
Console.info("\tCON. DLG.: " + this.doConfirm);
Console.empty();
for (int i = 0; i < 50; i++) {
final String text = locations.poll();
this.prefs.put(Strings.PREFS_GUI_MEMORY + i, String.valueOf(text));
if (text != null)
Console.info("\tTO HISTORY: " + text);
}
Console.empty();
}
private void loadPrefs() {
String version = this.prefs.get(Strings.PREFS_GUI_VERSION, "0.0.0");
Console.empty();
Console.info("LOADING VERSION: " + version);
if (AppVersion.compareTo(version) != 0) {
try {
prefs.clear();
Console.info("WARNING: Current Version is " + Ref.VERSION + "... Preferences will be erased to maintain compatibility.");
} catch (BackingStoreException e) {
Console.exception(e);
}
}
this.waitTimeProperty.set(this.prefs.getInt(Strings.PREFS_GUI_WAIT, 5000));
this.inputDelayProperty.set(this.prefs.getInt(Strings.PREFS_GUI_INPUTDELAY, 40));
this.minifyProperty.set(this.prefs.getBoolean(Strings.PREFS_GUI_MINIFY, true));
this.curRank = this.prefs.getInt(Strings.PREFS_GUI_SELECTED, 3);
this.doConfirm = this.prefs.getBoolean(Strings.PREFS_GUI_CONFIRM, true);
Console.info("\tWAIT TIME: " + waitTimeProperty.get());
Console.info("\tIN. DELAY: " + inputDelayProperty.get());
Console.info("\tSEL. BTN.: " + curRank);
Console.info("\tMINIFY? : " + minifyProperty.get());
Console.info("\tCON. DLG.: " + doConfirm);
Console.empty();
this.curRank = Math.max(1, Math.min(4, this.curRank));
saveToHistory("JCR8YTww", LocationHandler.PASTEBIN.tag());
saveToHistory("6gyLvm4K", LocationHandler.PASTEBIN.tag());
saveToHistory("nAinUn1h", LocationHandler.PASTEBIN.tag());
for (int i = 0; i < 50; i++) {
final String s = this.prefs.get(Strings.PREFS_GUI_MEMORY + i, "null");
if (s != null && !s.equals("null") && s.indexOf(':') != -1) {
int index = s.indexOf(':') + 1;
String tag = s.substring(0, index);
String main = s.substring(index);
saveToHistory(main, tag);
}
}
}
private void showError(String message) {
Alert alert = new Alert(Alert.AlertType.ERROR, message);
alert.setHeaderText("AutoTyper Error");
alert.setTitle("Error!");
alert.getDialogPane().setPrefSize(600, 400);
alert.show();
}
private void showMessage(String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION, message);
alert.setHeaderText("AutoTyper Message");
alert.setTitle("Message");
alert.getDialogPane().setPrefSize(600, 400);
alert.show();
}
private static final HashFunction hf = Hashing.murmur3_32();
private Set<HashCode> locationHashes = Sets.newHashSet();
private boolean saveToHistory(String loc, String tag) {
HashCode hash = hf.newHasher().putString(loc, Charsets.UTF_8).hash();
Console.debug("Location '" + loc + "' hashed to '" + hash.toString() + "', New? = " + !locationHashes.contains(hash));
if (!this.locationHashes.contains(hash)) {
this.locations.add(tag + loc);
this.locationHashes.add(hash);
return true;
}
return false;
}
private void obtainKeyboard() {
this.keys = Keyboard.retrieveKeyboard(KeyboardMethodology.TYPING);
}
private boolean isValid(String input) {
try {
return Integer.valueOf(input) != null;
} catch (final NumberFormatException e) {
// Ignored, just a check
}
return false;
}
private boolean approve(Path code) throws IOException {
if (FXConfirmDialog.isAvailable())
return FXConfirmDialog.confirm(this.primaryStage, code);
else
return new ConfirmFileDialog(null, code.toFile()).isApproved();
}
private boolean promptContinue(String msg) {
Alert prompt = new Alert(Alert.AlertType.CONFIRMATION, msg, ButtonType.YES, ButtonType.NO);
prompt.setHeaderText("Continue Task?");
prompt.initModality(Modality.APPLICATION_MODAL);
Optional<ButtonType> selection = prompt.showAndWait();
return selection.isPresent() && selection.get() == ButtonType.YES;
}
public static void launch() {
Application.launch(FXAutotyperWindow.class);
}
private static final GuiAccessor FX_ACCESSOR = new GuiAccessor() {
@Override
public void doShow() {
if (FXAutotyperWindow.INSTANCE == null) {
FXAutotyperWindow.launch();
} else {
FXAutotyperWindow.INSTANCE.primaryStage.show();
}
}
@Override
public void doHide() {
FXAutotyperWindow.INSTANCE.primaryStage.hide();
}
@Override
public void openSite(String url) {
FXAutotyperWindow.INSTANCE.getHostServices().showDocument(url);
}
};
public static GuiAccessor getAccessor() {
return FX_ACCESSOR;
}
}