/* * Follow the Bitcoin * Copyright (C) 2014 Danno Ferrin * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.shemnon.btc.ftm; import com.shemnon.btc.analysis.Pyramid; import com.shemnon.btc.analysis.SpendAndChange; import com.shemnon.btc.coinbase.CBAddress; import com.shemnon.btc.coinbase.CBTransaction; import com.shemnon.btc.coinbase.CoinBaseAPI; import com.shemnon.btc.coinbase.CoinBaseOAuth; import com.shemnon.btc.model.*; import com.shemnon.btc.view.GraphViewMXGraph; import com.shemnon.btc.view.ZoomPane; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableSet; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.web.WebView; import javafx.util.Duration; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.stream.Collectors; import static javafx.beans.binding.Bindings.isEmpty; import static javafx.beans.binding.Bindings.not; /** * * Created by shemnon on 4 Mar 2014. */ public class FTM { @FXML HBox boxHeader; @FXML VBox boxRightSide; @FXML VBox boxSide; @FXML Button buttonLogin; @FXML Button buttonLogout; @FXML CheckBox checkTxBtc; @FXML CheckBox checkTxCoins; @FXML CheckBox checkTxDate; @FXML CheckBox checkTxHash; @FXML CheckBox checkTxHeight; @FXML CheckBox checkTxUsd; @FXML CheckBox checkUnspentBtc; @FXML CheckBox checkUnspentHash; @FXML CheckBox checkUnspentUsd; @FXML CheckBox helpCheckbox; @FXML AnchorPane helpPane; @FXML Label labelProgressBacklog; @FXML ToggleGroup lines; @FXML AnchorPane mapPane; @FXML AnchorPane paneLogin; @FXML ProgressIndicator progressIndicator; @FXML RadioButton radioLineAddr; @FXML RadioButton radioLineBTC; @FXML RadioButton radioLineNone; @FXML RadioButton radioLineUSD; @FXML TextField textSearch; @FXML ToggleButton toggleHelp; @FXML ToggleButton toggleRightSidebar; @FXML ToggleButton toggleSidebar; @FXML TreeView<IBase> treeViewEntries; @FXML WebView webViewLogin; TreeItem<IBase> treeRoot = new TreeItem<>(new JsonBaseLabel("")); TreeItem<IBase> coinbaseTreeLabel = new TreeItem<>(new JsonBaseLabel("Coinbase Entries")); TreeItem<IBase> coinbaseAddresses = new TreeItem<>(new JsonBaseLabel("Addresses")); TreeItem<IBase> coinbaseTransactions = new TreeItem<>(new JsonBaseLabel("Transactions")); CoinBaseOAuth coinBaseAuth; CoinBaseAPI coinBaseAPI; Duration slideTime = Duration.millis(400); Timeline sidebarTimeline; Timeline rightSidebarTimeline; ExecutorService offThreadExecutor = Executors.newSingleThreadExecutor(); ObservableList<Future<?>> futures = FXCollections.observableArrayList(); ObjectProperty<IBase> menuSelectedItem = new SimpleObjectProperty<>(null); MenuItem miExpand = new MenuItem("Expand"); MenuItem miExpandOutputs = new MenuItem("Expand Outputs"); MenuItem miExpandAllOutputs = new MenuItem("Expand All Outputs"); MenuItem miExpandInputs = new MenuItem("Expand Inputs"); MenuItem miExpandAllInputs = new MenuItem("Expand All Inputs"); MenuItem miClimbPyramid = new MenuItem("Climb Pyramid"); MenuItem miDescendPyramid = new MenuItem("Descend Pyramid"); MenuItem miClimbSNC = new MenuItem("Climb Spend and Change"); MenuItem miDescendSNC = new MenuItem("Descend Spend and Change"); MenuItem miRemoveTX = new MenuItem("Remove Transaction"); MenuItem miPruneUpwards = new MenuItem("Prune Tx and Inputs"); MenuItem miPruneDownwards = new MenuItem("Prune Tx and Outputs"); ContextMenu nodeContextMenu = new ContextMenu( miExpand, new SeparatorMenuItem(), miExpandOutputs, miExpandAllOutputs, new SeparatorMenuItem(), miExpandInputs, miExpandAllInputs, new SeparatorMenuItem(), miClimbPyramid, miDescendPyramid, new SeparatorMenuItem(), miClimbSNC, miDescendSNC, new SeparatorMenuItem(), miRemoveTX, miPruneUpwards, miPruneDownwards ); private GraphViewMXGraph gv; private ObservableSet<IBase> graphSelections; private ZoomPane zp; public void offThread(Runnable r) { Future<?> f = offThreadExecutor.submit(() -> { try { r.run(); } catch (Exception e) { e.printStackTrace(); throw e; } }); futures.add(f); Timeline t = new Timeline(new KeyFrame(Duration.millis(100), event -> { futures.removeIf(Future::isDone); if (futures.isEmpty()) { labelProgressBacklog.setText(""); } else { labelProgressBacklog.setText(Integer.toString(futures.size())); } } )); t.setCycleCount(Animation.INDEFINITE); t.play(); } @FXML public void newHash(ActionEvent event) { String hash = textSearch.getText(); offThread(() -> { IBase jd; // switch guessing o type if (hash.length() == 64) { if (hash.startsWith("00000000")) { // guess block hash jd = IBlock.query(hash); } else { // guess transaction jd = ITx.query(hash); } } else if (hash.matches(IAddress.BITCOIN_ADDRESS_REGEX)) { // guess address jd = IAddress.query(hash); } else if (hash.matches("\\d+")) { // guess block height jd = IBlock.query(hash); } else { // guess failure //TODO complain? return; } IBase jb = jd; expandObject(jb); Platform.runLater(() -> { gv.layout(); gv.rebuildGraph(true); zp.layout(); zp.centerOnNode(gv.getNode(jb)); }); }); } private void expandBlock(IBlock bi) { if (Platform.isFxApplicationThread()) { offThread(() -> expandBlock(bi)); } else { bi.getTXs().forEach(this::expandTransaction); } } private void expandTransaction(ITx tx) { if (Platform.isFxApplicationThread()) { offThread(() -> expandTransaction(tx)); } else { expandInputs(tx); expandOutputs(tx); } } private void expandOutputs(ITx tx) { //todo check flag to visualize unspent outputs tx.getOutputs().forEach(gv::addCoin); gv.updateExpanded(); } private void expandInputs(ITx tx) { tx.getInputs().forEach(gv::addCoin); gv.updateExpanded(); } private void expandAddress(IAddress ai) { String address = ai.getAddress(); if (address == null) return; if (Platform.isFxApplicationThread()) { offThread(() -> expandAddress(ai)); } else { ai.getTXs().forEach(tx -> { Consumer<? super ICoin> maybeShowCoin = coin -> { if (address.equals(coin.getAddr())) { gv.addCoin(coin); } }; tx.getInputs().forEach(maybeShowCoin); tx.getOutputs().forEach(maybeShowCoin); }); gv.updateExpanded(); } } @FXML public void onReset(ActionEvent event) { gv.reset(); gv.layout(); gv.rebuildGraph(false); } @FXML public void onCenter(ActionEvent event) { zp.center(); } @FXML public void onZoomToFit(ActionEvent event) { zp.fit(); } @FXML public void onDeZoom(ActionEvent event) { zp.zoomOneToOne(); } @FXML public void toggleSidebar(ActionEvent event) { if (toggleSidebar.isSelected()) { showSidebar(); } else { hideSidebar(); } } private void showSidebar() { sidebarTimeline.setRate(1); Duration time = sidebarTimeline.getCurrentTime(); sidebarTimeline.stop(); sidebarTimeline.playFrom(time); hideRightSidebar(); toggleRightSidebar.setSelected(false); } private void hideSidebar() { sidebarTimeline.setRate(-1); Duration time = sidebarTimeline.getCurrentTime(); sidebarTimeline.stop(); sidebarTimeline.playFrom(time); } @FXML public void toggleRightSidebar(ActionEvent event) { if (toggleRightSidebar.isSelected()) { showRightSidebar(); } else { hideRightSidebar(); } } private void showRightSidebar() { rightSidebarTimeline.setRate(1); Duration time = rightSidebarTimeline.getCurrentTime(); rightSidebarTimeline.stop(); rightSidebarTimeline.playFrom(time); hideSidebar(); toggleSidebar.setSelected(false); } private void hideRightSidebar() { rightSidebarTimeline.setRate(-1); Duration time = rightSidebarTimeline.getCurrentTime(); rightSidebarTimeline.stop(); rightSidebarTimeline.playFrom(time); } @FXML void loginToCoinbase(ActionEvent event) { coinBaseAuth.checkTokens(true, true); } @FXML void logoutOfCoinbase(ActionEvent event) { coinBaseAuth.clearTokens(); } @FXML void closeWebView(ActionEvent event) { coinBaseAuth.setVisualAuthInProgress(false); paneLogin.setVisible(false); } @FXML void closeHelp(ActionEvent event) { helpPane.setVisible(false); } @FXML void toggleHelp(ActionEvent event) { helpPane.setVisible(!helpPane.isVisible()); } public void updateCoinbaseData() { String token = coinBaseAuth.getAccessToken(); //noinspection StatementWithEmptyBody if (token == null) { // do nothing } else if (token.isEmpty()) { Platform.runLater(() -> { // expand famous transactions TreeItem<IBase> famousTransactions = treeViewEntries.getRoot().getChildren().get(0); famousTransactions.getChildren().forEach(ti -> ti.setExpanded(true)); famousTransactions.setExpanded(true); //JavaFX bug workaround treeViewEntries.setShowRoot(true); treeViewEntries.setShowRoot(false); showSidebar(); toggleSidebar.setSelected(true); }); } else { String un = coinBaseAPI.getUserName(); List<CBAddress> addresses = coinBaseAPI.getAddresses(); List<CBTransaction> transactions = coinBaseAPI.getTransactions(); // scrub in-coinbase transaction since they supply no blockchain hash transactions.removeIf(trans -> (trans.getHash() == null)); Platform.runLater(() -> { buttonLogout.setText("Logout " + un); // expand coinbase children // expand coinbase coinbaseTreeLabel.setExpanded(true); coinbaseAddresses.setExpanded(true); coinbaseTransactions.setExpanded(true); //noinspection Convert2Diamond,Convert2MethodRef coinbaseAddresses.getChildren().setAll(addresses.stream() .map(addr -> new TreeItem<IBase>(addr)) .collect(Collectors.toList()) ); //noinspection Convert2Diamond,Convert2MethodRef coinbaseTransactions.getChildren().setAll(transactions.stream() .map(tx -> new TreeItem<IBase>(tx)) .collect(Collectors.toList()) ); //JavaFX bug workaround treeViewEntries.setShowRoot(true); treeViewEntries.setShowRoot(false); showSidebar(); toggleSidebar.setSelected(true); }); } } public void expandObject(Object jbo) { if (Platform.isFxApplicationThread()) { offThread(() -> expandObject(jbo)); } else { // thunk out coinbase to blockchain Object jb = jbo; if (jb instanceof CBAddress) { jb = IAddress.query(((CBAddress) jb).getAddress()); } else if (jb instanceof CBTransaction) { jb = ITx.query(((CBTransaction) jb).getHash()); } // expand blockchain if (jb instanceof IAddress) { expandAddress((IAddress) jb); } else if (jb instanceof ITx) { expandTransaction((ITx) jb); } else if (jb instanceof IBlock) { expandBlock((IBlock) jb); } else if (jb instanceof ICoin) { gv.addCoin((ICoin) jb); gv.updateExpanded(); } else { System.out.println("playSound(StandardSounds.SAD_TROMBONE)"); } } } private void updateHighlights() { } private void expandSelected(ActionEvent event) { offThread(() -> { expandObject(menuSelectedItem.get()); graphNeedsUpdating(true); }); } private void expandOutputs(ActionEvent event) { offThread(() -> { expandOutputs((ITx) menuSelectedItem.get()); graphNeedsUpdating(true); }); } private void expandAllOutputs(ActionEvent event) { offThread(() -> { for (ITx tx : gv.findUnexpandedOutputTX((ITx) menuSelectedItem.get())) { expandOutputs(tx); } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void expandInputs(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ICoin) { gv.addCoin((ICoin) o); } else if (o instanceof ITx) { expandInputs((ITx) o); } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void expandAllInputs(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ICoin) { gv.addCoin((ICoin)o); o = ((ICoin)o).getSourceTX(); } if (o instanceof ITx) { for (ITx tx : gv.findUnexpandedInputTX((ITx) o)) { expandInputs(tx); } } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void climbPyramid(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ITx) { for (ITx tx : Pyramid.climbPyramid((ITx) o, 20)) { expandInputs(tx); } } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void descendPyramid(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ITx) { for (ITx tx : Pyramid.descendPyramid((ITx) o, 2)) { expandOutputs(tx); } } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void climbSNC(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ITx) { for (ITx tx : SpendAndChange.climbSpendAndChange((ITx) o, 20)) { expandTransaction(tx); } } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void descendSND(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ITx) { for (ITx tx : SpendAndChange.descendSpendAndChange((ITx) o, 20)) { expandTransaction(tx); } } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void removeSelected(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ICoin) { gv.removeCoinAsNode((ICoin) o); } else if (o instanceof ITx) { gv.removeTX((ITx) o); } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void pruneUpwards(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ICoin) { gv.removeTXAndAllInputs(((ICoin)o).getSourceTX()); gv.removeCoinAsNode((ICoin) o); } else if (o instanceof ITx) { gv.removeTXAndAllInputs((ITx) o); } gv.updateExpanded(); graphNeedsUpdating(true); }); } private void pruneDownwards(ActionEvent event) { offThread(() -> { Object o = menuSelectedItem.get(); if (o instanceof ITx) { gv.removeTXAndAllOutputs((ITx) o); } gv.updateExpanded(); graphNeedsUpdating(true); }); } @FXML void initialize() { try { miExpand.setOnAction(this::expandSelected); miExpandOutputs.setOnAction(this::expandOutputs); miExpandAllOutputs.setOnAction(this::expandAllOutputs); miExpandInputs.setOnAction(this::expandInputs); miExpandAllInputs.setOnAction(this::expandAllInputs); miClimbPyramid.setOnAction(this::climbPyramid); miDescendPyramid.setOnAction(this::descendPyramid); miClimbSNC.setOnAction(this::climbSNC); miDescendSNC.setOnAction(this::descendSND); miRemoveTX.setOnAction(this::removeSelected); miPruneUpwards.setOnAction(this::pruneUpwards); miPruneDownwards.setOnAction(this::pruneDownwards); menuSelectedItem.addListener((obv, newN, oldN) -> { if (oldN instanceof ITx) { miExpand.setDisable(false); // TODO check expanded miExpandOutputs.setDisable(false); // TODO check expanded miExpandAllOutputs.setDisable(false); miExpandInputs.setDisable(false); // TODO check expanded miExpandAllInputs.setDisable(false); miClimbPyramid.setDisable(false); miDescendPyramid.setDisable(false); miClimbSNC.setDisable(false); miDescendSNC.setDisable(false); miRemoveTX.setDisable(false); miPruneUpwards.setDisable(false); miPruneDownwards.setDisable(false); } else if (oldN instanceof ICoin) { miExpand.setDisable(false); // TODO check expanded miExpandOutputs.setDisable(true); miExpandAllOutputs.setDisable(true); miExpandInputs.setDisable(true); miExpandAllInputs.setDisable(false); miClimbPyramid.setDisable(true); //FIXME this can be done miDescendPyramid.setDisable(true); miClimbSNC.setDisable(true); //FIXME this can be done miDescendSNC.setDisable(true); miRemoveTX.setDisable(false); miPruneUpwards.setDisable(false); miPruneDownwards.setDisable(true); } else { miExpand.setDisable(true); miExpandOutputs.setDisable(true); miExpandAllOutputs.setDisable(true); miExpandInputs.setDisable(true); miExpandAllInputs.setDisable(true); miClimbPyramid.setDisable(true); miDescendPyramid.setDisable(true); miClimbSNC.setDisable(true); miDescendSNC.setDisable(true); miRemoveTX.setDisable(true); miPruneUpwards.setDisable(true); miPruneDownwards.setDisable(true); } }); progressIndicator.visibleProperty().bind(not(isEmpty(futures))); //noinspection unchecked treeRoot.getChildren().setAll(FamousEntries.createFamousTree(), coinbaseTreeLabel); //noinspection unchecked coinbaseTreeLabel.getChildren().setAll(coinbaseAddresses, coinbaseTransactions); treeViewEntries.setRoot(treeRoot); treeViewEntries.setCellFactory(list -> new CoinbaseDataCell(jd -> { expandObject(jd); offThread(() -> Platform.runLater(() -> { gv.layout(); gv.rebuildGraph(true); zp.layout(); zp.centerOnNode(gv.getNode(jd)); })); })); treeViewEntries.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); treeViewEntries.getSelectionModel().getSelectedItems().addListener((InvalidationListener) event -> updateHighlights()); gv = new GraphViewMXGraph((event, obj) -> { if (event.isPopupTrigger() || (event.getButton() == MouseButton.SECONDARY && event.getClickCount() == 1)) { // popup menu click menuSelectedItem.setValue(obj); nodeContextMenu.show(event.getPickResult().getIntersectedNode(), event.getScreenX(), event.getScreenY()); } else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2 && obj instanceof ITx) { // double click - default action expandTransaction((ITx) obj); graphNeedsUpdating(true); } else { // selection click // for now punt: any modifier is in multi-select if (event.isAltDown() || event.isControlDown() || event.isShiftDown() || event.isMetaDown()) { if (graphSelections.contains(obj)) { graphSelections.remove(obj); } else { graphSelections.add(obj); } } else { // not multi-select, so set to current node graphSelections.clear(); graphSelections.add(obj); } } }); graphSelections = gv.getSelectedItems(); graphSelections.addListener((javafx.collections.SetChangeListener<? super IBase>) change -> { IBase obj = change.getElementAdded(); if (obj instanceof ICoin) { textSearch.setText(((ICoin)obj).getAddr()); } else if (obj instanceof ITx) { textSearch.setText(((ITx)obj).getHash()); } else if (obj instanceof IBlock) { textSearch.setText(((IBlock)obj).getHash()); } else { textSearch.setText(""); } }); zp = new ZoomPane(gv.getGraphPane()); mapPane.getChildren().setAll(zp); AnchorPane.setTopAnchor(zp, 0.0); AnchorPane.setRightAnchor(zp, 0.0); AnchorPane.setBottomAnchor(zp, 0.0); AnchorPane.setLeftAnchor(zp, 0.0); coinBaseAuth = new CoinBaseOAuth(webViewLogin); //can't bind because we want a weak push coinBaseAuth.visualAuthInProgressProperty().addListener((obv, o, n) -> paneLogin.setVisible(n)); buttonLogin.managedProperty().bind(buttonLogin.visibleProperty()); buttonLogout.managedProperty().bind(buttonLogout.visibleProperty()); buttonLogout.visibleProperty().bind( Bindings.and( Bindings.isNotNull(coinBaseAuth.accessTokenProperty()), Bindings.notEqual("", coinBaseAuth.accessTokenProperty()))); buttonLogin.visibleProperty().bind( Bindings.and( Bindings.isNotNull(coinBaseAuth.accessTokenProperty()), Bindings.equal("", coinBaseAuth.accessTokenProperty()))); WritableValue<Number> leftOffset = new WritableValue<Number>() { @Override public Number getValue() { return AnchorPane.getLeftAnchor(boxHeader); } @Override public void setValue(Number value) { AnchorPane.setLeftAnchor(boxHeader, value.doubleValue()); } }; WritableValue<Number> rightOffset = new WritableValue<Number>() { @Override public Number getValue() { return AnchorPane.getRightAnchor(boxHeader); } @Override public void setValue(Number value) { AnchorPane.setRightAnchor(boxHeader, value.doubleValue()); } }; sidebarTimeline = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(boxSide.translateXProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(leftOffset, 0, Interpolator.EASE_BOTH)), new KeyFrame(slideTime, new KeyValue(boxSide.translateXProperty(), 250, Interpolator.EASE_BOTH), new KeyValue(leftOffset, 250, Interpolator.EASE_BOTH)) ); sidebarTimeline.setAutoReverse(false); rightSidebarTimeline = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(boxRightSide.translateXProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(rightOffset, 0, Interpolator.EASE_BOTH)), new KeyFrame(slideTime, new KeyValue(boxRightSide.translateXProperty(), -250, Interpolator.EASE_BOTH), new KeyValue(rightOffset, 250, Interpolator.EASE_BOTH)) ); rightSidebarTimeline.setAutoReverse(false); gv.showTxHashProperty().bindBidirectional(checkTxHash.selectedProperty()); gv.showTxBitcoinProperty().bindBidirectional(checkTxBtc.selectedProperty()); gv.showTxUSDProperty().bindBidirectional(checkTxUsd.selectedProperty()); gv.showTxDateProperty().bindBidirectional(checkTxDate.selectedProperty()); gv.showTxHeightProperty().bindBidirectional(checkTxHeight.selectedProperty()); gv.showTxCoinCountProperty().bindBidirectional(checkTxCoins.selectedProperty()); gv.showCoinNodeHashProperty().bindBidirectional(checkUnspentHash.selectedProperty()); gv.showCoinNodeBitcoinProperty().bindBidirectional(checkUnspentBtc.selectedProperty()); gv.showCoinNodeUSDProperty().bindBidirectional(checkUnspentUsd.selectedProperty()); gv.layoutFlags().forEach(bp -> bp.addListener((obv, o, n) -> { gv.resizeGraphNodes(); gv.layout(); gv.rebuildGraph(false); }) ); radioLineBTC.selectedProperty().addListener((obv, o, n) -> { if (n) { changeLineSummary(true, false, false); } }); radioLineUSD.selectedProperty().addListener((obv, o, n) -> { if (n) { changeLineSummary(false, true, false); } }); radioLineAddr.selectedProperty().addListener((obv, o, n) -> { if (n) { changeLineSummary(false, false, true); } }); radioLineNone.selectedProperty().addListener((obv, o, n) -> { if (n) { changeLineSummary(false, false, false); } }); coinBaseAPI = new CoinBaseAPI(coinBaseAuth, false, false); coinBaseAuth.accessTokenProperty().addListener(change -> offThread(this::updateCoinbaseData)); offThread(() -> coinBaseAuth.checkTokens(true, false)); helpCheckbox.selectedProperty().addListener((obv, o, n) -> coinBaseAuth.saveToken(Boolean.toString(n), "hideHelp")); String hideHelp = coinBaseAuth.loadToken("hideHelp"); if (Boolean.valueOf(hideHelp)) { helpCheckbox.setSelected(true); toggleHelp.setSelected(false); helpPane.setVisible(false); } else { helpCheckbox.setSelected(false); toggleHelp.setSelected(true); helpPane.setVisible(true); } } catch (Exception e) { e.printStackTrace(System.out); } } private void changeLineSummary(boolean btc, boolean usd, boolean addr) { ICoin.setShowEdgeBTC(btc); ICoin.setShowEdgeUSD(!btc && usd); ICoin.setShowEdgeAddr(!btc && !usd && addr); gv.resizeGraphNodes(); gv.layout(); gv.rebuildGraph(false); } private void graphNeedsUpdating(boolean animate) { offThread(() -> Platform.runLater(() -> { gv.layout(); gv.rebuildGraph(animate); })); } }