Refactor MainViewCB and Navigation.Item

Changes to MainViewCB
---------------------

This is a siginificant restructuring of the main controller in the
system and suggests a number of ideas and practices that should be
applied when refactoring the rest of the controllers or designing new
ones. UI code is inherently verbose; as such, the best we can do is to
structure a controller such as MainViewCB in a way that minimizes the
verbosity as much as possible and focuses on making what is happening as
clear as possible. That's why (as is described further below), almost
everything important now happens in the #initialize method. A major goal
of this change is that developers are able to look at MainViewCB and
read its #initialize method like a script. Indirections to other methods
are minimized as much as possible. The primary focus is on readability
(and therefore maintainability).

These changes began as an effort to substitute a mocked-out "backend",
i.e. bitcoin and p2p infrastructure, such that the application could
be run as quickly as possible, without the need for network
sychronization, bootstrapping, etc, for the purposes of UI development
and testing. Trying to make that change naturally evolved into this set
of refactorings. So at this point, MainViewCB is still "hard-wired" to
live bitcoin and tomp2p backends, but changing that, i.e. providing
mocked-out backends will be that much easier and clearer to accomplish
now.

Specifics:

 - Use public vs. private contstructor. This allows for the possibility
   of unit testing, avoids spurious warnings in the IDE, and generally
   adheres to the principle of least surprise.

 - Orchestrate creation and composition of components within the
   #initialize method. This avoids the need for member fields almost
   entirely.

 - Extract and delegate to private methods from #initialize only where
   it helps readibility. In general, this change assumes that initialize
   should be "where the action is": if the layout of a particular view
   is complex, then deal with that complexity directly within the
   #initialize method. However, if the creation of a given component is
   particularly verbose--for example the creation of the splash screen,
   then extract a #createSplashScreen method that returns a VBox. The
   same approach has been applied in extracting the
   #createBankAccountComboBox, #applyPendingTradesInfoIcon and
   #loadSelectedNavigation methods.

 - Extract the NavButton member class to cleanly encapsulate what it
   means to be a ToggleButton on the Bitsquare application navigation.
   This approach is similar to the MenuItem class in
   AccountSettingsViewCB.

 - Use "double-brace initialization" syntax for JavaFX components
   where appropriate, e.g.:

    HBox rightNavPane =
            new HBox(accountComboBox, settingsButton, accountButton) {{
        setSpacing(10);
        setRightAnchor(this, 10d);
        setTopAnchor(this, 0d);
    }};

   This approach, while typically rarely used, is an especially
   appropriate fit for JavaFX UI components, as the the allows for both
   maximum concision and clarity. Most JavaFX components are configured
   through a combination of constructor parameters and setter method
   invocations, and this approach allows all of them to happen at
   construction/initialization time.

 - Remove class section comments. In general--and as @mkarrer and I have
   discussed--these `////...` blocks shouldn't be necessary going
   forward. The goal is classes that are as small and focused as they
   can be, and they should be self-documenting to the greatest degree
   possible.

 - Remove empty lifecycle methods from the ViewCB superclass.

 - Remove Profiler statements. These are fine for specific debugging
   sessions, but should otherwise be removed for normal use.

Changes to Navigation.Item
--------------------------

These changes to the Navigation.Item enum were made in support of the
refactorings above, particularly in support of the newly extracted
NavButton class being as concise to construct as possible.

 - Introduce Navigation.Item#getDisplayName

   Push navigation button titles, such as "Overview" for HOME, "Buy BTC"
   for BUY, etc. into the Navigation.Item enum itself. This can later be
   refactored to support I18N, e.g. by embedding a message label instead
   of the actual english word. Not all Navigation items have a display
   name yet (and it may or may not make sense for them all to have one).
   The default value is "NONE".
This commit is contained in:
Chris Beams 2014-11-15 13:30:10 +01:00
parent 3811cabba1
commit cc75aec8f0
No known key found for this signature in database
GPG Key ID: 3D214F8F5BC5ED73
2 changed files with 182 additions and 287 deletions

View File

@ -159,14 +159,14 @@ public class Navigation {
// Main menu screens
///////////////////////////////////////////////////////////////////////////////////////////
HOME("/io/bitsquare/gui/main/home/HomeView.fxml"),
BUY("/io/bitsquare/gui/main/trade/BuyView.fxml"),
SELL("/io/bitsquare/gui/main/trade/SellView.fxml"),
PORTFOLIO("/io/bitsquare/gui/main/portfolio/PortfolioView.fxml"),
FUNDS("/io/bitsquare/gui/main/funds/FundsView.fxml"),
MSG("/io/bitsquare/gui/main/msg/MsgView.fxml"),
SETTINGS("/io/bitsquare/gui/main/settings/SettingsView.fxml"),
ACCOUNT("/io/bitsquare/gui/main/account/AccountView.fxml"),
HOME("/io/bitsquare/gui/main/home/HomeView.fxml", "Overview"),
BUY("/io/bitsquare/gui/main/trade/BuyView.fxml", "Buy BTC"),
SELL("/io/bitsquare/gui/main/trade/SellView.fxml", "Sell BTC"),
PORTFOLIO("/io/bitsquare/gui/main/portfolio/PortfolioView.fxml", "Portfolio"),
FUNDS("/io/bitsquare/gui/main/funds/FundsView.fxml", "Funds"),
MSG("/io/bitsquare/gui/main/msg/MsgView.fxml", "Messages"),
SETTINGS("/io/bitsquare/gui/main/settings/SettingsView.fxml", "Settings"),
ACCOUNT("/io/bitsquare/gui/main/account/AccountView.fxml", "Account"),
///////////////////////////////////////////////////////////////////////////////////////////
@ -218,13 +218,19 @@ public class Navigation {
ARBITRATOR_PROFILE("/io/bitsquare/gui/main/account/arbitrator/profile/ArbitratorProfileView.fxml"),
ARBITRATOR_BROWSER("/io/bitsquare/gui/main/account/arbitrator/browser/ArbitratorBrowserView.fxml"),
ARBITRATOR_REGISTRATION("/io/bitsquare/gui/main/account/arbitrator/registration/ArbitratorRegistrationView" +
".fxml");
ARBITRATOR_REGISTRATION(
"/io/bitsquare/gui/main/account/arbitrator/registration/ArbitratorRegistrationView.fxml");
private final String displayName;
private final String fxmlUrl;
Item(String fxmlUrl) {
this(fxmlUrl, "NONE");
}
Item(String fxmlUrl, String displayName) {
this.displayName = displayName;
this.fxmlUrl = fxmlUrl;
}
@ -232,5 +238,13 @@ public class Navigation {
public String getFxmlUrl() {
return fxmlUrl;
}
public String getDisplayName() {
return displayName;
}
public String getId() {
return fxmlUrl.substring(fxmlUrl.lastIndexOf("/") + 1, fxmlUrl.lastIndexOf("View.fxml")).toLowerCase();
}
}
}

View File

@ -17,6 +17,7 @@
package io.bitsquare.gui.main;
import io.bitsquare.BitsquareException;
import io.bitsquare.bank.BankAccount;
import io.bitsquare.btc.BitcoinNetwork;
import io.bitsquare.gui.Navigation;
@ -25,7 +26,6 @@ import io.bitsquare.gui.ViewCB;
import io.bitsquare.gui.ViewLoader;
import io.bitsquare.gui.components.Popups;
import io.bitsquare.gui.components.SystemNotification;
import io.bitsquare.gui.util.Profiler;
import io.bitsquare.gui.util.Transitions;
import io.bitsquare.trade.TradeManager;
@ -37,7 +37,6 @@ import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.*;
@ -48,38 +47,31 @@ import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.bitsquare.gui.Navigation.Item.*;
import static javafx.scene.layout.AnchorPane.*;
public class MainViewCB extends ViewCB<MainPM> {
private static final Logger log = LoggerFactory.getLogger(MainViewCB.class);
private final ToggleGroup navButtons = new ToggleGroup();
private final AnchorPane contentContainer = new AnchorPane() {{
setId("content-pane");
setLeftAnchor(this, 0d);
setRightAnchor(this, 0d);
setTopAnchor(this, 60d);
setBottomAnchor(this, 25d);
}};
private final Navigation navigation;
private final OverlayManager overlayManager;
private final ToggleGroup navButtonsGroup = new ToggleGroup();
private Transitions transitions;
private BitcoinNetwork bitcoinNetwork;
private final Transitions transitions;
private final BitcoinNetwork bitcoinNetwork;
private final String title;
private BorderPane baseApplicationContainer;
private VBox splashScreen;
private AnchorPane contentContainer;
private HBox leftNavPane, rightNavPane;
private ToggleButton buyButton, sellButton, homeButton, msgButton, portfolioButton, fundsButton, settingsButton,
accountButton;
private Pane portfolioButtonButtonPane;
private Label numPendingTradesLabel;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private MainViewCB(MainPM presentationModel, Navigation navigation, OverlayManager overlayManager,
TradeManager tradeManager, Transitions transitions,
BitcoinNetwork bitcoinNetwork,
@Named(TITLE_KEY) String title) {
public MainViewCB(MainPM presentationModel, Navigation navigation, OverlayManager overlayManager,
TradeManager tradeManager, Transitions transitions, BitcoinNetwork bitcoinNetwork,
@Named(TITLE_KEY) String title) {
super(presentationModel);
this.navigation = navigation;
@ -96,188 +88,113 @@ public class MainViewCB extends ViewCB<MainPM> {
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void initialize(URL url, ResourceBundle rb) {
super.initialize(url, rb);
Profiler.printMsgWithTime("MainController.initialize");
// just temp. ugly hack... Popups will be removed
Popups.setOverlayManager(overlayManager);
ToggleButton homeButton = new NavButton(HOME) {{ setDisable(true); }};
ToggleButton buyButton = new NavButton(BUY);
ToggleButton sellButton = new NavButton(SELL);
ToggleButton portfolioButton = new NavButton(PORTFOLIO);
ToggleButton fundsButton = new NavButton(FUNDS);
ToggleButton msgButton = new NavButton(MSG) {{ setDisable(true); }};
ToggleButton settingsButton = new NavButton(SETTINGS);
ToggleButton accountButton = new NavButton(ACCOUNT);
Pane portfolioButtonHolder = new Pane(portfolioButton);
Pane bankAccountComboBoxHolder = new Pane();
HBox leftNavPane = new HBox(
homeButton, buyButton, sellButton, portfolioButtonHolder, fundsButton, new Pane(msgButton)) {{
setSpacing(10);
setLeftAnchor(this, 10d);
setTopAnchor(this, 0d);
}};
HBox rightNavPane = new HBox(bankAccountComboBoxHolder, settingsButton, accountButton) {{
setSpacing(10);
setRightAnchor(this, 10d);
setTopAnchor(this, 0d);
}};
AnchorPane applicationContainer = new AnchorPane(leftNavPane, rightNavPane, contentContainer) {{
setId("content-pane");
}};
BorderPane baseApplicationContainer = new BorderPane(applicationContainer) {{
setId("base-content-container");
}};
navigation.addListener(navigationItems -> {
if (navigationItems != null && navigationItems.length == 2) {
if (navigationItems[0] == Navigation.Item.MAIN) {
loadView(navigationItems[1]);
selectMainMenuButton(navigationItems[1]);
}
}
if (isRequestToChangeNavigation(navigationItems))
loadSelectedNavigation(navigationItems[1]);
});
overlayManager.addListener(new OverlayManager.OverlayListener() {
@Override
public void onBlurContentRequested() {
transitions.blur(baseApplicationContainer);
}
configureBlurring(baseApplicationContainer);
@Override
public void onRemoveBlurContentRequested() {
transitions.removeBlur(baseApplicationContainer);
}
});
VBox splashScreen = createSplashScreen();
startup();
}
((Pane) root).getChildren().addAll(baseApplicationContainer, splashScreen);
@SuppressWarnings("EmptyMethod")
@Override
public void terminate() {
super.terminate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Navigation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected Initializable loadView(Navigation.Item navigationItem) {
super.loadView((navigationItem));
final ViewLoader loader = new ViewLoader(navigationItem);
final Node view = loader.load();
contentContainer.getChildren().setAll(view);
childController = loader.getController();
if (childController instanceof ViewCB)
((ViewCB) childController).setParent(this);
return childController;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private Methods: Startup
///////////////////////////////////////////////////////////////////////////////////////////
private void startup() {
baseApplicationContainer = getBaseApplicationContainer();
splashScreen = getSplashScreen();
((StackPane) root).getChildren().addAll(baseApplicationContainer, splashScreen);
baseApplicationContainer.setCenter(getApplicationContainer());
Platform.runLater(this::onSplashScreenAdded);
}
private void onSplashScreenAdded() {
presentationModel.backendReady.addListener((ov, oldValue, newValue) -> {
if (newValue)
onBackendReady();
});
presentationModel.initBackend();
}
if (newValue) {
bankAccountComboBoxHolder.getChildren().setAll(createBankAccountComboBox());
private void onBackendReady() {
Profiler.printMsgWithTime("MainController.onBackendInited");
addMainNavigation();
}
applyPendingTradesInfoIcon(presentationModel.numPendingTrades.get(), portfolioButtonHolder);
presentationModel.numPendingTrades.addListener((ov1, oldValue1, newValue1) ->
applyPendingTradesInfoIcon((int) newValue1, portfolioButtonHolder));
private void applyPendingTradesInfoIcon(int numPendingTrades) {
log.debug("numPendingTrades " + numPendingTrades);
if (numPendingTrades > 0) {
if (portfolioButtonButtonPane.getChildren().size() == 1) {
ImageView icon = new ImageView();
icon.setLayoutX(0.5);
icon.setId("image-alert-round");
numPendingTradesLabel = new Label(String.valueOf(numPendingTrades));
numPendingTradesLabel.relocate(5, 1);
numPendingTradesLabel.setId("nav-alert-label");
Pane alert = new Pane();
alert.relocate(30, 9);
alert.setMouseTransparent(true);
alert.setEffect(new DropShadow(4, 1, 2, Color.GREY));
alert.getChildren().addAll(icon, numPendingTradesLabel);
portfolioButtonButtonPane.getChildren().add(alert);
navigation.navigateToLastStoredItem();
transitions.fadeOutAndRemove(splashScreen, 1500);
}
else {
numPendingTradesLabel.setText(String.valueOf(numPendingTrades));
}
SystemNotification.openInfoNotification(title, "You got a new trade message.");
}
else {
if (portfolioButtonButtonPane.getChildren().size() > 1)
portfolioButtonButtonPane.getChildren().remove(1);
}
}
private void onMainNavigationAdded() {
Profiler.printMsgWithTime("MainController.ondMainNavigationAdded");
presentationModel.numPendingTrades.addListener((ov, oldValue, newValue) ->
{
//if ((int) newValue > (int) oldValue)
applyPendingTradesInfoIcon((int) newValue);
});
applyPendingTradesInfoIcon(presentationModel.numPendingTrades.get());
navigation.navigateToLastStoredItem();
onContentAdded();
Platform.runLater(presentationModel::initBackend);
}
private void onContentAdded() {
Profiler.printMsgWithTime("MainController.onContentAdded");
transitions.fadeOutAndRemove(splashScreen, 1500);
private void loadSelectedNavigation(Navigation.Item selected) {
ViewLoader loader = new ViewLoader(selected);
contentContainer.getChildren().setAll(loader.<Node>load());
childController = loader.getController();
if (childController != null)
childController.setParent(this);
navButtons.getToggles().stream()
.filter(toggle -> toggle instanceof ToggleButton)
.filter(button -> selected.getDisplayName().equals(((ToggleButton) button).getText()))
.findFirst()
.orElseThrow(() -> new BitsquareException("No button matching %s found", selected.getDisplayName()))
.setSelected(true);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void selectMainMenuButton(Navigation.Item item) {
switch (item) {
case HOME:
homeButton.setSelected(true);
break;
case FUNDS:
fundsButton.setSelected(true);
break;
case MSG:
msgButton.setSelected(true);
break;
case PORTFOLIO:
portfolioButton.setSelected(true);
break;
case SETTINGS:
settingsButton.setSelected(true);
break;
case SELL:
sellButton.setSelected(true);
break;
case BUY:
buyButton.setSelected(true);
break;
case ACCOUNT:
accountButton.setSelected(true);
break;
default:
log.error(item.getFxmlUrl() + " is no main navigation item");
break;
private void applyPendingTradesInfoIcon(int numPendingTrades, Pane targetPane) {
if (numPendingTrades <= 0) {
if (targetPane.getChildren().size() > 1) {
targetPane.getChildren().remove(1);
}
return;
}
Label numPendingTradesLabel = new Label(String.valueOf(numPendingTrades));
if (targetPane.getChildren().size() == 1) {
ImageView icon = new ImageView();
icon.setLayoutX(0.5);
icon.setId("image-alert-round");
numPendingTradesLabel.relocate(5, 1);
numPendingTradesLabel.setId("nav-alert-label");
Pane alert = new Pane();
alert.relocate(30, 9);
alert.setMouseTransparent(true);
alert.setEffect(new DropShadow(4, 1, 2, Color.GREY));
alert.getChildren().addAll(icon, numPendingTradesLabel);
targetPane.getChildren().add(alert);
}
SystemNotification.openInfoNotification(title, "You got a new trade message.");
}
private BorderPane getBaseApplicationContainer() {
BorderPane borderPane = new BorderPane();
borderPane.setId("base-content-container");
return borderPane;
}
private VBox getSplashScreen() {
private VBox createSplashScreen() {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(10);
@ -367,97 +284,7 @@ public class MainViewCB extends ViewCB<MainPM> {
return vBox;
}
private AnchorPane getApplicationContainer() {
AnchorPane anchorPane = new AnchorPane();
anchorPane.setId("content-pane");
leftNavPane = new HBox();
leftNavPane.setSpacing(10);
AnchorPane.setLeftAnchor(leftNavPane, 10d);
AnchorPane.setTopAnchor(leftNavPane, 0d);
rightNavPane = new HBox();
rightNavPane.setSpacing(10);
AnchorPane.setRightAnchor(rightNavPane, 10d);
AnchorPane.setTopAnchor(rightNavPane, 0d);
contentContainer = new AnchorPane();
contentContainer.setId("content-pane");
AnchorPane.setLeftAnchor(contentContainer, 0d);
AnchorPane.setRightAnchor(contentContainer, 0d);
AnchorPane.setTopAnchor(contentContainer, 60d);
AnchorPane.setBottomAnchor(contentContainer, 25d);
anchorPane.getChildren().addAll(leftNavPane, rightNavPane, contentContainer);
return anchorPane;
}
private void addMainNavigation() {
homeButton = addNavButton(leftNavPane, "Overview", Navigation.Item.HOME);
buyButton = addNavButton(leftNavPane, "Buy BTC", Navigation.Item.BUY);
sellButton = addNavButton(leftNavPane, "Sell BTC", Navigation.Item.SELL);
portfolioButtonButtonPane = new Pane();
portfolioButton = addNavButton(portfolioButtonButtonPane, "Portfolio", Navigation.Item.PORTFOLIO);
leftNavPane.getChildren().add(portfolioButtonButtonPane);
fundsButton = addNavButton(leftNavPane, "Funds", Navigation.Item.FUNDS);
final Pane msgButtonHolder = new Pane();
msgButton = addNavButton(msgButtonHolder, "Messages", Navigation.Item.MSG);
leftNavPane.getChildren().add(msgButtonHolder);
addBankAccountComboBox(rightNavPane);
settingsButton = addNavButton(rightNavPane, "Settings", Navigation.Item.SETTINGS);
accountButton = addNavButton(rightNavPane, "Account", Navigation.Item.ACCOUNT);
// for irc demo
homeButton.setDisable(true);
msgButton.setDisable(true);
onMainNavigationAdded();
}
private ToggleButton addNavButton(Pane parent, String title, Navigation.Item navigationItem) {
final String url = navigationItem.getFxmlUrl();
int lastSlash = url.lastIndexOf("/") + 1;
int end = url.lastIndexOf("View.fxml");
final String id = url.substring(lastSlash, end).toLowerCase();
ImageView iconImageView = new ImageView();
iconImageView.setId("image-nav-" + id);
final ToggleButton toggleButton = new ToggleButton(title, iconImageView);
toggleButton.setToggleGroup(navButtonsGroup);
toggleButton.setId("nav-button");
toggleButton.setPadding(new Insets(0, -10, -10, -10));
toggleButton.setMinSize(50, 50);
toggleButton.setMaxSize(50, 50);
toggleButton.setContentDisplay(ContentDisplay.TOP);
toggleButton.setGraphicTextGap(0);
toggleButton.selectedProperty().addListener((ov, oldValue, newValue) -> {
toggleButton.setMouseTransparent(newValue);
toggleButton.setMinSize(50, 50);
toggleButton.setMaxSize(50, 50);
toggleButton.setGraphicTextGap(newValue ? -1 : 0);
if (newValue) {
toggleButton.getGraphic().setId("image-nav-" + id + "-active");
}
else {
toggleButton.getGraphic().setId("image-nav-" + id);
}
});
toggleButton.setOnAction(e -> navigation.navigationTo(Navigation.Item.MAIN, navigationItem));
parent.getChildren().add(toggleButton);
return toggleButton;
}
private void addBankAccountComboBox(Pane parent) {
private VBox createBankAccountComboBox() {
final ComboBox<BankAccount> comboBox = new ComboBox<>(presentationModel.getBankAccounts());
comboBox.setLayoutY(12);
comboBox.setVisibleRowCount(5);
@ -485,6 +312,60 @@ public class MainViewCB extends ViewCB<MainPM> {
vBox.setSpacing(2);
vBox.setAlignment(Pos.CENTER);
vBox.getChildren().setAll(comboBox, titleLabel);
parent.getChildren().add(vBox);
return vBox;
}
private void configureBlurring(Node node) {
Popups.setOverlayManager(overlayManager);
overlayManager.addListener(new OverlayManager.OverlayListener() {
@Override
public void onBlurContentRequested() {
transitions.blur(node);
}
@Override
public void onRemoveBlurContentRequested() {
transitions.removeBlur(node);
}
});
}
private boolean isRequestToChangeNavigation(Navigation.Item[] navigationItems) {
return navigationItems != null && navigationItems.length == 2 && navigationItems[0] == Navigation.Item.MAIN;
}
private class NavButton extends ToggleButton {
public NavButton(Navigation.Item item) {
super(item.getDisplayName(), new ImageView() {{
setId("image-nav-" + item.getId());
}});
this.setToggleGroup(navButtons);
this.setId("nav-button");
this.setPadding(new Insets(0, -10, -10, -10));
this.setMinSize(50, 50);
this.setMaxSize(50, 50);
this.setContentDisplay(ContentDisplay.TOP);
this.setGraphicTextGap(0);
this.selectedProperty().addListener((ov, oldValue, newValue) -> {
this.setMouseTransparent(newValue);
this.setMinSize(50, 50);
this.setMaxSize(50, 50);
this.setGraphicTextGap(newValue ? -1 : 0);
if (newValue) {
this.getGraphic().setId("image-nav-" + item.getId() + "-active");
}
else {
this.getGraphic().setId("image-nav-" + item.getId());
}
});
this.setOnAction(e -> navigation.navigationTo(Navigation.Item.MAIN, item));
}
}
}