/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main;
import io.bitsquare.BitsquareException;
import io.bitsquare.app.BitsquareApp;
import io.bitsquare.app.DevFlags;
import io.bitsquare.btc.pricefeed.PriceFeedService;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.common.util.Tuple3;
import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.view.*;
import io.bitsquare.gui.components.BusyAnimation;
import io.bitsquare.gui.main.account.AccountView;
import io.bitsquare.gui.main.disputes.DisputesView;
import io.bitsquare.gui.main.funds.FundsView;
import io.bitsquare.gui.main.market.MarketView;
import io.bitsquare.gui.main.offer.BuyOfferView;
import io.bitsquare.gui.main.offer.SellOfferView;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.portfolio.PortfolioView;
import io.bitsquare.gui.main.settings.SettingsView;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.gui.util.Transitions;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.List;
import static javafx.beans.binding.Bindings.createStringBinding;
import static javafx.scene.layout.AnchorPane.*;
@FxmlView
public class MainView extends InitializableView<StackPane, MainViewModel> {
private static final Logger log = LoggerFactory.getLogger(MainView.class);
public static final String TITLE_KEY = "viewTitle";
public static StackPane getRootContainer() {
return MainView.rootContainer;
}
public static void blur() {
if (!DevFlags.STRESS_TEST_MODE)
transitions.blur(MainView.rootContainer);
}
public static void blurLight() {
if (!DevFlags.STRESS_TEST_MODE)
transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.1, false, 5);
}
public static void blurUltraLight() {
if (!DevFlags.STRESS_TEST_MODE)
transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.1, false, 2);
}
public static void darken() {
if (!DevFlags.STRESS_TEST_MODE)
transitions.darken(MainView.rootContainer, Transitions.DEFAULT_DURATION, false);
}
public static void removeEffect() {
transitions.removeEffect(MainView.rootContainer);
}
private final ToggleGroup navButtons = new ToggleGroup();
private final ViewLoader viewLoader;
private final Navigation navigation;
private static Transitions transitions;
private BSFormatter formatter;
private ChangeListener<String> walletServiceErrorMsgListener;
private ChangeListener<String> btcSyncIconIdListener;
private ChangeListener<String> splashP2PNetworkErrorMsgListener;
private ChangeListener<String> splashP2PNetworkIconIdListener;
private ChangeListener<Boolean> splashP2PNetworkVisibleListener;
private BusyAnimation splashP2PNetworkBusyAnimation;
private Label splashP2PNetworkLabel;
private ProgressBar btcSyncIndicator;
private Label btcSplashInfo;
private List<String> persistedFilesCorrupted;
private Popup<?> p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup;
private static StackPane rootContainer;
@Inject
public MainView(MainViewModel model, CachingViewLoader viewLoader, Navigation navigation, Transitions transitions,
BSFormatter formatter, @Named(MainView.TITLE_KEY) String title) {
super(model);
this.viewLoader = viewLoader;
this.navigation = navigation;
this.formatter = formatter;
MainView.transitions = transitions;
}
@Override
protected void initialize() {
MainView.rootContainer = this.root;
ToggleButton marketButton = new NavButton(MarketView.class, "Market");
ToggleButton buyButton = new NavButton(BuyOfferView.class, "Buy BTC");
ToggleButton sellButton = new NavButton(SellOfferView.class, "Sell BTC");
ToggleButton portfolioButton = new NavButton(PortfolioView.class, "Portfolio");
ToggleButton fundsButton = new NavButton(FundsView.class, "Funds");
ToggleButton disputesButton = new NavButton(DisputesView.class, "Support");
ToggleButton settingsButton = new NavButton(SettingsView.class, "Settings");
ToggleButton accountButton = new NavButton(AccountView.class, "Account");
Pane portfolioButtonHolder = new Pane(portfolioButton);
Pane disputesButtonHolder = new Pane(disputesButton);
HBox leftNavPane = new HBox(marketButton, buyButton, sellButton, portfolioButtonHolder, fundsButton, disputesButtonHolder) {{
setLeftAnchor(this, 10d);
setTopAnchor(this, 0d);
}};
Tuple3<ComboBox<PriceFeedComboBoxItem>, Label, VBox> marketPriceBox = getMarketPriceBox("Market price");
ComboBox<PriceFeedComboBoxItem> priceComboBox = marketPriceBox.first;
priceComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
model.setPriceFeedComboBoxItem(newValue);
});
ChangeListener<PriceFeedComboBoxItem> selectedPriceFeedItemListender = (observable, oldValue, newValue) -> {
if (newValue != null)
priceComboBox.getSelectionModel().select(newValue);
};
model.selectedPriceFeedComboBoxItemProperty.addListener(selectedPriceFeedItemListender);
priceComboBox.setItems(model.priceFeedComboBoxItems);
marketPriceBox.second.textProperty().bind(createStringBinding(
() -> {
PriceFeedService.Type type = model.typeProperty.get();
return type != null ? "Market price (" + type.name + ")" : "";
},
model.marketPriceCurrencyCode, model.typeProperty));
HBox.setMargin(marketPriceBox.third, new Insets(0, 0, 0, 0));
Tuple2<TextField, VBox> availableBalanceBox = getBalanceBox("Available balance");
availableBalanceBox.first.textProperty().bind(model.availableBalance);
Tuple2<TextField, VBox> reservedBalanceBox = getBalanceBox("Reserved in offers");
reservedBalanceBox.first.textProperty().bind(model.reservedBalance);
Tuple2<TextField, VBox> lockedBalanceBox = getBalanceBox("Locked in trades");
lockedBalanceBox.first.textProperty().bind(model.lockedBalance);
HBox rightNavPane = new HBox(marketPriceBox.third, availableBalanceBox.second, reservedBalanceBox.second, lockedBalanceBox.second,
settingsButton, accountButton) {{
setRightAnchor(this, 10d);
setTopAnchor(this, 0d);
}};
root.widthProperty().addListener((observable, oldValue, newValue) -> {
double w = (double) newValue;
if (w > 0) {
leftNavPane.setSpacing(w >= 1080 ? 10 : 5);
rightNavPane.setSpacing(w >= 1080 ? 10 : 5);
}
});
AnchorPane contentContainer = new AnchorPane() {{
setId("content-pane");
setLeftAnchor(this, 0d);
setRightAnchor(this, 0d);
setTopAnchor(this, 60d);
setBottomAnchor(this, 10d);
}};
AnchorPane applicationContainer = new AnchorPane(leftNavPane, rightNavPane, contentContainer) {{
setId("content-pane");
}};
BorderPane baseApplicationContainer = new BorderPane(applicationContainer) {{
setId("base-content-container");
}};
baseApplicationContainer.setBottom(createFooter());
setupNotificationIcon(portfolioButtonHolder);
setupDisputesIcon(disputesButtonHolder);
navigation.addListener(viewPath -> {
if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0)
return;
Class<? extends View> viewClass = viewPath.tip();
View view = viewLoader.load(viewClass);
contentContainer.getChildren().setAll(view.getRoot());
navButtons.getToggles().stream()
.filter(toggle -> toggle instanceof NavButton)
.filter(button -> viewClass == ((NavButton) button).viewClass)
.findFirst()
.orElseThrow(() -> new BitsquareException("No button matching %s found", viewClass))
.setSelected(true);
});
VBox splashScreen = createSplashScreen();
root.getChildren().addAll(baseApplicationContainer, splashScreen);
model.showAppScreen.addListener((ov, oldValue, newValue) -> {
if (newValue) {
navigation.navigateToPreviousVisitedView();
if (!persistedFilesCorrupted.isEmpty()) {
if (persistedFilesCorrupted.size() > 1 || !persistedFilesCorrupted.get(0).equals("Navigation")) {
// show warning that some files has been corrupted
new Popup().warning("We detected incompatible data base files!\n\n" +
"Those database file(s) are not compatible with our current code base:" +
"\n" + persistedFilesCorrupted.toString() +
"\n\nWe made a backup of the corrupted file(s) and applied the default values to a new " +
"database version." +
"\n\nThe backup is located at:\n[you local app data directory]/db/backup_of_corrupted_data.\n\n" +
"Please check if you have the latest version of Bitsquare installed.\n" +
"You can download it at:\nhttps://github.com/bitsquare/bitsquare/releases\n\n" +
"Please restart the application.")
.closeButtonText("Shut down")
.onClose(BitsquareApp.shutDownHandler::run)
.show();
} else {
log.debug("We detected incompatible data base file for Navigation. That is a minor issue happening with refactoring of UI classes " +
"and we don't display a warning popup to the user.");
}
}
transitions.fadeOutAndRemove(splashScreen, 1500, actionEvent -> disposeSplashScreen());
}
});
// Delay a bit to give time for rendering the splash screen
UserThread.execute(model::start);
}
private Tuple2<TextField, VBox> getBalanceBox(String text) {
TextField textField = new TextField();
textField.setEditable(false);
textField.setPrefWidth(110); //140
textField.setMouseTransparent(true);
textField.setFocusTraversable(false);
textField.setStyle("-fx-alignment: center; -fx-background-color: white;");
Label label = new Label(text);
label.setId("nav-balance-label");
label.setPadding(new Insets(0, 5, 0, 5));
label.setPrefWidth(textField.getPrefWidth());
VBox vBox = new VBox();
vBox.setSpacing(3);
vBox.setPadding(new Insets(11, 0, 0, 0));
vBox.getChildren().addAll(textField, label);
return new Tuple2<>(textField, vBox);
}
private ListCell<PriceFeedComboBoxItem> getPriceFeedComboBoxListCell() {
return new ListCell<PriceFeedComboBoxItem>() {
@Override
protected void updateItem(PriceFeedComboBoxItem item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
textProperty().bind(item.displayStringProperty);
} else {
textProperty().unbind();
}
}
};
}
private Tuple3<ComboBox<PriceFeedComboBoxItem>, Label, VBox> getMarketPriceBox(String text) {
ComboBox<PriceFeedComboBoxItem> priceComboBox = new ComboBox<>();
priceComboBox.setVisibleRowCount(20);
priceComboBox.setMaxWidth(220);
priceComboBox.setMinWidth(220);
priceComboBox.setFocusTraversable(false);
priceComboBox.setId("price-feed-combo");
priceComboBox.setCellFactory(p -> getPriceFeedComboBoxListCell());
ListCell<PriceFeedComboBoxItem> buttonCell = getPriceFeedComboBoxListCell();
buttonCell.setId("price-feed-combo");
priceComboBox.setButtonCell(buttonCell);
Label label = new Label(text);
label.setId("nav-balance-label");
label.setPadding(new Insets(0, 0, 0, 2));
final ImageView btcAverageIcon = new ImageView();
btcAverageIcon.setId("btcaverage");
final Button btcAverageIconButton = new Button("", btcAverageIcon);
btcAverageIconButton.setPadding(new Insets(-1, 0, -1, 0));
btcAverageIconButton.setFocusTraversable(false);
btcAverageIconButton.setStyle("-fx-background-color: transparent;");
HBox.setMargin(btcAverageIconButton, new Insets(0, 5, 0, 0));
btcAverageIconButton.setOnAction(e -> GUIUtil.openWebPage("https://bitcoinaverage.com"));
btcAverageIconButton.setVisible(model.isFiatCurrencyPriceFeedSelected.get());
btcAverageIconButton.setManaged(model.isFiatCurrencyPriceFeedSelected.get());
btcAverageIconButton.visibleProperty().bind(model.isFiatCurrencyPriceFeedSelected);
btcAverageIconButton.managedProperty().bind(model.isFiatCurrencyPriceFeedSelected);
btcAverageIconButton.setOnMouseEntered(e -> {
btcAverageIconButton.setTooltip(new Tooltip("Market price is provided by https://bitcoinaverage.com\n" +
"Last update: " + formatter.formatTime(model.priceFeedService.getLastRequestTimeStampBtcAverage())));
});
final ImageView poloniexIcon = new ImageView();
poloniexIcon.setId("poloniex");
final Button poloniexIconButton = new Button("", poloniexIcon);
poloniexIconButton.setPadding(new Insets(-3, 0, -3, 0));
poloniexIconButton.setFocusTraversable(false);
poloniexIconButton.setStyle("-fx-background-color: transparent;");
HBox.setMargin(poloniexIconButton, new Insets(1, 3, 0, 0));
poloniexIconButton.setOnAction(e -> GUIUtil.openWebPage("https://poloniex.com"));
poloniexIconButton.setVisible(model.isCryptoCurrencyPriceFeedSelected.get());
poloniexIconButton.setManaged(model.isCryptoCurrencyPriceFeedSelected.get());
poloniexIconButton.visibleProperty().bind(model.isCryptoCurrencyPriceFeedSelected);
poloniexIconButton.managedProperty().bind(model.isCryptoCurrencyPriceFeedSelected);
poloniexIconButton.setOnMouseEntered(e -> {
poloniexIconButton.setTooltip(new Tooltip("Market price is provided by https://poloniex.com.\n" +
"If the altcoin is not available at Poloniex we use https://coinmarketcap.com\n" +
"Last update: " + formatter.formatTime(model.priceFeedService.getLastRequestTimeStampPoloniex())));
});
Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS);
HBox hBox2 = new HBox();
hBox2.getChildren().setAll(label, spacer, btcAverageIconButton, poloniexIconButton);
VBox vBox = new VBox();
vBox.setSpacing(3);
vBox.setPadding(new Insets(11, 0, 0, 0));
vBox.getChildren().addAll(priceComboBox, hBox2);
return new Tuple3<>(priceComboBox, label, vBox);
}
public void setPersistedFilesCorrupted(List<String> persistedFilesCorrupted) {
this.persistedFilesCorrupted = persistedFilesCorrupted;
}
private VBox createSplashScreen() {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(0);
vBox.setId("splash");
ImageView logo = new ImageView();
logo.setId("image-splash-logo");
// createBitcoinInfoBox
btcSplashInfo = new Label();
btcSplashInfo.textProperty().bind(model.btcInfo);
walletServiceErrorMsgListener = (ov, oldValue, newValue) -> {
btcSplashInfo.setId("splash-error-state-msg");
};
model.walletServiceErrorMsg.addListener(walletServiceErrorMsgListener);
btcSyncIndicator = new ProgressBar();
btcSyncIndicator.setPrefWidth(120);
btcSyncIndicator.progressProperty().bind(model.btcSyncProgress);
ImageView btcSyncIcon = new ImageView();
btcSyncIcon.setVisible(false);
btcSyncIcon.setManaged(false);
btcSyncIconIdListener = (ov, oldValue, newValue) -> {
btcSyncIcon.setId(newValue);
btcSyncIcon.setVisible(true);
btcSyncIcon.setManaged(true);
btcSyncIndicator.setVisible(false);
btcSyncIndicator.setManaged(false);
};
model.btcSplashSyncIconId.addListener(btcSyncIconIdListener);
HBox blockchainSyncBox = new HBox();
blockchainSyncBox.setSpacing(10);
blockchainSyncBox.setAlignment(Pos.CENTER);
blockchainSyncBox.setPadding(new Insets(40, 0, 0, 0));
blockchainSyncBox.setPrefHeight(50);
blockchainSyncBox.getChildren().addAll(btcSplashInfo, btcSyncIndicator, btcSyncIcon);
// create P2PNetworkBox
splashP2PNetworkLabel = new Label();
splashP2PNetworkLabel.setWrapText(true);
splashP2PNetworkLabel.setMaxWidth(500);
splashP2PNetworkLabel.setTextAlignment(TextAlignment.CENTER);
splashP2PNetworkLabel.textProperty().bind(model.p2PNetworkInfo);
splashP2PNetworkBusyAnimation = new BusyAnimation();
splashP2PNetworkErrorMsgListener = (ov, oldValue, newValue) -> {
if (newValue != null) {
splashP2PNetworkLabel.setId("splash-error-state-msg");
splashP2PNetworkBusyAnimation.stop();
} else if (model.splashP2PNetworkAnimationVisible.get()) {
splashP2PNetworkBusyAnimation.play();
}
};
model.p2pNetworkWarnMsg.addListener(splashP2PNetworkErrorMsgListener);
ImageView splashP2PNetworkIcon = new ImageView();
splashP2PNetworkIcon.setId("image-connection-tor");
splashP2PNetworkIcon.setVisible(false);
splashP2PNetworkIcon.setManaged(false);
HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0));
splashP2PNetworkIconIdListener = (ov, oldValue, newValue) -> {
splashP2PNetworkIcon.setId(newValue);
splashP2PNetworkIcon.setVisible(true);
splashP2PNetworkIcon.setManaged(true);
};
model.p2PNetworkIconId.addListener(splashP2PNetworkIconIdListener);
splashP2PNetworkVisibleListener = (ov, oldValue, newValue) -> splashP2PNetworkBusyAnimation.setIsRunning(newValue);
model.splashP2PNetworkAnimationVisible.addListener(splashP2PNetworkVisibleListener);
HBox splashP2PNetworkBox = new HBox();
splashP2PNetworkBox.setSpacing(10);
splashP2PNetworkBox.setAlignment(Pos.CENTER);
splashP2PNetworkBox.setPrefHeight(50);
splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon);
vBox.getChildren().addAll(logo, blockchainSyncBox, splashP2PNetworkBox);
return vBox;
}
private void disposeSplashScreen() {
model.walletServiceErrorMsg.removeListener(walletServiceErrorMsgListener);
model.btcSplashSyncIconId.removeListener(btcSyncIconIdListener);
model.p2pNetworkWarnMsg.removeListener(splashP2PNetworkErrorMsgListener);
model.p2PNetworkIconId.removeListener(splashP2PNetworkIconIdListener);
model.splashP2PNetworkAnimationVisible.removeListener(splashP2PNetworkVisibleListener);
btcSplashInfo.textProperty().unbind();
btcSyncIndicator.progressProperty().unbind();
splashP2PNetworkLabel.textProperty().unbind();
model.onSplashScreenRemoved();
}
private AnchorPane createFooter() {
// line
Separator separator = new Separator();
separator.setId("footer-pane-line");
separator.setPrefHeight(1);
setLeftAnchor(separator, 0d);
setRightAnchor(separator, 0d);
setTopAnchor(separator, 0d);
// BTC
Label btcInfoLabel = new Label();
btcInfoLabel.setId("footer-pane");
btcInfoLabel.textProperty().bind(model.btcInfo);
ProgressBar blockchainSyncIndicator = new ProgressBar(-1);
blockchainSyncIndicator.setPrefWidth(120);
blockchainSyncIndicator.setMaxHeight(10);
blockchainSyncIndicator.progressProperty().bind(model.btcSyncProgress);
model.walletServiceErrorMsg.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
btcInfoLabel.setId("splash-error-state-msg");
if (btcNetworkWarnMsgPopup == null) {
btcNetworkWarnMsgPopup = new Popup<>().warning(newValue);
btcNetworkWarnMsgPopup.show();
}
} else {
btcInfoLabel.setId("footer-pane");
if (btcNetworkWarnMsgPopup != null)
btcNetworkWarnMsgPopup.hide();
}
});
model.btcSyncProgress.addListener((ov, oldValue, newValue) -> {
if ((double) newValue >= 1) {
blockchainSyncIndicator.setVisible(false);
blockchainSyncIndicator.setManaged(false);
}
});
HBox blockchainSyncBox = new HBox();
blockchainSyncBox.setSpacing(10);
blockchainSyncBox.setAlignment(Pos.CENTER);
blockchainSyncBox.getChildren().addAll(btcInfoLabel, blockchainSyncIndicator);
setLeftAnchor(blockchainSyncBox, 10d);
setBottomAnchor(blockchainSyncBox, 7d);
// version
Label versionLabel = new Label();
versionLabel.setId("footer-pane");
versionLabel.setTextAlignment(TextAlignment.CENTER);
versionLabel.setAlignment(Pos.BASELINE_CENTER);
versionLabel.setText(model.version);
root.widthProperty().addListener((ov, oldValue, newValue) -> {
versionLabel.setLayoutX(((double) newValue - versionLabel.getWidth()) / 2);
});
setBottomAnchor(versionLabel, 7d);
// P2P Network
Label p2PNetworkLabel = new Label();
p2PNetworkLabel.setId("footer-pane");
setRightAnchor(p2PNetworkLabel, 33d);
setBottomAnchor(p2PNetworkLabel, 7d);
p2PNetworkLabel.textProperty().bind(model.p2PNetworkInfo);
ImageView p2PNetworkIcon = new ImageView();
setRightAnchor(p2PNetworkIcon, 10d);
setBottomAnchor(p2PNetworkIcon, 7d);
p2PNetworkIcon.setOpacity(0.4);
p2PNetworkIcon.idProperty().bind(model.p2PNetworkIconId);
p2PNetworkLabel.idProperty().bind(model.p2pNetworkLabelId);
model.p2pNetworkWarnMsg.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
p2PNetworkWarnMsgPopup = new Popup<>().warning(newValue);
p2PNetworkWarnMsgPopup.show();
} else if (p2PNetworkWarnMsgPopup != null) {
p2PNetworkWarnMsgPopup.hide();
}
});
model.bootstrapComplete.addListener((observable, oldValue, newValue) -> {
p2PNetworkIcon.setOpacity(1);
});
return new AnchorPane(separator, blockchainSyncBox, versionLabel, p2PNetworkLabel, p2PNetworkIcon) {{
setId("footer-pane");
setMinHeight(30);
setMaxHeight(30);
}};
}
private void setupNotificationIcon(Pane buttonHolder) {
Label label = new Label();
label.textProperty().bind(model.numPendingTradesAsString);
label.relocate(5, 1);
label.setId("nav-alert-label");
ImageView icon = new ImageView();
icon.setLayoutX(0.5);
icon.setId("image-alert-round");
Pane notification = new Pane();
notification.relocate(30, 9);
notification.setMouseTransparent(true);
notification.setEffect(new DropShadow(4, 1, 2, Color.GREY));
notification.getChildren().addAll(icon, label);
notification.visibleProperty().bind(model.showPendingTradesNotification);
buttonHolder.getChildren().add(notification);
}
private void setupDisputesIcon(Pane buttonHolder) {
Label label = new Label();
label.textProperty().bind(model.numOpenDisputesAsString);
label.relocate(5, 1);
label.setId("nav-alert-label");
ImageView icon = new ImageView();
icon.setLayoutX(0.5);
icon.setId("image-alert-round");
Pane notification = new Pane();
notification.relocate(30, 9);
notification.setMouseTransparent(true);
notification.setEffect(new DropShadow(4, 1, 2, Color.GREY));
notification.getChildren().addAll(icon, label);
notification.visibleProperty().bind(model.showOpenDisputesNotification);
buttonHolder.getChildren().add(notification);
}
private class NavButton extends ToggleButton {
private final Class<? extends View> viewClass;
public NavButton(Class<? extends View> viewClass, String title) {
super(title, new ImageView() {{
setId("image-nav-" + viewId(viewClass));
}});
this.viewClass = viewClass;
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-" + viewId(viewClass) + "-active");
} else {
this.getGraphic().setId("image-nav-" + viewId(viewClass));
}
});
this.setOnAction(e -> navigation.navigateTo(MainView.class, viewClass));
}
}
private static String viewId(Class<? extends View> viewClass) {
String viewName = viewClass.getSimpleName();
String suffix = "View";
int suffixIdx = viewName.indexOf(suffix);
if (suffixIdx != viewName.length() - suffix.length())
throw new IllegalArgumentException("Cannot get ID for " + viewClass + ": class must end in " + suffix);
return viewName.substring(0, suffixIdx).toLowerCase();
}
}