From 17aeea2d75ec57a0cf3d18be1895e44831204edf Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Sat, 18 Sep 2021 19:22:38 -0700 Subject: [PATCH] walletfx: move OverlayUI from Main to MainController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/main/java/wallettemplate/Main.java | 114 +-------------- .../java/wallettemplate/MainController.java | 132 +++++++++++++++++- .../wallettemplate/SendMoneyController.java | 6 +- .../WalletPasswordController.java | 2 +- .../WalletSetPasswordController.java | 2 +- .../WalletSettingsController.java | 10 +- .../controls/ClickableBitcoinAddress.java | 3 +- 7 files changed, 144 insertions(+), 125 deletions(-) diff --git a/wallettemplate/src/main/java/wallettemplate/Main.java b/wallettemplate/src/main/java/wallettemplate/Main.java index 576b411f8..9d138cd13 100644 --- a/wallettemplate/src/main/java/wallettemplate/Main.java +++ b/wallettemplate/src/main/java/wallettemplate/Main.java @@ -30,21 +30,17 @@ import org.bitcoinj.wallet.DeterministicSeed; import javafx.application.Application; import javafx.application.Platform; import javafx.fxml.FXMLLoader; -import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import wallettemplate.controls.NotificationBarPane; import org.bitcoinj.walletfx.utils.GuiUtils; -import org.bitcoinj.walletfx.utils.TextFieldValidator; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; 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 static NetworkParameters params = TestNet3Params.get(); @@ -56,11 +52,7 @@ public class Main extends Application { public static WalletAppKit bitcoin; public static Main instance; - private StackPane uiStack; - private Pane mainUI; public MainController controller; - public NotificationBarPane notificationBar; - public Stage mainWindow; @Override public void start(Stage mainWindow) throws Exception { @@ -73,7 +65,6 @@ public class Main extends Application { } private void realStart(Stage mainWindow) throws IOException { - this.mainWindow = mainWindow; instance = this; // Show the crash dialog for any exceptions that we don't handle and that hit the main loop. GuiUtils.handleCrashesOnThisThread(); @@ -87,19 +78,10 @@ public class Main extends Application { // Load the GUI. The MainController class will be automagically created and wired up. URL location = getClass().getResource("main.fxml"); FXMLLoader loader = new FXMLLoader(location); - mainUI = loader.load(); + Pane mainUI = loader.load(); 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 - // 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); + + Scene scene = controller.controllerStart(mainUI, "wallet.css"); mainWindow.setScene(scene); // Make log output concise. @@ -154,94 +136,6 @@ public class Main extends Application { bitcoin.restoreWalletFromSeed(seed); } - private Node stopClickPane = new Pane(); - - public class OverlayUI { - 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 OverlayUI overlayUI(Node node, T controller) { - checkGuiThread(); - OverlayUI 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 OverlayUI 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 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 public void stop() throws Exception { diff --git a/wallettemplate/src/main/java/wallettemplate/MainController.java b/wallettemplate/src/main/java/wallettemplate/MainController.java index dee6d4172..4c1295a83 100644 --- a/wallettemplate/src/main/java/wallettemplate/MainController.java +++ b/wallettemplate/src/main/java/wallettemplate/MainController.java @@ -19,6 +19,12 @@ package wallettemplate; import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; 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.Coin; import org.bitcoinj.utils.MonetaryFormat; @@ -30,6 +36,8 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.util.Duration; +import org.bitcoinj.walletfx.utils.GuiUtils; +import org.bitcoinj.walletfx.utils.TextFieldValidator; import wallettemplate.controls.ClickableBitcoinAddress; import wallettemplate.controls.NotificationBarPane; 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.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; /** @@ -44,20 +57,44 @@ import static wallettemplate.Main.bitcoin; * after. This class handles all the updates and event handling for the main UI. */ public class MainController { + public static MainController instance; public HBox controlsBox; public Label balance; public Button sendMoneyOutBtn; public ClickableBitcoinAddress addressControl; - private BitcoinUIModel model = new BitcoinUIModel(); + private final BitcoinUIModel model = new BitcoinUIModel(); private NotificationBarPane.Item syncItem; 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. public void initialize() { + instance = this; 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() { model.setWallet(bitcoin.wallet()); addressControl.addressProperty().bind(model.addressProperty()); @@ -88,16 +125,16 @@ public class MainController { } 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) { // 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) { - Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml"); + OverlayUI screen = overlayUI("wallet_settings.fxml"); screen.controller.initialize(null); } @@ -132,4 +169,91 @@ public class MainController { public DownloadProgressTracker progressBarUpdater() { return model.getDownloadProgressTracker(); } + + public class OverlayUI { + 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 OverlayUI overlayUI(Node node, T controller) { + checkGuiThread(); + OverlayUI 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 OverlayUI 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 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. + } + } } diff --git a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java index fbabb8be1..bb82fa284 100644 --- a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java +++ b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java @@ -47,7 +47,7 @@ public class SendMoneyController { public TextField amountEdit; public Label btcLabel; - public Main.OverlayUI overlayUI; + public MainController.OverlayUI overlayUI; private Wallet.SendResult sendResult; private KeyParameter aesKey; @@ -114,14 +114,14 @@ public class SendMoneyController { } private void askForPasswordAndRetry() { - Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml"); + MainController.OverlayUI pwd = MainController.instance.overlayUI("wallet_password.fxml"); final String addressStr = address.getText(); final String amountStr = amountEdit.getText(); 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 // the main UI screen. By now the send money screen is history so we must recreate it. checkGuiThread(); - Main.OverlayUI screen = Main.instance.overlayUI("send_money.fxml"); + MainController.OverlayUI screen = MainController.instance.overlayUI("send_money.fxml"); screen.controller.aesKey = cur; screen.controller.address.setText(addressStr); screen.controller.amountEdit.setText(amountStr); diff --git a/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java b/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java index 94b8a69b5..e5a6e5b6c 100644 --- a/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java +++ b/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java @@ -54,7 +54,7 @@ public class WalletPasswordController { @FXML GridPane widgetGrid; @FXML Label explanationLabel; - public Main.OverlayUI overlayUI; + public MainController.OverlayUI overlayUI; private SimpleObjectProperty aesKey = new SimpleObjectProperty<>(); diff --git a/wallettemplate/src/main/java/wallettemplate/WalletSetPasswordController.java b/wallettemplate/src/main/java/wallettemplate/WalletSetPasswordController.java index 782e92e0f..9b73f064e 100644 --- a/wallettemplate/src/main/java/wallettemplate/WalletSetPasswordController.java +++ b/wallettemplate/src/main/java/wallettemplate/WalletSetPasswordController.java @@ -43,7 +43,7 @@ public class WalletSetPasswordController { public Button closeButton; 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, // 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() diff --git a/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java b/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java index 01e9936bd..8ed1887c8 100644 --- a/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java +++ b/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java @@ -55,7 +55,7 @@ public class WalletSettingsController { @FXML TextArea wordsArea; @FXML Button restoreButton; - public Main.OverlayUI overlayUI; + public MainController.OverlayUI overlayUI; private KeyParameter aesKey; @@ -132,12 +132,12 @@ public class WalletSettingsController { } private void askForPasswordAndRetry() { - Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml"); + MainController.OverlayUI pwd = MainController.instance.overlayUI("wallet_password.fxml"); 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 // the main UI screen. checkGuiThread(); - Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml"); + MainController.OverlayUI screen = MainController.instance.overlayUI("wallet_settings.fxml"); screen.controller.initialize(cur); }); } @@ -166,7 +166,7 @@ public class WalletSettingsController { informationalAlert("Wallet restore in progress", "Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets."); overlayUI.done(); - Main.instance.controller.restoreFromSeedAnimation(); + MainController.instance.restoreFromSeedAnimation(); long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC); DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday); @@ -184,7 +184,7 @@ public class WalletSettingsController { public void passwordButtonClicked(ActionEvent event) { if (aesKey == null) { - Main.instance.overlayUI("wallet_set_password.fxml"); + MainController.instance.overlayUI("wallet_set_password.fxml"); } else { Main.bitcoin.wallet().decrypt(aesKey); informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings."); diff --git a/wallettemplate/src/main/java/wallettemplate/controls/ClickableBitcoinAddress.java b/wallettemplate/src/main/java/wallettemplate/controls/ClickableBitcoinAddress.java index 193b9c5a6..0d584685d 100644 --- a/wallettemplate/src/main/java/wallettemplate/controls/ClickableBitcoinAddress.java +++ b/wallettemplate/src/main/java/wallettemplate/controls/ClickableBitcoinAddress.java @@ -48,6 +48,7 @@ import org.bitcoinj.walletfx.utils.QRCodeImages; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import wallettemplate.MainController; 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. Pane pane = new Pane(view); pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight()); - final Main.OverlayUI overlay = Main.instance.overlayUI(pane, this); + final MainController.OverlayUI overlay = MainController.instance.overlayUI(pane, this); view.setOnMouseClicked(event1 -> overlay.done()); }