walletfx: Extract OverlayableStackPaneController

* Extract abstract class OverlayableStackPaneController from MainController
* Rename OverlayWindowController to OverlayController (Window was misleading)

Rationale:

1. Overlay functionality is independent of MainContoller’s wallet functions
2. Increases reusability of classes in module
3. Prepares for further refactoring
This commit is contained in:
Sean Gilligan 2021-09-20 11:34:59 -07:00
parent 08a9853c40
commit 93f0bb7a54
8 changed files with 161 additions and 118 deletions

View file

@ -15,14 +15,12 @@
*/
package org.bitcoinj.walletfx.overlay;
import wallettemplate.MainController;
/**
* Interface for controllers displayed via OverlayWindow.OverlayUI
*/
public interface OverlayWindowController<T> {
public interface OverlayController<T> {
/**
* @param ui The overlay UI (node, controller pair)
*/
void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<T>> ui);
void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<T>> ui);
}

View file

@ -0,0 +1,128 @@
/*
* Copyright by the original author or authors.
*
* 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 org.bitcoinj.walletfx.overlay;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import org.bitcoinj.walletfx.utils.GuiUtils;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.blurIn;
import static org.bitcoinj.walletfx.utils.GuiUtils.blurOut;
import static org.bitcoinj.walletfx.utils.GuiUtils.checkGuiThread;
import static org.bitcoinj.walletfx.utils.GuiUtils.explodeOut;
import static org.bitcoinj.walletfx.utils.GuiUtils.fadeIn;
import static org.bitcoinj.walletfx.utils.GuiUtils.fadeOutAndRemove;
import static org.bitcoinj.walletfx.utils.GuiUtils.zoomIn;
/**
* Abstract Controller for a {@link StackPane} that can have other {@link Pane}s displayed on top of it as a modals.
*/
public abstract class OverlayableStackPaneController {
/**
* The {@link StackPane} that contains the {@code mainUI} and any active overlays. Typically, this
* will be the root node of a {@code Scene} contained in a {@code Stage}.
*/
protected final StackPane uiStack = new StackPane();
/**
* The {@link Pane} containing the bottom-most view, that can be overlaid with other views.
*/
protected Pane mainUI;
private final Node stopClickPane = new Pane();
@Nullable
private OverlayUI<? extends OverlayController<?>> currentOverlay;
public <T extends OverlayController<T>> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<>(node, controller);
controller.setOverlayUI(pair);
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 extends OverlayController<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);
controller.setOverlayUI(pair);
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.
}
}
public class OverlayUI<T extends OverlayController<T>> {
private 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;
}
}
}

View file

@ -19,12 +19,9 @@ 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;
@ -36,7 +33,7 @@ import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.util.Duration;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.utils.TextFieldValidator;
import wallettemplate.controls.ClickableBitcoinAddress;
@ -45,18 +42,13 @@ import org.bitcoinj.walletfx.utils.BitcoinUIModel;
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;
/**
* Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named
* after. This class handles all the updates and event handling for the main UI.
*/
public class MainController {
public class MainController extends OverlayableStackPaneController {
public static MainController instance;
public HBox controlsBox;
public Label balance;
@ -67,10 +59,7 @@ public class MainController {
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() {
@ -85,7 +74,6 @@ public class MainController {
// 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.
@ -170,80 +158,4 @@ public class MainController {
return model.getDownloadProgressTracker();
}
public class OverlayUI<T extends OverlayWindowController<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<? extends OverlayWindowController<?>> currentOverlay;
public <T extends OverlayWindowController<T>> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<>(node, controller);
controller.setOverlayUI(pair);
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 extends OverlayWindowController<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);
controller.setOverlayUI(pair);
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.
}
}
}

View file

@ -29,7 +29,8 @@ import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.bouncycastle.crypto.params.KeyParameter;
import wallettemplate.controls.BitcoinAddressValidator;
import org.bitcoinj.walletfx.utils.TextFieldValidator;
@ -40,7 +41,7 @@ import static org.bitcoinj.walletfx.utils.GuiUtils.*;
import javax.annotation.Nullable;
public class SendMoneyController implements OverlayWindowController<SendMoneyController> {
public class SendMoneyController implements OverlayController<SendMoneyController> {
public Button sendBtn;
public Button cancelBtn;
public TextField address;
@ -48,13 +49,13 @@ public class SendMoneyController implements OverlayWindowController<SendMoneyCon
public TextField amountEdit;
public Label btcLabel;
private MainController.OverlayUI<? extends OverlayWindowController<SendMoneyController>> overlayUI;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<SendMoneyController>> overlayUI;
private Wallet.SendResult sendResult;
private KeyParameter aesKey;
@Override
public void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<SendMoneyController>> ui) {
public void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<SendMoneyController>> ui) {
overlayUI = ui;
}
@ -120,14 +121,14 @@ public class SendMoneyController implements OverlayWindowController<SendMoneyCon
}
private void askForPasswordAndRetry() {
MainController.OverlayUI<WalletPasswordController> pwd = MainController.instance.overlayUI("wallet_password.fxml");
OverlayableStackPaneController.OverlayUI<WalletPasswordController> 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();
MainController.OverlayUI<SendMoneyController> screen = MainController.instance.overlayUI("send_money.fxml");
OverlayableStackPaneController.OverlayUI<SendMoneyController> screen = MainController.instance.overlayUI("send_money.fxml");
screen.controller.aesKey = cur;
screen.controller.address.setText(addressStr);
screen.controller.amountEdit.setText(amountStr);

View file

@ -30,7 +30,8 @@ import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.bouncycastle.crypto.params.KeyParameter;
@ -45,7 +46,7 @@ import static org.bitcoinj.walletfx.utils.GuiUtils.*;
* User interface for entering a password on demand, e.g. to send money. Also used when encrypting a wallet. Shows a
* progress meter as we scrypt the password.
*/
public class WalletPasswordController implements OverlayWindowController<WalletPasswordController> {
public class WalletPasswordController implements OverlayController<WalletPasswordController> {
private static final Logger log = LoggerFactory.getLogger(WalletPasswordController.class);
@FXML HBox buttonsBox;
@ -55,12 +56,12 @@ public class WalletPasswordController implements OverlayWindowController<WalletP
@FXML GridPane widgetGrid;
@FXML Label explanationLabel;
private MainController.OverlayUI<? extends OverlayWindowController<WalletPasswordController>> overlayUI;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletPasswordController>> overlayUI;
private SimpleObjectProperty<KeyParameter> aesKey = new SimpleObjectProperty<>();
@Override
public void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<WalletPasswordController>> ui) {
public void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletPasswordController>> ui) {
overlayUI = ui;
}

View file

@ -23,7 +23,8 @@ import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.bitcoinj.crypto.*;
import org.bitcoinj.wallet.*;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.slf4j.*;
import org.bouncycastle.crypto.params.*;
@ -35,7 +36,7 @@ import java.util.concurrent.*;
import org.bitcoinj.walletfx.utils.KeyDerivationTasks;
import static org.bitcoinj.walletfx.utils.GuiUtils.*;
public class WalletSetPasswordController implements OverlayWindowController<WalletSetPasswordController> {
public class WalletSetPasswordController implements OverlayController<WalletSetPasswordController> {
private static final Logger log = LoggerFactory.getLogger(WalletSetPasswordController.class);
public PasswordField pass1, pass2;
@ -44,7 +45,7 @@ public class WalletSetPasswordController implements OverlayWindowController<Wall
public Button closeButton;
public Label explanationLabel;
private MainController.OverlayUI<? extends OverlayWindowController<WalletSetPasswordController>> overlayUI;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSetPasswordController>> 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()
@ -55,7 +56,7 @@ public class WalletSetPasswordController implements OverlayWindowController<Wall
.build();
@Override
public void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<WalletSetPasswordController>> ui) {
public void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSetPasswordController>> ui) {
overlayUI = ui;
}

View file

@ -28,7 +28,8 @@ import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TextArea;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.bouncycastle.crypto.params.KeyParameter;
@ -48,7 +49,7 @@ import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert;
import static org.bitcoinj.walletfx.utils.WTUtils.didThrow;
import static org.bitcoinj.walletfx.utils.WTUtils.unchecked;
public class WalletSettingsController implements OverlayWindowController<WalletSettingsController> {
public class WalletSettingsController implements OverlayController<WalletSettingsController> {
private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class);
@FXML Button passwordButton;
@ -56,12 +57,12 @@ public class WalletSettingsController implements OverlayWindowController<WalletS
@FXML TextArea wordsArea;
@FXML Button restoreButton;
private MainController.OverlayUI<? extends OverlayWindowController<WalletSettingsController>> overlayUI;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSettingsController>> overlayUI;
private KeyParameter aesKey;
@Override
public void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<WalletSettingsController>> ui) {
public void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSettingsController>> ui) {
overlayUI = ui;
}
@ -138,12 +139,12 @@ public class WalletSettingsController implements OverlayWindowController<WalletS
}
private void askForPasswordAndRetry() {
MainController.OverlayUI<WalletPasswordController> pwd = MainController.instance.overlayUI("wallet_password.fxml");
OverlayableStackPaneController.OverlayUI<WalletPasswordController> 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();
MainController.OverlayUI<WalletSettingsController> screen = MainController.instance.overlayUI("wallet_settings.fxml");
OverlayableStackPaneController.OverlayUI<WalletSettingsController> screen = MainController.instance.overlayUI("wallet_settings.fxml");
screen.controller.initialize(cur);
});
}

View file

@ -42,7 +42,8 @@ import javafx.scene.layout.Pane;
import org.bitcoinj.core.Address;
import org.bitcoinj.uri.BitcoinURI;
import org.bitcoinj.walletfx.overlay.OverlayWindowController;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import wallettemplate.Main;
import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.utils.QRCodeImages;
@ -58,7 +59,7 @@ import static javafx.beans.binding.Bindings.convert;
* 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 implements OverlayWindowController<ClickableBitcoinAddress> {
public class ClickableBitcoinAddress extends AnchorPane implements OverlayController<ClickableBitcoinAddress> {
@FXML protected Label addressLabel;
@FXML protected ContextMenu addressMenu;
@FXML protected Label copyWidget;
@ -68,7 +69,7 @@ public class ClickableBitcoinAddress extends AnchorPane implements OverlayWindow
private final StringExpression addressStr;
@Override
public void setOverlayUI(MainController.OverlayUI<? extends OverlayWindowController<ClickableBitcoinAddress>> ui) {
public void setOverlayUI(OverlayableStackPaneController.OverlayUI<? extends OverlayController<ClickableBitcoinAddress>> ui) {
}
public ClickableBitcoinAddress() {
@ -149,7 +150,7 @@ public class ClickableBitcoinAddress extends AnchorPane implements OverlayWindow
// non-centered on the screen. Finally fade/blur it in.
Pane pane = new Pane(view);
pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight());
final MainController.OverlayUI<ClickableBitcoinAddress> overlay = MainController.instance.overlayUI(pane, this);
final OverlayableStackPaneController.OverlayUI<ClickableBitcoinAddress> overlay = MainController.instance.overlayUI(pane, this);
view.setOnMouseClicked(event1 -> overlay.done());
}