17 changed files with 834 additions and 117 deletions
@ -0,0 +1,65 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.event.FollowPayNymEvent; |
||||
|
import com.sparrowwallet.sparrow.soroban.PayNym; |
||||
|
import javafx.geometry.Insets; |
||||
|
import javafx.geometry.Pos; |
||||
|
import javafx.scene.control.Button; |
||||
|
import javafx.scene.control.ContentDisplay; |
||||
|
import javafx.scene.control.Label; |
||||
|
import javafx.scene.control.ListCell; |
||||
|
import javafx.scene.layout.BorderPane; |
||||
|
import javafx.scene.layout.HBox; |
||||
|
|
||||
|
public class PayNymCell extends ListCell<PayNym> { |
||||
|
private final String walletId; |
||||
|
|
||||
|
public PayNymCell(String walletId) { |
||||
|
super(); |
||||
|
setAlignment(Pos.CENTER_LEFT); |
||||
|
setContentDisplay(ContentDisplay.LEFT); |
||||
|
getStyleClass().add("paynym-cell"); |
||||
|
setPrefHeight(50); |
||||
|
this.walletId = walletId; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void updateItem(PayNym payNym, boolean empty) { |
||||
|
super.updateItem(payNym, empty); |
||||
|
|
||||
|
if(empty || payNym == null) { |
||||
|
setText(null); |
||||
|
setGraphic(null); |
||||
|
} else { |
||||
|
BorderPane pane = new BorderPane(); |
||||
|
pane.setPadding(new Insets(5, 5,5, 5)); |
||||
|
|
||||
|
PayNymAvatar payNymAvatar = new PayNymAvatar(); |
||||
|
payNymAvatar.setPrefWidth(30); |
||||
|
payNymAvatar.setPrefHeight(30); |
||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode()); |
||||
|
|
||||
|
HBox labelBox = new HBox(); |
||||
|
labelBox.setAlignment(Pos.CENTER); |
||||
|
Label label = new Label(payNym.nymName(), payNymAvatar); |
||||
|
label.setGraphicTextGap(10); |
||||
|
labelBox.getChildren().add(label); |
||||
|
pane.setLeft(labelBox); |
||||
|
|
||||
|
if(getListView().getUserData() == Boolean.TRUE) { |
||||
|
HBox hBox = new HBox(); |
||||
|
hBox.setAlignment(Pos.CENTER); |
||||
|
Button button = new Button("Follow"); |
||||
|
hBox.getChildren().add(button); |
||||
|
pane.setRight(hBox); |
||||
|
button.setOnAction(event -> { |
||||
|
EventManager.get().post(new FollowPayNymEvent(walletId, payNym.paymentCode())); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
setText(null); |
||||
|
setGraphic(pane); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package com.sparrowwallet.sparrow.event; |
||||
|
|
||||
|
import com.samourai.wallet.bip47.rpc.PaymentCode; |
||||
|
|
||||
|
public class FollowPayNymEvent { |
||||
|
private final String walletId; |
||||
|
private final PaymentCode paymentCode; |
||||
|
|
||||
|
public FollowPayNymEvent(String walletId, PaymentCode paymentCode) { |
||||
|
this.walletId = walletId; |
||||
|
this.paymentCode = paymentCode; |
||||
|
} |
||||
|
|
||||
|
public String getWalletId() { |
||||
|
return walletId; |
||||
|
} |
||||
|
|
||||
|
public PaymentCode getPaymentCode() { |
||||
|
return paymentCode; |
||||
|
} |
||||
|
} |
@ -0,0 +1,273 @@ |
|||||
|
package com.sparrowwallet.sparrow.soroban; |
||||
|
|
||||
|
import com.google.common.eventbus.Subscribe; |
||||
|
import com.samourai.wallet.bip47.rpc.PaymentCode; |
||||
|
import com.sparrowwallet.sparrow.AppServices; |
||||
|
import com.sparrowwallet.sparrow.control.*; |
||||
|
import com.sparrowwallet.sparrow.event.FollowPayNymEvent; |
||||
|
import javafx.beans.property.ObjectProperty; |
||||
|
import javafx.beans.property.SimpleObjectProperty; |
||||
|
import javafx.beans.property.SimpleStringProperty; |
||||
|
import javafx.beans.property.StringProperty; |
||||
|
import javafx.collections.FXCollections; |
||||
|
import javafx.collections.ObservableList; |
||||
|
import javafx.event.ActionEvent; |
||||
|
import javafx.fxml.FXML; |
||||
|
import javafx.scene.control.*; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Optional; |
||||
|
import java.util.function.UnaryOperator; |
||||
|
|
||||
|
public class PayNymController extends SorobanController { |
||||
|
private static final Logger log = LoggerFactory.getLogger(PayNymController.class); |
||||
|
|
||||
|
private String walletId; |
||||
|
private PayNym walletPayNym; |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableTextField payNymName; |
||||
|
|
||||
|
@FXML |
||||
|
private PaymentCodeTextField paymentCode; |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableTextField searchPayNyms; |
||||
|
|
||||
|
@FXML |
||||
|
private ProgressIndicator findPayNym; |
||||
|
|
||||
|
@FXML |
||||
|
private PayNymAvatar payNymAvatar; |
||||
|
|
||||
|
@FXML |
||||
|
private ListView<PayNym> followingList; |
||||
|
|
||||
|
@FXML |
||||
|
private ListView<PayNym> followersList; |
||||
|
|
||||
|
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>(null); |
||||
|
|
||||
|
private final StringProperty findNymProperty = new SimpleStringProperty(); |
||||
|
|
||||
|
public void initializeView(String walletId) { |
||||
|
this.walletId = walletId; |
||||
|
|
||||
|
findNymProperty.addListener((observable, oldValue, nymIdentifier) -> { |
||||
|
if(nymIdentifier != null) { |
||||
|
searchFollowing(nymIdentifier); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
UnaryOperator<TextFormatter.Change> paymentCodeFilter = change -> { |
||||
|
String input = change.getControlNewText(); |
||||
|
if(input.startsWith("P") && !input.contains("...")) { |
||||
|
try { |
||||
|
PaymentCode paymentCode = new PaymentCode(input); |
||||
|
if(paymentCode.isValid()) { |
||||
|
findNymProperty.set(input); |
||||
|
|
||||
|
TextInputControl control = (TextInputControl)change.getControl(); |
||||
|
change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5)); |
||||
|
change.setRange(0, control.getLength()); |
||||
|
change.setAnchor(change.getText().length()); |
||||
|
change.setCaretPosition(change.getText().length()); |
||||
|
} |
||||
|
} catch(Exception e) { |
||||
|
//ignore
|
||||
|
} |
||||
|
} else if(PAYNYM_REGEX.matcher(input).matches()) { |
||||
|
findNymProperty.set(input); |
||||
|
} else { |
||||
|
findNymProperty.set(null); |
||||
|
resetFollowing(); |
||||
|
} |
||||
|
|
||||
|
return change; |
||||
|
}; |
||||
|
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter)); |
||||
|
findPayNym.managedProperty().bind(findPayNym.visibleProperty()); |
||||
|
findPayNym.maxHeightProperty().bind(searchPayNyms.heightProperty()); |
||||
|
findPayNym.setVisible(false); |
||||
|
|
||||
|
followingList.setCellFactory(param -> { |
||||
|
return new PayNymCell(walletId); |
||||
|
}); |
||||
|
|
||||
|
followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> { |
||||
|
payNymProperty.set(payNym); |
||||
|
}); |
||||
|
|
||||
|
followersList.setCellFactory(param -> { |
||||
|
return new PayNymCell(walletId); |
||||
|
}); |
||||
|
|
||||
|
followersList.setSelectionModel(new NoSelectionModel<>()); |
||||
|
followersList.setFocusTraversable(false); |
||||
|
|
||||
|
refresh(); |
||||
|
} |
||||
|
|
||||
|
private void refresh() { |
||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); |
||||
|
if(soroban.getPaymentCode() == null) { |
||||
|
throw new IllegalStateException("Payment code has not been set"); |
||||
|
} |
||||
|
|
||||
|
soroban.getPayNym(soroban.getPaymentCode().toString()).subscribe(payNym -> { |
||||
|
walletPayNym = payNym; |
||||
|
payNymName.setText(payNym.nymName()); |
||||
|
paymentCode.setPaymentCode(payNym.paymentCode()); |
||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode()); |
||||
|
followingList.setUserData(null); |
||||
|
followingList.setItems(FXCollections.observableList(payNym.following())); |
||||
|
followersList.setItems(FXCollections.observableList(payNym.followers())); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void resetFollowing() { |
||||
|
if(followingList.getUserData() != null) { |
||||
|
followingList.setUserData(null); |
||||
|
followingList.setItems(FXCollections.observableList(walletPayNym.following())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void searchFollowing(String nymIdentifier) { |
||||
|
Optional<PayNym> optExisting = walletPayNym.following().stream().filter(payNym -> payNym.nymName().equals(nymIdentifier) || payNym.paymentCode().toString().equals(nymIdentifier)).findFirst(); |
||||
|
if(optExisting.isPresent()) { |
||||
|
followingList.setUserData(Boolean.FALSE); |
||||
|
List<PayNym> existingPayNym = new ArrayList<>(); |
||||
|
existingPayNym.add(optExisting.get()); |
||||
|
followingList.setItems(FXCollections.observableList(existingPayNym)); |
||||
|
} else { |
||||
|
followingList.setUserData(Boolean.TRUE); |
||||
|
followingList.setItems(FXCollections.observableList(new ArrayList<>())); |
||||
|
findPayNym.setVisible(true); |
||||
|
|
||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); |
||||
|
soroban.getPayNym(nymIdentifier).subscribe(searchedPayNym -> { |
||||
|
findPayNym.setVisible(false); |
||||
|
List<PayNym> searchList = new ArrayList<>(); |
||||
|
searchList.add(searchedPayNym); |
||||
|
followingList.setUserData(Boolean.TRUE); |
||||
|
followingList.setItems(FXCollections.observableList(searchList)); |
||||
|
}, error -> { |
||||
|
findPayNym.setVisible(false); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void showQR(ActionEvent event) { |
||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); |
||||
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString()); |
||||
|
qrDisplayDialog.showAndWait(); |
||||
|
} |
||||
|
|
||||
|
public void scanQR(ActionEvent event) { |
||||
|
QRScanDialog qrScanDialog = new QRScanDialog(); |
||||
|
Optional<QRScanDialog.Result> optResult = qrScanDialog.showAndWait(); |
||||
|
if(optResult.isPresent()) { |
||||
|
QRScanDialog.Result result = optResult.get(); |
||||
|
if(result.payload != null) { |
||||
|
searchPayNyms.setText(result.payload); |
||||
|
} else { |
||||
|
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a payment code"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public PayNym getPayNym() { |
||||
|
return payNymProperty.get(); |
||||
|
} |
||||
|
|
||||
|
public ObjectProperty<PayNym> payNymProperty() { |
||||
|
return payNymProperty; |
||||
|
} |
||||
|
|
||||
|
@Subscribe |
||||
|
public void followPayNym(FollowPayNymEvent event) { |
||||
|
if(event.getWalletId().equals(walletId)) { |
||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); |
||||
|
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> { |
||||
|
String signature = soroban.getSignature(authToken); |
||||
|
soroban.followPaymentCode(event.getPaymentCode(), authToken, signature).subscribe(followMap -> { |
||||
|
refresh(); |
||||
|
}, error -> { |
||||
|
log.error("Could not follow payment code", error); |
||||
|
AppServices.showErrorDialog("Could not follow payment code", error.getMessage()); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> { |
||||
|
|
||||
|
@Override |
||||
|
public ObservableList<Integer> getSelectedIndices() { |
||||
|
return FXCollections.emptyObservableList(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ObservableList<T> getSelectedItems() { |
||||
|
return FXCollections.emptyObservableList(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectIndices(int index, int... indices) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectAll() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectFirst() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectLast() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void clearAndSelect(int index) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void select(int index) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void select(T obj) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void clearSelection(int index) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void clearSelection() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isSelected(int index) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isEmpty() { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectPrevious() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void selectNext() { |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,55 @@ |
|||||
|
package com.sparrowwallet.sparrow.soroban; |
||||
|
|
||||
|
import com.sparrowwallet.sparrow.AppServices; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import javafx.fxml.FXMLLoader; |
||||
|
import javafx.scene.control.*; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
public class PayNymDialog extends Dialog<PayNym> { |
||||
|
public PayNymDialog(String walletId, boolean selectPayNym) { |
||||
|
final DialogPane dialogPane = getDialogPane(); |
||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow()); |
||||
|
AppServices.onEscapePressed(dialogPane.getScene(), this::close); |
||||
|
|
||||
|
try { |
||||
|
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml")); |
||||
|
dialogPane.setContent(payNymLoader.load()); |
||||
|
PayNymController payNymController = payNymLoader.getController(); |
||||
|
payNymController.initializeView(walletId); |
||||
|
EventManager.get().register(payNymController); |
||||
|
|
||||
|
dialogPane.setPrefWidth(730); |
||||
|
dialogPane.setPrefHeight(600); |
||||
|
AppServices.moveToActiveWindowScreen(this); |
||||
|
|
||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); |
||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm()); |
||||
|
|
||||
|
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select PayNym", ButtonBar.ButtonData.APPLY); |
||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); |
||||
|
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); |
||||
|
|
||||
|
if(selectPayNym) { |
||||
|
dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType); |
||||
|
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); |
||||
|
selectButton.setDisable(true); |
||||
|
selectButton.setDefaultButton(true); |
||||
|
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> { |
||||
|
selectButton.setDisable(payNym == null); |
||||
|
}); |
||||
|
} else { |
||||
|
dialogPane.getButtonTypes().add(doneButtonType); |
||||
|
} |
||||
|
|
||||
|
setOnCloseRequest(event -> { |
||||
|
EventManager.get().unregister(payNymController); |
||||
|
}); |
||||
|
|
||||
|
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null); |
||||
|
} catch(IOException e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,56 @@ |
|||||
|
.paynym-pane { |
||||
|
-fx-padding: 0; |
||||
|
} |
||||
|
|
||||
|
.title-area { |
||||
|
-fx-background-color: -fx-control-inner-background; |
||||
|
-fx-padding: 10 25 10 25; |
||||
|
-fx-border-width: 0px 0px 1px 0px; |
||||
|
-fx-border-color: #e5e5e6; |
||||
|
} |
||||
|
|
||||
|
.button-bar { |
||||
|
-fx-padding: 10 25 25 25; |
||||
|
} |
||||
|
|
||||
|
.button-bar .container { |
||||
|
-fx-padding: 0 0 15px 0; |
||||
|
} |
||||
|
|
||||
|
.title-label { |
||||
|
-fx-font-size: 24px; |
||||
|
} |
||||
|
|
||||
|
.title-text { |
||||
|
-fx-font-size: 20px; |
||||
|
-fx-padding: 0 0 15px 0; |
||||
|
-fx-graphic-text-gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.content-text { |
||||
|
-fx-font-size: 16px; |
||||
|
-fx-text-fill: derive(-fx-text-base-color, 15%); |
||||
|
} |
||||
|
|
||||
|
.field-box { |
||||
|
-fx-pref-height: 30px; |
||||
|
-fx-alignment: CENTER_LEFT; |
||||
|
} |
||||
|
|
||||
|
.wide-field-label { |
||||
|
-fx-pref-width: 180px; |
||||
|
} |
||||
|
|
||||
|
.field-label { |
||||
|
-fx-pref-width: 110px; |
||||
|
} |
||||
|
|
||||
|
.field-control { |
||||
|
-fx-pref-width: 184px; |
||||
|
} |
||||
|
|
||||
|
.listview-label { |
||||
|
-fx-font-weight: bold; |
||||
|
-fx-font-size: 1.2em; |
||||
|
-fx-padding: 10 0 10 0; |
||||
|
} |
@ -0,0 +1,108 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
|
||||
|
<?import java.lang.*?> |
||||
|
<?import java.util.*?> |
||||
|
<?import javafx.scene.*?> |
||||
|
<?import javafx.scene.control.*?> |
||||
|
<?import javafx.scene.layout.*?> |
||||
|
<?import javafx.scene.image.ImageView?> |
||||
|
<?import javafx.scene.image.Image?> |
||||
|
<?import javafx.geometry.Insets?> |
||||
|
<?import com.sparrowwallet.sparrow.control.PayNymAvatar?> |
||||
|
<?import com.sparrowwallet.sparrow.control.CopyableTextField?> |
||||
|
<?import com.sparrowwallet.sparrow.control.PaymentCodeTextField?> |
||||
|
<?import org.controlsfx.glyphfont.Glyph?> |
||||
|
|
||||
|
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@paynym.css, @../general.css" styleClass="paynym-pane" fx:controller="com.sparrowwallet.sparrow.soroban.PayNymController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"> |
||||
|
<VBox spacing="10"> |
||||
|
<HBox styleClass="title-area"> |
||||
|
<HBox alignment="CENTER_LEFT"> |
||||
|
<Label text="PayNym" styleClass="title-label" /> |
||||
|
</HBox> |
||||
|
<Region HBox.hgrow="ALWAYS"/> |
||||
|
<ImageView AnchorPane.rightAnchor="0"> |
||||
|
<Image url="/image/paynym.png" requestedWidth="50" requestedHeight="50" smooth="false" /> |
||||
|
</ImageView> |
||||
|
</HBox> |
||||
|
<BorderPane> |
||||
|
<padding> |
||||
|
<Insets top="20" left="25" right="100" /> |
||||
|
</padding> |
||||
|
<center> |
||||
|
<VBox spacing="15"> |
||||
|
<HBox styleClass="field-box"> |
||||
|
<Label text="PayNym:" styleClass="field-label" /> |
||||
|
<CopyableTextField fx:id="payNymName" promptText="Retrieving..." styleClass="field-control" editable="false"/> |
||||
|
</HBox> |
||||
|
<HBox styleClass="field-box"> |
||||
|
<Label text="Payment code:" styleClass="field-label" /> |
||||
|
<HBox spacing="10"> |
||||
|
<PaymentCodeTextField fx:id="paymentCode" styleClass="field-control" editable="false"/> |
||||
|
<Button onAction="#showQR"> |
||||
|
<graphic> |
||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" /> |
||||
|
</graphic> |
||||
|
<tooltip> |
||||
|
<Tooltip text="Show as QR code" /> |
||||
|
</tooltip> |
||||
|
</Button> |
||||
|
</HBox> |
||||
|
</HBox> |
||||
|
<HBox styleClass="field-box"> |
||||
|
<padding> |
||||
|
<Insets top="35" /> |
||||
|
</padding> |
||||
|
<Label text="Find:" styleClass="field-label" /> |
||||
|
<HBox spacing="10"> |
||||
|
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/> |
||||
|
<Button onAction="#scanQR"> |
||||
|
<graphic> |
||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" /> |
||||
|
</graphic> |
||||
|
<tooltip> |
||||
|
<Tooltip text="Scan payment code" /> |
||||
|
</tooltip> |
||||
|
</Button> |
||||
|
<ProgressIndicator fx:id="findPayNym" /> |
||||
|
</HBox> |
||||
|
</HBox> |
||||
|
</VBox> |
||||
|
</center> |
||||
|
<right> |
||||
|
<PayNymAvatar fx:id="payNymAvatar" prefHeight="150" prefWidth="150" /> |
||||
|
</right> |
||||
|
</BorderPane> |
||||
|
<GridPane hgap="15"> |
||||
|
<padding> |
||||
|
<Insets right="25" left="25" /> |
||||
|
</padding> |
||||
|
<columnConstraints> |
||||
|
<ColumnConstraints percentWidth="50.0" /> |
||||
|
<ColumnConstraints percentWidth="50.0" /> |
||||
|
</columnConstraints> |
||||
|
<rowConstraints> |
||||
|
<RowConstraints percentHeight="100" /> |
||||
|
</rowConstraints> |
||||
|
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0"> |
||||
|
<top> |
||||
|
<HBox alignment="CENTER_LEFT"> |
||||
|
<Label styleClass="listview-label" text="Following"/> |
||||
|
</HBox> |
||||
|
</top> |
||||
|
<center> |
||||
|
<ListView fx:id="followingList" prefHeight="220" /> |
||||
|
</center> |
||||
|
</BorderPane> |
||||
|
<BorderPane GridPane.columnIndex="1" GridPane.rowIndex="0"> |
||||
|
<top> |
||||
|
<HBox alignment="CENTER_LEFT"> |
||||
|
<Label styleClass="listview-label" text="Followers"/> |
||||
|
</HBox> |
||||
|
</top> |
||||
|
<center> |
||||
|
<ListView fx:id="followersList" prefHeight="220" /> |
||||
|
</center> |
||||
|
</BorderPane> |
||||
|
</GridPane> |
||||
|
</VBox> |
||||
|
</StackPane> |
Loading…
Reference in new issue