package com.mattc.autotyper.gui;
import com.google.common.collect.Lists;
import com.mattc.autotyper.Autotyper;
import com.mattc.autotyper.Parameters;
import com.mattc.autotyper.Ref;
import com.mattc.autotyper.Strings;
import com.mattc.autotyper.Strings.Resources;
import com.mattc.autotyper.Strings.Resources.Resource;
import com.mattc.autotyper.meta.Outcome;
import com.mattc.autotyper.robot.Keyboard;
import com.mattc.autotyper.robot.KeyboardMethodology;
import com.mattc.autotyper.util.Console;
import com.mattc.autotyper.util.IOUtils;
import org.fife.ui.autocomplete.AutoCompletion;
import org.fife.ui.autocomplete.BasicCompletion;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionProvider;
import org.fife.ui.autocomplete.DefaultCompletionProvider;
import org.fife.ui.rtextarea.RTextArea;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.prefs.Preferences;
/**
* Main Window for a Swing Autotyper GUI <br />
* <br />
* Handles such things as auto-completion and launching the application. Attempts to
* be slick... using Swing...
*
* @author Matthew
*/
public class AutotyperWindow extends JFrame implements GuiAccessor {
private static final long serialVersionUID = -776172984918939880L;
private static final String RANK = "rank";
private final Keyboard keys;
private final Preferences prefs;
private final Timer timer = new Timer(true);
private final String[] locations = new String[50];
private int pointer = 0;
private boolean doConfirm;
private int waitTime, inDelay;
private MetaButtonGroup group;
public AutotyperWindow() {
super(Ref.APP_NAME + " | " + Ref.VERSION);
// Initialize Preferences and Swing based Keyboard (we know FX won't work
this.keys = Keyboard.retrieveKeyboard(KeyboardMethodology.TYPING);
this.keys.setInputDelay(Parameters.DEFAULT_DELAY);
this.prefs = Preferences.userNodeForPackage(AutotyperWindow.class);
EventQueue.invokeLater(() -> {
Thread.currentThread().setName("GUI");
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setIcons();
init();
initMenuBar();
setSize(500, 280);
setResizable(false);
setLocationRelativeTo(null);
setVisible(false);
});
}
/**
* Create and Layout the various components.
*/
private void init() {
final Container cont = getContentPane();
cont.setLayout(new BoxLayout(cont, BoxLayout.Y_AXIS));
loadPrefs();
this.group = new MetaButtonGroup();
// Auto Completion set up
final DefaultCompletionProvider provider = new DefaultCompletionProvider();
final AutoCompletion locationCompleter = new AutoCompletion(updateLocationCompletionProvider(provider));
// TODO NodeParser Equivalent/InteractiveBox Equivalent
final JPanel inputPanel = new JPanel();
final JPanel radioPanel = new JPanel();
final JLabel wLabel1 = new JLabel("Wait");
final JLabel wLabel2 = new JLabel("seconds before typing.");
final JLabel iLabel1 = new JLabel("Wait");
final JLabel iLabel2 = new JLabel("milliseconds between keystrokes.");
final JLabel cLabel1 = new JLabel("I");
final JLabel cLabel2 = new JLabel("want to confirm the file.");
final JTextField wField = new JTextField(2);
final JTextField iField = new JTextField(2);
final RTextArea lField = new RTextArea(1, 50);
final JToggleButton cButton = new JToggleButton(this.doConfirm ? "do" : "do not");
final JRadioButton file = new JRadioButton("Local File");
final JRadioButton url = new JRadioButton("Website File");
final JRadioButton paste = new JRadioButton("Pastebin Code");
final JRadioButton auto = new JRadioButton("Auto Detect");
final JButton type = new JButton("Start");
radioPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 0));
cButton.setSelected(this.doConfirm);
wField.setDocument(new TextLimitDocument(2));
iField.setDocument(new TextLimitDocument(2));
wField.setText(Integer.toString(this.waitTime / 1000));
iField.setText(Integer.toString(this.inDelay));
wField.setHorizontalAlignment(SwingConstants.CENTER);
iField.setHorizontalAlignment(SwingConstants.CENTER);
lField.setMargin(new Insets(0, 5, 0, 5));
lField.setHighlightCurrentLine(false);
locationCompleter.install(lField);
final JPanel s1 = new JPanel();
final JPanel s2 = new JPanel();
final JPanel s3 = new JPanel();
s1.add(wLabel1);
s1.add(wField);
s1.add(wLabel2);
s2.add(iLabel1);
s2.add(iField);
s2.add(iLabel2);
s3.add(cLabel1);
s3.add(cButton);
s3.add(cLabel2);
// Initialize Meta Button Properties
this.group.add(auto, Strings.GHOST_TEXT_ASELECT);
this.group.add(url, Strings.GHOST_TEXT_USELECT);
this.group.add(file, Strings.GHOST_TEXT_FSELECT);
this.group.add(paste, Strings.GHOST_TEXT_PSELECT);
this.group.putProperty(file, RANK, 1);
this.group.putProperty(url, RANK, 2);
this.group.putProperty(paste, RANK, 3);
this.group.putProperty(auto, RANK, 4);
this.group.setSelectedForProperty(RANK, this.prefs.getInt(Strings.PREFS_GUI_SELECTED, 3));
addGhostText(lField, this.group.getMetaStringForSelected());
radioPanel.add(file);
radioPanel.add(url);
radioPanel.add(paste);
radioPanel.add(auto);
radioPanel.add(type);
inputPanel.add(lField);
inputPanel.add(radioPanel);
cont.add(s1);
cont.add(s2);
cont.add(s3);
cont.add(inputPanel);
locationCompleter.setAutoCompleteEnabled(true);
locationCompleter.setAutoActivationEnabled(true);
locationCompleter.setAutoCompleteSingleChoices(false);
locationCompleter.setShowDescWindow(false);
wField.addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
final JTextField field = (JTextField) e.getSource();
if (isValid(field.getText())) {
AutotyperWindow.this.waitTime = Integer.parseInt(field.getText()) * 1000;
} else {
field.setText(Integer.toString(AutotyperWindow.this.waitTime / 1000));
}
}
@Override
public void focusGained(FocusEvent e) {
}
});
iField.addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
final JTextField field = (JTextField) e.getSource();
if (isValid(field.getText())) {
AutotyperWindow.this.inDelay = Integer.parseInt(field.getText());
} else {
field.setText(Integer.toString(AutotyperWindow.this.inDelay));
}
}
@Override
public void focusGained(FocusEvent e) {
}
});
lField.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
final RTextArea field = (RTextArea) e.getSource();
if (isGhostText(field)) {
removeGhostText(field);
}
}
@Override
public void focusLost(FocusEvent e) {
final RTextArea field = (RTextArea) e.getSource();
if (field.getText().trim().isEmpty()) {
addGhostText(field, AutotyperWindow.this.group.getMetaStringForSelected());
}
}
});
type.addActionListener(new ActionListener() {
private Thread prevThread;
@Override
public void actionPerformed(ActionEvent e) {
final String text = lField.getText().trim();
if (isGhostText(lField) || (text.length() == 0)) {
showError("You must provide some kind of location!");
return;
} else if ((this.prevThread != null) && this.prevThread.isAlive()) {
showError("Can Only Autotype one File at a Time!");
return;
} else {
LocationHandler handler;
Outcome outcome;
if (file.isSelected()) {
handler = LocationHandler.FILE;
} else if (url.isSelected()) {
handler = LocationHandler.URL;
} else if (paste.isSelected()) {
handler = LocationHandler.PASTEBIN;
} else if (auto.isSelected()) {
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;
}
} else
return;
Console.debug("Using " + handler.name() + " LocationHandler...");
outcome = handler.canHandle(text);
if (outcome.isFailure()) {
Console.debug(outcome);
showError(outcome.reason);
} else {
final File file = handler.handle(text).toFile();
try {
// Confirmation Check, Uses Swing
if (AutotyperWindow.this.doConfirm) {
final ConfirmFileDialog dialog = new ConfirmFileDialog(AutotyperWindow.this, file);
if (!dialog.isApproved()) return;
}
// Pre-Typing Prompt
final boolean ok = showPrompt("Start Autotyping in " + (AutotyperWindow.this.waitTime / 1000) + " seconds?");
if (ok) {
// Block Input and Execute Typing on Separate Thread
setInput(false);
this.prevThread = makeExecutionThread(file);
this.prevThread.start();
toBack();
saveToHistory(text);
updateLocationCompletionProvider((DefaultCompletionProvider) locationCompleter.getCompletionProvider());
} else
return;
} catch (final IOException e1) {
e1.printStackTrace();
}
}
}
}
/**
* Handles a Singular Autotyping Operation
*
* @param file
* @return
*/
private Thread makeExecutionThread(final File file) {
return new Thread(new Runnable() {
@Override
public void run() {
try {
AutotyperWindow.this.keys.setInputDelay(AutotyperWindow.this.inDelay);
IOUtils.sleep(AutotyperWindow.this.waitTime);
AutotyperWindow.this.keys.typeFile(file);
setInput(true);
showMessage("Finished typing " + lField.getText() + "!");
} catch (final IOException ex) {
Console.exception(ex);
showError("Failure to Autotype, Exception of type " + ex.getClass() + " occurred...");
}
}
}, "TYPER");
}
/**
* Enable or Disabled Input Fields
*
* @param state
*/
private void setInput(boolean state) {
lField.setEnabled(state);
wField.setEnabled(state);
iField.setEnabled(state);
cButton.setEnabled(state);
}
});
/*
* Auto Update Ghost Text for Location Field every 200 ms.
*/
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (lField.hasFocus() || (!lField.getText().trim().isEmpty() && !isGhostText(lField))) return;
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
lField.setText(AutotyperWindow.this.group.getMetaStringForSelected());
}
});
}
}, 0, 200);
/*
* When clicked, switch between "do" and "don't" confirm.
*/
cButton.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
AutotyperWindow.this.doConfirm = e.getStateChange() == ItemEvent.SELECTED;
cButton.setText(AutotyperWindow.this.doConfirm ? "do" : "do not");
}
});
}
// Create and Add Init Menu Bar
private void initMenuBar() {
try {
// Create Pseudo JMenu's that are images and act as buttons
final JMenuBar bar = new JMenuBar();
final Image icoImg = ImageIO.read(ClassLoader.getSystemClassLoader().getResource("res/about_icon.png"));
final Image cpyImg = ImageIO.read(ClassLoader.getSystemClassLoader().getResource("res/copyright_icon.png"));
final JMenu about = new JMenu();
final JMenu copy = new JMenu();
about.setIcon(new ImageIcon(icoImg.getScaledInstance(12, 12, Image.SCALE_SMOOTH)));
copy.setIcon(new ImageIcon(cpyImg.getScaledInstance(12, 12, Image.SCALE_SMOOTH)));
about.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
try {
Desktop.getDesktop().browse(new URL(Strings.GITHUB_URL).toURI());
} catch (IOException | URISyntaxException e) {
Console.exception(e);
}
}
});
copy.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
if (evt.getButton() == MouseEvent.BUTTON1) {
Autotyper.printCopyrightStatement(true);
}
}
});
bar.add(about);
bar.add(copy);
setJMenuBar(bar);
} catch (final IOException e) {
Console.exception(e);
showError("A Non-Fatal Error Has Occurred!: " + e.getMessage());
}
}
/**
* Check for and add Icons if possible. If this fails it may indicate corruption
* or a bad transfer as the images are included in the JAR File.
*/
private void setIcons() {
final List<Image> icons = Lists.newArrayList();
for (int size = 32; size <= 128; size += 16) {
final Resource res = Resources.getImage("icon" + size + ".png");
if ((res != null) && (res.stream() != null)) {
Console.debug("Found icon" + size + ".png");
try {
icons.add(ImageIO.read(res.stream()));
} catch (final IOException e) {
Console.exception(e);
}
} else if ((((size % 32) == 0) || (size == 48)) && (size != 96)) {
Console.error("Could not find icon" + size + ".png!");
}
}
setIconImages(icons);
}
private void savePrefs(int waitTime, int delay, int selected, String[] locations) {
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_CONFIRM, this.doConfirm);
for (int i = 0; i < locations.length; i++) {
this.prefs.put(Strings.PREFS_GUI_MEMORY + i, locations[i] == null ? "null" : locations[i]);
}
}
private void loadPrefs() {
this.waitTime = this.prefs.getInt(Strings.PREFS_GUI_WAIT, 5000);
this.inDelay = this.prefs.getInt(Strings.PREFS_GUI_INPUTDELAY, 40);
this.doConfirm = this.prefs.getBoolean(Strings.PREFS_GUI_CONFIRM, true);
for (int i = 0; i < this.locations.length; i++) {
final String s = this.prefs.get(Strings.PREFS_GUI_MEMORY + i, "null");
if (!s.equals("null")) {
this.locations[i] = s;
}
}
this.pointer = getPointer(this.locations);
}
// Make sure we execute on the EDT
@Override
public void setVisible(final boolean visible) {
if (!EventQueue.isDispatchThread()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
AutotyperWindow.super.setVisible(visible);
}
});
} else {
super.setVisible(visible);
}
}
@Override
public void dispose() {
savePrefs(this.waitTime, this.inDelay, this.group.getMetaForSelected(RANK, Integer.class), this.locations);
super.dispose();
this.timer.cancel();
this.keys.destroy();
}
private boolean isValid(String input) {
try {
Integer.parseInt(input);
return true;
} catch (final NumberFormatException e) {
}
return false;
}
private boolean isGhostText(JTextComponent comp) {
return comp.getFont().equals(GHOST_FONT);
}
private static final Font NORMAL_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 14);
private static final Font GHOST_FONT = new Font(Font.MONOSPACED, Font.ITALIC, 14);
private void addGhostText(JTextComponent comp, String text) {
comp.setForeground(Color.LIGHT_GRAY);
comp.setText(text);
comp.setFont(GHOST_FONT);
}
private void removeGhostText(JTextComponent comp) {
comp.setForeground(Color.BLACK);
comp.setText("");
comp.setFont(NORMAL_FONT);
}
private void showError(String message) {
JOptionPane.showMessageDialog(this, message, "Autotyper Error", JOptionPane.ERROR_MESSAGE);
}
private boolean showPrompt(String message) {
final int val = JOptionPane.showConfirmDialog(this, message, "Autotyper Prompt", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
return val == JOptionPane.YES_OPTION;
}
private void showMessage(String message) {
JOptionPane.showMessageDialog(this, message, "Autotyper Message", JOptionPane.INFORMATION_MESSAGE);
}
private void saveToHistory(String loc) {
if (Arrays.asList(this.locations).contains(loc)) return;
if (this.pointer < this.locations.length) {
this.locations[this.pointer++] = loc;
} else {
for (int i = 1; i < (this.locations.length - 1); i++) {
this.locations[i - 1] = this.locations[i];
}
this.locations[this.pointer - 1] = loc;
}
}
private int getPointer(Object[] arr) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == null) return i;
}
return arr.length;
}
private CompletionProvider updateLocationCompletionProvider(DefaultCompletionProvider provider) {
provider.clear();
if (this.pointer == 0) {
this.locations[0] = "JCR8YTww";
this.locations[1] = "6gyLvm4K";
this.locations[2] = "nAinUn1h";
this.pointer = 3;
}
for (int i = 0; i < this.locations.length; i++) {
if (this.locations[i] == null) {
break;
}
provider.addCompletion(new BasicCompletion(provider, this.locations[i]));
}
checkDefaultCompletions(provider);
return provider;
}
private void checkDefaultCompletions(DefaultCompletionProvider provider) {
final List<Completion> bubbles = Lists.newArrayList();
final List<Completion> milkshake = Lists.newArrayList();
final List<Completion> calc = Lists.newArrayList();
if (provider.getCompletionByInputText("JCR8YTww") != null) {
bubbles.addAll(provider.getCompletionByInputText("JCR8YTww"));
}
if (provider.getCompletionByInputText("6gyLvm4K") != null) {
milkshake.addAll(provider.getCompletionByInputText("6gyLvm4K"));
}
if (provider.getCompletionByInputText("nAinUn1h") != null) {
calc.addAll(provider.getCompletionByInputText("nAinUn1h"));
}
if (bubbles.size() != 0) {
provider.removeCompletion(bubbles.get(0));
provider.addCompletion(new BasicCompletion(provider, "JCR8YTww", "Bubbles! by KingofGamesYami"));
}
if (milkshake.size() != 0) {
provider.removeCompletion(milkshake.get(0));
provider.addCompletion(new BasicCompletion(provider, this.locations[1], "Milkshake GUI by lednerg"));
}
if (calc.size() != 0) {
provider.removeCompletion(calc.get(0));
provider.addCompletion(new BasicCompletion(provider, this.locations[2], "Advanced Calculator by Cranium"));
}
}
@Override
public void doShow() {
setVisible(true);
}
@Override
public void doHide() {
setVisible(false);
}
@Override
public void openSite(String url) {
try {
Desktop.getDesktop().browse(new URL(url).toURI());
} catch (URISyntaxException | IOException e) {
Console.exception(e);
}
}
}