├── LICENSE
├── README.md
├── mac-package.sh
├── mac.icns
├── pom.xml
└── src
└── main
├── java
└── timestamper
│ ├── Main.java
│ ├── MainController.java
│ ├── SendMoneyController.java
│ ├── WalletPasswordController.java
│ ├── WalletSetPasswordController.java
│ ├── WalletSettingsController.java
│ ├── controls
│ ├── BitcoinAddressValidator.java
│ ├── ClickableBitcoinAddress.java
│ └── NotificationBarPane.java
│ └── utils
│ ├── AlertWindowController.java
│ ├── BitcoinUIModel.java
│ ├── GuiUtils.java
│ ├── KeyDerivationTasks.java
│ ├── TextFieldValidator.java
│ ├── ThrottledRunLater.java
│ ├── WTUtils.java
│ └── easing
│ ├── EasingInterpolator.java
│ ├── EasingMode.java
│ └── ElasticInterpolator.java
└── resources
└── timestamper
├── 200px-Padlock.svg.png
├── bitcoin_logo_plain.png
├── checkpoints
├── checkpoints.testnet
├── controls
└── bitcoin_address.fxml
├── main.fxml
├── send_money.fxml
├── utils
├── alert.fxml
└── text-validation.css
├── wallet.css
├── wallet_password.fxml
├── wallet_set_password.fxml
└── wallet_settings.fxml
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Timestamper
2 |
3 | The simple tutorial app from Mike Hearn's DevCore 2015 talk: how to build a program that timestamps documents in 30
4 | minutes.
--------------------------------------------------------------------------------
/mac-package.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | jh=`/usr/libexec/java_home`
4 |
5 | $jh/bin/javapackager -deploy \
6 | -BappVersion=1 \
7 | -Bmac.CFBundleIdentifier=net.plan99.timestamper \
8 | -Bmac.CFBundleName=Timestamper \
9 | -Bicon=mac.icns \
10 | -Bruntime="$jh/../../" \
11 | -native dmg \
12 | -name Timestamper \
13 | -title Timestamper \
14 | -vendor Vinumeris \
15 | -outdir deploy \
16 | -appclass timestamper.Main \
17 | -srcfiles target/timestamper-shaded.jar \
18 | -outfile Timestamper
--------------------------------------------------------------------------------
/mac.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/mac.icns
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | timestamper
8 | net.plan99
9 | 0.1
10 |
11 | Timestamper
12 |
13 |
14 |
15 |
16 | org.apache.maven.plugins
17 | maven-compiler-plugin
18 |
19 | 1.8
20 | 1.8
21 | true
22 | true
23 |
24 |
25 |
26 |
27 | org.apache.maven.plugins
28 | maven-shade-plugin
29 |
30 |
31 |
32 | package
33 |
34 | shade
35 |
36 |
37 | false
38 |
39 |
40 |
41 | *:*
42 |
43 | META-INF/*.SF
44 | META-INF/*.DSA
45 | META-INF/*.RSA
46 |
47 |
48 |
49 | true
50 | bundled
51 |
52 |
53 | ${project.artifactId}.Main
54 |
55 |
56 | target/${project.artifactId}-shaded.jar
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | org.bitcoinj
68 | bitcoinj-core
69 | 0.13-SNAPSHOT
70 |
71 |
72 | org.slf4j
73 | slf4j-jdk14
74 | 1.7.10
75 |
76 |
77 |
78 | com.google.guava
79 | guava
80 | 16.0.1
81 |
82 |
89 |
90 | org.fxmisc.easybind
91 | easybind
92 | 1.0.2
93 |
94 |
95 | de.jensd
96 | fontawesomefx
97 | 8.0.0
98 |
99 |
100 | net.glxn
101 | qrgen
102 | 1.3
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/Main.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import com.google.common.util.concurrent.*;
4 | import javafx.scene.input.*;
5 | import org.bitcoinj.core.NetworkParameters;
6 | import org.bitcoinj.kits.WalletAppKit;
7 | import org.bitcoinj.params.*;
8 | import org.bitcoinj.utils.BriefLogFormatter;
9 | import org.bitcoinj.utils.Threading;
10 | import org.bitcoinj.wallet.DeterministicSeed;
11 | import javafx.application.Application;
12 | import javafx.application.Platform;
13 | import javafx.fxml.FXMLLoader;
14 | import javafx.scene.Node;
15 | import javafx.scene.Scene;
16 | import javafx.scene.layout.Pane;
17 | import javafx.scene.layout.StackPane;
18 | import javafx.stage.Stage;
19 | import timestamper.controls.NotificationBarPane;
20 | import timestamper.utils.GuiUtils;
21 | import timestamper.utils.TextFieldValidator;
22 |
23 | import javax.annotation.Nullable;
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.net.URL;
27 |
28 | import static timestamper.utils.GuiUtils.*;
29 |
30 | public class Main extends Application {
31 | public static String APP_NAME = "Timestamper";
32 |
33 | public static NetworkParameters params = RegTestParams.get();
34 | public static WalletAppKit bitcoin;
35 | public static Main instance;
36 |
37 | private StackPane uiStack;
38 | private Pane mainUI;
39 | public MainController controller;
40 | public NotificationBarPane notificationBar;
41 | public Stage mainWindow;
42 |
43 | @Override
44 | public void start(Stage mainWindow) throws Exception {
45 | try {
46 | realStart(mainWindow);
47 | } catch (Throwable e) {
48 | GuiUtils.crashAlert(e);
49 | throw e;
50 | }
51 | }
52 |
53 | private void realStart(Stage mainWindow) throws IOException {
54 | instance = this;
55 | // Show the crash dialog for any exceptions that we don't handle and that hit the main loop.
56 | GuiUtils.handleCrashesOnThisThread();
57 |
58 | this.mainWindow = mainWindow;
59 |
60 | if (System.getProperty("os.name").toLowerCase().contains("mac")) {
61 | // We could match the Mac Aqua style here, except that (a) Modena doesn't look that bad, and (b)
62 | // the date picker widget is kinda broken in AquaFx and I can't be bothered fixing it.
63 | // AquaFx.style();
64 | }
65 |
66 | // Load the GUI. The MainController class will be automagically created and wired up.
67 | URL location = getClass().getResource("main.fxml");
68 | FXMLLoader loader = new FXMLLoader(location);
69 | mainUI = loader.load();
70 | controller = loader.getController();
71 | // Configure the window with a StackPane so we can overlay things on top of the main UI, and a
72 | // NotificationBarPane so we can slide messages and progress bars in from the bottom. Note that
73 | // ordering of the construction and connection matters here, otherwise we get (harmless) CSS error
74 | // spew to the logs.
75 | notificationBar = new NotificationBarPane(mainUI);
76 | mainWindow.setTitle(APP_NAME);
77 | uiStack = new StackPane();
78 | Scene scene = new Scene(uiStack);
79 | TextFieldValidator.configureScene(scene); // Add CSS that we need.
80 | scene.getStylesheets().add(getClass().getResource("wallet.css").toString());
81 | uiStack.getChildren().add(notificationBar);
82 | mainWindow.setScene(scene);
83 |
84 | // Make log output concise.
85 | BriefLogFormatter.init();
86 | // Tell bitcoinj to run event handlers on the JavaFX UI thread. This keeps things simple and means
87 | // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener
88 | // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in
89 | // a future version.
90 | Threading.USER_THREAD = Platform::runLater;
91 | // Create the app kit. It won't do any heavyweight initialization until after we start it.
92 | setupWalletKit(null);
93 |
94 | if (bitcoin.isChainFileLocked()) {
95 | informationalAlert("Already running", "This application is already running and cannot be started twice.");
96 | Platform.exit();
97 | return;
98 | }
99 |
100 | mainWindow.show();
101 |
102 | WalletSetPasswordController.estimateKeyDerivationTimeMsec();
103 |
104 | bitcoin.addListener(new Service.Listener() {
105 | @Override
106 | public void failed(Service.State from, Throwable failure) {
107 | GuiUtils.crashAlert(failure);
108 | }
109 | }, Platform::runLater);
110 | bitcoin.startAsync();
111 |
112 | scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close());
113 | }
114 |
115 | public void setupWalletKit(@Nullable DeterministicSeed seed) {
116 | // If seed is non-null it means we are restoring from backup.
117 | bitcoin = new WalletAppKit(params, new File("."), APP_NAME + "-" + params.getPaymentProtocolId()) {
118 | @Override
119 | protected void onSetupCompleted() {
120 | // Don't make the user wait for confirmations for now, as the intention is they're sending it
121 | // their own money!
122 | bitcoin.wallet().allowSpendingUnconfirmedTransactions();
123 | Platform.runLater(controller::onBitcoinSetup);
124 | }
125 | };
126 | // Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen
127 | // or progress widget to keep the user engaged whilst we initialise, but we don't.
128 | if (params == RegTestParams.get()) {
129 | bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally.
130 | } else if (params == TestNet3Params.get()) {
131 | // As an example!
132 | bitcoin.useTor();
133 | // bitcoin.setDiscovery(new HttpDiscovery(params, URI.create("http://localhost:8080/peers"), ECKey.fromPublicOnly(BaseEncoding.base16().decode("02cba68cfd0679d10b186288b75a59f9132b1b3e222f6332717cb8c4eb2040f940".toUpperCase()))));
134 | } else {
135 | bitcoin.useTor();
136 | }
137 | bitcoin.setDownloadListener(controller.progressBarUpdater())
138 | .setBlockingStartup(false)
139 | .setUserAgent(APP_NAME, "1.0");
140 | if (seed != null)
141 | bitcoin.restoreWalletFromSeed(seed);
142 | }
143 |
144 | private Node stopClickPane = new Pane();
145 |
146 | public class OverlayUI {
147 | public Node ui;
148 | public T controller;
149 |
150 | public OverlayUI(Node ui, T controller) {
151 | this.ui = ui;
152 | this.controller = controller;
153 | }
154 |
155 | public void show() {
156 | checkGuiThread();
157 | if (currentOverlay == null) {
158 | uiStack.getChildren().add(stopClickPane);
159 | uiStack.getChildren().add(ui);
160 | blurOut(mainUI);
161 | //darken(mainUI);
162 | fadeIn(ui);
163 | zoomIn(ui);
164 | } else {
165 | // Do a quick transition between the current overlay and the next.
166 | // Bug here: we don't pay attention to changes in outsideClickDismisses.
167 | explodeOut(currentOverlay.ui);
168 | fadeOutAndRemove(uiStack, currentOverlay.ui);
169 | uiStack.getChildren().add(ui);
170 | ui.setOpacity(0.0);
171 | fadeIn(ui, 100);
172 | zoomIn(ui, 100);
173 | }
174 | currentOverlay = this;
175 | }
176 |
177 | public void outsideClickDismisses() {
178 | stopClickPane.setOnMouseClicked((ev) -> done());
179 | }
180 |
181 | public void done() {
182 | checkGuiThread();
183 | if (ui == null) return; // In the middle of being dismissed and got an extra click.
184 | explodeOut(ui);
185 | fadeOutAndRemove(uiStack, ui, stopClickPane);
186 | blurIn(mainUI);
187 | //undark(mainUI);
188 | this.ui = null;
189 | this.controller = null;
190 | currentOverlay = null;
191 | }
192 | }
193 |
194 | @Nullable
195 | private OverlayUI currentOverlay;
196 |
197 | public OverlayUI overlayUI(Node node, T controller) {
198 | checkGuiThread();
199 | OverlayUI pair = new OverlayUI(node, controller);
200 | // Auto-magically set the overlayUI member, if it's there.
201 | try {
202 | controller.getClass().getField("overlayUI").set(controller, pair);
203 | } catch (IllegalAccessException | NoSuchFieldException ignored) {
204 | }
205 | pair.show();
206 | return pair;
207 | }
208 |
209 | /** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */
210 | public OverlayUI overlayUI(String name) {
211 | try {
212 | checkGuiThread();
213 | // Load the UI from disk.
214 | URL location = GuiUtils.getResource(name);
215 | FXMLLoader loader = new FXMLLoader(location);
216 | Pane ui = loader.load();
217 | T controller = loader.getController();
218 | OverlayUI pair = new OverlayUI(ui, controller);
219 | // Auto-magically set the overlayUI member, if it's there.
220 | try {
221 | if (controller != null)
222 | controller.getClass().getField("overlayUI").set(controller, pair);
223 | } catch (IllegalAccessException | NoSuchFieldException ignored) {
224 | ignored.printStackTrace();
225 | }
226 | pair.show();
227 | return pair;
228 | } catch (IOException e) {
229 | throw new RuntimeException(e); // Can't happen.
230 | }
231 | }
232 |
233 | @Override
234 | public void stop() throws Exception {
235 | bitcoin.stopAsync();
236 | bitcoin.awaitTerminated();
237 | // Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere.
238 | Runtime.getRuntime().exit(0);
239 | }
240 |
241 | public static void main(String[] args) {
242 | launch(args);
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/MainController.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import com.subgraph.orchid.*;
4 | import javafx.animation.*;
5 | import javafx.application.*;
6 | import javafx.beans.property.*;
7 | import javafx.event.*;
8 | import javafx.scene.control.*;
9 | import javafx.scene.layout.*;
10 | import javafx.stage.*;
11 | import javafx.util.*;
12 | import org.bitcoinj.core.*;
13 | import org.bitcoinj.script.*;
14 | import org.bitcoinj.store.*;
15 | import org.bitcoinj.utils.*;
16 | import org.fxmisc.easybind.*;
17 | import timestamper.controls.*;
18 | import timestamper.utils.*;
19 | import timestamper.utils.easing.*;
20 |
21 | import java.io.*;
22 | import java.nio.file.*;
23 | import java.util.*;
24 |
25 | import static timestamper.Main.*;
26 | import static timestamper.utils.GuiUtils.*;
27 |
28 | /**
29 | * Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named
30 | * after. This class handles all the updates and event handling for the main UI.
31 | */
32 | public class MainController {
33 | public HBox controlsBox;
34 | public Label balance;
35 | public Button sendMoneyOutBtn;
36 | public ClickableBitcoinAddress addressControl;
37 | public ListView pendingProofsList;
38 |
39 | private static class Proof implements Serializable {
40 | byte[] tx, partialMerkleTree;
41 | Sha256Hash blockHash;
42 |
43 | transient SimpleIntegerProperty depth = new SimpleIntegerProperty();
44 | transient String filename;
45 |
46 | public void saveTo(String filename) throws IOException {
47 | try (ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(filename)))) {
48 | oos.writeObject(this);
49 | }
50 | }
51 |
52 | public static Proof readFrom(String filename) throws IOException {
53 | try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filename)))) {
54 | return (Proof) ois.readObject();
55 | } catch (ClassNotFoundException e) {
56 | throw new RuntimeException(e);
57 | }
58 | }
59 | }
60 |
61 | private BitcoinUIModel model = new BitcoinUIModel();
62 | private NotificationBarPane.Item syncItem;
63 |
64 | // Called by FXMLLoader.
65 | public void initialize() {
66 | addressControl.setOpacity(0.0);
67 |
68 | pendingProofsList.setCellFactory(new Callback, ListCell>() {
69 | @Override
70 | public ListCell call(ListView param) {
71 | return new ListCell() {
72 | @Override
73 | protected void updateItem(Proof item, boolean empty) {
74 | super.updateItem(item, empty);
75 | if (empty) {
76 | setText("");
77 | setGraphic(null);
78 | } else {
79 | setText("Proof for " + item.filename);
80 | ProgressBar bar = new ProgressBar();
81 | bar.progressProperty().bind(item.depth.divide(3.0));
82 | setGraphic(bar);
83 | }
84 | }
85 | };
86 | }
87 | });
88 | }
89 |
90 | public void onBitcoinSetup() {
91 | model.setWallet(bitcoin.wallet());
92 | addressControl.addressProperty().bind(model.addressProperty());
93 | balance.textProperty().bind(EasyBind.map(model.balanceProperty(), coin -> MonetaryFormat.BTC.noCode().format(coin).toString()));
94 | // Don't let the user click send money when the wallet is empty.
95 | sendMoneyOutBtn.disableProperty().bind(model.balanceProperty().isEqualTo(Coin.ZERO));
96 |
97 | TorClient torClient = Main.bitcoin.peerGroup().getTorClient();
98 | if (torClient != null) {
99 | SimpleDoubleProperty torProgress = new SimpleDoubleProperty(-1);
100 | String torMsg = "Initialising Tor";
101 | syncItem = Main.instance.notificationBar.pushItem(torMsg, torProgress);
102 | torClient.addInitializationListener(new TorInitializationListener() {
103 | @Override
104 | public void initializationProgress(String message, int percent) {
105 | Platform.runLater(() -> {
106 | syncItem.label.set(torMsg + ": " + message);
107 | torProgress.set(percent / 100.0);
108 | });
109 | }
110 |
111 | @Override
112 | public void initializationCompleted() {
113 | Platform.runLater(() -> {
114 | syncItem.cancel();
115 | showBitcoinSyncMessage();
116 | });
117 | }
118 | });
119 | } else {
120 | showBitcoinSyncMessage();
121 | }
122 | model.syncProgressProperty().addListener(x -> {
123 | if (model.syncProgressProperty().get() >= 1.0) {
124 | readyToGoAnimation();
125 | if (syncItem != null) {
126 | syncItem.cancel();
127 | syncItem = null;
128 | }
129 | } else if (syncItem == null) {
130 | showBitcoinSyncMessage();
131 | }
132 | });
133 | }
134 |
135 | private void showBitcoinSyncMessage() {
136 | syncItem = Main.instance.notificationBar.pushItem("Synchronising with the Bitcoin network", model.syncProgressProperty());
137 | }
138 |
139 | public void sendMoneyOut(ActionEvent event) {
140 | // Hide this UI and show the send money UI. This UI won't be clickable until the user dismisses send_money.
141 | Main.instance.overlayUI("send_money.fxml");
142 | }
143 |
144 | public void settingsClicked(ActionEvent event) {
145 | Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml");
146 | screen.controller.initialize(null);
147 | }
148 |
149 | public void restoreFromSeedAnimation() {
150 | // Buttons slide out ...
151 | TranslateTransition leave = new TranslateTransition(Duration.millis(1200), controlsBox);
152 | leave.setByY(80.0);
153 | leave.play();
154 | }
155 |
156 | public void readyToGoAnimation() {
157 | // Buttons slide in and clickable address appears simultaneously.
158 | TranslateTransition arrive = new TranslateTransition(Duration.millis(1200), controlsBox);
159 | arrive.setInterpolator(new ElasticInterpolator(EasingMode.EASE_OUT, 1, 2));
160 | arrive.setToY(0.0);
161 | FadeTransition reveal = new FadeTransition(Duration.millis(1200), addressControl);
162 | reveal.setToValue(1.0);
163 | ParallelTransition group = new ParallelTransition(arrive, reveal);
164 | group.setDelay(NotificationBarPane.ANIM_OUT_DURATION);
165 | group.setCycleCount(1);
166 | group.play();
167 | }
168 |
169 | public DownloadProgressTracker progressBarUpdater() {
170 | return model.getDownloadProgressTracker();
171 | }
172 |
173 | public void onTimestampClicked(ActionEvent event) {
174 | // Ask the user for the document to timestamp
175 | File doc = new FileChooser().showOpenDialog(Main.instance.mainWindow);
176 | if (doc == null) return; // User cancelled
177 | try {
178 | timestamp(doc);
179 | } catch (IOException e) {
180 | crashAlert(e);
181 | } catch (InsufficientMoneyException e) {
182 | informationalAlert("Insufficient funds",
183 | "You need bitcoins in this wallet in order to pay network fees.");
184 | }
185 | }
186 |
187 | private void timestamp(File doc) throws IOException, InsufficientMoneyException {
188 | // Hash it
189 | Sha256Hash hash = Sha256Hash.hashFileContents(doc);
190 |
191 | // Create a tx with an OP_RETURN output
192 | Transaction tx = new Transaction(Main.params);
193 | tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(hash.getBytes()));
194 |
195 | // Send it to the Bitcoin network
196 | Main.bitcoin.wallet().sendCoins(Wallet.SendRequest.forTx(tx));
197 |
198 | // Add it to the UI list
199 | Proof proof = new Proof();
200 | proof.tx = tx.bitcoinSerialize();
201 | proof.filename = doc.toString();
202 | pendingProofsList.getItems().add(proof);
203 |
204 | // Grab the merkle branch when it appears in the block chain
205 | Main.bitcoin.peerGroup().addEventListener(new AbstractPeerEventListener() {
206 | @Override
207 | public void onBlocksDownloaded(Peer peer, Block block, FilteredBlock filteredBlock, int blocksLeft) {
208 | List hashes = new ArrayList<>();
209 | PartialMerkleTree tree = filteredBlock.getPartialMerkleTree();
210 | tree.getTxnHashAndMerkleRoot(hashes);
211 | if (hashes.contains(tx.getHash())) {
212 | proof.partialMerkleTree = tree.bitcoinSerialize();
213 | proof.blockHash = filteredBlock.getHash();
214 | }
215 | }
216 | });
217 |
218 | // Wait for confirmations (3)
219 | tx.getConfidence().addEventListener((confidence, reason) -> {
220 | if (confidence.getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING)
221 | return;
222 | proof.depth.set(confidence.getDepthInBlocks());
223 | if (proof.depth.get() == 3) {
224 | // Save the proof to disk
225 | String filename = doc.toString() + ".timestamp";
226 | try {
227 | proof.saveTo(filename);
228 | // Remove it from the UI list
229 | pendingProofsList.getItems().remove(proof);
230 | // Notify the user that it's done
231 | informationalAlert("Proof complete", "Saved to " + filename);
232 | } catch (IOException e) {
233 | crashAlert(e);
234 | }
235 | }
236 | });
237 | }
238 |
239 | public void onVerifyClicked(ActionEvent event) {
240 | // Ask the user for the document to verify
241 | FileChooser chooser = new FileChooser();
242 | chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Timestamp proofs", "*.timestamp"));
243 | File proofFile = chooser.showOpenDialog(Main.instance.mainWindow);
244 | if (proofFile == null) return; // User cancelled.
245 |
246 | try {
247 | StoredBlock cursor = verify(proofFile);
248 | // Notify the user that the proof is valid and what the timestamp is
249 | informationalAlert("Proof valid!", "Document existed at " + cursor.getHeader().getTime());
250 | } catch (IOException | BlockStoreException e) {
251 | crashAlert(e);
252 | } catch (ProofException e) {
253 | informationalAlert("Proof was invalid", e.getMessage());
254 | }
255 | }
256 |
257 | private StoredBlock verify(File proofFile) throws IOException, ProofException, BlockStoreException {
258 | // Load the proof file
259 | Proof proof = Proof.readFrom(proofFile.getAbsolutePath());
260 |
261 | // Hash the document
262 | String docFile = proofFile.getAbsoluteFile().toString().replace(".timestamp", "");
263 | Sha256Hash hash = Sha256Hash.hashFileContents(new File(docFile));
264 |
265 | // Verify the hash is in the OP_RETURN output of the tx
266 | Transaction tx = new Transaction(Main.params, proof.tx);
267 | boolean found = false;
268 | for (TransactionOutput output : tx.getOutputs()) {
269 | if (!output.getScriptPubKey().isOpReturn()) continue;
270 | //noinspection ConstantConditions
271 | if (!Arrays.equals(output.getScriptPubKey().getChunks().get(1).data,
272 | hash.getBytes()))
273 | throw new ProofException("Hash does not match OP_RETURN output");
274 | found = true;
275 | break;
276 | }
277 | if (!found) throw new ProofException("No OP_RETURN output in transaction");
278 | // Verify the transaction is in the Merkle proof
279 | PartialMerkleTree tree = new PartialMerkleTree(Main.params, proof.partialMerkleTree, 0);
280 | List hashes = new ArrayList<>();
281 | Sha256Hash merkleRoot = tree.getTxnHashAndMerkleRoot(hashes);
282 | if (!hashes.contains(tx.getHash()))
283 | throw new ProofException("Transaction not found in Merkle proof");
284 |
285 | // Find the block given the hash
286 | StoredBlock cursor = Main.bitcoin.chain().getChainHead();
287 | while (cursor != null && !cursor.getHeader().getHash().equals(proof.blockHash)) {
288 | cursor = cursor.getPrev(Main.bitcoin.store());
289 | }
290 | if (cursor == null)
291 | throw new ProofException("Could not find given block hash: " + proof.blockHash);
292 |
293 | // Verify the Merkle proof is linked to the block header
294 | if (!cursor.getHeader().getMerkleRoot().equals(merkleRoot))
295 | throw new ProofException("Merkle root does not match block header");
296 | return cursor;
297 | }
298 |
299 | private class ProofException extends Exception {
300 | public ProofException(String s) {
301 | super(s);
302 | }
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/SendMoneyController.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import javafx.scene.layout.HBox;
4 | import org.bitcoinj.core.*;
5 | import com.google.common.util.concurrent.FutureCallback;
6 | import com.google.common.util.concurrent.Futures;
7 | import javafx.event.ActionEvent;
8 | import javafx.scene.control.Button;
9 | import javafx.scene.control.Label;
10 | import javafx.scene.control.TextField;
11 | import org.spongycastle.crypto.params.KeyParameter;
12 | import timestamper.controls.BitcoinAddressValidator;
13 | import timestamper.utils.TextFieldValidator;
14 | import timestamper.utils.WTUtils;
15 |
16 | import static com.google.common.base.Preconditions.checkState;
17 | import static timestamper.utils.GuiUtils.*;
18 |
19 | public class SendMoneyController {
20 | public Button sendBtn;
21 | public Button cancelBtn;
22 | public TextField address;
23 | public Label titleLabel;
24 | public TextField amountEdit;
25 | public Label btcLabel;
26 |
27 | public Main.OverlayUI overlayUI;
28 |
29 | private Wallet.SendResult sendResult;
30 | private KeyParameter aesKey;
31 |
32 | // Called by FXMLLoader
33 | public void initialize() {
34 | Coin balance = Main.bitcoin.wallet().getBalance();
35 | checkState(!balance.isZero());
36 | new BitcoinAddressValidator(Main.params, address, sendBtn);
37 | new TextFieldValidator(amountEdit, text ->
38 | !WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0)));
39 | amountEdit.setText(balance.toPlainString());
40 | }
41 |
42 | public void cancel(ActionEvent event) {
43 | overlayUI.done();
44 | }
45 |
46 | public void send(ActionEvent event) {
47 | // Address exception cannot happen as we validated it beforehand.
48 | try {
49 | Coin amount = Coin.parseCoin(amountEdit.getText());
50 | Address destination = new Address(Main.params, address.getText());
51 | Wallet.SendRequest req;
52 | if (amount.equals(Main.bitcoin.wallet().getBalance()))
53 | req = Wallet.SendRequest.emptyWallet(destination);
54 | else
55 | req = Wallet.SendRequest.to(destination, amount);
56 | req.aesKey = aesKey;
57 | sendResult = Main.bitcoin.wallet().sendCoins(req);
58 | Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() {
59 | @Override
60 | public void onSuccess(Transaction result) {
61 | checkGuiThread();
62 | overlayUI.done();
63 | }
64 |
65 | @Override
66 | public void onFailure(Throwable t) {
67 | // We died trying to empty the wallet.
68 | crashAlert(t);
69 | }
70 | });
71 | sendResult.tx.getConfidence().addEventListener((tx, reason) -> {
72 | if (reason == TransactionConfidence.Listener.ChangeReason.SEEN_PEERS)
73 | updateTitleForBroadcast();
74 | });
75 | sendBtn.setDisable(true);
76 | address.setDisable(true);
77 | ((HBox)amountEdit.getParent()).getChildren().remove(amountEdit);
78 | ((HBox)btcLabel.getParent()).getChildren().remove(btcLabel);
79 | updateTitleForBroadcast();
80 | } catch (InsufficientMoneyException e) {
81 | informationalAlert("Could not empty the wallet",
82 | "You may have too little money left in the wallet to make a transaction.");
83 | overlayUI.done();
84 | } catch (ECKey.KeyIsEncryptedException e) {
85 | askForPasswordAndRetry();
86 | } catch (AddressFormatException e) {
87 | // Cannot happen because we already validated it when the text field changed.
88 | throw new RuntimeException(e);
89 | }
90 | }
91 |
92 | private void askForPasswordAndRetry() {
93 | Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml");
94 | final String addressStr = address.getText();
95 | final String amountStr = amountEdit.getText();
96 | pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
97 | // We only get here if the user found the right password. If they don't or they cancel, we end up back on
98 | // the main UI screen. By now the send money screen is history so we must recreate it.
99 | checkGuiThread();
100 | Main.OverlayUI screen = Main.instance.overlayUI("send_money.fxml");
101 | screen.controller.aesKey = cur;
102 | screen.controller.address.setText(addressStr);
103 | screen.controller.amountEdit.setText(amountStr);
104 | screen.controller.send(null);
105 | });
106 | }
107 |
108 | private void updateTitleForBroadcast() {
109 | final int peers = sendResult.tx.getConfidence().numBroadcastPeers();
110 | titleLabel.setText(String.format("Broadcasting ... seen by %d peers", peers));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/WalletPasswordController.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import javafx.application.Platform;
4 | import org.bitcoinj.crypto.KeyCrypterScrypt;
5 | import com.google.common.primitives.Longs;
6 | import com.google.protobuf.ByteString;
7 | import javafx.beans.property.ReadOnlyObjectProperty;
8 | import javafx.beans.property.SimpleObjectProperty;
9 | import javafx.event.ActionEvent;
10 | import javafx.fxml.FXML;
11 | import javafx.scene.control.Label;
12 | import javafx.scene.control.PasswordField;
13 | import javafx.scene.control.ProgressIndicator;
14 | import javafx.scene.image.ImageView;
15 | import javafx.scene.layout.GridPane;
16 | import javafx.scene.layout.HBox;
17 | import org.slf4j.Logger;
18 | import org.slf4j.LoggerFactory;
19 | import org.spongycastle.crypto.params.KeyParameter;
20 | import timestamper.utils.KeyDerivationTasks;
21 |
22 | import java.time.Duration;
23 |
24 | import static com.google.common.base.Preconditions.checkNotNull;
25 | import static timestamper.utils.GuiUtils.*;
26 |
27 | /**
28 | * User interface for entering a password on demand, e.g. to send money. Also used when encrypting a wallet. Shows a
29 | * progress meter as we scrypt the password.
30 | */
31 | public class WalletPasswordController {
32 | private static final Logger log = LoggerFactory.getLogger(WalletPasswordController.class);
33 |
34 | @FXML HBox buttonsBox;
35 | @FXML PasswordField pass1;
36 | @FXML ImageView padlockImage;
37 | @FXML ProgressIndicator progressMeter;
38 | @FXML GridPane widgetGrid;
39 | @FXML Label explanationLabel;
40 |
41 | public Main.OverlayUI overlayUI;
42 |
43 | private SimpleObjectProperty aesKey = new SimpleObjectProperty<>();
44 |
45 | public void initialize() {
46 | progressMeter.setOpacity(0);
47 | Platform.runLater(pass1::requestFocus);
48 | }
49 |
50 | @FXML void confirmClicked(ActionEvent event) {
51 | String password = pass1.getText();
52 | if (password.isEmpty() || password.length() < 4) {
53 | informationalAlert("Bad password", "The password you entered is empty or too short.");
54 | return;
55 | }
56 |
57 | final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) Main.bitcoin.wallet().getKeyCrypter();
58 | checkNotNull(keyCrypter); // We should never arrive at this GUI if the wallet isn't actually encrypted.
59 | KeyDerivationTasks tasks = new KeyDerivationTasks(keyCrypter, password, getTargetTime()) {
60 | @Override
61 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) {
62 | checkGuiThread();
63 | if (Main.bitcoin.wallet().checkAESKey(aesKey)) {
64 | WalletPasswordController.this.aesKey.set(aesKey);
65 | } else {
66 | log.warn("User entered incorrect password");
67 | fadeOut(progressMeter);
68 | fadeIn(widgetGrid);
69 | fadeIn(explanationLabel);
70 | fadeIn(buttonsBox);
71 | informationalAlert("Wrong password",
72 | "Please try entering your password again, carefully checking for typos or spelling errors.");
73 | }
74 | }
75 | };
76 | progressMeter.progressProperty().bind(tasks.progress);
77 | tasks.start();
78 |
79 | fadeIn(progressMeter);
80 | fadeOut(widgetGrid);
81 | fadeOut(explanationLabel);
82 | fadeOut(buttonsBox);
83 | }
84 |
85 | public void cancelClicked(ActionEvent event) {
86 | overlayUI.done();
87 | }
88 |
89 | public ReadOnlyObjectProperty aesKeyProperty() {
90 | return aesKey;
91 | }
92 |
93 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
94 |
95 | public static final String TAG = WalletPasswordController.class.getName() + ".target-time";
96 |
97 | // Writes the given time to the wallet as a tag so we can find it again in this class.
98 | public static void setTargetTime(Duration targetTime) {
99 | ByteString bytes = ByteString.copyFrom(Longs.toByteArray(targetTime.toMillis()));
100 | Main.bitcoin.wallet().setTag(TAG, bytes);
101 | }
102 |
103 | // Reads target time or throws if not set yet (should never happen).
104 | public static Duration getTargetTime() throws IllegalArgumentException {
105 | return Duration.ofMillis(Longs.fromByteArray(Main.bitcoin.wallet().getTag(TAG).toByteArray()));
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/WalletSetPasswordController.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import com.google.protobuf.*;
4 | import javafx.application.*;
5 | import javafx.event.*;
6 | import javafx.fxml.*;
7 | import javafx.scene.control.*;
8 | import javafx.scene.layout.*;
9 | import org.bitcoinj.crypto.*;
10 | import org.bitcoinj.wallet.*;
11 | import org.slf4j.*;
12 | import org.spongycastle.crypto.params.*;
13 | import timestamper.utils.*;
14 |
15 | import java.time.*;
16 | import java.util.concurrent.*;
17 |
18 | import static timestamper.utils.GuiUtils.*;
19 |
20 | public class WalletSetPasswordController {
21 | private static final Logger log = LoggerFactory.getLogger(WalletSetPasswordController.class);
22 | public PasswordField pass1, pass2;
23 |
24 | public ProgressIndicator progressMeter;
25 | public GridPane widgetGrid;
26 | public Button closeButton;
27 | public Label explanationLabel;
28 |
29 | public Main.OverlayUI overlayUI;
30 | // These params were determined empirically on a top-range (as of 2014) MacBook Pro with native scrypt support,
31 | // using the scryptenc command line tool from the original scrypt distribution, given a memory limit of 40mb.
32 | public static final Protos.ScryptParameters SCRYPT_PARAMETERS = Protos.ScryptParameters.newBuilder()
33 | .setP(6)
34 | .setR(8)
35 | .setN(32768)
36 | .setSalt(ByteString.copyFrom(KeyCrypterScrypt.randomSalt()))
37 | .build();
38 |
39 | public void initialize() {
40 | progressMeter.setOpacity(0);
41 | }
42 |
43 | public static Duration estimatedKeyDerivationTime = null;
44 |
45 | public static CompletableFuture estimateKeyDerivationTimeMsec() {
46 | // This is run in the background after startup. If we haven't recorded it before, do a key derivation to see
47 | // how long it takes. This helps us produce better progress feedback, as on Windows we don't currently have a
48 | // native Scrypt impl and the Java version is ~3 times slower, plus it depends a lot on CPU speed.
49 | CompletableFuture future = new CompletableFuture<>();
50 | new Thread(() -> {
51 | log.info("Doing background test key derivation");
52 | KeyCrypterScrypt scrypt = new KeyCrypterScrypt(SCRYPT_PARAMETERS);
53 | long start = System.currentTimeMillis();
54 | scrypt.deriveKey("test password");
55 | long msec = System.currentTimeMillis() - start;
56 | log.info("Background test key derivation took {}msec", msec);
57 | Platform.runLater(() -> {
58 | estimatedKeyDerivationTime = Duration.ofMillis(msec);
59 | future.complete(estimatedKeyDerivationTime);
60 | });
61 | }).start();
62 | return future;
63 | }
64 |
65 | @FXML
66 | public void setPasswordClicked(ActionEvent event) {
67 | if (!pass1.getText().equals(pass2.getText())) {
68 | informationalAlert("Passwords do not match", "Try re-typing your chosen passwords.");
69 | return;
70 | }
71 | String password = pass1.getText();
72 | // This is kind of arbitrary and we could do much more to help people pick strong passwords.
73 | if (password.length() < 4) {
74 | informationalAlert("Password too short", "You need to pick a password at least five characters or longer.");
75 | return;
76 | }
77 |
78 | fadeIn(progressMeter);
79 | fadeOut(widgetGrid);
80 | fadeOut(explanationLabel);
81 | fadeOut(closeButton);
82 |
83 |
84 | KeyCrypterScrypt scrypt = new KeyCrypterScrypt(SCRYPT_PARAMETERS);
85 |
86 | // Deriving the actual key runs on a background thread. 500msec is empirical on my laptop (actual val is more like 333 but we give padding time).
87 | KeyDerivationTasks tasks = new KeyDerivationTasks(scrypt, password, estimatedKeyDerivationTime) {
88 | @Override
89 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) {
90 | // Write the target time to the wallet so we can make the progress bar work when entering the password.
91 | WalletPasswordController.setTargetTime(Duration.ofMillis(timeTakenMsec));
92 | // The actual encryption part doesn't take very long as most private keys are derived on demand.
93 | log.info("Key derived, now encrypting");
94 | Main.bitcoin.wallet().encrypt(scrypt, aesKey);
95 | log.info("Encryption done");
96 | informationalAlert("Wallet encrypted",
97 | "You can remove the password at any time from the settings screen.");
98 | overlayUI.done();
99 | }
100 | };
101 | progressMeter.progressProperty().bind(tasks.progress);
102 | tasks.start();
103 | }
104 |
105 | public void closeClicked(ActionEvent event) {
106 | overlayUI.done();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/WalletSettingsController.java:
--------------------------------------------------------------------------------
1 | package timestamper;
2 |
3 | import org.bitcoinj.crypto.MnemonicCode;
4 | import org.bitcoinj.wallet.DeterministicSeed;
5 | import com.google.common.base.Joiner;
6 | import com.google.common.base.Splitter;
7 | import com.google.common.util.concurrent.Service;
8 | import javafx.application.Platform;
9 | import javafx.beans.binding.BooleanBinding;
10 | import javafx.event.ActionEvent;
11 | import javafx.fxml.FXML;
12 | import javafx.scene.control.Button;
13 | import javafx.scene.control.DatePicker;
14 | import javafx.scene.control.TextArea;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 | import org.spongycastle.crypto.params.KeyParameter;
18 | import timestamper.utils.TextFieldValidator;
19 |
20 | import javax.annotation.Nullable;
21 | import java.time.Instant;
22 | import java.time.LocalDate;
23 | import java.time.ZoneId;
24 | import java.time.ZoneOffset;
25 | import java.util.List;
26 |
27 | import static com.google.common.base.Preconditions.checkNotNull;
28 | import static javafx.beans.binding.Bindings.*;
29 | import static timestamper.utils.GuiUtils.checkGuiThread;
30 | import static timestamper.utils.GuiUtils.informationalAlert;
31 | import static timestamper.utils.WTUtils.didThrow;
32 | import static timestamper.utils.WTUtils.unchecked;
33 |
34 | public class WalletSettingsController {
35 | private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class);
36 |
37 | @FXML Button passwordButton;
38 | @FXML DatePicker datePicker;
39 | @FXML TextArea wordsArea;
40 | @FXML Button restoreButton;
41 |
42 | public Main.OverlayUI overlayUI;
43 |
44 | private KeyParameter aesKey;
45 |
46 | // Note: NOT called by FXMLLoader!
47 | public void initialize(@Nullable KeyParameter aesKey) {
48 | DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed();
49 | if (aesKey == null) {
50 | if (seed.isEncrypted()) {
51 | log.info("Wallet is encrypted, requesting password first.");
52 | // Delay execution of this until after we've finished initialising this screen.
53 | Platform.runLater(() -> askForPasswordAndRetry());
54 | return;
55 | }
56 | } else {
57 | this.aesKey = aesKey;
58 | seed = seed.decrypt(checkNotNull(Main.bitcoin.wallet().getKeyCrypter()), "", aesKey);
59 | // Now we can display the wallet seed as appropriate.
60 | passwordButton.setText("Remove password");
61 | }
62 |
63 | // Set the date picker to show the birthday of this wallet.
64 | Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds());
65 | LocalDate origDate = creationTime.atZone(ZoneId.systemDefault()).toLocalDate();
66 | datePicker.setValue(origDate);
67 |
68 | // Set the mnemonic seed words.
69 | final List mnemonicCode = seed.getMnemonicCode();
70 | checkNotNull(mnemonicCode); // Already checked for encryption.
71 | String origWords = Joiner.on(" ").join(mnemonicCode);
72 | wordsArea.setText(origWords);
73 |
74 | // Validate words as they are being typed.
75 | MnemonicCode codec = unchecked(MnemonicCode::new);
76 | TextFieldValidator validator = new TextFieldValidator(wordsArea, text ->
77 | !didThrow(() -> codec.check(Splitter.on(' ').splitToList(text)))
78 | );
79 |
80 | // Clear the date picker if the user starts editing the words, if it contained the current wallets date.
81 | // This forces them to set the birthday field when restoring.
82 | wordsArea.textProperty().addListener(o -> {
83 | if (origDate.equals(datePicker.getValue()))
84 | datePicker.setValue(null);
85 | });
86 |
87 | BooleanBinding datePickerIsInvalid = or(
88 | datePicker.valueProperty().isNull(),
89 |
90 | createBooleanBinding(() ->
91 | datePicker.getValue().isAfter(LocalDate.now())
92 | , /* depends on */ datePicker.valueProperty())
93 | );
94 |
95 | // Don't let the user click restore if the words area contains the current wallet words, or are an invalid set,
96 | // or if the date field isn't set, or if it's in the future.
97 | restoreButton.disableProperty().bind(
98 | or(
99 | or(
100 | not(validator.valid),
101 | equal(origWords, wordsArea.textProperty())
102 | ),
103 |
104 | datePickerIsInvalid
105 | )
106 | );
107 |
108 | // Highlight the date picker in red if it's empty or in the future, so the user knows why restore is disabled.
109 | datePickerIsInvalid.addListener((dp, old, cur) -> {
110 | if (cur) {
111 | datePicker.getStyleClass().add("validation_error");
112 | } else {
113 | datePicker.getStyleClass().remove("validation_error");
114 | }
115 | });
116 | }
117 |
118 | private void askForPasswordAndRetry() {
119 | Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml");
120 | pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
121 | // We only get here if the user found the right password. If they don't or they cancel, we end up back on
122 | // the main UI screen.
123 | checkGuiThread();
124 | Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml");
125 | screen.controller.initialize(cur);
126 | });
127 | }
128 |
129 | public void closeClicked(ActionEvent event) {
130 | overlayUI.done();
131 | }
132 |
133 | public void restoreClicked(ActionEvent event) {
134 | // Don't allow a restore unless this wallet is presently empty. We don't want to end up with two wallets, too
135 | // much complexity, even though WalletAppKit will keep the current one as a backup file in case of disaster.
136 | if (Main.bitcoin.wallet().getBalance().value > 0) {
137 | informationalAlert("Wallet is not empty",
138 | "You must empty this wallet out before attempting to restore an older one, as mixing wallets " +
139 | "together can lead to invalidated backups.");
140 | return;
141 | }
142 |
143 | if (aesKey != null) {
144 | // This is weak. We should encrypt the new seed here.
145 | informationalAlert("Wallet is encrypted",
146 | "After restore, the wallet will no longer be encrypted and you must set a new password.");
147 | }
148 |
149 | log.info("Attempting wallet restore using seed '{}' from date {}", wordsArea.getText(), datePicker.getValue());
150 | informationalAlert("Wallet restore in progress",
151 | "Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets.");
152 | overlayUI.done();
153 | Main.instance.controller.restoreFromSeedAnimation();
154 |
155 | long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
156 | DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday);
157 | // Shut down bitcoinj and restart it with the new seed.
158 | Main.bitcoin.addListener(new Service.Listener() {
159 | @Override
160 | public void terminated(Service.State from) {
161 | Main.instance.setupWalletKit(seed);
162 | Main.bitcoin.startAsync();
163 | }
164 | }, Platform::runLater);
165 | Main.bitcoin.stopAsync();
166 | }
167 |
168 |
169 | public void passwordButtonClicked(ActionEvent event) {
170 | if (aesKey == null) {
171 | Main.instance.overlayUI("wallet_set_password.fxml");
172 | } else {
173 | Main.bitcoin.wallet().decrypt(aesKey);
174 | informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings.");
175 | passwordButton.setText("Set password");
176 | aesKey = null;
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/controls/BitcoinAddressValidator.java:
--------------------------------------------------------------------------------
1 | package timestamper.controls;
2 |
3 | import org.bitcoinj.core.Address;
4 | import org.bitcoinj.core.AddressFormatException;
5 | import org.bitcoinj.core.NetworkParameters;
6 | import javafx.scene.Node;
7 | import javafx.scene.control.TextField;
8 | import timestamper.utils.TextFieldValidator;
9 |
10 | /**
11 | * Given a text field, some network params and optionally some nodes, will make the text field an angry red colour
12 | * if the address is invalid for those params, and enable/disable the nodes.
13 | */
14 | public class BitcoinAddressValidator {
15 | private NetworkParameters params;
16 | private Node[] nodes;
17 |
18 | public BitcoinAddressValidator(NetworkParameters params, TextField field, Node... nodes) {
19 | this.params = params;
20 | this.nodes = nodes;
21 |
22 | // Handle the red highlighting, but don't highlight in red just when the field is empty because that makes
23 | // the example/prompt address hard to read.
24 | new TextFieldValidator(field, text -> text.isEmpty() || testAddr(text));
25 | // However we do want the buttons to be disabled when empty so we apply a different test there.
26 | field.textProperty().addListener((observableValue, prev, current) -> {
27 | toggleButtons(current);
28 | });
29 | toggleButtons(field.getText());
30 | }
31 |
32 | private void toggleButtons(String current) {
33 | boolean valid = testAddr(current);
34 | for (Node n : nodes) n.setDisable(!valid);
35 | }
36 |
37 | private boolean testAddr(String text) {
38 | try {
39 | new Address(params, text);
40 | return true;
41 | } catch (AddressFormatException e) {
42 | return false;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/controls/ClickableBitcoinAddress.java:
--------------------------------------------------------------------------------
1 | package timestamper.controls;
2 |
3 | import org.bitcoinj.core.Address;
4 | import org.bitcoinj.uri.BitcoinURI;
5 | import de.jensd.fx.fontawesome.AwesomeDude;
6 | import de.jensd.fx.fontawesome.AwesomeIcon;
7 | import javafx.beans.binding.StringExpression;
8 | import javafx.beans.property.ObjectProperty;
9 | import javafx.beans.property.SimpleObjectProperty;
10 | import javafx.event.ActionEvent;
11 | import javafx.fxml.FXML;
12 | import javafx.fxml.FXMLLoader;
13 | import javafx.scene.control.ContextMenu;
14 | import javafx.scene.control.Label;
15 | import javafx.scene.control.Tooltip;
16 | import javafx.scene.effect.DropShadow;
17 | import javafx.scene.image.Image;
18 | import javafx.scene.image.ImageView;
19 | import javafx.scene.input.Clipboard;
20 | import javafx.scene.input.ClipboardContent;
21 | import javafx.scene.input.MouseButton;
22 | import javafx.scene.input.MouseEvent;
23 | import javafx.scene.layout.AnchorPane;
24 | import javafx.scene.layout.Pane;
25 | import net.glxn.qrgen.QRCode;
26 | import net.glxn.qrgen.image.ImageType;
27 | import timestamper.Main;
28 | import timestamper.utils.GuiUtils;
29 |
30 | import java.awt.*;
31 | import java.io.ByteArrayInputStream;
32 | import java.io.IOException;
33 | import java.net.URI;
34 |
35 | import static javafx.beans.binding.Bindings.convert;
36 |
37 | /**
38 | * A custom control that implements a clickable, copyable Bitcoin address. Clicking it opens a local wallet app. The
39 | * address looks like a blue hyperlink. Next to it there are two icons, one that copies to the clipboard and another
40 | * that shows a QRcode.
41 | */
42 | public class ClickableBitcoinAddress extends AnchorPane {
43 | @FXML protected Label addressLabel;
44 | @FXML protected ContextMenu addressMenu;
45 | @FXML protected Label copyWidget;
46 | @FXML protected Label qrCode;
47 |
48 | protected SimpleObjectProperty address = new SimpleObjectProperty<>();
49 | private final StringExpression addressStr;
50 |
51 | public ClickableBitcoinAddress() {
52 | try {
53 | FXMLLoader loader = new FXMLLoader(getClass().getResource("bitcoin_address.fxml"));
54 | loader.setRoot(this);
55 | loader.setController(this);
56 | // The following line is supposed to help Scene Builder, although it doesn't seem to be needed for me.
57 | loader.setClassLoader(getClass().getClassLoader());
58 | loader.load();
59 |
60 | AwesomeDude.setIcon(copyWidget, AwesomeIcon.COPY);
61 | Tooltip.install(copyWidget, new Tooltip("Copy address to clipboard"));
62 |
63 | AwesomeDude.setIcon(qrCode, AwesomeIcon.QRCODE);
64 | Tooltip.install(qrCode, new Tooltip("Show a barcode scannable with a mobile phone for this address"));
65 |
66 | addressStr = convert(address);
67 | addressLabel.textProperty().bind(addressStr);
68 | } catch (IOException e) {
69 | throw new RuntimeException(e);
70 | }
71 | }
72 |
73 | public String uri() {
74 | return BitcoinURI.convertToBitcoinURI(address.get(), null, Main.APP_NAME, null);
75 | }
76 |
77 | public Address getAddress() {
78 | return address.get();
79 | }
80 |
81 | public void setAddress(Address address) {
82 | this.address.set(address);
83 | }
84 |
85 | public ObjectProperty addressProperty() {
86 | return address;
87 | }
88 |
89 | @FXML
90 | protected void copyAddress(ActionEvent event) {
91 | // User clicked icon or menu item.
92 | Clipboard clipboard = Clipboard.getSystemClipboard();
93 | ClipboardContent content = new ClipboardContent();
94 | content.putString(addressStr.get());
95 | content.putHtml(String.format("%s", uri(), addressStr.get()));
96 | clipboard.setContent(content);
97 | }
98 |
99 | @FXML
100 | protected void requestMoney(MouseEvent event) {
101 | if (event.getButton() == MouseButton.SECONDARY || (event.getButton() == MouseButton.PRIMARY && event.isMetaDown())) {
102 | // User right clicked or the Mac equivalent. Show the context menu.
103 | addressMenu.show(addressLabel, event.getScreenX(), event.getScreenY());
104 | } else {
105 | // User left clicked.
106 | try {
107 | Desktop.getDesktop().browse(URI.create(uri()));
108 | } catch (IOException e) {
109 | GuiUtils.informationalAlert("Opening wallet app failed", "Perhaps you don't have one installed?");
110 | }
111 | }
112 | }
113 |
114 | @FXML
115 | protected void copyWidgetClicked(MouseEvent event) {
116 | copyAddress(null);
117 | }
118 |
119 | @FXML
120 | protected void showQRCode(MouseEvent event) {
121 | // Serialize to PNG and back into an image. Pretty lame but it's the shortest code to write and I'm feeling
122 | // lazy tonight.
123 | final byte[] imageBytes = QRCode
124 | .from(uri())
125 | .withSize(320, 240)
126 | .to(ImageType.PNG)
127 | .stream()
128 | .toByteArray();
129 | Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
130 | ImageView view = new ImageView(qrImage);
131 | view.setEffect(new DropShadow());
132 | // Embed the image in a pane to ensure the drop-shadow interacts with the fade nicely, otherwise it looks weird.
133 | // Then fix the width/height to stop it expanding to fill the parent, which would result in the image being
134 | // non-centered on the screen. Finally fade/blur it in.
135 | Pane pane = new Pane(view);
136 | pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight());
137 | final Main.OverlayUI overlay = Main.instance.overlayUI(pane, this);
138 | view.setOnMouseClicked(event1 -> overlay.done());
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/controls/NotificationBarPane.java:
--------------------------------------------------------------------------------
1 | package timestamper.controls;
2 |
3 |
4 | import javafx.animation.Interpolator;
5 | import javafx.animation.KeyFrame;
6 | import javafx.animation.KeyValue;
7 | import javafx.animation.Timeline;
8 | import javafx.beans.property.SimpleStringProperty;
9 | import javafx.beans.value.ObservableDoubleValue;
10 | import javafx.collections.FXCollections;
11 | import javafx.collections.ListChangeListener;
12 | import javafx.collections.ObservableList;
13 | import javafx.scene.Node;
14 | import javafx.scene.control.Label;
15 | import javafx.scene.control.ProgressBar;
16 | import javafx.scene.layout.BorderPane;
17 | import javafx.scene.layout.HBox;
18 | import javafx.scene.layout.Priority;
19 | import javafx.util.Duration;
20 | import timestamper.utils.GuiUtils;
21 | import timestamper.utils.easing.EasingMode;
22 | import timestamper.utils.easing.ElasticInterpolator;
23 |
24 | import javax.annotation.Nullable;
25 |
26 | /**
27 | * Wraps the given Node in a BorderPane and allows a thin bar to slide in from the bottom or top, squeezing the content
28 | * node. The API allows different "items" to be added/removed and they will be displayed one at a time, fading between
29 | * them when the topmost is removed. Each item is meant to be used for e.g. a background task and can contain a button
30 | * and/or a progress bar.
31 | */
32 | public class NotificationBarPane extends BorderPane {
33 | public static final Duration ANIM_IN_DURATION = GuiUtils.UI_ANIMATION_TIME.multiply(2);
34 | public static final Duration ANIM_OUT_DURATION = GuiUtils.UI_ANIMATION_TIME;
35 |
36 | private HBox bar;
37 | private Label label;
38 | private double barHeight;
39 | private ProgressBar progressBar;
40 |
41 | public class Item {
42 | public final SimpleStringProperty label;
43 | @Nullable public final ObservableDoubleValue progress;
44 |
45 | public Item(String label, @Nullable ObservableDoubleValue progress) {
46 | this.label = new SimpleStringProperty(label);
47 | this.progress = progress;
48 | }
49 |
50 | public void cancel() {
51 | items.remove(this);
52 | }
53 | }
54 |
55 | public final ObservableList items;
56 |
57 | public NotificationBarPane(Node content) {
58 | super(content);
59 | progressBar = new ProgressBar();
60 | label = new Label("infobar!");
61 | bar = new HBox(label);
62 | bar.setMinHeight(0.0);
63 | bar.getStyleClass().add("info-bar");
64 | bar.setFillHeight(true);
65 | setBottom(bar);
66 | // Figure out the height of the bar based on the CSS. Must wait until after we've been added to the parent node.
67 | sceneProperty().addListener(o -> {
68 | if (getParent() == null) return;
69 | getParent().applyCss();
70 | getParent().layout();
71 | barHeight = bar.getHeight();
72 | bar.setPrefHeight(0.0);
73 | });
74 | items = FXCollections.observableArrayList();
75 | items.addListener((ListChangeListener super Item>) change -> {
76 | config();
77 | showOrHide();
78 | });
79 | }
80 |
81 | private void config() {
82 | if (items.isEmpty()) return;
83 | Item item = items.get(0);
84 |
85 | bar.getChildren().clear();
86 | label.textProperty().bind(item.label);
87 | label.setMaxWidth(Double.MAX_VALUE);
88 | HBox.setHgrow(label, Priority.ALWAYS);
89 | bar.getChildren().add(label);
90 | if (item.progress != null) {
91 | progressBar.setMinWidth(200);
92 | progressBar.progressProperty().bind(item.progress);
93 | bar.getChildren().add(progressBar);
94 | }
95 | }
96 |
97 | private void showOrHide() {
98 | if (items.isEmpty())
99 | animateOut();
100 | else
101 | animateIn();
102 | }
103 |
104 | public boolean isShowing() {
105 | return bar.getPrefHeight() > 0;
106 | }
107 |
108 | private void animateIn() {
109 | animate(barHeight);
110 | }
111 |
112 | private void animateOut() {
113 | animate(0.0);
114 | }
115 |
116 | private Timeline timeline;
117 | protected void animate(Number target) {
118 | if (timeline != null) {
119 | timeline.stop();
120 | timeline = null;
121 | }
122 | Duration duration;
123 | Interpolator interpolator;
124 | if (target.intValue() > 0) {
125 | interpolator = new ElasticInterpolator(EasingMode.EASE_OUT, 1, 2);
126 | duration = ANIM_IN_DURATION;
127 | } else {
128 | interpolator = Interpolator.EASE_OUT;
129 | duration = ANIM_OUT_DURATION;
130 | }
131 | KeyFrame kf = new KeyFrame(duration, new KeyValue(bar.prefHeightProperty(), target, interpolator));
132 | timeline = new Timeline(kf);
133 | timeline.setOnFinished(x -> timeline = null);
134 | timeline.play();
135 | }
136 |
137 | public Item pushItem(String string, @Nullable ObservableDoubleValue progress) {
138 | Item i = new Item(string, progress);
139 | items.add(i);
140 | return i;
141 | }
142 | }
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/AlertWindowController.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import javafx.scene.control.Button;
4 | import javafx.scene.control.Label;
5 | import javafx.stage.Stage;
6 |
7 | public class AlertWindowController {
8 | public Label messageLabel;
9 | public Label detailsLabel;
10 | public Button okButton;
11 | public Button cancelButton;
12 | public Button actionButton;
13 |
14 | /** Initialize this alert dialog for information about a crash. */
15 | public void crashAlert(Stage stage, String crashMessage) {
16 | messageLabel.setText("Unfortunately, we screwed up and the app crashed. Sorry about that!");
17 | detailsLabel.setText(crashMessage);
18 |
19 | cancelButton.setVisible(false);
20 | actionButton.setVisible(false);
21 | okButton.setOnAction(actionEvent -> stage.close());
22 | }
23 |
24 | /** Initialize this alert for general information: OK button only, nothing happens on dismissal. */
25 | public void informational(Stage stage, String message, String details) {
26 | messageLabel.setText(message);
27 | detailsLabel.setText(details);
28 | cancelButton.setVisible(false);
29 | actionButton.setVisible(false);
30 | okButton.setOnAction(actionEvent -> stage.close());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/BitcoinUIModel.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import org.bitcoinj.core.*;
4 | import javafx.application.Platform;
5 | import javafx.beans.property.ReadOnlyDoubleProperty;
6 | import javafx.beans.property.ReadOnlyObjectProperty;
7 | import javafx.beans.property.SimpleDoubleProperty;
8 | import javafx.beans.property.SimpleObjectProperty;
9 |
10 | import java.util.Date;
11 |
12 | /**
13 | * A class that exposes relevant bitcoin stuff as JavaFX bindable properties.
14 | */
15 | public class BitcoinUIModel {
16 | private SimpleObjectProperty address = new SimpleObjectProperty<>();
17 | private SimpleObjectProperty balance = new SimpleObjectProperty<>(Coin.ZERO);
18 | private SimpleDoubleProperty syncProgress = new SimpleDoubleProperty(-1);
19 | private ProgressBarUpdater syncProgressUpdater = new ProgressBarUpdater();
20 |
21 | public BitcoinUIModel() {
22 | }
23 |
24 | public BitcoinUIModel(Wallet wallet) {
25 | setWallet(wallet);
26 | }
27 |
28 | public void setWallet(Wallet wallet) {
29 | wallet.addEventListener(new AbstractWalletEventListener() {
30 | @Override
31 | public void onWalletChanged(Wallet wallet) {
32 | super.onWalletChanged(wallet);
33 | update(wallet);
34 | }
35 | }, Platform::runLater);
36 | update(wallet);
37 | }
38 |
39 | private void update(Wallet wallet) {
40 | balance.set(wallet.getBalance());
41 | address.set(wallet.currentReceiveAddress());
42 | }
43 |
44 | private class ProgressBarUpdater extends DownloadProgressTracker {
45 | @Override
46 | protected void progress(double pct, int blocksLeft, Date date) {
47 | super.progress(pct, blocksLeft, date);
48 | Platform.runLater(() -> syncProgress.set(pct / 100.0));
49 | }
50 |
51 | @Override
52 | protected void doneDownload() {
53 | super.doneDownload();
54 | Platform.runLater(() -> syncProgress.set(1.0));
55 | }
56 | }
57 |
58 | public DownloadProgressTracker getDownloadProgressTracker() { return syncProgressUpdater; }
59 |
60 | public ReadOnlyDoubleProperty syncProgressProperty() { return syncProgress; }
61 |
62 | public ReadOnlyObjectProperty addressProperty() {
63 | return address;
64 | }
65 |
66 | public ReadOnlyObjectProperty balanceProperty() {
67 | return balance;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/GuiUtils.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import com.google.common.base.Throwables;
4 | import javafx.animation.*;
5 | import javafx.application.Platform;
6 | import javafx.fxml.FXMLLoader;
7 | import javafx.scene.Node;
8 | import javafx.scene.Scene;
9 | import javafx.scene.effect.GaussianBlur;
10 | import javafx.scene.layout.Pane;
11 | import javafx.stage.Modality;
12 | import javafx.stage.Stage;
13 | import javafx.util.Duration;
14 | import timestamper.MainController;
15 |
16 | import java.io.IOException;
17 | import java.net.URL;
18 | import java.util.function.BiConsumer;
19 |
20 | import static com.google.common.base.Preconditions.checkState;
21 | import static timestamper.utils.WTUtils.unchecked;
22 |
23 | public class GuiUtils {
24 | public static void runAlert(BiConsumer setup) {
25 | try {
26 | // JavaFX2 doesn't actually have a standard alert template. Instead the Scene Builder app will create FXML
27 | // files for an alert window for you, and then you customise it as you see fit. I guess it makes sense in
28 | // an odd sort of way.
29 | Stage dialogStage = new Stage();
30 | dialogStage.initModality(Modality.APPLICATION_MODAL);
31 | FXMLLoader loader = new FXMLLoader(GuiUtils.class.getResource("alert.fxml"));
32 | Pane pane = loader.load();
33 | AlertWindowController controller = loader.getController();
34 | setup.accept(dialogStage, controller);
35 | dialogStage.setScene(new Scene(pane));
36 | dialogStage.showAndWait();
37 | } catch (IOException e) {
38 | // We crashed whilst trying to show the alert dialog (this should never happen). Give up!
39 | throw new RuntimeException(e);
40 | }
41 | }
42 |
43 | public static void crashAlert(Throwable t) {
44 | t.printStackTrace();
45 | Throwable rootCause = Throwables.getRootCause(t);
46 | Runnable r = () -> {
47 | runAlert((stage, controller) -> controller.crashAlert(stage, rootCause.toString()));
48 | Platform.exit();
49 | };
50 | if (Platform.isFxApplicationThread())
51 | r.run();
52 | else
53 | Platform.runLater(r);
54 | }
55 |
56 | /** Show a GUI alert box for any unhandled exceptions that propagate out of this thread. */
57 | public static void handleCrashesOnThisThread() {
58 | Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> {
59 | GuiUtils.crashAlert(Throwables.getRootCause(exception));
60 | });
61 | }
62 |
63 | public static void informationalAlert(String message, String details, Object... args) {
64 | String formattedDetails = String.format(details, args);
65 | Runnable r = () -> runAlert((stage, controller) -> controller.informational(stage, message, formattedDetails));
66 | if (Platform.isFxApplicationThread())
67 | r.run();
68 | else
69 | Platform.runLater(r);
70 | }
71 |
72 | public static final int UI_ANIMATION_TIME_MSEC = 600;
73 | public static final Duration UI_ANIMATION_TIME = Duration.millis(UI_ANIMATION_TIME_MSEC);
74 |
75 | public static Animation fadeIn(Node ui) {
76 | return fadeIn(ui, 0);
77 | }
78 |
79 | public static Animation fadeIn(Node ui, int delayMillis) {
80 | ui.setCache(true);
81 | FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui);
82 | ft.setFromValue(0.0);
83 | ft.setToValue(1.0);
84 | ft.setOnFinished(ev -> ui.setCache(false));
85 | ft.setDelay(Duration.millis(delayMillis));
86 | ft.play();
87 | return ft;
88 | }
89 |
90 | public static Animation fadeOut(Node ui) {
91 | FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui);
92 | ft.setFromValue(ui.getOpacity());
93 | ft.setToValue(0.0);
94 | ft.play();
95 | return ft;
96 | }
97 |
98 | public static Animation fadeOutAndRemove(Pane parentPane, Node... nodes) {
99 | Animation animation = fadeOut(nodes[0]);
100 | animation.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
101 | return animation;
102 | }
103 |
104 | public static Animation fadeOutAndRemove(Duration duration, Pane parentPane, Node... nodes) {
105 | nodes[0].setCache(true);
106 | FadeTransition ft = new FadeTransition(duration, nodes[0]);
107 | ft.setFromValue(nodes[0].getOpacity());
108 | ft.setToValue(0.0);
109 | ft.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
110 | ft.play();
111 | return ft;
112 | }
113 |
114 | public static void blurOut(Node node) {
115 | GaussianBlur blur = new GaussianBlur(0.0);
116 | node.setEffect(blur);
117 | Timeline timeline = new Timeline();
118 | KeyValue kv = new KeyValue(blur.radiusProperty(), 10.0);
119 | KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), kv);
120 | timeline.getKeyFrames().add(kf);
121 | timeline.play();
122 | }
123 |
124 | public static void blurIn(Node node) {
125 | GaussianBlur blur = (GaussianBlur) node.getEffect();
126 | Timeline timeline = new Timeline();
127 | KeyValue kv = new KeyValue(blur.radiusProperty(), 0.0);
128 | KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), kv);
129 | timeline.getKeyFrames().add(kf);
130 | timeline.setOnFinished(actionEvent -> node.setEffect(null));
131 | timeline.play();
132 | }
133 |
134 | public static ScaleTransition zoomIn(Node node) {
135 | return zoomIn(node, 0);
136 | }
137 |
138 | public static ScaleTransition zoomIn(Node node, int delayMillis) {
139 | return scaleFromTo(node, 0.95, 1.0, delayMillis);
140 | }
141 |
142 | public static ScaleTransition explodeOut(Node node) {
143 | return scaleFromTo(node, 1.0, 1.05, 0);
144 | }
145 |
146 | private static ScaleTransition scaleFromTo(Node node, double from, double to, int delayMillis) {
147 | ScaleTransition scale = new ScaleTransition(Duration.millis(UI_ANIMATION_TIME_MSEC / 2), node);
148 | scale.setFromX(from);
149 | scale.setFromY(from);
150 | scale.setToX(to);
151 | scale.setToY(to);
152 | scale.setDelay(Duration.millis(delayMillis));
153 | scale.play();
154 | return scale;
155 | }
156 |
157 | /**
158 | * A useful helper for development purposes. Used as a switch for loading files from local disk, allowing live
159 | * editing whilst the app runs without rebuilds.
160 | */
161 | public static URL getResource(String name) {
162 | if (false)
163 | return unchecked(() -> new URL("file:///your/path/here/src/main/timestamper/" + name));
164 | else
165 | return MainController.class.getResource(name);
166 | }
167 |
168 | public static void checkGuiThread() {
169 | checkState(Platform.isFxApplicationThread());
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/KeyDerivationTasks.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import org.bitcoinj.crypto.KeyCrypterScrypt;
4 | import com.google.common.util.concurrent.Uninterruptibles;
5 | import javafx.beans.property.ReadOnlyDoubleProperty;
6 | import javafx.concurrent.Task;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.spongycastle.crypto.params.KeyParameter;
10 |
11 | import javax.annotation.*;
12 | import java.time.Duration;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import static timestamper.utils.GuiUtils.checkGuiThread;
16 |
17 | /**
18 | * Background tasks for pumping a progress meter and deriving an AES key using scrypt.
19 | */
20 | public class KeyDerivationTasks {
21 | private static final Logger log = LoggerFactory.getLogger(KeyDerivationTasks.class);
22 |
23 | public final Task keyDerivationTask;
24 | public final ReadOnlyDoubleProperty progress;
25 |
26 | private final Task progressTask;
27 |
28 | private volatile int timeTakenMsec = -1;
29 |
30 | public KeyDerivationTasks(KeyCrypterScrypt scrypt, String password, @Nullable Duration targetTime) {
31 | keyDerivationTask = new Task() {
32 | @Override
33 | protected KeyParameter call() throws Exception {
34 | long start = System.currentTimeMillis();
35 | try {
36 | log.info("Started key derivation");
37 | KeyParameter result = scrypt.deriveKey(password);
38 | timeTakenMsec = (int) (System.currentTimeMillis() - start);
39 | log.info("Key derivation done in {}ms", timeTakenMsec);
40 | return result;
41 | } catch (Throwable e) {
42 | log.error("Exception during key derivation", e);
43 | throw e;
44 | }
45 | }
46 | };
47 |
48 | // And the fake progress meter ... if the vals were calculated correctly progress bar should reach 100%
49 | // a brief moment after the keys were derived successfully.
50 | progressTask = new Task() {
51 | private KeyParameter aesKey;
52 |
53 | @Override
54 | protected Void call() throws Exception {
55 | if (targetTime != null) {
56 | long startTime = System.currentTimeMillis();
57 | long curTime;
58 | long targetTimeMillis = targetTime.toMillis();
59 | while ((curTime = System.currentTimeMillis()) < startTime + targetTimeMillis) {
60 | double progress = (curTime - startTime) / (double) targetTimeMillis;
61 | updateProgress(progress, 1.0);
62 |
63 | // 60fps would require 16msec sleep here.
64 | Uninterruptibles.sleepUninterruptibly(20, TimeUnit.MILLISECONDS);
65 | }
66 | // Wait for the encryption thread before switching back to main UI.
67 | updateProgress(1.0, 1.0);
68 | } else {
69 | updateProgress(-1, -1);
70 | }
71 | aesKey = keyDerivationTask.get();
72 | return null;
73 | }
74 |
75 | @Override
76 | protected void succeeded() {
77 | checkGuiThread();
78 | onFinish(aesKey, timeTakenMsec);
79 | }
80 | };
81 | progress = progressTask.progressProperty();
82 | }
83 |
84 | public void start() {
85 | new Thread(keyDerivationTask, "Key derivation").start();
86 | new Thread(progressTask, "Progress ticker").start();
87 | }
88 |
89 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) {
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/TextFieldValidator.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import javafx.beans.property.BooleanProperty;
4 | import javafx.beans.property.SimpleBooleanProperty;
5 | import javafx.scene.Scene;
6 | import javafx.scene.control.TextInputControl;
7 |
8 | import java.util.function.Predicate;
9 |
10 | public class TextFieldValidator {
11 | public final BooleanProperty valid = new SimpleBooleanProperty(false);
12 |
13 | public TextFieldValidator(TextInputControl control, Predicate validator) {
14 | this.valid.set(validator.test(control.getText()));
15 | apply(control, valid.get());
16 | control.textProperty().addListener((observableValue, prev, current) -> {
17 | boolean nowValid = validator.test(current);
18 | if (nowValid == valid.get()) return;
19 | valid.set(nowValid);
20 | });
21 | valid.addListener(o -> apply(control, valid.get()));
22 | }
23 |
24 | private static void apply(TextInputControl textField, boolean nowValid) {
25 | if (nowValid) {
26 | textField.getStyleClass().remove("validation_error");
27 | } else {
28 | textField.getStyleClass().add("validation_error");
29 | }
30 | }
31 |
32 | public static void configureScene(Scene scene) {
33 | final String file = TextFieldValidator.class.getResource("text-validation.css").toString();
34 | scene.getStylesheets().add(file);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/ThrottledRunLater.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package timestamper.utils;
16 |
17 | import javafx.application.Platform;
18 |
19 | import java.util.concurrent.atomic.AtomicBoolean;
20 |
21 | /**
22 | * A simple wrapper around {@link javafx.application.Platform#runLater(Runnable)} which will do nothing if the previous
23 | * invocation of runLater didn't execute on the JavaFX UI thread yet. In this way you can avoid flooding
24 | * the event loop if you have a background thread that for whatever reason wants to update the UI very
25 | * frequently. Without this class you could end up bloating up memory usage and causing the UI to stutter
26 | * if the UI thread couldn't keep up with your background worker.
27 | */
28 | public class ThrottledRunLater implements Runnable {
29 | private final Runnable runnable;
30 | private final AtomicBoolean pending = new AtomicBoolean();
31 |
32 | /** Created this way, the no-args runLater will execute this classes run method. */
33 | public ThrottledRunLater() {
34 | this.runnable = null;
35 | }
36 |
37 | /** Created this way, the no-args runLater will execute the given runnable. */
38 | public ThrottledRunLater(Runnable runnable) {
39 | this.runnable = runnable;
40 | }
41 |
42 | public void runLater(Runnable runnable) {
43 | if (!pending.getAndSet(true)) {
44 | Platform.runLater(() -> {
45 | pending.set(false);
46 | runnable.run();
47 | });
48 | }
49 | }
50 |
51 | public void runLater() {
52 | runLater(runnable != null ? runnable : this);
53 | }
54 |
55 | @Override
56 | public void run() {
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/WTUtils.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | /**
7 | * Some generic utilities to make Java a bit less annoying.
8 | */
9 | public class WTUtils {
10 | private static final Logger log = LoggerFactory.getLogger(WTUtils.class);
11 |
12 | public interface UncheckedRun {
13 | public T run() throws Throwable;
14 | }
15 |
16 | public interface UncheckedRunnable {
17 | public void run() throws Throwable;
18 | }
19 |
20 | public static T unchecked(UncheckedRun run) {
21 | try {
22 | return run.run();
23 | } catch (Throwable throwable) {
24 | throw new RuntimeException(throwable);
25 | }
26 | }
27 |
28 | public static void uncheck(UncheckedRunnable run) {
29 | try {
30 | run.run();
31 | } catch (Throwable throwable) {
32 | throw new RuntimeException(throwable);
33 | }
34 | }
35 |
36 | public static void ignoreAndLog(UncheckedRunnable runnable) {
37 | try {
38 | runnable.run();
39 | } catch (Throwable t) {
40 | log.error("Ignoring error", t);
41 | }
42 | }
43 |
44 | public static T ignoredAndLogged(UncheckedRun runnable) {
45 | try {
46 | return runnable.run();
47 | } catch (Throwable t) {
48 | log.error("Ignoring error", t);
49 | return null;
50 | }
51 | }
52 |
53 | public static boolean didThrow(UncheckedRun run) {
54 | try {
55 | run.run();
56 | return false;
57 | } catch (Throwable throwable) {
58 | return true;
59 | }
60 | }
61 |
62 | public static boolean didThrow(UncheckedRunnable run) {
63 | try {
64 | run.run();
65 | return false;
66 | } catch (Throwable throwable) {
67 | return true;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/easing/EasingInterpolator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2013, Christian Schudt
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package timestamper.utils.easing;
26 |
27 | import javafx.animation.Interpolator;
28 | import javafx.beans.property.ObjectProperty;
29 | import javafx.beans.property.SimpleObjectProperty;
30 |
31 | /**
32 | * The abstract base class for all easing interpolators.
33 | *
34 | * @author Christian Schudt
35 | */
36 | public abstract class EasingInterpolator extends Interpolator {
37 |
38 | /**
39 | * The easing mode.
40 | */
41 | private ObjectProperty easingMode = new SimpleObjectProperty<>(EasingMode.EASE_OUT);
42 |
43 | /**
44 | * Constructs the interpolator with a specific easing mode.
45 | *
46 | * @param easingMode The easing mode.
47 | */
48 | public EasingInterpolator(EasingMode easingMode) {
49 | this.easingMode.set(easingMode);
50 | }
51 |
52 | /**
53 | * The easing mode property.
54 | *
55 | * @return The property.
56 | * @see #getEasingMode()
57 | * @see #setEasingMode(EasingMode)
58 | */
59 | public ObjectProperty easingModeProperty() {
60 | return easingMode;
61 | }
62 |
63 | /**
64 | * Gets the easing mode.
65 | *
66 | * @return The easing mode.
67 | * @see #easingModeProperty()
68 | */
69 | public EasingMode getEasingMode() {
70 | return easingMode.get();
71 | }
72 |
73 | /**
74 | * Sets the easing mode.
75 | *
76 | * @param easingMode The easing mode.
77 | * @see #easingModeProperty()
78 | */
79 | public void setEasingMode(EasingMode easingMode) {
80 | this.easingMode.set(easingMode);
81 | }
82 |
83 | /**
84 | * Defines the base curve for the interpolator.
85 | * The base curve is then transformed into an easing-in, easing-out easing-both curve.
86 | *
87 | * @param v The normalized value/time/progress of the interpolation (between 0 and 1).
88 | * @return The resulting value of the function, should return a value between 0 and 1.
89 | * @see Interpolator#curve(double)
90 | */
91 | protected abstract double baseCurve(final double v);
92 |
93 | /**
94 | * Curves the function depending on the easing mode.
95 | *
96 | * @param v The normalized value (between 0 and 1).
97 | * @return The resulting value of the function.
98 | */
99 | @Override
100 | protected final double curve(final double v) {
101 | switch (easingMode.get()) {
102 | case EASE_IN:
103 | return baseCurve(v);
104 | case EASE_OUT:
105 | return 1 - baseCurve(1 - v);
106 | case EASE_BOTH:
107 | if (v <= 0.5) {
108 | return baseCurve(2 * v) / 2;
109 | } else {
110 | return (2 - baseCurve(2 * (1 - v))) / 2;
111 | }
112 |
113 | }
114 | return baseCurve(v);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/easing/EasingMode.java:
--------------------------------------------------------------------------------
1 | package timestamper.utils.easing;
2 |
3 | /**
4 | * Defines the three easing modes, ease-in, ease-out and ease-both.
5 | *
6 | * @author Christian Schudt
7 | */
8 | public enum EasingMode {
9 | EASE_IN,
10 | EASE_OUT,
11 | EASE_BOTH
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/timestamper/utils/easing/ElasticInterpolator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2013, Christian Schudt
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | package timestamper.utils.easing;
26 |
27 | import javafx.beans.property.DoubleProperty;
28 | import javafx.beans.property.SimpleDoubleProperty;
29 |
30 | /**
31 | * This interpolator simulates an elastic behavior.
32 | *
33 | * The following curve illustrates the interpolation.
34 | *