diff --git a/pom.xml b/pom.xml index 46ab53d..b097caf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ nl.andrewlalis BlockBookBinder - 1.0.0 + 1.1.0 12 diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java deleted file mode 100644 index 14315f8..0000000 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java +++ /dev/null @@ -1,61 +0,0 @@ -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; - -import javax.swing.*; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Action listener that is used for when a user decides to begin exporting a - * book to minecraft. - */ -public class BookExportActionListener implements ActionListener { - private final BookPreviewPanel bookPreviewPanel; - private final Clipboard clipboard; - private final JButton cancelExportButton; - - public BookExportActionListener(BookPreviewPanel bookPreviewPanel, JButton cancelExportButton) { - this.bookPreviewPanel = bookPreviewPanel; - this.cancelExportButton = cancelExportButton; - this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - } - - @Override - public void actionPerformed(ActionEvent e) { - System.out.println("Starting export."); - final Book book = this.bookPreviewPanel.getBook(); - int choice = JOptionPane.showConfirmDialog( - this.bookPreviewPanel.getRootPane(), - "Press OK to initialize export.", - "Confirm Export", - JOptionPane.OK_CANCEL_OPTION - ); - if (choice == JOptionPane.CANCEL_OPTION) { - return; - } - this.cancelExportButton.setEnabled(true); - BookPagePasteListener pasteListener = new BookPagePasteListener(book, clipboard, this.bookPreviewPanel, this.cancelExportButton); - this.bookPreviewPanel.enableNavigation(false); - pasteListener.exportNextPage(); // Start by exporting the first page right away. - 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(pasteListener); - } catch (NativeHookException nativeHookException) { - System.err.println("Could not register native hook."); - nativeHookException.printStackTrace(); - } - } -} diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java deleted file mode 100644 index 78b845f..0000000 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java +++ /dev/null @@ -1,125 +0,0 @@ -package nl.andrewlalis.blockbookbinder.control; - -import nl.andrewlalis.blockbookbinder.model.Book; -import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel; -import org.jnativehook.GlobalScreen; -import org.jnativehook.NativeHookException; -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 - * pasted something into a book. - */ -public class BookPagePasteListener implements NativeKeyListener { - private final Book book; - private final Clipboard clipboard; - 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) { - this.book = book; - this.clipboard = clipboard; - this.bookPreviewPanel = bookPreviewPanel; - this.cancelExportButton = cancelExportButton; - 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() { - // Sleep a little bit to avoid rapid repeats. - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - this.bookPreviewPanel.setCurrentPage(this.nextPage); - boolean clipboardSuccess = false; - while (!clipboardSuccess) { - try { - clipboard.setContents( - new StringSelection(book.getPages().get(this.nextPage).toString()), - null - ); - clipboardSuccess = true; - } catch (IllegalStateException e) { - System.err.println("Could not open and set contents of system clipboard."); - } - if (!clipboardSuccess) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - System.out.println("Copied page " + this.nextPage + " into clipboard."); - this.nextPage++; - - // 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() { - try { - this.bookPreviewPanel.enableNavigation(true); - this.cancelExportButton.setEnabled(false); - this.cancelExportButton.removeActionListener(this.cancelExportActionListener); - GlobalScreen.removeNativeKeyListener(this); - GlobalScreen.unregisterNativeHook(); - System.out.println("Done pasting."); - } catch (NativeHookException nativeHookException) { - System.err.println("Could not unregister a native hook."); - nativeHookException.printStackTrace(); - } - } - - @Override - public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) { - } - - @Override - public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) { - if (nativeKeyEvent.getKeyCode() == NativeKeyEvent.VC_V && (nativeKeyEvent.getModifiers() & NativeKeyEvent.CTRL_MASK) > 0) { - this.exportNextPage(); - } - } - - @Override - public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {} -} diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java index 432cd03..c6c859d 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/export/BookExporter.java @@ -3,10 +3,13 @@ package nl.andrewlalis.blockbookbinder.control.export; import lombok.Setter; import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; +import nl.andrewlalis.blockbookbinder.view.export.ExportStatusPanel; +import nl.andrewlalis.blockbookbinder.view.export.ExportToBookDialog; import org.jnativehook.GlobalScreen; import org.jnativehook.NativeHookException; import javax.sound.sampled.*; +import javax.swing.*; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; @@ -24,10 +27,10 @@ import java.util.logging.Logger; 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; + private final int autoPasteDelay; @Setter private volatile boolean running; @@ -39,13 +42,19 @@ public class BookExporter implements Runnable { private final Clipboard clipboard; private Robot robot; + private final ExportStatusPanel statusPanel; + private final ExportToBookDialog dialog; + // Some sound clips to play as user feedback. private final Clip beepClip; private final Clip beginningExportClip; - public BookExporter(Book book, boolean autoPaste) { + public BookExporter(ExportToBookDialog dialog, ExportStatusPanel exportStatusPanel, Book book, boolean autoPaste, int autoPasteDelay) { + this.dialog = dialog; + this.statusPanel = exportStatusPanel; this.book = book; this.autoPaste = autoPaste; + this.autoPasteDelay = autoPasteDelay; 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(); @@ -77,26 +86,30 @@ public class BookExporter implements Runnable { } // 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.initStatusPanel(); this.initNativeListener(); this.playAudioClip(this.beginningExportClip); } this.exportPageToClipboard(nextPageToExport); if (this.autoPaste) { this.pasteAndTurnPage(); + } else { + this.addStatusMessage("Waiting to detect a CTRL+V keypress..."); } - this.playAudioClip(this.beepClip); nextPageToExport++; + this.updateStatusProgressBar(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."); + this.addStatusMessage("Export finished: " + this.book.getPageCount() + " pages exported."); if (!this.autoPaste) { this.stopNativeListener(); } this.running = false; + this.updateStatusLabel("Export finished"); + SwingUtilities.invokeLater(dialog::onExportFinished); break; } // Since there may be significant delay, get a fresh timestamp. @@ -132,7 +145,7 @@ public class BookExporter implements Runnable { throw new RuntimeException("Could not insert page into clipboard after " + attempts + " attempts."); } } - System.out.println("Exported page " + page + " to clipboard."); + this.addStatusMessage("Exported page " + (page + 1) + " to clipboard."); } /** @@ -143,26 +156,26 @@ public class BookExporter implements Runnable { this.robot.keyPress(KeyEvent.VK_CONTROL); this.robot.keyPress(KeyEvent.VK_V); try { - Thread.sleep(ROBOT_ACTION_DELAY_MS); + Thread.sleep(this.autoPasteDelay); } catch (InterruptedException e) { e.printStackTrace(); } this.robot.keyRelease(KeyEvent.VK_V); this.robot.keyRelease(KeyEvent.VK_CONTROL); - System.out.println("Pasted."); + this.addStatusMessage("Pasted page into book."); this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK); try { - Thread.sleep(ROBOT_ACTION_DELAY_MS); + Thread.sleep(this.autoPasteDelay); } 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); + Thread.sleep(this.autoPasteDelay); } catch (InterruptedException e) { e.printStackTrace(); } - System.out.println("Clicked mouse."); + this.addStatusMessage("Clicked to turn the page."); this.nextPageRequested = true; } @@ -211,4 +224,34 @@ public class BookExporter implements Runnable { return null; } } + + private void initStatusPanel() { + SwingUtilities.invokeLater(() -> { + JProgressBar bar = this.statusPanel.getExportProgressBar(); + bar.setMinimum(0); + bar.setMaximum(this.book.getPageCount()); + bar.setStringPainted(true); + this.updateStatusProgressBar(0); + }); + } + + private void updateStatusLabel(String text) { + SwingUtilities.invokeLater(() -> { + this.statusPanel.getStatusLabel().setText(text); + }); + } + + private void updateStatusProgressBar(int nextPage) { + SwingUtilities.invokeLater(() -> { + JProgressBar bar = this.statusPanel.getExportProgressBar(); + bar.setValue(nextPage); + bar.setString(String.format("%d of %d pages exported", nextPage, this.book.getPageCount())); + }); + } + + private void addStatusMessage(String message) { + SwingUtilities.invokeLater(() -> { + this.statusPanel.getOutputTextArea().append(message + "\n"); + }); + } } diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java index b749d26..0004f58 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java @@ -1,6 +1,5 @@ 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; diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java index c58202e..052e118 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportStatusPanel.java @@ -1,5 +1,7 @@ package nl.andrewlalis.blockbookbinder.view.export; +import lombok.Getter; + import javax.swing.*; import java.awt.*; @@ -8,11 +10,29 @@ import java.awt.*; * job. */ public class ExportStatusPanel extends JPanel { - private JProgressBar exportProgressBar; + @Getter + private final JLabel statusLabel; + @Getter + private final JTextArea outputTextArea; + @Getter + private final JProgressBar exportProgressBar; public ExportStatusPanel() { this.setLayout(new BorderLayout()); + this.statusLabel = new JLabel("Exporting..."); + this.add(this.statusLabel, BorderLayout.NORTH); + + this.outputTextArea = new JTextArea(); + this.outputTextArea.setEditable(false); + this.outputTextArea.setLineWrap(true); + this.outputTextArea.setWrapStyleWord(true); + this.outputTextArea.setAutoscrolls(true); + JScrollPane scrollPane = new JScrollPane(this.outputTextArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollPane.setAutoscrolls(true); + this.add(scrollPane, BorderLayout.CENTER); + this.exportProgressBar = new JProgressBar(); + this.add(this.exportProgressBar, BorderLayout.SOUTH); } } diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java index 325ea1d..998ddd8 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/export/ExportToBookDialog.java @@ -21,6 +21,7 @@ public class ExportToBookDialog extends JDialog { private JCheckBox autoCheckbox; private JSpinner firstPageSpinner; private JSpinner lastPageSpinner; + private JSpinner autoPasteDelaySpinner; private JButton startButton; private JButton stopButton; @@ -54,10 +55,21 @@ public class ExportToBookDialog extends JDialog { 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)); + JPanel firstPageSpinnerPanel = new JPanel(new BorderLayout()); + firstPageSpinnerPanel.add(new JLabel("First Page:"), BorderLayout.WEST); + firstPageSpinnerPanel.add(this.firstPageSpinner, BorderLayout.CENTER); this.lastPageSpinner = new JSpinner(new SpinnerNumberModel(this.book.getPageCount(), 1, this.book.getPageCount(), 1)); + JPanel lastPageSpinnerPanel = new JPanel(new BorderLayout()); + lastPageSpinnerPanel.add(new JLabel("Last Page:"), BorderLayout.WEST); + lastPageSpinnerPanel.add(this.lastPageSpinner, BorderLayout.CENTER); + this.autoPasteDelaySpinner = new JSpinner(new SpinnerNumberModel(0.2, 0.1, 5.0, 0.1)); + JPanel autoPasteDelaySpinnerPanel = new JPanel(new BorderLayout()); + autoPasteDelaySpinnerPanel.add(new JLabel("Auto-Paste Delay (s):"), BorderLayout.WEST); + autoPasteDelaySpinnerPanel.add(this.autoPasteDelaySpinner, BorderLayout.CENTER); setupPanel.add(this.autoCheckbox); - setupPanel.add(this.firstPageSpinner); - setupPanel.add(this.lastPageSpinner); + setupPanel.add(firstPageSpinnerPanel); + setupPanel.add(lastPageSpinnerPanel); + setupPanel.add(autoPasteDelaySpinnerPanel); this.exportStatusPanel = new ExportStatusPanel(); @@ -71,6 +83,9 @@ public class ExportToBookDialog extends JDialog { JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); this.startButton = new JButton("Start"); this.startButton.addActionListener(e -> { + if (!this.checkSpinnerValues()){ + return; + } int choice = JOptionPane.showConfirmDialog( this.rootPane, "Exporting will begin after roughly 10 seconds.\n" + @@ -97,21 +112,36 @@ public class ExportToBookDialog extends JDialog { return mainPanel; } + /** + * Starts up the exporter thread. + */ 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); + final double autoPasteDelay = (double) this.autoPasteDelaySpinner.getValue(); + final int autoPasteDelayMillis = (int) (autoPasteDelay * 1000); + 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.exporterRunnable = new BookExporter( + this, + this.exportStatusPanel, + pagesRange, + this.autoCheckbox.isSelected(), + autoPasteDelayMillis + ); this.exporterThread = new Thread(this.exporterRunnable); this.exporterThread.start(); } + /** + * Shuts down the exporter thread. + */ private void stopExporter() { this.exporterRunnable.setRunning(false); try { @@ -126,8 +156,49 @@ public class ExportToBookDialog extends JDialog { this.startButton.setVisible(true); } + /** + * This method is called by the exporter thread once it is done. + */ + public void onExportFinished() { + JOptionPane.showMessageDialog( + this, + "Book export has finished.", + "Export Complete", + JOptionPane.INFORMATION_MESSAGE + ); + this.stopExporter(); + this.dispose(); + } + private void showCardByName(String name) { CardLayout cl = (CardLayout) this.centerCardPanel.getLayout(); cl.show(this.centerCardPanel, name); } + + /** + * Checks the values of the spinners that are used to select the first and + * last pages, and shows a popup warning if they're not correct. + */ + private boolean checkSpinnerValues() { + final int firstPage = (int) this.firstPageSpinner.getValue(); + final int lastPage = (int) this.lastPageSpinner.getValue(); + + if ( + firstPage < 1 + || lastPage > this.book.getPageCount() + || firstPage > lastPage + || (lastPage - firstPage + 1 > ApplicationProperties.getIntProp("book.max_pages")) + ) { + JOptionPane.showMessageDialog( + this, + "Invalid page range. Please follow the rules below:\n" + + "1. First page must be lower or equal to the last page.\n" + + "2. Number of pages to export cannot exceed 100.\n", + "Invalid Page Range", + JOptionPane.WARNING_MESSAGE + ); + return false; + } + return true; + } }