walletfx: move OverlayUI from Main to MainController

Move OverlayUI and other related functionality from Main (Application) class
to MainController.

Motivation:

1. This simplifies the Main class
2. The code more logically belongs in the controller
3. The code being in the controller increases reusability
4. Is a first step towards additional refactoring made possible
   because MainController can subclass an abstract class and Main
   can’t because it must subclass Application
This commit is contained in:
Sean Gilligan 2021-09-18 19:22:38 -07:00 committed by Andreas Schildbach
parent 1e7fc7aad5
commit 17aeea2d75
7 changed files with 144 additions and 125 deletions

View file

@ -30,21 +30,17 @@ import org.bitcoinj.wallet.DeterministicSeed;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage; import javafx.stage.Stage;
import wallettemplate.controls.NotificationBarPane;
import org.bitcoinj.walletfx.utils.GuiUtils; import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.utils.TextFieldValidator;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.*; import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert;
public class Main extends Application { public class Main extends Application {
public static NetworkParameters params = TestNet3Params.get(); public static NetworkParameters params = TestNet3Params.get();
@ -56,11 +52,7 @@ public class Main extends Application {
public static WalletAppKit bitcoin; public static WalletAppKit bitcoin;
public static Main instance; public static Main instance;
private StackPane uiStack;
private Pane mainUI;
public MainController controller; public MainController controller;
public NotificationBarPane notificationBar;
public Stage mainWindow;
@Override @Override
public void start(Stage mainWindow) throws Exception { public void start(Stage mainWindow) throws Exception {
@ -73,7 +65,6 @@ public class Main extends Application {
} }
private void realStart(Stage mainWindow) throws IOException { private void realStart(Stage mainWindow) throws IOException {
this.mainWindow = mainWindow;
instance = this; instance = this;
// Show the crash dialog for any exceptions that we don't handle and that hit the main loop. // Show the crash dialog for any exceptions that we don't handle and that hit the main loop.
GuiUtils.handleCrashesOnThisThread(); GuiUtils.handleCrashesOnThisThread();
@ -87,19 +78,10 @@ public class Main extends Application {
// Load the GUI. The MainController class will be automagically created and wired up. // Load the GUI. The MainController class will be automagically created and wired up.
URL location = getClass().getResource("main.fxml"); URL location = getClass().getResource("main.fxml");
FXMLLoader loader = new FXMLLoader(location); FXMLLoader loader = new FXMLLoader(location);
mainUI = loader.load(); Pane mainUI = loader.load();
controller = loader.getController(); controller = loader.getController();
// Configure the window with a StackPane so we can overlay things on top of the main UI, and a
// NotificationBarPane so we can slide messages and progress bars in from the bottom. Note that Scene scene = controller.controllerStart(mainUI, "wallet.css");
// ordering of the construction and connection matters here, otherwise we get (harmless) CSS error
// spew to the logs.
notificationBar = new NotificationBarPane(mainUI);
mainWindow.setTitle(APP_NAME);
uiStack = new StackPane();
Scene scene = new Scene(uiStack);
TextFieldValidator.configureScene(scene); // Add CSS that we need.
scene.getStylesheets().add(getClass().getResource("wallet.css").toString());
uiStack.getChildren().add(notificationBar);
mainWindow.setScene(scene); mainWindow.setScene(scene);
// Make log output concise. // Make log output concise.
@ -154,94 +136,6 @@ public class Main extends Application {
bitcoin.restoreWalletFromSeed(seed); bitcoin.restoreWalletFromSeed(seed);
} }
private Node stopClickPane = new Pane();
public class OverlayUI<T> {
public Node ui;
public T controller;
public OverlayUI(Node ui, T controller) {
this.ui = ui;
this.controller = controller;
}
public void show() {
checkGuiThread();
if (currentOverlay == null) {
uiStack.getChildren().add(stopClickPane);
uiStack.getChildren().add(ui);
blurOut(mainUI);
//darken(mainUI);
fadeIn(ui);
zoomIn(ui);
} else {
// Do a quick transition between the current overlay and the next.
// Bug here: we don't pay attention to changes in outsideClickDismisses.
explodeOut(currentOverlay.ui);
fadeOutAndRemove(uiStack, currentOverlay.ui);
uiStack.getChildren().add(ui);
ui.setOpacity(0.0);
fadeIn(ui, 100);
zoomIn(ui, 100);
}
currentOverlay = this;
}
public void outsideClickDismisses() {
stopClickPane.setOnMouseClicked((ev) -> done());
}
public void done() {
checkGuiThread();
if (ui == null) return; // In the middle of being dismissed and got an extra click.
explodeOut(ui);
fadeOutAndRemove(uiStack, ui, stopClickPane);
blurIn(mainUI);
//undark(mainUI);
this.ui = null;
this.controller = null;
currentOverlay = null;
}
}
@Nullable
private OverlayUI currentOverlay;
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<>(node, controller);
// Auto-magically set the overlayUI member, if it's there.
try {
controller.getClass().getField("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 = GuiUtils.getResource(name);
FXMLLoader loader = new FXMLLoader(location);
Pane ui = loader.load();
T controller = loader.getController();
OverlayUI<T> pair = new OverlayUI<>(ui, controller);
// Auto-magically set the overlayUI member, if it's there.
try {
if (controller != null)
controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
ignored.printStackTrace();
}
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.
}
}
@Override @Override
public void stop() throws Exception { public void stop() throws Exception {

View file

@ -19,6 +19,12 @@ package wallettemplate;
import javafx.beans.binding.Binding; import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.core.listeners.DownloadProgressTracker;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.MonetaryFormat;
@ -30,6 +36,8 @@ import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.util.Duration; import javafx.util.Duration;
import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.utils.TextFieldValidator;
import wallettemplate.controls.ClickableBitcoinAddress; import wallettemplate.controls.ClickableBitcoinAddress;
import wallettemplate.controls.NotificationBarPane; import wallettemplate.controls.NotificationBarPane;
import org.bitcoinj.walletfx.utils.BitcoinUIModel; import org.bitcoinj.walletfx.utils.BitcoinUIModel;
@ -37,6 +45,11 @@ import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.utils.easing.EasingMode; import org.bitcoinj.walletfx.utils.easing.EasingMode;
import org.bitcoinj.walletfx.utils.easing.ElasticInterpolator; import org.bitcoinj.walletfx.utils.easing.ElasticInterpolator;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.*;
import static wallettemplate.Main.bitcoin; import static wallettemplate.Main.bitcoin;
/** /**
@ -44,20 +57,44 @@ import static wallettemplate.Main.bitcoin;
* after. This class handles all the updates and event handling for the main UI. * after. This class handles all the updates and event handling for the main UI.
*/ */
public class MainController { public class MainController {
public static MainController instance;
public HBox controlsBox; public HBox controlsBox;
public Label balance; public Label balance;
public Button sendMoneyOutBtn; public Button sendMoneyOutBtn;
public ClickableBitcoinAddress addressControl; public ClickableBitcoinAddress addressControl;
private BitcoinUIModel model = new BitcoinUIModel(); private final BitcoinUIModel model = new BitcoinUIModel();
private NotificationBarPane.Item syncItem; private NotificationBarPane.Item syncItem;
private static final MonetaryFormat MONETARY_FORMAT = MonetaryFormat.BTC.noCode(); private static final MonetaryFormat MONETARY_FORMAT = MonetaryFormat.BTC.noCode();
private Pane mainUI;
private StackPane uiStack;
private NotificationBarPane notificationBar;
private final Node stopClickPane = new Pane();
// Called by FXMLLoader. // Called by FXMLLoader.
public void initialize() { public void initialize() {
instance = this;
addressControl.setOpacity(0.0); addressControl.setOpacity(0.0);
} }
Scene controllerStart(Pane mainUI, String cssResourceName) {
this.mainUI = mainUI;
// Configure the window with a StackPane so we can overlay things on top of the main UI, and a
// NotificationBarPane so we can slide messages and progress bars in from the bottom. Note that
// ordering of the construction and connection matters here, otherwise we get (harmless) CSS error
// spew to the logs.
notificationBar = new NotificationBarPane(mainUI);
uiStack = new StackPane();
Scene scene = new Scene(uiStack);
TextFieldValidator.configureScene(scene);
// Add CSS that we need. cssResourceName will be loaded from the same package as this class.
scene.getStylesheets().add(getClass().getResource(cssResourceName).toString());
uiStack.getChildren().add(notificationBar);
scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close());
return scene;
}
public void onBitcoinSetup() { public void onBitcoinSetup() {
model.setWallet(bitcoin.wallet()); model.setWallet(bitcoin.wallet());
addressControl.addressProperty().bind(model.addressProperty()); addressControl.addressProperty().bind(model.addressProperty());
@ -88,16 +125,16 @@ public class MainController {
} }
private void showBitcoinSyncMessage() { private void showBitcoinSyncMessage() {
syncItem = Main.instance.notificationBar.pushItem("Synchronising with the Bitcoin network", model.syncProgressProperty()); syncItem = notificationBar.pushItem("Synchronising with the Bitcoin network", model.syncProgressProperty());
} }
public void sendMoneyOut(ActionEvent event) { 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. // 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"); overlayUI("send_money.fxml");
} }
public void settingsClicked(ActionEvent event) { public void settingsClicked(ActionEvent event) {
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml"); OverlayUI<WalletSettingsController> screen = overlayUI("wallet_settings.fxml");
screen.controller.initialize(null); screen.controller.initialize(null);
} }
@ -132,4 +169,91 @@ public class MainController {
public DownloadProgressTracker progressBarUpdater() { public DownloadProgressTracker progressBarUpdater() {
return model.getDownloadProgressTracker(); return model.getDownloadProgressTracker();
} }
public class OverlayUI<T> {
public Node ui;
public T controller;
public OverlayUI(Node ui, T controller) {
this.ui = ui;
this.controller = controller;
}
public void show() {
checkGuiThread();
if (currentOverlay == null) {
uiStack.getChildren().add(stopClickPane);
uiStack.getChildren().add(ui);
blurOut(mainUI);
//darken(mainUI);
fadeIn(ui);
zoomIn(ui);
} else {
// Do a quick transition between the current overlay and the next.
// Bug here: we don't pay attention to changes in outsideClickDismisses.
explodeOut(currentOverlay.ui);
fadeOutAndRemove(uiStack, currentOverlay.ui);
uiStack.getChildren().add(ui);
ui.setOpacity(0.0);
fadeIn(ui, 100);
zoomIn(ui, 100);
}
currentOverlay = this;
}
public void outsideClickDismisses() {
stopClickPane.setOnMouseClicked((ev) -> done());
}
public void done() {
checkGuiThread();
if (ui == null) return; // In the middle of being dismissed and got an extra click.
explodeOut(ui);
fadeOutAndRemove(uiStack, ui, stopClickPane);
blurIn(mainUI);
//undark(mainUI);
this.ui = null;
this.controller = null;
currentOverlay = null;
}
}
@Nullable
private OverlayUI currentOverlay;
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<>(node, controller);
// Auto-magically set the overlayUI member, if it's there.
try {
controller.getClass().getField("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 = GuiUtils.getResource(name);
FXMLLoader loader = new FXMLLoader(location);
Pane ui = loader.load();
T controller = loader.getController();
OverlayUI<T> pair = new OverlayUI<>(ui, controller);
// Auto-magically set the overlayUI member, if it's there.
try {
if (controller != null)
controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
ignored.printStackTrace();
}
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.
}
}
} }

View file

@ -47,7 +47,7 @@ public class SendMoneyController {
public TextField amountEdit; public TextField amountEdit;
public Label btcLabel; public Label btcLabel;
public Main.OverlayUI overlayUI; public MainController.OverlayUI overlayUI;
private Wallet.SendResult sendResult; private Wallet.SendResult sendResult;
private KeyParameter aesKey; private KeyParameter aesKey;
@ -114,14 +114,14 @@ public class SendMoneyController {
} }
private void askForPasswordAndRetry() { private void askForPasswordAndRetry() {
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml"); MainController.OverlayUI<WalletPasswordController> pwd = MainController.instance.overlayUI("wallet_password.fxml");
final String addressStr = address.getText(); final String addressStr = address.getText();
final String amountStr = amountEdit.getText(); final String amountStr = amountEdit.getText();
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> { pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
// We only get here if the user found the right password. If they don't or they cancel, we end up back on // We only get here if the user found the right password. If they don't or they cancel, we end up back on
// the main UI screen. By now the send money screen is history so we must recreate it. // the main UI screen. By now the send money screen is history so we must recreate it.
checkGuiThread(); checkGuiThread();
Main.OverlayUI<SendMoneyController> screen = Main.instance.overlayUI("send_money.fxml"); MainController.OverlayUI<SendMoneyController> screen = MainController.instance.overlayUI("send_money.fxml");
screen.controller.aesKey = cur; screen.controller.aesKey = cur;
screen.controller.address.setText(addressStr); screen.controller.address.setText(addressStr);
screen.controller.amountEdit.setText(amountStr); screen.controller.amountEdit.setText(amountStr);

View file

@ -54,7 +54,7 @@ public class WalletPasswordController {
@FXML GridPane widgetGrid; @FXML GridPane widgetGrid;
@FXML Label explanationLabel; @FXML Label explanationLabel;
public Main.OverlayUI overlayUI; public MainController.OverlayUI overlayUI;
private SimpleObjectProperty<KeyParameter> aesKey = new SimpleObjectProperty<>(); private SimpleObjectProperty<KeyParameter> aesKey = new SimpleObjectProperty<>();

View file

@ -43,7 +43,7 @@ public class WalletSetPasswordController {
public Button closeButton; public Button closeButton;
public Label explanationLabel; public Label explanationLabel;
public Main.OverlayUI overlayUI; public MainController.OverlayUI overlayUI;
// These params were determined empirically on a top-range (as of 2014) MacBook Pro with native scrypt support, // These params were determined empirically on a top-range (as of 2014) MacBook Pro with native scrypt support,
// using the scryptenc command line tool from the original scrypt distribution, given a memory limit of 40mb. // using the scryptenc command line tool from the original scrypt distribution, given a memory limit of 40mb.
public static final Protos.ScryptParameters SCRYPT_PARAMETERS = Protos.ScryptParameters.newBuilder() public static final Protos.ScryptParameters SCRYPT_PARAMETERS = Protos.ScryptParameters.newBuilder()

View file

@ -55,7 +55,7 @@ public class WalletSettingsController {
@FXML TextArea wordsArea; @FXML TextArea wordsArea;
@FXML Button restoreButton; @FXML Button restoreButton;
public Main.OverlayUI overlayUI; public MainController.OverlayUI overlayUI;
private KeyParameter aesKey; private KeyParameter aesKey;
@ -132,12 +132,12 @@ public class WalletSettingsController {
} }
private void askForPasswordAndRetry() { private void askForPasswordAndRetry() {
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml"); MainController.OverlayUI<WalletPasswordController> pwd = MainController.instance.overlayUI("wallet_password.fxml");
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> { pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
// We only get here if the user found the right password. If they don't or they cancel, we end up back on // We only get here if the user found the right password. If they don't or they cancel, we end up back on
// the main UI screen. // the main UI screen.
checkGuiThread(); checkGuiThread();
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml"); MainController.OverlayUI<WalletSettingsController> screen = MainController.instance.overlayUI("wallet_settings.fxml");
screen.controller.initialize(cur); screen.controller.initialize(cur);
}); });
} }
@ -166,7 +166,7 @@ public class WalletSettingsController {
informationalAlert("Wallet restore in progress", informationalAlert("Wallet restore in progress",
"Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets."); "Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets.");
overlayUI.done(); overlayUI.done();
Main.instance.controller.restoreFromSeedAnimation(); MainController.instance.restoreFromSeedAnimation();
long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC); long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday); DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday);
@ -184,7 +184,7 @@ public class WalletSettingsController {
public void passwordButtonClicked(ActionEvent event) { public void passwordButtonClicked(ActionEvent event) {
if (aesKey == null) { if (aesKey == null) {
Main.instance.overlayUI("wallet_set_password.fxml"); MainController.instance.overlayUI("wallet_set_password.fxml");
} else { } else {
Main.bitcoin.wallet().decrypt(aesKey); Main.bitcoin.wallet().decrypt(aesKey);
informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings."); informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings.");

View file

@ -48,6 +48,7 @@ import org.bitcoinj.walletfx.utils.QRCodeImages;
import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.fontawesome.AwesomeIcon;
import wallettemplate.MainController;
import static javafx.beans.binding.Bindings.convert; import static javafx.beans.binding.Bindings.convert;
@ -143,7 +144,7 @@ public class ClickableBitcoinAddress extends AnchorPane {
// non-centered on the screen. Finally fade/blur it in. // non-centered on the screen. Finally fade/blur it in.
Pane pane = new Pane(view); Pane pane = new Pane(view);
pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight()); pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight());
final Main.OverlayUI<ClickableBitcoinAddress> overlay = Main.instance.overlayUI(pane, this); final MainController.OverlayUI<ClickableBitcoinAddress> overlay = MainController.instance.overlayUI(pane, this);
view.setOnMouseClicked(event1 -> overlay.done()); view.setOnMouseClicked(event1 -> overlay.done());
} }