diff --git a/pom.xml b/pom.xml
index 861494d..46ab53d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
nl.andrewlalis
BlockBookBinder
- 0.0.2
+ 1.0.0
12
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java
index 88fde8a..14315f8 100644
--- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java
@@ -2,6 +2,7 @@ package nl.andrewlalis.blockbookbinder.control;
import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel;
+import nl.andrewlalis.blockbookbinder.view.export.ExportToBookDialog;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
@@ -34,7 +35,7 @@ public class BookExportActionListener implements ActionListener {
final Book book = this.bookPreviewPanel.getBook();
int choice = JOptionPane.showConfirmDialog(
this.bookPreviewPanel.getRootPane(),
- "Press CTRL+V to initialize export.",
+ "Press OK to initialize export.",
"Confirm Export",
JOptionPane.OK_CANCEL_OPTION
);
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java
index 3f4c3a3..78b845f 100644
--- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java
@@ -8,9 +8,12 @@ import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;
import javax.swing.*;
+import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
/**
* Listener that listens for native key-presses that indicate the user has
@@ -22,6 +25,7 @@ public class BookPagePasteListener implements NativeKeyListener {
private final BookPreviewPanel bookPreviewPanel;
private final JButton cancelExportButton;
private final ActionListener cancelExportActionListener;
+ private Robot robot;
private int nextPage;
public BookPagePasteListener(Book book, Clipboard clipboard, BookPreviewPanel bookPreviewPanel, JButton cancelExportButton) {
@@ -32,6 +36,11 @@ public class BookPagePasteListener implements NativeKeyListener {
this.nextPage = 0;
this.cancelExportActionListener = (e) -> this.cancelExport();
this.cancelExportButton.addActionListener(this.cancelExportActionListener);
+ try {
+ this.robot = new Robot();
+ } catch (AWTException e) {
+ e.printStackTrace();
+ }
}
public void exportNextPage() {
@@ -68,7 +77,22 @@ public class BookPagePasteListener implements NativeKeyListener {
// If we've reached the end of the book, unregister this listener and remove native hooks.
if (this.nextPage >= this.book.getPageCount()) {
this.cancelExport();
+ return;
}
+
+ // Automatically do a CTRL+V and click the mouse to go to the next page.
+ this.robot.keyPress(KeyEvent.VK_CONTROL);
+ this.robot.keyPress(KeyEvent.VK_V);
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.robot.keyRelease(KeyEvent.VK_V);
+ this.robot.keyRelease(KeyEvent.VK_CONTROL);
+
+ this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK);
+ this.robot.mouseRelease(MouseEvent.BUTTON1_DOWN_MASK);
}
public void cancelExport() {
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java
new file mode 100644
index 0000000..432cd03
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java
@@ -0,0 +1,214 @@
+package nl.andrewlalis.blockbookbinder.control.export;
+
+import lombok.Setter;
+import nl.andrewlalis.blockbookbinder.model.Book;
+import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
+import org.jnativehook.GlobalScreen;
+import org.jnativehook.NativeHookException;
+
+import javax.sound.sampled.*;
+import java.awt.*;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A separate runnable process which handles exporting a book, page by page,
+ * into one's clipboard and pasting the pages.
+ */
+public class BookExporter implements Runnable {
+ private final static int START_DELAY = 10;
+ private final static int CLIPBOARD_RETRY_DELAY_MS = 100;
+ private final static int ROBOT_ACTION_DELAY_MS = 100;
+
+ private final Book book;
+ private final boolean autoPaste;
+
+ @Setter
+ private volatile boolean running;
+
+ @Setter
+ private volatile boolean nextPageRequested;
+
+ private final PagePasteListener pagePasteListener;
+ private final Clipboard clipboard;
+ private Robot robot;
+
+ // Some sound clips to play as user feedback.
+ private final Clip beepClip;
+ private final Clip beginningExportClip;
+
+ public BookExporter(Book book, boolean autoPaste) {
+ this.book = book;
+ this.autoPaste = autoPaste;
+ this.beepClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beep_sound"));
+ this.beginningExportClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beginning_export"));
+ this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ this.pagePasteListener = new PagePasteListener(this);
+ if (this.autoPaste) { // Only initialize the robot if we'll need it.
+ try {
+ this.robot = new Robot();
+ } catch (AWTException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ this.running = true;
+ this.nextPageRequested = true;
+ long startTime = System.currentTimeMillis();
+ long lastAudioPlayedAt = 0;
+ long lastPageExportedAt = 0;
+ int nextPageToExport = 0;
+ while (this.running) {
+ long currentTime = System.currentTimeMillis();
+ // Check if we're still in the first few seconds of runtime.
+ boolean inStartPhase = (currentTime - startTime < (START_DELAY * 1000));
+ if (inStartPhase && currentTime - lastAudioPlayedAt > 1000) {
+ this.playAudioClip(this.beepClip);
+ lastAudioPlayedAt = currentTime;
+ }
+ // Otherwise, export one page.
+ if (!inStartPhase && this.nextPageRequested) {
+ System.out.println("Page requested: " + nextPageToExport);
+ this.nextPageRequested = false; // Reset the flag so that some other process has to set it before the next page is exported.
+ // If this is the first time we're exporting, play a sound.
+ if (lastPageExportedAt == 0) {
+ this.initNativeListener();
+ this.playAudioClip(this.beginningExportClip);
+ }
+ this.exportPageToClipboard(nextPageToExport);
+ if (this.autoPaste) {
+ this.pasteAndTurnPage();
+ }
+ this.playAudioClip(this.beepClip);
+ nextPageToExport++;
+ // If we've reached the end of the book, stop the exporter.
+ if (nextPageToExport >= this.book.getPageCount()) {
+ System.out.println("Export finished: " + this.book.getPageCount() + " pages exported.");
+ if (!this.autoPaste) {
+ this.stopNativeListener();
+ }
+ this.running = false;
+ break;
+ }
+ // Since there may be significant delay, get a fresh timestamp.
+ lastPageExportedAt = System.currentTimeMillis();
+ }
+ }
+ }
+
+ /**
+ * Loads the given page onto the system clipboard so either a user or this
+ * program can paste it into a minecraft book.
+ * @param page The index of the page to export.
+ */
+ private void exportPageToClipboard(int page) {
+ boolean clipboardSuccess = false;
+ int attempts = 0;
+ while (!clipboardSuccess) {
+ try {
+ attempts++;
+ clipboard.setContents(new StringSelection(book.getPages().get(page).toString()), null);
+ clipboardSuccess = true;
+ } catch (IllegalStateException e) {
+ System.err.println("Could not open and set contents of system clipboard.");
+ }
+ if (!clipboardSuccess) {
+ try {
+ Thread.sleep(CLIPBOARD_RETRY_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ if (attempts > 10) {
+ throw new RuntimeException("Could not insert page into clipboard after " + attempts + " attempts.");
+ }
+ }
+ System.out.println("Exported page " + page + " to clipboard.");
+ }
+
+ /**
+ * Automatically pastes and turns the page of the minecraft book, so that
+ * the next page can be pasted in.
+ */
+ private void pasteAndTurnPage() {
+ this.robot.keyPress(KeyEvent.VK_CONTROL);
+ this.robot.keyPress(KeyEvent.VK_V);
+ try {
+ Thread.sleep(ROBOT_ACTION_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.robot.keyRelease(KeyEvent.VK_V);
+ this.robot.keyRelease(KeyEvent.VK_CONTROL);
+ System.out.println("Pasted.");
+ this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK);
+ try {
+ Thread.sleep(ROBOT_ACTION_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.robot.mouseRelease(MouseEvent.BUTTON1_DOWN_MASK);
+ try { // Wait for minecraft to turn the page.
+ Thread.sleep(ROBOT_ACTION_DELAY_MS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("Clicked mouse.");
+ this.nextPageRequested = true;
+ }
+
+ private void initNativeListener() {
+ try {
+ // For catching native events, set logging here.
+ Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName());
+ logger.setLevel(Level.WARNING);
+ logger.setUseParentHandlers(false);
+ GlobalScreen.registerNativeHook();
+ GlobalScreen.addNativeKeyListener(this.pagePasteListener);
+ } catch (NativeHookException nativeHookException) {
+ System.err.println("Could not register native hook.");
+ nativeHookException.printStackTrace();
+ }
+ }
+
+ private void stopNativeListener() {
+ try {
+ GlobalScreen.removeNativeKeyListener(this.pagePasteListener);
+ GlobalScreen.unregisterNativeHook();
+ } catch (NativeHookException nativeHookException) {
+ System.err.println("Could not unregister a native hook.");
+ nativeHookException.printStackTrace();
+ }
+ }
+
+ private void playAudioClip(Clip clip) {
+ clip.setFramePosition(0);
+ clip.start();
+ }
+
+ private Clip loadAudioClip(String path) {
+ try {
+ Clip clip = AudioSystem.getClip();
+ InputStream fileInputStream = this.getClass().getClassLoader().getResourceAsStream(path);
+ if (fileInputStream == null) {
+ return null;
+ }
+ AudioInputStream ais = AudioSystem.getAudioInputStream(fileInputStream);
+ clip.open(ais);
+ return clip;
+ } catch (LineUnavailableException | IOException | UnsupportedAudioFileException e) {
+ System.err.println("Could not load audio clip.");
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/export/PagePasteListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/PagePasteListener.java
new file mode 100644
index 0000000..a5b0899
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/PagePasteListener.java
@@ -0,0 +1,31 @@
+package nl.andrewlalis.blockbookbinder.control.export;
+
+import org.jnativehook.keyboard.NativeKeyEvent;
+import org.jnativehook.keyboard.NativeKeyListener;
+
+public class PagePasteListener implements NativeKeyListener {
+ private BookExporter exporterRunnable;
+
+ public PagePasteListener(BookExporter exporterRunnable) {
+ this.exporterRunnable = exporterRunnable;
+ }
+
+ @Override
+ public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {}
+
+ @Override
+ public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) {
+ if (nativeKeyEvent.getKeyCode() == NativeKeyEvent.VC_V && (nativeKeyEvent.getModifiers() & NativeKeyEvent.CTRL_MASK) > 0) {
+ // Wait a little bit so that we can let the system do whatever it was planning to do with the original paste action.
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ this.exporterRunnable.setNextPageRequested(true);
+ }
+ }
+
+ @Override
+ public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {}
+}
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java b/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java
index d51a0d9..9277706 100644
--- a/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java
@@ -21,6 +21,20 @@ public class Book {
this.pages.add(page);
}
+ /**
+ * Gets a book containing the pages specified by the range.
+ * @param firstIndex The index of the first page to include.
+ * @param count The number of pages to include.
+ * @return The book containing the range of pages.
+ */
+ public Book getPageRange(int firstIndex, int count) {
+ Book book = new Book();
+ for (int i = 0; i < count; i++) {
+ book.addPage(this.pages.get(firstIndex + i));
+ }
+ return book;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Book of " + this.getPageCount() + " pages:\n");
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java
index 4f83b40..b749d26 100644
--- a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java
@@ -2,7 +2,9 @@ package nl.andrewlalis.blockbookbinder.view;
import nl.andrewlalis.blockbookbinder.control.BookExportActionListener;
import nl.andrewlalis.blockbookbinder.control.ImportAction;
+import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
+import nl.andrewlalis.blockbookbinder.view.export.ExportToBookDialog;
import javax.swing.*;
import java.awt.*;
@@ -67,7 +69,15 @@ public class MainFrame extends JFrame {
JButton exportButton = new JButton("Export to Book");
JButton cancelExportButton = new JButton("Cancel Export");
cancelExportButton.setEnabled(false);
- exportButton.addActionListener(new BookExportActionListener(bookPreviewPanel, cancelExportButton));
+ exportButton.addActionListener(e -> {
+ final Book book = bookPreviewPanel.getBook();
+ if (book == null || book.getPageCount() == 0) {
+ JOptionPane.showMessageDialog(this, "Cannot export an empty book.", "Empty Book", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+ ExportToBookDialog dialog = new ExportToBookDialog(this, bookPreviewPanel.getBook());
+ dialog.setupAndShow();
+ });
bottomPanel.add(exportButton);
bottomPanel.add(cancelExportButton);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java
new file mode 100644
index 0000000..c58202e
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java
@@ -0,0 +1,18 @@
+package nl.andrewlalis.blockbookbinder.view.export;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * A panel with some components for displaying the current status of an export
+ * job.
+ */
+public class ExportStatusPanel extends JPanel {
+ private JProgressBar exportProgressBar;
+
+ public ExportStatusPanel() {
+ this.setLayout(new BorderLayout());
+
+ this.exportProgressBar = new JProgressBar();
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java
new file mode 100644
index 0000000..325ea1d
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java
@@ -0,0 +1,133 @@
+package nl.andrewlalis.blockbookbinder.view.export;
+
+import nl.andrewlalis.blockbookbinder.control.export.BookExporter;
+import nl.andrewlalis.blockbookbinder.model.Book;
+import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * A special dialog box that's shown during the process of exporting a book into
+ * minecraft.
+ */
+public class ExportToBookDialog extends JDialog {
+ private final static String SETUP_CARD = "SETUP";
+ private final static String STATUS_CARD = "STATUS";
+
+ private final Book book;
+
+ // Setup input fields.
+ private JCheckBox autoCheckbox;
+ private JSpinner firstPageSpinner;
+ private JSpinner lastPageSpinner;
+
+ private JButton startButton;
+ private JButton stopButton;
+ private JPanel centerCardPanel;
+ private ExportStatusPanel exportStatusPanel;
+
+ private Thread exporterThread;
+ private BookExporter exporterRunnable;
+
+ public ExportToBookDialog(Frame owner, Book book) {
+ super(owner, "Export to Book", true);
+ this.book = book;
+ }
+
+ public void setupAndShow() {
+ this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ this.setContentPane(this.buildContentPane());
+ this.setMinimumSize(new Dimension(
+ ApplicationProperties.getIntProp("export_dialog.min_width"),
+ ApplicationProperties.getIntProp("export_dialog.min_height")
+ ));
+ this.pack();
+ this.setLocationRelativeTo(null);
+ this.setVisible(true);
+ }
+
+ private Container buildContentPane() {
+ JPanel mainPanel = new JPanel(new BorderLayout());
+
+ JPanel setupPanel = new JPanel();
+ setupPanel.setLayout(new BoxLayout(setupPanel, BoxLayout.PAGE_AXIS));
+ this.autoCheckbox = new JCheckBox("Auto-paste", true);
+ this.firstPageSpinner = new JSpinner(new SpinnerNumberModel(1, 1, this.book.getPageCount(), 1));
+ this.lastPageSpinner = new JSpinner(new SpinnerNumberModel(this.book.getPageCount(), 1, this.book.getPageCount(), 1));
+ setupPanel.add(this.autoCheckbox);
+ setupPanel.add(this.firstPageSpinner);
+ setupPanel.add(this.lastPageSpinner);
+
+ this.exportStatusPanel = new ExportStatusPanel();
+
+ this.centerCardPanel = new JPanel(new CardLayout());
+ centerCardPanel.add(setupPanel, SETUP_CARD);
+ centerCardPanel.add(this.exportStatusPanel, STATUS_CARD);
+ this.showCardByName(SETUP_CARD);
+
+ mainPanel.add(centerCardPanel, BorderLayout.CENTER);
+
+ JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ this.startButton = new JButton("Start");
+ this.startButton.addActionListener(e -> {
+ int choice = JOptionPane.showConfirmDialog(
+ this.rootPane,
+ "Exporting will begin after roughly 10 seconds.\n" +
+ "If you have selected \"Auto-paste\", then place\n" +
+ "your mouse cursor over the right arrow of the book\n" +
+ "so that BlockBookBinder can automatically click it.\n\n" +
+ "You have chosen to export pages " + this.firstPageSpinner.getValue() + " to " + this.lastPageSpinner.getValue() + ".",
+ "Export Start Confirmation",
+ JOptionPane.OK_CANCEL_OPTION,
+ JOptionPane.INFORMATION_MESSAGE
+ );
+ if (choice == JOptionPane.OK_OPTION) {
+ this.startExporter();
+ }
+ });
+ controlPanel.add(this.startButton);
+ this.stopButton = new JButton("Stop");
+ this.stopButton.setVisible(false);
+ this.stopButton.setEnabled(false);
+ this.stopButton.addActionListener(e -> this.stopExporter());
+ controlPanel.add(this.stopButton);
+ mainPanel.add(controlPanel, BorderLayout.SOUTH);
+
+ return mainPanel;
+ }
+
+ private void startExporter() {
+ final int firstPage = (int) this.firstPageSpinner.getValue();
+ final int lastPage = (int) this.lastPageSpinner.getValue();
+ final Book pagesRange = this.book.getPageRange(firstPage - 1, lastPage - firstPage + 1);
+
+ this.startButton.setEnabled(false);
+ this.startButton.setVisible(false);
+ this.stopButton.setEnabled(true);
+ this.stopButton.setVisible(true);
+ this.showCardByName(STATUS_CARD);
+ this.exporterRunnable = new BookExporter(pagesRange, this.autoCheckbox.isSelected());
+ this.exporterThread = new Thread(this.exporterRunnable);
+ this.exporterThread.start();
+ }
+
+ private void stopExporter() {
+ this.exporterRunnable.setRunning(false);
+ try {
+ this.exporterThread.join();
+ } catch (InterruptedException interruptedException) {
+ interruptedException.printStackTrace();
+ }
+ this.showCardByName(SETUP_CARD);
+ this.stopButton.setEnabled(false);
+ this.stopButton.setVisible(false);
+ this.startButton.setEnabled(true);
+ this.startButton.setVisible(true);
+ }
+
+ private void showCardByName(String name) {
+ CardLayout cl = (CardLayout) this.centerCardPanel.getLayout();
+ cl.show(this.centerCardPanel, name);
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index fd49145..f68ff1b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,6 +3,11 @@ frame.title=Block Book Binder
frame.default_width=800
frame.default_height=600
+export_dialog.min_width=400
+export_dialog.min_height=300
+export_dialog.beep_sound=sound/andrew_beep.wav
+export_dialog.beginning_export=sound/beginning_export.wav
+
# Settings for Minecraft book interaction.
book.max_pages=100
book.page_max_lines=14
diff --git a/src/main/resources/sound/andrew_beep.wav b/src/main/resources/sound/andrew_beep.wav
new file mode 100644
index 0000000..d425af5
Binary files /dev/null and b/src/main/resources/sound/andrew_beep.wav differ
diff --git a/src/main/resources/sound/beginning_export.wav b/src/main/resources/sound/beginning_export.wav
new file mode 100644
index 0000000..e44e7cd
Binary files /dev/null and b/src/main/resources/sound/beginning_export.wav differ