Some final cleanup

This commit is contained in:
Andrew Lalis 2020-11-09 12:53:35 +01:00
parent e2ebb3085f
commit 6da3127427
7 changed files with 150 additions and 203 deletions

View File

@ -6,7 +6,7 @@
<groupId>nl.andrewlalis</groupId> <groupId>nl.andrewlalis</groupId>
<artifactId>BlockBookBinder</artifactId> <artifactId>BlockBookBinder</artifactId>
<version>1.0.0</version> <version>1.1.0</version>
<properties> <properties>
<maven.compiler.source>12</maven.compiler.source> <maven.compiler.source>12</maven.compiler.source>

View File

@ -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();
}
}
}

View File

@ -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) {}
}

View File

@ -3,10 +3,13 @@ package nl.andrewlalis.blockbookbinder.control.export;
import lombok.Setter; import lombok.Setter;
import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; 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.GlobalScreen;
import org.jnativehook.NativeHookException; import org.jnativehook.NativeHookException;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
@ -24,10 +27,10 @@ import java.util.logging.Logger;
public class BookExporter implements Runnable { public class BookExporter implements Runnable {
private final static int START_DELAY = 10; private final static int START_DELAY = 10;
private final static int CLIPBOARD_RETRY_DELAY_MS = 100; private final static int CLIPBOARD_RETRY_DELAY_MS = 100;
private final static int ROBOT_ACTION_DELAY_MS = 100;
private final Book book; private final Book book;
private final boolean autoPaste; private final boolean autoPaste;
private final int autoPasteDelay;
@Setter @Setter
private volatile boolean running; private volatile boolean running;
@ -39,13 +42,19 @@ public class BookExporter implements Runnable {
private final Clipboard clipboard; private final Clipboard clipboard;
private Robot robot; private Robot robot;
private final ExportStatusPanel statusPanel;
private final ExportToBookDialog dialog;
// Some sound clips to play as user feedback. // Some sound clips to play as user feedback.
private final Clip beepClip; private final Clip beepClip;
private final Clip beginningExportClip; 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.book = book;
this.autoPaste = autoPaste; this.autoPaste = autoPaste;
this.autoPasteDelay = autoPasteDelay;
this.beepClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beep_sound")); this.beepClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beep_sound"));
this.beginningExportClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beginning_export")); this.beginningExportClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.beginning_export"));
this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
@ -77,26 +86,30 @@ public class BookExporter implements Runnable {
} }
// Otherwise, export one page. // Otherwise, export one page.
if (!inStartPhase && this.nextPageRequested) { 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. 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 this is the first time we're exporting, play a sound.
if (lastPageExportedAt == 0) { if (lastPageExportedAt == 0) {
this.initStatusPanel();
this.initNativeListener(); this.initNativeListener();
this.playAudioClip(this.beginningExportClip); this.playAudioClip(this.beginningExportClip);
} }
this.exportPageToClipboard(nextPageToExport); this.exportPageToClipboard(nextPageToExport);
if (this.autoPaste) { if (this.autoPaste) {
this.pasteAndTurnPage(); this.pasteAndTurnPage();
} else {
this.addStatusMessage("Waiting to detect a CTRL+V keypress...");
} }
this.playAudioClip(this.beepClip);
nextPageToExport++; nextPageToExport++;
this.updateStatusProgressBar(nextPageToExport);
// If we've reached the end of the book, stop the exporter. // If we've reached the end of the book, stop the exporter.
if (nextPageToExport >= this.book.getPageCount()) { 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) { if (!this.autoPaste) {
this.stopNativeListener(); this.stopNativeListener();
} }
this.running = false; this.running = false;
this.updateStatusLabel("Export finished");
SwingUtilities.invokeLater(dialog::onExportFinished);
break; break;
} }
// Since there may be significant delay, get a fresh timestamp. // 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."); 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_CONTROL);
this.robot.keyPress(KeyEvent.VK_V); this.robot.keyPress(KeyEvent.VK_V);
try { try {
Thread.sleep(ROBOT_ACTION_DELAY_MS); Thread.sleep(this.autoPasteDelay);
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
this.robot.keyRelease(KeyEvent.VK_V); this.robot.keyRelease(KeyEvent.VK_V);
this.robot.keyRelease(KeyEvent.VK_CONTROL); this.robot.keyRelease(KeyEvent.VK_CONTROL);
System.out.println("Pasted."); this.addStatusMessage("Pasted page into book.");
this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK); this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK);
try { try {
Thread.sleep(ROBOT_ACTION_DELAY_MS); Thread.sleep(this.autoPasteDelay);
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
this.robot.mouseRelease(MouseEvent.BUTTON1_DOWN_MASK); this.robot.mouseRelease(MouseEvent.BUTTON1_DOWN_MASK);
try { // Wait for minecraft to turn the page. try { // Wait for minecraft to turn the page.
Thread.sleep(ROBOT_ACTION_DELAY_MS); Thread.sleep(this.autoPasteDelay);
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
System.out.println("Clicked mouse."); this.addStatusMessage("Clicked to turn the page.");
this.nextPageRequested = true; this.nextPageRequested = true;
} }
@ -211,4 +224,34 @@ public class BookExporter implements Runnable {
return null; 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");
});
}
} }

View File

@ -1,6 +1,5 @@
package nl.andrewlalis.blockbookbinder.view; package nl.andrewlalis.blockbookbinder.view;
import nl.andrewlalis.blockbookbinder.control.BookExportActionListener;
import nl.andrewlalis.blockbookbinder.control.ImportAction; import nl.andrewlalis.blockbookbinder.control.ImportAction;
import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;

View File

@ -1,5 +1,7 @@
package nl.andrewlalis.blockbookbinder.view.export; package nl.andrewlalis.blockbookbinder.view.export;
import lombok.Getter;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@ -8,11 +10,29 @@ import java.awt.*;
* job. * job.
*/ */
public class ExportStatusPanel extends JPanel { 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() { public ExportStatusPanel() {
this.setLayout(new BorderLayout()); 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.exportProgressBar = new JProgressBar();
this.add(this.exportProgressBar, BorderLayout.SOUTH);
} }
} }

View File

@ -21,6 +21,7 @@ public class ExportToBookDialog extends JDialog {
private JCheckBox autoCheckbox; private JCheckBox autoCheckbox;
private JSpinner firstPageSpinner; private JSpinner firstPageSpinner;
private JSpinner lastPageSpinner; private JSpinner lastPageSpinner;
private JSpinner autoPasteDelaySpinner;
private JButton startButton; private JButton startButton;
private JButton stopButton; private JButton stopButton;
@ -54,10 +55,21 @@ public class ExportToBookDialog extends JDialog {
setupPanel.setLayout(new BoxLayout(setupPanel, BoxLayout.PAGE_AXIS)); setupPanel.setLayout(new BoxLayout(setupPanel, BoxLayout.PAGE_AXIS));
this.autoCheckbox = new JCheckBox("Auto-paste", true); this.autoCheckbox = new JCheckBox("Auto-paste", true);
this.firstPageSpinner = new JSpinner(new SpinnerNumberModel(1, 1, this.book.getPageCount(), 1)); 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)); 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.autoCheckbox);
setupPanel.add(this.firstPageSpinner); setupPanel.add(firstPageSpinnerPanel);
setupPanel.add(this.lastPageSpinner); setupPanel.add(lastPageSpinnerPanel);
setupPanel.add(autoPasteDelaySpinnerPanel);
this.exportStatusPanel = new ExportStatusPanel(); this.exportStatusPanel = new ExportStatusPanel();
@ -71,6 +83,9 @@ public class ExportToBookDialog extends JDialog {
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
this.startButton = new JButton("Start"); this.startButton = new JButton("Start");
this.startButton.addActionListener(e -> { this.startButton.addActionListener(e -> {
if (!this.checkSpinnerValues()){
return;
}
int choice = JOptionPane.showConfirmDialog( int choice = JOptionPane.showConfirmDialog(
this.rootPane, this.rootPane,
"Exporting will begin after roughly 10 seconds.\n" + "Exporting will begin after roughly 10 seconds.\n" +
@ -97,21 +112,36 @@ public class ExportToBookDialog extends JDialog {
return mainPanel; return mainPanel;
} }
/**
* Starts up the exporter thread.
*/
private void startExporter() { private void startExporter() {
final int firstPage = (int) this.firstPageSpinner.getValue(); final int firstPage = (int) this.firstPageSpinner.getValue();
final int lastPage = (int) this.lastPageSpinner.getValue(); final int lastPage = (int) this.lastPageSpinner.getValue();
final Book pagesRange = this.book.getPageRange(firstPage - 1, lastPage - firstPage + 1); 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.setEnabled(false);
this.startButton.setVisible(false); this.startButton.setVisible(false);
this.stopButton.setEnabled(true); this.stopButton.setEnabled(true);
this.stopButton.setVisible(true); this.stopButton.setVisible(true);
this.showCardByName(STATUS_CARD); 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 = new Thread(this.exporterRunnable);
this.exporterThread.start(); this.exporterThread.start();
} }
/**
* Shuts down the exporter thread.
*/
private void stopExporter() { private void stopExporter() {
this.exporterRunnable.setRunning(false); this.exporterRunnable.setRunning(false);
try { try {
@ -126,8 +156,49 @@ public class ExportToBookDialog extends JDialog {
this.startButton.setVisible(true); 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) { private void showCardByName(String name) {
CardLayout cl = (CardLayout) this.centerCardPanel.getLayout(); CardLayout cl = (CardLayout) this.centerCardPanel.getLayout();
cl.show(this.centerCardPanel, name); 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;
}
} }