Compare commits
21 Commits
Author | SHA1 | Date |
---|---|---|
Andrew Lalis | 5450f69c3b | |
Andrew Lalis | 7735602a2c | |
Andrew Lalis | cfb70b14e8 | |
Andrew Lalis | 9ae48ec944 | |
Andrew Lalis | 2a9edf125e | |
Andrew Lalis | 605d050783 | |
Andrew Lalis | e32976fb8b | |
Andrew Lalis | deb10b1099 | |
Andrew Lalis | 8793202622 | |
Andrew Lalis | c748662760 | |
Andrew Lalis | 392114e3e6 | |
Andrew Lalis | ec06cf56cf | |
Andrew Lalis | 5c6587386e | |
Andrew Lalis | e5bc16a6a4 | |
Andrew Lalis | 53122afcf5 | |
Andrew Lalis | 6da3127427 | |
Andrew Lalis | e2ebb3085f | |
Andrew Lalis | e5e3c08c6e | |
Andrew Lalis | 91ad9b191f | |
Andrew Lalis | ae0da77f93 | |
Andrew Lalis | e1036940f9 |
15
README.md
|
@ -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
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
@ -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>
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) {}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 505 B |
After Width: | Height: | Size: 494 B |
After Width: | Height: | Size: 515 B |
After Width: | Height: | Size: 504 B |