Compare commits

...

21 Commits
0.0.1 ... main

Author SHA1 Message Date
Andrew Lalis 5450f69c3b Remove unused import. 2023-07-14 22:51:18 -04:00
Andrew Lalis 7735602a2c Removed VersionReader.java, simplified version fetching from props, added warning about compile-from-source when no source text present. Version 1.3.1. 2023-07-14 22:50:57 -04:00
Andrew Lalis cfb70b14e8 Set version 1.3.0, update about. 2023-07-09 21:57:20 -04:00
Andrew Lalis 9ae48ec944 Cleaned up temporary classes during work. 2023-07-09 20:30:25 -04:00
Andrew Lalis 2a9edf125e Cleanup 2023-07-09 20:19:30 -04:00
Andrew Lalis 605d050783 Made new book builder algorithm work, cleaned up export dialog. 2023-07-09 20:18:48 -04:00
Andrew Lalis e32976fb8b Started work on modernizing stuff. 2023-07-08 11:50:32 -04:00
Andrew Lalis deb10b1099
Update README.md 2021-03-23 16:52:22 +01:00
Andrew Lalis 8793202622 Fixed cleaning source. 2021-03-13 14:34:24 +01:00
Andrew Lalis c748662760 Icons for book navigation. 2021-03-13 14:25:40 +01:00
Andrew Lalis 392114e3e6 Cleaned up the user interface a lot. 2021-03-13 14:00:18 +01:00
Andrew Lalis ec06cf56cf Lots of cleanup. 2021-03-10 00:42:22 +01:00
Andrew Lalis 5c6587386e Added system exit on mainframe dispose. 2020-11-09 14:02:27 +01:00
Andrew Lalis e5bc16a6a4 Added finish sound and countdown in title. 2020-11-09 13:55:54 +01:00
Andrew Lalis 53122afcf5 Added nice about page. 2020-11-09 13:38:05 +01:00
Andrew Lalis 6da3127427 Some final cleanup 2020-11-09 12:53:35 +01:00
Andrew Lalis e2ebb3085f Upgrade version, add improved export flow. 2020-11-09 11:44:29 +01:00
Andrew Lalis e5e3c08c6e Added CleanSourceActionListener. 2020-11-08 19:03:23 +01:00
Andrew Lalis 91ad9b191f version bump 2020-11-08 00:34:51 +01:00
Andrew Lalis ae0da77f93 Slight cleanups 2020-11-08 00:34:37 +01:00
Andrew Lalis e1036940f9 Increased default char width. 2020-11-08 00:25:59 +01:00
38 changed files with 1511 additions and 373 deletions

View File

@ -1,2 +1,15 @@
# BlockBookBinder # BlockBookBinder
Simple utility for preparing and pasting large texts into Minecraft books. _A simple utility for preparing and pasting large texts into Minecraft books._
This application was developed in order to make the production of written books in Minecraft easier, by splitting up large texts into pre-formatted chunks that are automatically loaded onto the user's clipboard for pasting into the book.
## Using Block Book Binder
To get started, look for the [**Releases**](https://github.com/andrewlalis/BlockBookBinder/releases) section on this page, and find the latest release. Download the executable JAR file, and make sure you have Java version 17 or higher installed to run it.
Start the program, and you'll be greeted with a window that has **Book Preview** and **Source Text** panels. Enter the text you'd like to work with into the **Source Text** panel. In the top menu under **Book**, you'll find a **Clean Source** button, which will remove extra whitespace from the text to make it more friendly for Minecraft's cramped style. You'll also find **Compile from Source**, which will compile your source text into a book in the **Book Preview** panel.
Once you're happy with how the pages are formatted, you can click the **Export to Minecraft** button to begin exporting pages to your clipboard.
## Demo Video on YouTube
https://youtu.be/Mu7Hv1na7Sw

80
design/page_first.svg Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666667 8.4666667"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="page_first.svg"
inkscape:export-filename="D:\Programming\Projects\BlockBookBinder\src\main\resources\images\page_first.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.4"
inkscape:cx="7.7529167"
inkscape:cy="13.546332"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53333)">
<g
id="g823"
transform="translate(0.82682292,0.09982333)">
<path
inkscape:connector-curvature="0"
id="path815"
d="m 6.7794176,289.72688 -5.0921685,2.93996"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.7794176,295.6068 1.6872491,292.66684"
id="path817"
inkscape:connector-curvature="0" />
</g>
<path
inkscape:connector-curvature="0"
id="path819"
d="m 1.1316191,295.70662 4.3e-6,-5.87992"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

80
design/page_last.svg Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666667 8.4666667"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="page_last.svg"
inkscape:export-filename="D:\Programming\Projects\BlockBookBinder\src\main\resources\images\page_last.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.4"
inkscape:cx="15.007381"
inkscape:cy="13.546332"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53333)">
<g
id="g823"
transform="matrix(-1,0,0,1,7.9175178,0.09982333)">
<path
inkscape:connector-curvature="0"
id="path815"
d="m 6.7794176,289.72688 -5.0921685,2.93996"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.7794176,295.6068 1.6872491,292.66684"
id="path817"
inkscape:connector-curvature="0" />
</g>
<path
inkscape:connector-curvature="0"
id="path819"
d="m 7.6127216,295.70662 -4.3e-6,-5.87992"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

71
design/page_left.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666667 8.4666667"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="page_left.svg"
inkscape:export-filename="D:\Programming\Projects\BlockBookBinder\src\main\resources\images\page_left.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.4"
inkscape:cx="7.5743453"
inkscape:cy="13.546332"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53333)">
<path
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 6.7794176,289.72688 -5.0921685,2.93996"
id="path815"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path817"
d="M 6.7794176,295.6068 1.6872491,292.66684"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

71
design/page_right.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666667 8.4666667"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="page_right.svg"
inkscape:export-filename="D:\Programming\Projects\BlockBookBinder\src\main\resources\images\page_right.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.4"
inkscape:cx="7.5743453"
inkscape:cy="13.546332"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53333)">
<path
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1.6872491,289.72688 5.0921685,2.93996"
id="path815"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path817"
d="m 1.6872491,295.6068 5.0921685,-2.93996"
style="fill:none;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

49
pom.xml
View File

@ -6,19 +6,48 @@
<groupId>nl.andrewlalis</groupId> <groupId>nl.andrewlalis</groupId>
<artifactId>BlockBookBinder</artifactId> <artifactId>BlockBookBinder</artifactId>
<version>0.0.1</version> <version>1.3.1</version>
<properties> <properties>
<maven.compiler.source>12</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>12</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>nl.andrewlalis.blockbookbinder.BlockBookBinder</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies> <dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.14</version> <version>1.18.28</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
@ -28,5 +57,17 @@
<artifactId>jnativehook</artifactId> <artifactId>jnativehook</artifactId>
<version>2.1.0</version> <version>2.1.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>3.9.3</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,11 +1,9 @@
package nl.andrewlalis.blockbookbinder; package nl.andrewlalis.blockbookbinder;
import com.formdev.flatlaf.FlatDarkLaf;
import nl.andrewlalis.blockbookbinder.view.MainFrame; import nl.andrewlalis.blockbookbinder.view.MainFrame;
import org.jnativehook.GlobalScreen;
import javax.swing.*; import javax.swing.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* The main class for the application. * The main class for the application.
@ -13,6 +11,7 @@ import java.util.logging.Logger;
public class BlockBookBinder { public class BlockBookBinder {
public static void main(String[] args) { public static void main(String[] args) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
FlatDarkLaf.setup();
var mainFrame = new MainFrame(); var mainFrame = new MainFrame();
mainFrame.setupAndShow(); mainFrame.setupAndShow();
}); });

View File

@ -1,56 +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 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;
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 CTRL+V 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,101 +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.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionListener;
/**
* 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 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);
}
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();
}
}
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

@ -1,29 +0,0 @@
package nl.andrewlalis.blockbookbinder.control;
import nl.andrewlalis.blockbookbinder.model.build.BookBuilder;
import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel;
import nl.andrewlalis.blockbookbinder.view.SourceTextPanel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Action listener that, when activated, converts the text from the source panel
* into a formatted book.
*/
public class ConvertToBookActionListener implements ActionListener {
private final SourceTextPanel sourceTextPanel;
private final BookPreviewPanel bookPreviewPanel;
public ConvertToBookActionListener(SourceTextPanel sourceTextPanel, BookPreviewPanel bookPreviewPanel) {
this.sourceTextPanel = sourceTextPanel;
this.bookPreviewPanel = bookPreviewPanel;
}
@Override
public void actionPerformed(ActionEvent e) {
this.bookPreviewPanel.setBook(
new BookBuilder().build(this.sourceTextPanel.getSourceText())
);
}
}

View File

@ -1,19 +0,0 @@
package nl.andrewlalis.blockbookbinder.control;
import javax.swing.*;
import java.awt.event.ActionEvent;
public class ImportAction extends AbstractAction {
public ImportAction(String name) {
super(name);
}
public ImportAction(String name, Icon icon) {
super(name, icon);
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Import!");
}
}

View File

@ -0,0 +1,260 @@
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;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.BufferedInputStream;
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 Book book;
private final boolean autoPaste;
private final int autoPasteDelay;
@Setter
private volatile boolean running;
@Setter
private volatile boolean nextPageRequested;
private final ExporterKeyListener exporterKeyListener;
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;
private final Clip finishClip;
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.finishClip = this.loadAudioClip(ApplicationProperties.getProp("export_dialog.finish_sound"));
this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
this.exporterKeyListener = new ExporterKeyListener(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;
int secondsLeft = (START_DELAY) - (int) (currentTime - startTime) / 1000;
this.updateStatusLabel("Starting in " + secondsLeft + " seconds.");
}
// Otherwise, export one page.
if (!inStartPhase && this.nextPageRequested) {
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.updateStatusLabel("Exporting.");
this.initNativeListener();
this.playAudioClip(this.beginningExportClip);
}
this.exportPageToClipboard(nextPageToExport);
if (this.autoPaste) {
this.pasteAndTurnPage();
} else {
this.addStatusMessage("Waiting to detect a CTRL+V keypress...");
}
nextPageToExport++;
this.updateStatusProgressBar(nextPageToExport);
// If we've reached the end of the book, stop the exporter.
if (nextPageToExport >= this.book.getPageCount()) {
this.playAudioClip(this.finishClip);
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.
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.");
}
}
this.addStatusMessage("Exported page " + (page + 1) + " 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(this.autoPasteDelay);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.robot.keyRelease(KeyEvent.VK_V);
this.robot.keyRelease(KeyEvent.VK_CONTROL);
this.addStatusMessage("Pasted page into book.");
this.robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK);
try {
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(this.autoPasteDelay);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.addStatusMessage("Clicked to turn the page.");
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.exporterKeyListener);
} catch (NativeHookException nativeHookException) {
System.err.println("Could not register native hook.");
nativeHookException.printStackTrace();
}
}
private void stopNativeListener() {
try {
GlobalScreen.removeNativeKeyListener(this.exporterKeyListener);
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(new BufferedInputStream(fileInputStream));
clip.open(ais);
return clip;
} catch (LineUnavailableException | IOException | UnsupportedAudioFileException e) {
System.err.println("Could not load audio clip.");
e.printStackTrace();
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

@ -0,0 +1,39 @@
package nl.andrewlalis.blockbookbinder.control.export;
import lombok.Getter;
import lombok.Setter;
import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.view.book.BookPreviewPanel;
import nl.andrewlalis.blockbookbinder.view.export.ExportToBookDialog;
import javax.swing.*;
import java.awt.event.ActionEvent;
public class ExportBookToMinecraftAction extends AbstractAction {
@Getter
private static final ExportBookToMinecraftAction instance = new ExportBookToMinecraftAction();
public ExportBookToMinecraftAction() {
super("Export to Minecraft");
this.putValue(SHORT_DESCRIPTION, "Export the current book to Minecraft.");
}
@Setter
private BookPreviewPanel bookPreviewPanel;
@Override
public void actionPerformed(ActionEvent e) {
final Book book = bookPreviewPanel.getBook();
if (book == null || book.getPageCount() == 0) {
JOptionPane.showMessageDialog(
this.bookPreviewPanel.getRootPane(),
"Cannot export an empty book.\nChoose \"Compile to Source\" first, and then export.",
"Empty Book",
JOptionPane.WARNING_MESSAGE
);
return;
}
ExportToBookDialog dialog = new ExportToBookDialog(SwingUtilities.getWindowAncestor(this.bookPreviewPanel), bookPreviewPanel.getBook());
dialog.setupAndShow();
}
}

View File

@ -0,0 +1,35 @@
package nl.andrewlalis.blockbookbinder.control.export;
import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;
/**
* Native key listener that's used during the export process, to detect when the
* user performs certain key actions outside the focus of this program.
*/
public class ExporterKeyListener implements NativeKeyListener {
private final BookExporter exporterRunnable;
public ExporterKeyListener(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) {}
}

View File

@ -0,0 +1,49 @@
package nl.andrewlalis.blockbookbinder.control.source;
import lombok.Getter;
import lombok.Setter;
import nl.andrewlalis.blockbookbinder.view.SourceTextPanel;
import javax.swing.*;
import java.awt.event.ActionEvent;
public class CleanSourceAction extends AbstractAction {
@Getter
private final static CleanSourceAction instance = new CleanSourceAction();
@Setter
private SourceTextPanel sourceTextPanel;
public CleanSourceAction() {
super("Clean Source");
this.putValue(SHORT_DESCRIPTION, "Clean up the source text.");
}
@Override
public void actionPerformed(ActionEvent e) {
final String source = this.sourceTextPanel.getSourceText();
String updated = source.trim()
.replaceAll("(?>\\v)+(\\v)", "\n\n") // Replace large chunks of newline with just two.
.replaceAll("\\t", " ") // Replace tabs with single-spaces, for space savings.
.replaceAll(" +", " "); // Remove any double spaces.
updated = this.removeNewlineWrapping(updated);
this.sourceTextPanel.setSourceText(updated);
}
private String removeNewlineWrapping(String source) {
final StringBuilder sb = new StringBuilder(source.length());
final char[] sourceChars = source.toCharArray();
for (int i = 0; i < sourceChars.length; i++) {
char c = sourceChars[i];
if (
c == '\n'
&& (i - 1 >= 0 && !Character.isWhitespace(sourceChars[i - 1]))
&& (i + 1 < sourceChars.length && !Character.isWhitespace(sourceChars[i + 1]))
) {
c = ' ';
}
sb.append(c);
}
return sb.toString();
}
}

View File

@ -0,0 +1,48 @@
package nl.andrewlalis.blockbookbinder.control.source;
import lombok.Getter;
import lombok.Setter;
import nl.andrewlalis.blockbookbinder.model.build.BookBuilder;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import nl.andrewlalis.blockbookbinder.view.SourceTextPanel;
import nl.andrewlalis.blockbookbinder.view.book.BookPreviewPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
public class CompileFromSourceAction extends AbstractAction {
@Getter
private static final CompileFromSourceAction instance = new CompileFromSourceAction();
@Setter
private SourceTextPanel sourceTextPanel;
@Setter
private BookPreviewPanel bookPreviewPanel;
public CompileFromSourceAction() {
super("Compile From Source");
this.putValue(SHORT_DESCRIPTION, "Compile the current source text into a book.");
}
@Override
public void actionPerformed(ActionEvent e) {
String text = this.sourceTextPanel.getSourceText();
if (text.isBlank()) {
JOptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor((Component) e.getSource()),
"No source text to compile.\nEnter some text into the \"Source Text\" panel first.",
"No Source Text",
JOptionPane.WARNING_MESSAGE
);
} else {
this.bookPreviewPanel.setBook(
new BookBuilder(
ApplicationProperties.getIntProp("book.page_max_lines"),
ApplicationProperties.getIntProp("book.page_max_chars"),
ApplicationProperties.getIntProp("book.page_max_width")
).addText(this.sourceTextPanel.getSourceText()).build()
);
}
}
}

View File

@ -0,0 +1,52 @@
package nl.andrewlalis.blockbookbinder.control.source;
import lombok.Getter;
import lombok.Setter;
import nl.andrewlalis.blockbookbinder.BlockBookBinder;
import nl.andrewlalis.blockbookbinder.view.SourceTextPanel;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.prefs.Preferences;
public class ImportSourceAction extends AbstractAction {
@Getter
private static final ImportSourceAction instance = new ImportSourceAction();
@Setter
private SourceTextPanel sourceTextPanel;
public ImportSourceAction() {
super("Import Source");
this.putValue(SHORT_DESCRIPTION, "Import source text from a file.");
}
@Override
public void actionPerformed(ActionEvent e) {
Preferences prefs = Preferences.userNodeForPackage(BlockBookBinder.class);
String dir = prefs.get("source-import-dir", ".");
JFileChooser fileChooser = new JFileChooser(dir);
fileChooser.setFileFilter(new FileNameExtensionFilter("Text files", ".txt"));
fileChooser.setAcceptAllFileFilterUsed(true);
fileChooser.setMultiSelectionEnabled(false);
final Component parent = SwingUtilities.getWindowAncestor((Component) e.getSource());
int result = fileChooser.showOpenDialog(parent);
if (result == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
try {
Path filePath = file.toPath();
sourceTextPanel.setSourceText(Files.readString(filePath));
prefs.put("source-import-dir", filePath.getParent().toAbsolutePath().toString());
} catch (IOException exc) {
exc.printStackTrace();
JOptionPane.showMessageDialog(parent, "Failed to read file:\n" + exc.getMessage(), "Read Failed", JOptionPane.ERROR_MESSAGE);
}
}
}
}

View File

@ -1,6 +1,7 @@
package nl.andrewlalis.blockbookbinder.model; package nl.andrewlalis.blockbookbinder.model;
import lombok.Getter; import lombok.Getter;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -21,6 +22,34 @@ public class Book {
this.pages.add(page); 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;
}
public List<Book> splitByPageLimit() {
final int pagesPerBook = ApplicationProperties.getIntProp("book.max_pages");
List<Book> books = new ArrayList<>((this.getPageCount() / pagesPerBook) + 1);
Book currentBook = new Book();
for (BookPage page : this.getPages()) {
currentBook.addPage(page.copy());
if (currentBook.getPageCount() == pagesPerBook) {
books.add(currentBook);
currentBook = new Book();
}
}
return books;
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder("Book of " + this.getPageCount() + " pages:\n"); StringBuilder sb = new StringBuilder("Book of " + this.getPageCount() + " pages:\n");

View File

@ -13,7 +13,7 @@ public class BookPage {
} }
public boolean addLine(String line) { public boolean addLine(String line) {
if (this.lines.size() == ApplicationProperties.getIntProp("book.page_max_lines")) { if (this.lines.size() >= ApplicationProperties.getIntProp("book.page_max_lines")) {
return false; return false;
} }
this.lines.add(line); this.lines.add(line);
@ -24,8 +24,24 @@ public class BookPage {
return !this.lines.isEmpty(); return !this.lines.isEmpty();
} }
public BookPage copy() {
BookPage c = new BookPage();
for (String line : this.lines) {
c.addLine(line);
}
return c;
}
@Override @Override
public String toString() { public String toString() {
return String.join("\n", this.lines); return String.join("\n", this.lines);
} }
public static BookPage fromString(String s) {
BookPage p = new BookPage();
for (String line : s.split("\n")) {
p.addLine(line);
}
return p;
}
} }

View File

@ -1,11 +1,15 @@
package nl.andrewlalis.blockbookbinder.model; package nl.andrewlalis.blockbookbinder.model;
import lombok.Getter;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class CharWidthMapper { public class CharWidthMapper {
@Getter
private static final CharWidthMapper instance = new CharWidthMapper();
private final Map<Character, Integer> charWidthMap; private final Map<Character, Integer> charWidthMap;
public CharWidthMapper() { public CharWidthMapper() {
@ -13,8 +17,20 @@ public class CharWidthMapper {
this.initCharWidthMap(); this.initCharWidthMap();
} }
public int getWidth(char c) { public static int getWidth(char c) {
return this.charWidthMap.getOrDefault(c, 0); return instance.charWidthMap.getOrDefault(c, 6);
}
public static int getWidth(String s) {
if (s.length() == 0) return 0;
int width = 0;
for (int i = 0; i < s.length(); i++) {
width += getWidth(s.charAt(i));
if (i < s.length() - 1) {
width++;
}
}
return width;
} }
private void initCharWidthMap() { private void initCharWidthMap() {

View File

@ -3,114 +3,123 @@ package nl.andrewlalis.blockbookbinder.model.build;
import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.model.BookPage; import nl.andrewlalis.blockbookbinder.model.BookPage;
import nl.andrewlalis.blockbookbinder.model.CharWidthMapper; import nl.andrewlalis.blockbookbinder.model.CharWidthMapper;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class BookBuilder { public class BookBuilder {
private final CharWidthMapper charWidthMapper = new CharWidthMapper(); private final int MAX_LINES_PER_PAGE;
private final int MAX_CHARS_PER_PAGE;
private final int MAX_LINE_PIXEL_WIDTH;
/** private final List<String> lines;
* Builds a full book of pages from the given source text.
* @param source The source text to convert. private final StringBuilder lineBuilder;
* @return A book containing the source text formatted for a minecraft book. private final StringBuilder wordBuilder;
*/
public Book build(String source) { public BookBuilder(int maxLinesPerPage, int maxCharsPerPage, int maxLinePixelWidth) {
final int maxLines = ApplicationProperties.getIntProp("book.page_max_lines"); this.MAX_LINES_PER_PAGE = maxLinesPerPage;
List<String> lines = this.convertSourceToLines(source); this.MAX_CHARS_PER_PAGE = maxCharsPerPage;
this.MAX_LINE_PIXEL_WIDTH = maxLinePixelWidth;
this.lines = new ArrayList<>();
this.lineBuilder = new StringBuilder(64);
this.wordBuilder = new StringBuilder(64);
}
public BookBuilder addText(String text) {
int idx = 0;
while (idx < text.length()) {
final char c = text.charAt(idx++);
if (c == '\n') {
appendLine();
} else if (c == ' ' && lineBuilder.length() == 0) {
continue; // Skip spaces at the start of lines.
} else if (Character.isWhitespace(c)) {
if (CharWidthMapper.getWidth(lineBuilder.toString() + c) > MAX_LINE_PIXEL_WIDTH) {
appendLine();
if (c != ' ') {
lineBuilder.append(c);
}
} else {
lineBuilder.append(c);
}
} else { // Read a continuous word.
String word = readWord(text, idx - 1);
idx += word.length() - 1;
if (CharWidthMapper.getWidth(lineBuilder + word) <= MAX_LINE_PIXEL_WIDTH) {
// Append the word if it'll fit completely.
lineBuilder.append(word);
} else if (CharWidthMapper.getWidth(word) <= MAX_LINE_PIXEL_WIDTH) {
// Go to the next line and put the word there, since it'll fit.
appendLine();
lineBuilder.append(word);
} else {
// The word is so large that it doesn't fit on a line on its own.
// Find the largest substring of the word that'll fit with a hyphen.
int subStringSize = word.length() - 2;
while (CharWidthMapper.getWidth(word.substring(0, subStringSize) + "-") > MAX_LINE_PIXEL_WIDTH) {
subStringSize--;
}
appendLine();
lineBuilder.append(word, 0, subStringSize).append('-');
appendLine();
lineBuilder.append(word.substring(subStringSize));
}
}
}
return this;
}
public Book build() {
Book book = new Book(); Book book = new Book();
BookPage page = new BookPage(); BookPage page = new BookPage();
int currentPageLineCount = 0; int currentPageLineCount = 0;
int currentPageCharCount = 0;
// Flush anything remaining in lineBuilder to a final line.
if (lineBuilder.length() > 0) {
appendLine();
}
for (String line : lines) { for (String line : lines) {
page.addLine(line); if (currentPageCharCount + line.length() > MAX_CHARS_PER_PAGE) {
currentPageLineCount++;
if (currentPageLineCount == maxLines) {
book.addPage(page); book.addPage(page);
page = new BookPage(); page = new BookPage();
currentPageLineCount = 0; currentPageLineCount = 0;
currentPageCharCount = 0;
}
page.addLine(line);
currentPageLineCount++;
currentPageCharCount += line.length();
if (currentPageLineCount == MAX_LINES_PER_PAGE) {
book.addPage(page);
page = new BookPage();
currentPageLineCount = 0;
currentPageCharCount = 0;
} }
} }
if (page.hasContent()) { if (page.hasContent()) {
book.addPage(page); book.addPage(page);
} }
return book; return book;
} }
/** private String readWord(String text, int firstCharIdx) {
* Converts the given source string into a formatted list of lines that can wordBuilder.setLength(0);
* be copied to a minecraft book. int idx = firstCharIdx;
* @param source The source string. while (idx < text.length()) {
* @return A list of lines. char c = text.charAt(idx++);
*/ if (!Character.isWhitespace(c)) {
private List<String> convertSourceToLines(String source) { wordBuilder.append(c);
List<String> lines = new ArrayList<>(); } else {
final char[] sourceChars = source.toCharArray(); break;
final int maxLinePixelWidth = ApplicationProperties.getIntProp("book.page_max_width"); }
int sourceIndex = 0; }
StringBuilder lineBuilder = new StringBuilder(64); return wordBuilder.toString();
int linePixelWidth = 0;
StringBuilder symbolBuilder = new StringBuilder(64);
while (sourceIndex < sourceChars.length) {
final char c = sourceChars[sourceIndex];
sourceIndex++;
symbolBuilder.setLength(0);
symbolBuilder.append(c);
int symbolWidth = this.charWidthMapper.getWidth(c);
// Since there's a 1-pixel gap between characters, add it to the width if this isn't the first char.
if (lineBuilder.length() > 0) {
symbolWidth++;
} }
// If we encounter a non-newline whitespace at the beginning of the line, skip it. private void appendLine() {
if (c == ' ' && lineBuilder.length() == 0) { this.lines.add(this.lineBuilder.toString());
continue; this.lineBuilder.setLength(0);
}
// If we encounter a newline, immediately skip to a new line.
if (c == '\n') {
lines.add(lineBuilder.toString());
lineBuilder.setLength(0);
linePixelWidth = 0;
continue;
}
// If we encounter a word, keep accepting characters until we reach the end.
if (Character.isLetterOrDigit(c)) {
while (
sourceIndex < sourceChars.length
&& Character.isLetterOrDigit(sourceChars[sourceIndex])
) {
char nextChar = sourceChars[sourceIndex];
symbolBuilder.append(nextChar);
symbolWidth += 1 + this.charWidthMapper.getWidth(nextChar);
sourceIndex++;
}
}
final String symbol = symbolBuilder.toString();
// Check if we need to go to the next line to fit the symbol.
if (linePixelWidth + symbolWidth > maxLinePixelWidth) {
lines.add(lineBuilder.toString());
lineBuilder.setLength(0);
linePixelWidth = 0;
}
// Finally, append the symbol.
lineBuilder.append(symbol);
linePixelWidth += symbolWidth;
}
// Append any remaining text.
if (lineBuilder.length() > 0) {
lines.add(lineBuilder.toString());
}
return lines;
} }
} }

View File

@ -0,0 +1,24 @@
package nl.andrewlalis.blockbookbinder.util;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
public class IconLoader {
public static Icon load(String resourceName, int width, int height) {
try {
InputStream is = IconLoader.class.getClassLoader().getResourceAsStream(resourceName);
if (is == null) {
throw new IOException("Could not open resource: " + resourceName);
}
Image img = ImageIO.read(is).getScaledInstance(width, height, Image.SCALE_SMOOTH);
return new ImageIcon(img);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -1,8 +1,12 @@
package nl.andrewlalis.blockbookbinder.view; package nl.andrewlalis.blockbookbinder.view;
import nl.andrewlalis.blockbookbinder.control.BookExportActionListener; import nl.andrewlalis.blockbookbinder.control.export.ExportBookToMinecraftAction;
import nl.andrewlalis.blockbookbinder.control.ImportAction; import nl.andrewlalis.blockbookbinder.control.source.CleanSourceAction;
import nl.andrewlalis.blockbookbinder.control.source.CompileFromSourceAction;
import nl.andrewlalis.blockbookbinder.control.source.ImportSourceAction;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import nl.andrewlalis.blockbookbinder.view.about.AboutDialog;
import nl.andrewlalis.blockbookbinder.view.book.BookPreviewPanel;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@ -12,20 +16,18 @@ import java.net.URL;
* The main window of the application. * The main window of the application.
*/ */
public class MainFrame extends JFrame { public class MainFrame extends JFrame {
private Action importAction;
public void setupAndShow() { public void setupAndShow() {
final int width = Integer.parseInt(ApplicationProperties.getProp("frame.default_width")); this.setPreferredSize(new Dimension(
final int height = Integer.parseInt(ApplicationProperties.getProp("frame.default_height")); ApplicationProperties.getIntProp("frame.default_width"),
this.setPreferredSize(new Dimension(width, height)); ApplicationProperties.getIntProp("frame.default_height")
this.setTitle(ApplicationProperties.getProp("frame.title")); ));
this.setTitle(ApplicationProperties.getProp("frame.title") + " Version " + ApplicationProperties.getProp("version"));
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
final URL iconUrl = this.getClass().getClassLoader().getResource("images/book_and_quill.png"); final URL iconUrl = this.getClass().getClassLoader().getResource("images/book_and_quill.png");
if (iconUrl != null) { if (iconUrl != null) {
this.setIconImage(new ImageIcon(iconUrl).getImage()); this.setIconImage(new ImageIcon(iconUrl).getImage());
} }
this.initActions();
this.setContentPane(this.buildContentPane()); this.setContentPane(this.buildContentPane());
this.setJMenuBar(this.buildMenuBar()); this.setJMenuBar(this.buildMenuBar());
@ -34,44 +36,57 @@ public class MainFrame extends JFrame {
this.setVisible(true); this.setVisible(true);
} }
private void initActions() { private Container buildContentPane() {
this.importAction = new ImportAction("Import"); JPanel mainPanel = new JPanel(new BorderLayout());
JPanel doublePanel = new JPanel(new GridLayout(1, 2));
BookPreviewPanel bookPreviewPanel = new BookPreviewPanel();
doublePanel.add(bookPreviewPanel);
CompileFromSourceAction.getInstance().setBookPreviewPanel(bookPreviewPanel);
ExportBookToMinecraftAction.getInstance().setBookPreviewPanel(bookPreviewPanel);
SourceTextPanel sourceTextPanel = new SourceTextPanel();
doublePanel.add(sourceTextPanel);
CompileFromSourceAction.getInstance().setSourceTextPanel(sourceTextPanel);
CleanSourceAction.getInstance().setSourceTextPanel(sourceTextPanel);
ImportSourceAction.getInstance().setSourceTextPanel(sourceTextPanel);
mainPanel.add(doublePanel, BorderLayout.CENTER);
return mainPanel;
} }
private JMenuBar buildMenuBar() { private JMenuBar buildMenuBar() {
JMenuBar menuBar = new JMenuBar(); JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File"); JMenu fileMenu = new JMenu("File");
fileMenu.add(new JMenuItem(this.importAction)); fileMenu.add(ImportSourceAction.getInstance());
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.addActionListener(e -> this.dispose());
fileMenu.add(exitItem);
menuBar.add(fileMenu); menuBar.add(fileMenu);
JMenu bookMenu = new JMenu("Book");
bookMenu.add(CompileFromSourceAction.getInstance());
bookMenu.add(CleanSourceAction.getInstance());
bookMenu.add(ExportBookToMinecraftAction.getInstance());
menuBar.add(bookMenu);
JMenu helpMenu = new JMenu("Help"); JMenu helpMenu = new JMenu("Help");
JMenuItem aboutItem = new JMenuItem("About"); JMenuItem aboutItem = new JMenuItem("About");
aboutItem.addActionListener(e -> {
AboutDialog dialog = new AboutDialog(this);
dialog.setupAndShow();
});
helpMenu.add(aboutItem); helpMenu.add(aboutItem);
menuBar.add(helpMenu); menuBar.add(helpMenu);
return menuBar; return menuBar;
} }
private Container buildContentPane() { @Override
JPanel mainPanel = new JPanel(new BorderLayout()); public void dispose() {
super.dispose();
JPanel doublePanel = new JPanel(new GridLayout(1, 2)); System.exit(0);
BookPreviewPanel bookPreviewPanel = new BookPreviewPanel();
doublePanel.add(bookPreviewPanel);
SourceTextPanel sourceTextPanel = new SourceTextPanel(bookPreviewPanel);
doublePanel.add(sourceTextPanel);
mainPanel.add(doublePanel, BorderLayout.CENTER);
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton exportButton = new JButton("Export to Book");
JButton cancelExportButton = new JButton("Cancel Export");
cancelExportButton.setEnabled(false);
exportButton.addActionListener(new BookExportActionListener(bookPreviewPanel, cancelExportButton));
bottomPanel.add(exportButton);
bottomPanel.add(cancelExportButton);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
return mainPanel;
} }
} }

View File

@ -1,14 +1,8 @@
package nl.andrewlalis.blockbookbinder.view; package nl.andrewlalis.blockbookbinder.view;
import nl.andrewlalis.blockbookbinder.control.ConvertToBookActionListener;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
import java.awt.*; import java.awt.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* A panel dedicated to displaying an interacting with a raw source of text for * A panel dedicated to displaying an interacting with a raw source of text for
@ -17,7 +11,7 @@ import java.nio.file.Path;
public class SourceTextPanel extends JPanel { public class SourceTextPanel extends JPanel {
private final JTextArea textArea; private final JTextArea textArea;
public SourceTextPanel(BookPreviewPanel bookPreviewPanel) { public SourceTextPanel() {
super(new BorderLayout()); super(new BorderLayout());
this.add(new JLabel("Source Text"), BorderLayout.NORTH); this.add(new JLabel("Source Text"), BorderLayout.NORTH);
@ -26,26 +20,15 @@ public class SourceTextPanel extends JPanel {
this.textArea = new JTextArea(); this.textArea = new JTextArea();
this.textArea.setWrapStyleWord(true); this.textArea.setWrapStyleWord(true);
this.textArea.setLineWrap(true); this.textArea.setLineWrap(true);
try {
this.textArea.setText(Files.readString(Path.of(this.getClass().getClassLoader().getResource("sample/lorem_ipsum.txt").toURI())));
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
JScrollPane scrollWrappedMainTextArea = new JScrollPane(this.textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); JScrollPane scrollWrappedMainTextArea = new JScrollPane(this.textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
this.add(scrollWrappedMainTextArea, BorderLayout.CENTER); this.add(scrollWrappedMainTextArea, BorderLayout.CENTER);
JPanel rightPanelButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
this.add(rightPanelButtonPanel, BorderLayout.SOUTH);
JButton importButton = new JButton("Import");
importButton.setActionCommand("importSource");
rightPanelButtonPanel.add(importButton);
JButton convertButton = new JButton("Convert to Book");
convertButton.addActionListener(new ConvertToBookActionListener(this, bookPreviewPanel));
rightPanelButtonPanel.add(convertButton);
} }
public String getSourceText() { public String getSourceText() {
return this.textArea.getText(); return this.textArea.getText();
} }
public void setSourceText(String text) {
this.textArea.setText(text);
}
} }

View File

@ -0,0 +1,64 @@
package nl.andrewlalis.blockbookbinder.view.about;
import nl.andrewlalis.blockbookbinder.util.ApplicationProperties;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
/**
* Simple dialog whose main purpose is rendering an HTML document.
*/
public class AboutDialog extends JDialog {
public AboutDialog(Frame owner) {
super(owner, "About", true);
}
public void setupAndShow() {
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
this.setPreferredSize(new Dimension(
ApplicationProperties.getIntProp("about_dialog.min_width"),
ApplicationProperties.getIntProp("about_dialog.min_height")
));
JTextPane textPane = new JTextPane();
textPane.setEditable(false);
textPane.setContentType("text/html");
textPane.setText(this.getAboutHtml());
textPane.setCaretPosition(0);
textPane.addHyperlinkListener(e -> {
try {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
Desktop.getDesktop().browse(e.getURL().toURI());
}
} catch (URISyntaxException | IOException uriSyntaxException) {
uriSyntaxException.printStackTrace();
}
});
JScrollPane scrollPane = new JScrollPane(
textPane,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
);
this.setContentPane(scrollPane);
this.pack();
this.setLocationRelativeTo(this.getOwner());
this.setVisible(true);
}
private String getAboutHtml() {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(
ApplicationProperties.getProp("about_dialog.source")
);
if (is == null) {
return "";
}
return new BufferedReader(new InputStreamReader(is)).lines()
.parallel().collect(Collectors.joining("\n"));
}
}

View File

@ -0,0 +1,23 @@
package nl.andrewlalis.blockbookbinder.view.book;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
public class BookPageDocumentFilter extends DocumentFilter {
@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
super.remove(fb, offset, length);
}
@Override
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
super.insertString(fb, offset, string, attr);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
super.replace(fb, offset, length, text, attrs);
}
}

View File

@ -1,8 +1,9 @@
package nl.andrewlalis.blockbookbinder.view; package nl.andrewlalis.blockbookbinder.view.book;
import lombok.Getter; import lombok.Getter;
import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.model.Book;
import nl.andrewlalis.blockbookbinder.model.BookPage; import nl.andrewlalis.blockbookbinder.model.BookPage;
import nl.andrewlalis.blockbookbinder.util.IconLoader;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
@ -50,13 +51,15 @@ public class BookPreviewPanel extends JPanel {
this.add(previewPageScrollPane, BorderLayout.CENTER); this.add(previewPageScrollPane, BorderLayout.CENTER);
JPanel previewButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); JPanel previewButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
this.firstPageButton = new JButton("First"); this.firstPageButton = new JButton();
this.firstPageButton.setIcon(IconLoader.load("images/page_first.png", 16, 16));
this.firstPageButton.addActionListener(e -> { this.firstPageButton.addActionListener(e -> {
this.currentPage = 0; this.currentPage = 0;
displayCurrentPage(); displayCurrentPage();
}); });
this.previousPageButton = new JButton("Previous Page"); this.previousPageButton = new JButton();
this.previousPageButton.setIcon(IconLoader.load("images/page_left.png", 16, 16));
this.previousPageButton.addActionListener(e -> { this.previousPageButton.addActionListener(e -> {
if (currentPage > 0) { if (currentPage > 0) {
currentPage--; currentPage--;
@ -64,15 +67,16 @@ public class BookPreviewPanel extends JPanel {
} }
}); });
this.nextPageButton = new JButton("Next Page"); this.nextPageButton = new JButton();
this.nextPageButton.setIcon(IconLoader.load("images/page_right.png", 16, 16));
this.nextPageButton.addActionListener(e -> { this.nextPageButton.addActionListener(e -> {
if (currentPage < book.getPageCount() - 1) { if (currentPage < book.getPageCount() - 1) {
currentPage++; currentPage++;
displayCurrentPage(); displayCurrentPage();
} }
}); });
this.lastPageButton = new JButton();
this.lastPageButton = new JButton("Last"); this.lastPageButton.setIcon(IconLoader.load("images/page_last.png", 16, 16));
this.lastPageButton.addActionListener(e -> { this.lastPageButton.addActionListener(e -> {
this.currentPage = Math.max(this.book.getPageCount() - 1, 0); this.currentPage = Math.max(this.book.getPageCount() - 1, 0);
displayCurrentPage(); displayCurrentPage();

View File

@ -0,0 +1,38 @@
package nl.andrewlalis.blockbookbinder.view.export;
import lombok.Getter;
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 {
@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);
}
}

View File

@ -0,0 +1,213 @@
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 JSpinner autoPasteDelaySpinner;
private JButton startButton;
private JButton stopButton;
private JPanel centerCardPanel;
private ExportStatusPanel exportStatusPanel;
private Thread exporterThread;
private BookExporter exporterRunnable;
public ExportToBookDialog(Window owner, Book book) {
super(owner, "Export to Book");
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(this.getOwner());
this.setVisible(true);
}
private Container buildContentPane() {
JPanel mainPanel = new JPanel(new BorderLayout());
JPanel setupPanel = new JPanel(new GridBagLayout());
String[] labels = {"", "First Page", "Last Page", "Auto-Paste Delay (Seconds)"};
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));
this.autoPasteDelaySpinner = new JSpinner(new SpinnerNumberModel(0.2, 0.1, 5.0, 0.1));
JComponent[] fields = {autoCheckbox, firstPageSpinner, lastPageSpinner, autoPasteDelaySpinner};
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.LINE_START;
c.weightx = 0.5;
c.fill = GridBagConstraints.NONE;
c.insets = new Insets(5, 5, 5, 5);
for (String label : labels) {
setupPanel.add(new JLabel(label), c);
c.gridy++;
}
c.gridx = 1;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
for (JComponent field : fields) {
setupPanel.add(field, c);
c.gridy++;
}
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 -> {
if (!this.checkSpinnerValues()){
return;
}
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;
}
/**
* 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(
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 {
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);
}
/**
* 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:
1. First page must be lower or equal to the last page.
2. Number of pages to export cannot exceed 100.
""",
"Invalid Page Range",
JOptionPane.WARNING_MESSAGE
);
return false;
}
return true;
}
}

View File

@ -1,11 +1,23 @@
version=1.3.1
# Settings for the application's GUI. # Settings for the application's GUI.
frame.title=Block Book Binder frame.title=Block Book Binder
frame.default_width=800 frame.default_width=800
frame.default_height=600 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
export_dialog.finish_sound=sound/LOZFDS_Fanfare.wav
about_dialog.min_width=600
about_dialog.min_height=800
about_dialog.source=html/about.html
# Settings for Minecraft book interaction. # Settings for Minecraft book interaction.
book.max_pages=100 book.max_pages=100
book.page_max_lines=14 book.page_max_lines=14
book.page_max_width=113 book.page_max_width=113
book.page_max_chars=255 book.page_max_chars=700
book.default_char_width=5 book.default_char_width=5

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About</title>
</head>
<body>
<h1>About BlockBookBinder</h1>
<p>
The <em>BlockBookBinder</em> is a simple application that gives you the power to write books faster than ever before! All you need to do is copy and paste some text into the <em>Source</em> panel on the right-hand side of the application, and click <strong>Compile from Source</strong>. Your text will be transformed so that it can fit onto written books in Minecraft, and you can preview the book in the <em>Book Preview</em> panel on the left-hand side of the application, using the navigation buttons below.
</p>
<p><em>
Note that your text might benefit from the <strong>Clean Source</strong> functionality, where extra new-line and other unnecessary whitespace characters are removed so that the text can be better compressed into Minecraft's limited format.
</em></p>
<p>
Once you've prepared a book and you want to actually copy it into Minecraft, click on <strong>Export to Minecraft</strong>. This will open up a popup with a few different buttons:
</p>
<ul>
<li><strong>Auto-paste</strong>: When checked, the program will automatically press CTRL+V and click your mouse to insert pages into the book, so that you don't need to do anything. If you select this, don't forget to move your mouse so that it's hovering over the <em>next page</em> button in Minecraft, otherwise auto-pasting won't work properly.</li>
<li><strong>First Page & Last Page</strong>: These inputs allow you to select the range of pages to export. Because Minecraft written books are limited to <strong>100</strong> pages only, this range cannot exceed that amount. Both the first and last page are included in the range.</li>
<li><strong>Auto-Paste Delay</strong>: The amount of time, in seconds, to wait between the various actions required for auto-pasting. If your server is experiencing a lot of lag, consider bumping this up to at least 1 second. The default value should be fine for most cases, though.</li>
</ul>
<p>
After setting any options that you want to, go ahead and press <strong>Start</strong>, and navigate back to your Minecraft window. You should hear ten <em>beep</em> sounds before exporting begins. If you selected auto-paste, you must make sure your mouse is hovering over the <em>next page</em> button in your written book before the ten seconds are up.
</p>
<p>
Once those first ten seconds pass, the first page will be loaded into your clipboard, and if auto-paste is enabled, automatically pasted into the book in your game. If auto-paste is not enabled, you'll need to manually paste each page, and then turn to the next page. You can see your progress in the popup, along with any important status updates as exporting progresses.
</p>
<p>
When all pages have been exported, you'll be prompted with a little popup, after which the export popup closes and you have the option to export again, or simply close the application if you're done.
</p>
<h3>Troubleshooting</h3>
<p>
This program works by hijacking your PC's input system to automatically transfer text to the system's clipboard, and then paste it by pressing <em>CTRL+V</em> for you. Because of the nature of such applications, there's no guarantee that this process will work on every computer. Here are some tips to try and get things working.
</p>
<ul>
<li>Try to increase the auto-paste delay to 1 second or more, if you notice some pages being skipped during the export process.</li>
<li>If the automated approach fails, you can always select and copy each page from the book preview.</li>
</ul>
<hr>
<p>
This program was written by Andrew Lalis. The source code is available on GitHub <a target="_blank" href="https://github.com/andrewlalis/BlockBookBinder">here</a>. It is copyrighted with the MIT license, which means you can do whatever you want with it, so long as you keep the included copyright notice in any copies or modifications of the program.
</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Binary file not shown.

Binary file not shown.