/* * 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.view; import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; import com.mxgraph.layout.mxGraphLayout; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxGeometry; import com.mxgraph.model.mxIGraphModel; import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxGraphView; import com.shemnon.btc.coinbase.CBTransaction; import com.shemnon.btc.model.*; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.util.Duration; import javax.swing.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.BiConsumer; import java.util.function.Consumer; /** * * Created by shemnon on 3 Mar 2014. */ public class GraphViewMXGraph { ObservableSet<IBase> selectedItems = FXCollections.checkedObservableSet( FXCollections.observableSet(new HashSet<>()), IBase.class); BiConsumer<MouseEvent, IBase> clickHandler; mxGraph mxGraph; mxIGraphModel mxGraphModel; private mxGraphView mxGraphView; mxGraphLayout mxGraphLayout; Map<String, Object> keyToVertexCell = new ConcurrentHashMap<>(); Map<String, Object> keyToEdgeCell = new ConcurrentHashMap<>(); Map<String, Pane> keyToVertexNode = new ConcurrentHashMap<>(); Map<String, List<Line>> keyToEdgeNode = new ConcurrentHashMap<>(); Map<String, Label> keyToEdgeLabel = new ConcurrentHashMap<>(); Pane graphPane; BooleanProperty showTxHash = new SimpleBooleanProperty(true); BooleanProperty showTxBitcoin = new SimpleBooleanProperty(true); BooleanProperty showTxUSD = new SimpleBooleanProperty(false); BooleanProperty showTxDate = new SimpleBooleanProperty(true); BooleanProperty showTxHeight = new SimpleBooleanProperty(false); BooleanProperty showTxCoinCount = new SimpleBooleanProperty(false); BooleanProperty showCoinNodeHash = new SimpleBooleanProperty(true); BooleanProperty showCoinNodeBitcoin = new SimpleBooleanProperty(true); BooleanProperty showCoinNodeUSD = new SimpleBooleanProperty(true); Timeline animatingTimeline; public GraphViewMXGraph(BiConsumer<MouseEvent, IBase> clickHandler) { this.clickHandler = clickHandler; mxGraph = new mxGraph(); mxGraphModel = mxGraph.getModel(); mxGraphView = mxGraph.getView(); mxGraphLayout = new mxHierarchicalLayout(mxGraph, SwingConstants.NORTH); graphPane = new Pane(); selectedItems.addListener((SetChangeListener<? super IBase>) change -> updateSelectedNodes()); } public Object addTransaction(ITx tx) { try { String key = tx.getHash(); Object o = keyToVertexCell.get(key); if (o == null || !mxGraphModel.isVertex(o)) { Node n = getFXNodeForVertexCell(tx); o = mxGraph.insertVertex(mxGraph.getDefaultParent(), tx.getHash(), tx, 0, 0, n.prefWidth(0), n.prefHeight(0)); keyToVertexCell.put(key, o); } return o; } catch (Exception e) { e.printStackTrace(); throw e; } } public Object addCoinAsNode(ICoin coin) { try { String key = coin.getCompkey(); Object o = keyToVertexCell.get(key); if (o == null || !mxGraphModel.isVertex(o)) { Node n = getFXNodeForVertexCell(coin); o = mxGraph.insertVertex(mxGraph.getDefaultParent(), coin.getCompkey(), coin, 0, 0, n.prefWidth(0), n.prefHeight(0)); keyToVertexCell.put(key, o); } return o; } catch (Exception e) { e.printStackTrace(); throw e; } } public Object addCoin(ICoin ci) { try { String key = ci.getCompkey(); Object o = keyToEdgeCell.get(key); if ((o == null || !mxGraphModel.isEdge(o)) && ci.getSourceTXID() != null) { ITx fromTX = ci.getSourceTX(); ITx toTX = ci.getTargetTX(); Object from = addTransaction(fromTX); Object to; if (toTX == null) { to = addCoinAsNode(ci); } else { to = addTransaction(toTX); } o = mxGraph.insertEdge(mxGraph.getDefaultParent(), key, ci, from, to); keyToEdgeCell.put(key, o); } return o; } catch (Exception e) { e.printStackTrace(); throw e; } } private void updateSelectedNodes() { Consumer<Object> checkCell = cell -> { Object v = ((mxCell)cell).getValue(); if (v instanceof ITx) { updateSelected((ITx) v); } else if (v instanceof ICoin) { updateSelected((ICoin) v); } }; keyToVertexCell.values().forEach(checkCell); keyToEdgeCell.values().forEach(checkCell); } private void updateSelected(ITx tx) { if (tx == null) return; Node n = keyToVertexNode.get(tx.getHash()); if (n == null) return; boolean selected = selectedItems.contains(tx); Platform.runLater(() -> { if (selected) { if (!n.getStyleClass().contains("selected")) { n.getStyleClass().add("selected"); } } else { while (n.getStyleClass().remove("selected")) ; } }); } private void updateSelected(ICoin coin) { if (coin == null) return; boolean selected = selectedItems.contains(coin); Node n = keyToVertexNode.get(coin.getCompkey()); if (n != null) { Platform.runLater(() -> { if (selected) { if (!n.getStyleClass().contains("selected")) { n.getStyleClass().add("selected"); } } else { while (n.getStyleClass().remove("selected")) ; } }); } List<Line> lines = keyToEdgeNode.get(coin.getCompkey()); if (lines != null) { Platform.runLater(() -> { for (Line l : lines) { if (selected) { if (!l.getStyleClass().contains("selected")) { l.getStyleClass().add("selected"); } } else { while (l.getStyleClass().remove("selected")) ; } } }); } Label l = keyToEdgeLabel.get(coin.getCompkey()); if (l != null) { Platform.runLater(() -> { if (selected) { if (!l.getStyleClass().contains("selected")) { l.getStyleClass().add("selected"); } } else { while (l.getStyleClass().remove("selected")) ; } }); } } public void updateExpanded() { keyToVertexCell.values().forEach(cell -> { Object v = ((mxCell)cell).getValue(); if (v instanceof ITx) { updateExpanded((ITx) v); } }); } private void updateExpanded(ITx tx) { if (tx == null) return; Node n = keyToVertexNode.get(tx.getHash()); if (n == null) return; boolean inputsExpanded = tx.getInputs().stream().allMatch(ci -> { String srcTXID = ci.getSourceTXID(); boolean exists = srcTXID != null && keyToVertexCell.containsKey(srcTXID); if (exists && !keyToEdgeCell.containsKey(ci.getCompkey())) { addCoin(ci); } return exists; }); boolean outputsExpanded = tx.getOutputs().stream().allMatch(ci -> { String targetTXID = ci.getTargetTXID(); if (targetTXID == null) { boolean exists = keyToVertexCell.containsKey(ci.getCompkey()); if (exists && !keyToEdgeCell.containsKey(ci.getCompkey())) { addCoin(ci); } return exists; } else { boolean exists = keyToVertexCell.containsKey(targetTXID); if (exists && !keyToEdgeCell.containsKey(ci.getCompkey())) { addCoin(ci); } return exists; } }); Platform.runLater(() -> { if (inputsExpanded && outputsExpanded) { if (!n.getStyleClass().contains("expanded")) { n.getStyleClass().add("expanded"); } } else { while (n.getStyleClass().remove("expanded")); } }); } public void resizeGraphNodes() { for (Map.Entry<String, Object> e : keyToVertexCell.entrySet()) { if (e.getValue() instanceof mxCell) { String key = e.getKey(); if (key.contains("#")) { recreateUnspentOutput(ICoin.query(key)); } else { recreateTXNode(ITx.query(key)); } Pane p = keyToVertexNode.get(e.getKey()); ((mxCell) e.getValue()).getGeometry().setWidth(p.getWidth()); ((mxCell) e.getValue()).getGeometry().setHeight(p.getHeight()); } } } public void layout() { mxGraphLayout.execute(mxGraph.getDefaultParent()); } public mxRectangle getGraphBounds() { return mxGraphView.getGraphBounds(); } public void rebuildGraph(boolean animate) { mxRectangle bounds = mxGraphView.getGraphBounds(); LinkedList<Node> newKids = new LinkedList<>(); Set<KeyValue> animations = new CopyOnWriteArraySet<>(); List<Node> nodesToDelete = new ArrayList<>(); if (animatingTimeline != null && animatingTimeline.getStatus() == Animation.Status.RUNNING) { animatingTimeline.jumpTo(animatingTimeline.getTotalDuration().subtract(Duration.ONE)); } mxGraphView.getStates().entrySet().forEach( entry -> { mxCell cell = (mxCell) entry.getKey(); if (cell.isEdge()) { ICoin coin = (ICoin) cell.getValue(); String key = coin.getCompkey(); EventHandler<MouseEvent> click = event -> clickHandler.accept(event, coin); mxCellState cellState = entry.getValue(); List<Line> newLines = new ArrayList<>(cellState.getAbsolutePointCount() - 1); List<Line> prevEdge = null; Label prevLabel = null; if (animate) { prevEdge = keyToEdgeNode.get(key); prevLabel = keyToEdgeLabel.get(key); } if (prevEdge != null && prevEdge.size() == 0) { prevEdge = null; } // add the line label and fade in/move if (cellState.getLabel().isEmpty()) { if (prevLabel != null) { nodesToDelete.add(prevLabel); keyToEdgeLabel.remove(key, prevLabel); } } else { mxRectangle labelPoint = cellState.getLabelBounds(); if (prevLabel == null) { prevLabel = new Label(cellState.getLabel()); prevLabel.setOnMouseClicked(click); prevLabel.getStyleClass().add("edgeLabel"); prevLabel.resizeRelocate(labelPoint.getX(), labelPoint.getY(), labelPoint.getWidth(), labelPoint.getHeight()); prevLabel.setOpacity(0.0); animations.add(new KeyValue(prevLabel.opacityProperty(), 1.0, Interpolator.EASE_IN)); } else { prevLabel.setText(cellState.getLabel()); animations.add( new KeyValue(prevLabel.layoutXProperty(), labelPoint.getX(), Interpolator.EASE_BOTH)); animations.add( new KeyValue(prevLabel.layoutYProperty(), labelPoint.getY(), Interpolator.EASE_BOTH) ); } keyToEdgeLabel.put(key, prevLabel); newKids.add(0, prevLabel); } // move the lines and fade in/create for (int i = 1; i < cellState.getAbsolutePointCount(); i ++) { mxPoint prevPoint = cellState.getAbsolutePoint(i-1); mxPoint nextPoint = cellState.getAbsolutePoint(i); if (prevEdge != null) { Line l; if (prevEdge.size() >= i) { l = prevEdge.get(i-1); } else { // if this is a new line, animate it from the start point Line lastLine = prevEdge.get(prevEdge.size() - 1); l = new Line(lastLine.getEndX(), lastLine.getEndY(), lastLine.getEndX(), lastLine.getEndY()); l.getStyleClass().add("coinLine"); l.setOnMouseClicked(click); } newLines.add(l); animations.add(new KeyValue(l.startXProperty(), prevPoint.getX(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.startYProperty(), prevPoint.getY(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.endXProperty(), nextPoint.getX(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.endYProperty(), nextPoint.getY(), Interpolator.EASE_BOTH)); } else { Line l = new Line(prevPoint.getX(), prevPoint.getY(), nextPoint.getX(), nextPoint.getY()); l.getStyleClass().add("coinLine"); l.setOnMouseClicked(click); newLines.add(l); l.setOpacity(0.0); animations.add(new KeyValue(l.opacityProperty(), 1.0, Interpolator.EASE_IN)); } } // if we have less lines than before, animate them into the end point if (animate && prevEdge != null && prevEdge.size() > newLines.size()) { mxPoint end = cellState.getAbsolutePoint(cellState.getAbsolutePointCount() - 1); for (int i = newLines.size(); i < prevEdge.size(); i++) { Line l = prevEdge.get(i); animations.add(new KeyValue(l.startXProperty(), end.getX(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.startYProperty(), end.getY(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.endXProperty(), end.getX(), Interpolator.EASE_BOTH)); animations.add(new KeyValue(l.endYProperty(), end.getY(), Interpolator.EASE_BOTH)); nodesToDelete.add(l); } } keyToEdgeNode.put(key, newLines); newKids.addAll(0, newLines); } else if (cell.isVertex()) { mxGeometry geom = cell.getGeometry(); Object o = cell.getValue(); String key = null; if (o instanceof ICoin) { key = ((ICoin)o).getCompkey(); } else if (o instanceof ITx) { key = ((ITx)o).getHash(); } boolean setInitialValue = !keyToVertexNode.containsKey(key); Node node = getFXNodeForVertexCell(cell.getValue()); if (setInitialValue || (node.getLayoutX() == 0 && node.getLayoutY() == 0)) { node.setLayoutX(geom.getX()); node.setLayoutY(geom.getY()); node.setOpacity(0); animations.add(new KeyValue(node.opacityProperty(), 1.0, Interpolator.EASE_IN)); } else { animations.add( new KeyValue(node.layoutXProperty(), geom.getX(), Interpolator.EASE_BOTH)); animations.add( new KeyValue(node.layoutYProperty(), geom.getY(), Interpolator.EASE_BOTH) ); } newKids.addLast(node); } } ); graphPane.getChildren().setAll(newKids); Timeline t = new Timeline(new KeyFrame(Duration.millis(400), "move", finishedEvent -> { graphPane.getChildren().removeAll(nodesToDelete); }, animations)); t.play(); animatingTimeline = t; } private Node getFXNodeForVertexCell(Object value) { Node node; if (value instanceof ITx) { node = createTXNode((ITx)value); } else if (value instanceof ICoin) { node = createUnspentOutput((ICoin)value); } else { node = new Rectangle(100, 100); ((Rectangle)node).setFill(Color.ORANGE); } return node; } public Pane getGraphPane() { return graphPane; } public void expandTransaction(ITx tx) { tx.getOutputs().forEach(this::addCoin); tx.getInputs().forEach(this::addCoin); layout(); rebuildGraph(true); } Pane createUnspentOutput(ICoin coin) { if (keyToVertexCell.containsKey(coin.getCompkey())) { return keyToVertexNode.get(coin.getCompkey()); } else { VBox box = new VBox(); box.getStyleClass().add("coin"); box.setOnMouseClicked(event -> clickHandler.accept(event, coin)); fillUnspentOutput(coin, box); keyToVertexNode.put(coin.getCompkey(), box); return box; } } Pane recreateUnspentOutput(ICoin ci) { if (keyToVertexNode.containsKey(ci.getCompkey())) { Pane p = keyToVertexNode.get(ci.getCompkey()); p.getChildren().clear(); fillUnspentOutput(ci, p); return p; } else { return createUnspentOutput(ci); } } private void fillUnspentOutput(ICoin coin, Pane box) { if (showCoinNodeHash.get()) { box.getChildren().add(new Text(IBase.shortHash(coin.getAddr()))); } if (showCoinNodeUSD.get()) { box.getChildren().add(new Text(IBase.USD_FORMAT.format(coin.getValueUSD()))); } if (showCoinNodeBitcoin.get()) { box.getChildren().add(new Text(IBase.BTC_FORMAT.format(coin.getValue()))); } box.autosize(); } Pane recreateTXNode(ITx tx) { if (keyToVertexNode.containsKey(tx.getHash())) { Pane p = keyToVertexNode.get(tx.getHash()); p.getChildren().clear(); fillTXNode(tx, p); return p; } else { return createTXNode(tx); } } Pane createTXNode(ITx tx) { if (keyToVertexNode.containsKey(tx.getHash())) { return keyToVertexNode.get(tx.getHash()); } else { VBox box = new VBox(); box.getStyleClass().setAll("tx"); box.setOnMouseClicked(event -> clickHandler.accept(event, tx)); fillTXNode(tx, box); // This eliminates jiggly text at non 1.0 scale //box.setCache(true); //box.setCacheHint(CacheHint.QUALITY); keyToVertexNode.put(tx.getHash(), box); if (tx.getInputs().get(0).isCoinbase()) { box.getStyleClass().add("coinbase"); } return box; } } private void fillTXNode(ITx tx, Pane box) { if (showTxHash.get()) { box.getChildren().add(new Text(IBase.shortHash(tx.getHash()))); } if (showTxBitcoin.get()) { box.getChildren().add(new Text(IBase.BTC_FORMAT.format(tx.getOutputValue()))); } if (showTxUSD.get()) { if (tx.isConfirmed()) { box.getChildren().add(new Text(IBase.USD_FORMAT.format(tx.getOutputValueUSD()))); } else { //TODO add spot prince to CBAPI box.getChildren().add(new Text(" ?? ")); } } if (showTxDate.get()) { box.getChildren().add(new Text(tx.getTimeString())); } if (showTxHeight.get()) { int height = tx.getBlockHeight(); box.getChildren().add(new Text((height < 0) ? "Unconfirmed" : ("Block # " + tx.getBlockHeight()))); } if (showTxCoinCount.get()) { box.getChildren().add(new Text(tx.getInputs().size() + " in " + tx.getOutputs().size() + " out " + tx.getUnspentCoins().size() + " unspent")); } box.autosize(); } public void reset() { mxGraph = new mxGraph(); mxGraphModel = mxGraph.getModel(); mxGraphView = mxGraph.getView(); mxGraphLayout = new mxHierarchicalLayout(mxGraph, SwingConstants.NORTH); keyToVertexCell = new ConcurrentHashMap<>(); keyToEdgeCell = new ConcurrentHashMap<>(); keyToVertexNode = new ConcurrentHashMap<>(); //edgeToNode = new ConcurrentHashMap<>(); graphPane.getChildren().clear(); } public void removeTX(ITx tx) { mxGraph.removeCells(tx.getOutputs().stream().map(coin -> keyToEdgeCell.get(coin.getCompkey())).toArray()); mxGraph.removeCells(new Object[] {keyToVertexCell.get(tx.getHash())}); tx.getOutputs().forEach(ICoin -> { keyToEdgeCell.remove(ICoin.getCompkey()); keyToEdgeNode.remove(ICoin.getCompkey()); keyToEdgeLabel.remove(ICoin.getCompkey()); }); mxGraph.removeCells(tx.getInputs().stream().map(coin -> keyToEdgeCell.get(coin.getCompkey())).toArray()); mxGraph.removeCells(new Object[] {keyToVertexCell.get(tx.getHash())}); tx.getInputs().forEach(ICoin -> { keyToEdgeCell.remove(ICoin.getCompkey()); keyToEdgeNode.remove(ICoin.getCompkey()); keyToEdgeLabel.remove(ICoin.getCompkey()); }); keyToVertexCell.remove(tx.getHash()); keyToVertexNode.remove(tx.getHash()); } public void removeTXAndAllInputs(ITx tx) { tx.getInputs().forEach(coin -> { if (keyToEdgeCell.containsKey(coin.getCompkey())) { removeTXAndAllInputs(coin.getSourceTX()); } }); removeTX(tx); } public void removeTXAndAllOutputs(ITx tx) { tx.getOutputs().forEach(coin -> { if (keyToEdgeCell.containsKey(coin.getCompkey())) { removeTXAndAllOutputs(coin.getTargetTX()); } }); removeTX(tx); } public void removeCoinAsNode(ICoin ci) { mxGraph.removeCells(new Object[] {keyToEdgeCell.get(ci.getCompkey()), keyToVertexCell.get(ci.getCompkey())}); keyToEdgeCell.remove(ci.getCompkey()); keyToVertexCell.remove(ci.getCompkey()); } public Collection<ITx> findUnexpandedOutputTX(ITx tx) { Set<ITx> outputs = new HashSet<>(); findUnexpandedOutputTX(tx, outputs); return outputs; } void findUnexpandedOutputTX(ITx tx, Collection<ITx> collector) { // not the most efficient, some nodes could be considered multiple times for diamond merges, // but since this is a DAG there are no loops. tx.getOutputs().forEach(coin -> { String targetID = coin.getTargetTXID(); if (targetID != null && keyToVertexCell.containsKey(targetID)) { findUnexpandedOutputTX(ITx.query(targetID), collector); } else { collector.add(tx); } }); } public Collection<ITx> findUnexpandedInputTX(ITx tx) { Set<ITx> inputs = new HashSet<>(); findUnexpandedInputTX(tx, inputs); return inputs; } void findUnexpandedInputTX(ITx tx, Collection<ITx> collector) { // not the most efficient, some nodes could be considered multiple times for diamond TXes, // but since this is a DAG there are no loops. tx.getInputs().forEach(coin -> { String targetID = coin.getTargetTXID(); if (targetID != null && keyToVertexCell.containsKey(targetID)) { findUnexpandedInputTX(ITx.query(targetID), collector); } else { collector.add(tx); } }); } public Node getNode(IBase jb) { if (jb instanceof ITx) { return keyToVertexNode.get(((ITx) jb).getHash()); } else if (jb instanceof ICoin) { return keyToVertexNode.get(((ICoin) jb).getCompkey()); } else if (jb instanceof CBTransaction) { return keyToVertexNode.get(((CBTransaction) jb).getHash()); } else if (jb instanceof IBlock) { // focus on the coinbase TX return keyToVertexNode.get(((IBlock) jb).getTXs().get(0).getHash()); } else if (jb instanceof IAddress) { // return the first one, truely arbitrary List<ITx> txs = ((IAddress) jb).getTXs(); if (txs.isEmpty()) return null; return keyToVertexNode.get(txs.get(0).getHash()); } else { return null; } } public ObservableSet<IBase> getSelectedItems() { return selectedItems; } public void setSelectedItems(ObservableSet<IBase> selectedItems) { this.selectedItems = selectedItems; } public boolean getShowTxHash() { return showTxHash.get(); } public BooleanProperty showTxHashProperty() { return showTxHash; } public void setShowTxHash(boolean showTxHash) { this.showTxHash.set(showTxHash); } public boolean getShowTxBitcoin() { return showTxBitcoin.get(); } public BooleanProperty showTxBitcoinProperty() { return showTxBitcoin; } public void setShowTxBitcoin(boolean showTxBitcoin) { this.showTxBitcoin.set(showTxBitcoin); } public boolean getShowTxUSD() { return showTxUSD.get(); } public BooleanProperty showTxUSDProperty() { return showTxUSD; } public void setShowTxUSD(boolean showTxUSD) { this.showTxUSD.set(showTxUSD); } public boolean getShowTxDate() { return showTxDate.get(); } public BooleanProperty showTxDateProperty() { return showTxDate; } public void setShowTxDate(boolean showTxDate) { this.showTxDate.set(showTxDate); } public boolean getShowTxHeight() { return showTxHeight.get(); } public BooleanProperty showTxHeightProperty() { return showTxHeight; } public void setShowTxHeight(boolean showTxHeight) { this.showTxHeight.set(showTxHeight); } public boolean getShowTxCoinCount() { return showTxCoinCount.get(); } public BooleanProperty showTxCoinCountProperty() { return showTxCoinCount; } public void setShowTxCoinCount(boolean showTxCoinCount) { this.showTxCoinCount.set(showTxCoinCount); } public boolean getShowCoinNodeHash() { return showCoinNodeHash.get(); } public BooleanProperty showCoinNodeHashProperty() { return showCoinNodeHash; } public void setShowCoinNodeHash(boolean showCoinNodeHash) { this.showCoinNodeHash.set(showCoinNodeHash); } public boolean getShowCoinNodeBitcoin() { return showCoinNodeBitcoin.get(); } public BooleanProperty showCoinNodeBitcoinProperty() { return showCoinNodeBitcoin; } public void setShowCoinNodeBitcoin(boolean showCoinNodeBitcoin) { this.showCoinNodeBitcoin.set(showCoinNodeBitcoin); } public boolean getShowCoinNodeUSD() { return showCoinNodeUSD.get(); } public BooleanProperty showCoinNodeUSDProperty() { return showCoinNodeUSD; } public void setShowCoinNodeUSD(boolean showCoinNodeUSD) { this.showCoinNodeUSD.set(showCoinNodeUSD); } public List<BooleanProperty> layoutFlags() { return Arrays.asList( showTxHash, showTxBitcoin, showTxUSD, showTxDate, showTxHeight, showTxCoinCount, showCoinNodeHash, showCoinNodeBitcoin, showCoinNodeUSD ); } }