3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 07:12:17 +00:00

Wallet template: various updates.

Backport misc improvements from PayFile.
Refactor the clickable address out into a custom widget.
Use FontAwesome and the wrapper class for icons instead of a custom image.
QRcode support.
This commit is contained in:
Mike Hearn 2013-10-26 02:28:44 +02:00
parent 6ec7880079
commit 387717c6c5
11 changed files with 280 additions and 106 deletions

View File

@ -306,7 +306,8 @@ public class BitcoinURI {
* @param message A message
* @return A String containing the Bitcoin URI
*/
public static String convertToBitcoinURI(String address, BigInteger amount, String label, String message) {
public static String convertToBitcoinURI(String address, @Nullable BigInteger amount, @Nullable String label,
@Nullable String message) {
checkNotNull(address);
if (amount != null && amount.compareTo(BigInteger.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be positive");

View File

@ -44,5 +44,15 @@
<artifactId>aquafx</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>de.jensd</groupId>
<artifactId>fontawesomefx</artifactId>
<version>8.0.0</version>
</dependency>
<dependency>
<groupId>net.glxn</groupId>
<artifactId>qrgen</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
</project>

View File

@ -1,27 +1,22 @@
package wallettemplate;
import com.google.bitcoin.core.*;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.core.AbstractWalletEventListener;
import com.google.bitcoin.core.DownloadListener;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.*;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import wallettemplate.utils.GuiUtils;
import wallettemplate.controls.ClickableBitcoinAddress;
import java.awt.*;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.util.Date;
import static com.google.common.base.Preconditions.checkState;
@ -36,66 +31,23 @@ public class Controller {
public ProgressBar syncProgress;
public VBox syncBox;
public HBox controlsBox;
public Label requestMoneyLink;
public Label balance;
public ContextMenu addressMenu;
public HBox addressLabelBox;
public Button sendMoneyOutBtn;
public ImageView copyWidget;
private Address primaryAddress;
public ClickableBitcoinAddress addressControl;
// Called by FXMLLoader.
public void initialize() {
syncProgress.setProgress(-1);
addressLabelBox.setOpacity(0.0);
Tooltip tooltip = new Tooltip("Copy address to clipboard");
Tooltip.install(copyWidget, tooltip);
addressControl.setOpacity(0.0);
}
public void onBitcoinSetup() {
bitcoin.wallet().addEventListener(new BalanceUpdater());
primaryAddress = bitcoin.wallet().getKeys().get(0).toAddress(Main.params);
addressControl.setAddress(bitcoin.wallet().getKeys().get(0).toAddress(Main.params).toString());
refreshBalanceLabel();
}
public void requestMoney(MouseEvent event) {
// User clicked on the address.
if (event.getButton() == MouseButton.SECONDARY || (event.getButton() == MouseButton.PRIMARY && event.isMetaDown())) {
addressMenu.show(requestMoneyLink, event.getScreenX(), event.getScreenY());
} else {
String uri = getURI();
System.out.println("Opening " + uri);
try {
Desktop.getDesktop().browse(URI.create(uri));
} catch (IOException e) {
// Couldn't open wallet app.
GuiUtils.informationalAlert("Opening wallet app failed", "Perhaps you don't have one installed?");
}
}
}
private String getURI() {
return BitcoinURI.convertToBitcoinURI(getAddress(), Utils.COIN, Main.APP_NAME, null);
}
private String getAddress() {
return primaryAddress.toString();
}
public void copyWidgetClicked(MouseEvent event) {
copyAddress(null);
}
public void copyAddress(ActionEvent event) {
// User clicked icon or menu item.
Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putString(getAddress());
content.putHtml(String.format("<a href='%s'>%s</a>", getURI(), getAddress()));
clipboard.setContent(content);
}
public void sendMoneyOut(ActionEvent event) {
// Hide this UI and show the send money UI. This UI won't be clickable until the user dismisses send_money.
Main.instance.overlayUI("send_money.fxml");
@ -122,8 +74,7 @@ public class Controller {
// Buttons slide in and clickable address appears simultaneously.
TranslateTransition arrive = new TranslateTransition(Duration.millis(600), controlsBox);
arrive.setToY(0.0);
requestMoneyLink.setText(primaryAddress.toString());
FadeTransition reveal = new FadeTransition(Duration.millis(500), addressLabelBox);
FadeTransition reveal = new FadeTransition(Duration.millis(500), addressControl);
reveal.setToValue(1.0);
ParallelTransition group = new ParallelTransition(arrive, reveal);
// Slide out happens then slide in/fade happens.

View File

@ -24,6 +24,8 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import static wallettemplate.utils.GuiUtils.*;
public class Main extends Application {
public static String APP_NAME = "WalletTemplate";
@ -37,6 +39,7 @@ public class Main extends Application {
@Override
public void start(Stage mainWindow) throws Exception {
instance = this;
// Show the crash dialog for any exceptions that we don't handle and that hit the main loop.
GuiUtils.handleCrashesOnThisThread();
try {
init(mainWindow);
@ -57,7 +60,7 @@ public class Main extends Application {
// Load the GUI. The Controller class will be automagically created and wired up.
URL location = getClass().getResource("main.fxml");
FXMLLoader loader = new FXMLLoader(location);
mainUI = (Pane) loader.load();
mainUI = loader.load();
Controller controller = loader.getController();
// Configure the window with a StackPane so we can overlay things on top of the main UI.
uiStack = new StackPane(mainUI);
@ -89,6 +92,7 @@ public class Main extends Application {
// or progress widget to keep the user engaged whilst we initialise, but we don't.
bitcoin.setDownloadListener(controller.progressBarUpdater())
.setBlockingStartup(false)
.setUserAgent(APP_NAME, "1.0")
.startAndWait();
// Don't make the user wait for confirmations for now, as the intention is they're sending it their own money!
bitcoin.wallet().allowSpendingUnconfirmedTransactions();
@ -97,40 +101,58 @@ public class Main extends Application {
mainWindow.show();
}
public class OverlayUI {
Node ui;
Object controller;
public class OverlayUI<T> {
public Node ui;
public T controller;
public OverlayUI(Node ui, Object controller) {
public OverlayUI(Node ui, T controller) {
this.ui = ui;
this.controller = controller;
}
public void show() {
blurOut(mainUI);
uiStack.getChildren().add(ui);
fadeIn(ui);
}
public void done() {
GuiUtils.fadeOutAndRemove(ui, uiStack);
GuiUtils.blurIn(mainUI);
checkGuiThread();
fadeOutAndRemove(ui, uiStack);
blurIn(mainUI);
this.ui = null;
this.controller = null;
}
}
/** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */
public OverlayUI overlayUI(String name) {
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<T>(node, controller);
// Auto-magically set the overlayUi member, if it's there.
try {
controller.getClass().getDeclaredField("overlayUi").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
}
pair.show();
return pair;
}
/** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */
public <T> OverlayUI<T> overlayUI(String name) {
try {
checkGuiThread();
// Load the UI from disk.
URL location = getClass().getResource(name);
FXMLLoader loader = new FXMLLoader(location);
Pane ui = (Pane) loader.load();
Object controller = loader.getController();
OverlayUI pair = new OverlayUI(ui, controller);
Pane ui = loader.load();
T controller = loader.getController();
OverlayUI<T> pair = new OverlayUI<T>(ui, controller);
// Auto-magically set the overlayUi member, if it's there.
try {
controller.getClass().getDeclaredField("overlayUi").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
}
GuiUtils.blurOut(mainUI);
uiStack.getChildren().add(ui);
GuiUtils.fadeIn(ui);
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.

View File

@ -11,6 +11,7 @@ import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import wallettemplate.controls.BitcoinAddressValidator;
import wallettemplate.utils.GuiUtils;
public class SendMoneyController {

View File

@ -1,4 +1,4 @@
package wallettemplate;
package wallettemplate.controls;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.AddressFormatException;

View File

@ -0,0 +1,144 @@
package wallettemplate.controls;
import com.google.bitcoin.uri.BitcoinURI;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import net.glxn.qrgen.QRCode;
import net.glxn.qrgen.image.ImageType;
import wallettemplate.Main;
import wallettemplate.utils.GuiUtils;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
// This control can be used with Scene Builder as long as we don't use any Java 8 features yet. Once Oracle release
// a new Scene Builder compiled against Java 8, we'll be able to use lambdas and so on here. Until that day comes,
// this file specifically must be recompiled against Java 7 for main.fxml to be editable visually.
//
// From the java directory:
//
// javac -cp $HOME/.m2/repository/net/glxn/qrgen/1.3/qrgen-1.3.jar:$HOME/.m2/repository/de/jensd/fontawesomefx/8.0.0/fontawesomefx-8.0.0.jar:../../../target/classes:../../../../core/target/bitcoinj-0.11-SNAPSHOT.jar -d ../../../target/classes/ -source 1.7 -target 1.7 wallettemplate/controls/ClickableBitcoinAddress.java
/**
* A custom control that implements a clickable, copyable Bitcoin address. Clicking it opens a local wallet app. The
* address looks like a blue hyperlink. Next to it there are two icons, one that copies to the clipboard and another
* that shows a QRcode.
*/
public class ClickableBitcoinAddress extends AnchorPane {
@FXML protected Label addressLabel;
@FXML protected ContextMenu addressMenu;
@FXML protected Label copyWidget;
@FXML protected Label qrCode;
public ClickableBitcoinAddress() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("bitcoin_address.fxml"));
loader.setRoot(this);
loader.setController(this);
// The following line is supposed to help Scene Builder, although it doesn't seem to be needed for me.
loader.setClassLoader(getClass().getClassLoader());
loader.load();
AwesomeDude.setIcon(copyWidget, AwesomeIcon.COPY);
Tooltip.install(copyWidget, new Tooltip("Copy address to clipboard"));
AwesomeDude.setIcon(qrCode, AwesomeIcon.QRCODE);
Tooltip.install(qrCode, new Tooltip("Show a barcode scannable with a mobile phone for this address"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String uri() {
return BitcoinURI.convertToBitcoinURI(getAddress(), null, Main.APP_NAME, null);
}
public String getAddress() {
return addressLabel.getText();
}
public void setAddress(String address) {
addressLabel.setText(address);
}
public StringProperty addressProperty() {
return addressLabel.textProperty();
}
@FXML
protected void copyAddress(ActionEvent event) {
// User clicked icon or menu item.
Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putString(getAddress());
content.putHtml(String.format("<a href='%s'>%s</a>", uri(), getAddress()));
clipboard.setContent(content);
}
@FXML
protected void requestMoney(MouseEvent event) {
if (event.getButton() == MouseButton.SECONDARY || (event.getButton() == MouseButton.PRIMARY && event.isMetaDown())) {
// User right clicked or the Mac equivalent. Show the context menu.
addressMenu.show(addressLabel, event.getScreenX(), event.getScreenY());
} else {
// User left clicked.
try {
Desktop.getDesktop().browse(URI.create(uri()));
} catch (IOException e) {
GuiUtils.informationalAlert("Opening wallet app failed", "Perhaps you don't have one installed?");
}
}
}
@FXML
protected void copyWidgetClicked(MouseEvent event) {
copyAddress(null);
}
@FXML
protected void showQRCode(MouseEvent event) {
// Serialize to PNG and back into an image. Pretty lame but it's the shortest code to write and I'm feeling
// lazy tonight.
final byte[] imageBytes = QRCode
.from(uri())
.withSize(320, 240)
.to(ImageType.PNG)
.stream()
.toByteArray();
Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
ImageView view = new ImageView(qrImage);
view.setEffect(new DropShadow());
// Embed the image in a pane to ensure the drop-shadow interacts with the fade nicely, otherwise it looks weird.
// Then fix the width/height to stop it expanding to fill the parent, which would result in the image being
// non-centered on the screen. Finally fade/blur it in.
Pane pane = new Pane(view);
pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight());
final Main.OverlayUI<ClickableBitcoinAddress> overlay = Main.instance.overlayUI(pane, this);
view.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
overlay.done();
}
});
}
}

View File

@ -15,6 +15,8 @@ import javafx.util.Duration;
import java.io.IOException;
import java.util.function.BiConsumer;
import static com.google.common.base.Preconditions.checkState;
public class GuiUtils {
private static void runAlert(BiConsumer<Stage, AlertWindowController> setup) {
try {
@ -24,8 +26,8 @@ public class GuiUtils {
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.WINDOW_MODAL);
FXMLLoader loader = new FXMLLoader(GuiUtils.class.getResource("alert.fxml"));
Pane pane = (Pane) loader.load();
AlertWindowController controller = (AlertWindowController) loader.getController();
Pane pane = loader.load();
AlertWindowController controller = loader.getController();
setup.accept(dialogStage, controller);
dialogStage.setScene(new Scene(pane));
dialogStage.showAndWait();
@ -52,8 +54,9 @@ public class GuiUtils {
});
}
public static void informationalAlert(String message, String details) {
Runnable r = () -> runAlert((stage, controller) -> controller.informational(stage, message, details));
public static void informationalAlert(String message, String details, Object... args) {
String formattedDetails = String.format(details, args);
Runnable r = () -> runAlert((stage, controller) -> controller.informational(stage, message, formattedDetails));
if (Platform.isFxApplicationThread())
r.run();
else
@ -102,4 +105,8 @@ public class GuiUtils {
timeline.setOnFinished(actionEvent -> node.setEffect(null));
timeline.play();
}
public static void checkGuiThread() {
checkState(Platform.isFxApplicationThread());
}
}

View File

@ -0,0 +1,58 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package wallettemplate.utils;
import javafx.application.Platform;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A simple wrapper around {@link javafx.application.Platform#runLater(Runnable)} which will do nothing if the previous
* invocation of runLater didn't execute on the JavaFX UI thread yet. In this way you can avoid flooding
* the event loop if you have a background thread that for whatever reason wants to update the UI very
* frequently. Without this class you could end up bloating up memory usage and causing the UI to stutter
* if the UI thread couldn't keep up with your background worker.
*/
public class ThrottledRunLater implements Runnable {
private final Runnable runnable;
private final AtomicBoolean pending = new AtomicBoolean();
/** Created this way, the no-args runLater will execute this classes run method. */
public ThrottledRunLater() {
this.runnable = null;
}
/** Created this way, the no-args runLater will execute the given runnable. */
public ThrottledRunLater(Runnable runnable) {
this.runnable = runnable;
}
public void runLater(Runnable runnable) {
if (!pending.getAndSet(true)) {
Platform.runLater(() -> {
pending.set(false);
runnable.run();
});
}
}
public void runLater() {
runLater(runnable != null ? runnable : this);
}
@Override
public void run() {
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 B

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?scenebuilder-classpath-element ../../../../target/classes?>
<?scenebuilder-classpath-element ../../../../../core/target/bitcoinj-0.11-SNAPSHOT.jar?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.geometry.*?>
@ -10,6 +12,8 @@
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<?import wallettemplate.controls.*?>
<?import wallettemplate.controls.ClickableBitcoinAddress ?>
<AnchorPane id="AnchorPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="200.0" minWidth="300.0" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="wallettemplate.Controller">
<children>
@ -59,31 +63,7 @@
<Image url="@bitcoin_logo_plain.png" />
</image>
</ImageView>
<HBox fx:id="addressLabelBox" alignment="CENTER_LEFT" layoutY="45.0" prefHeight="21.0" prefWidth="391.0" spacing="10.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="195.0">
<children>
<Label fx:id="requestMoneyLink" onMouseClicked="#requestMoney" style="-fx-cursor: hand" text="&lt;address goes here&gt;" textFill="BLUE" underline="true">
<contextMenu>
<ContextMenu fx:id="addressMenu">
<items>
<MenuItem mnemonicParsing="false" onAction="#copyAddress" text="Copy to clipboard">
<accelerator>
<KeyCodeCombination alt="UP" code="C" control="DOWN" meta="UP" shift="UP" shortcut="UP" />
</accelerator>
</MenuItem>
</items>
</ContextMenu>
</contextMenu>
</Label>
<ImageView fx:id="copyWidget" fitHeight="16.0" fitWidth="16.0" focusTraversable="true" onMouseClicked="#copyWidgetClicked" pickOnBounds="true" preserveRatio="true" smooth="true">
<image>
<Image url="@copy-icon.png" />
</image>
<HBox.margin>
<Insets />
</HBox.margin>
</ImageView>
</children>
</HBox>
<ClickableBitcoinAddress fx:id="addressControl" layoutY="45.0" prefHeight="21.0" prefWidth="391.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="195.0" />
<StackPane id="connectionsListView" layoutX="14.0" layoutY="81.0" prefHeight="249.0" prefWidth="572.0">
<children>
<Label text="Your content goes here" />