1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 14:22:39 +01:00

Readibility improvements in GUI (#801)

* Improved amounts readability (fixes #542) and added the Bits unit 
denomination in the documentation

* Improved channel panel UI layout

* Added a confirmation dialog when closing a channel, with a short summary 
of the channel to be closed

The balance bar has been shrunk in size so that it should not be mistaken as 
a separator element between channels. The channel's balance, capacity and 
peer node id are now more visible. Also added the short channel id to the 
channel's pane.

fixes #690

* Added node's aggregated balance in the status bar

fixes #775
This commit is contained in:
Dominique 2019-01-14 16:53:52 +01:00 committed by GitHub
parent 10ea7bdc23
commit e3b2992934
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 311 additions and 192 deletions

View file

@ -103,7 +103,7 @@ name | description
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
eclair.bitcoind.zmqblock | Bitcoin Core ZMQ block address | "tcp://127.0.0.1:29000"
eclair.bitcoind.zmqtx | Bitcoin Core ZMQ tx address | "tcp://127.0.0.1:29000"
eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, mbtc, btc) | btc
eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, bits, mbtc, btc) | btc
Quotes are not required unless the value contains special characters. Full syntax guide [here](https://github.com/lightbend/config/blob/master/HOCON.md).

View file

@ -16,7 +16,7 @@
package fr.acinq.eclair
import java.text.DecimalFormat
import java.text.{DecimalFormat, NumberFormat}
import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, MilliSatoshi, Satoshi}
import grizzled.slf4j.Logging
@ -93,19 +93,44 @@ case object BtcUnit extends CoinUnit {
object CoinUtils extends Logging {
val COIN_PATTERN = "###,###,###,##0.###########"
var COIN_FORMAT = new DecimalFormat(COIN_PATTERN)
// msat pattern, no decimals allowed
val MILLI_SAT_PATTERN = "#,###,###,###,###,###,##0"
def setCoinPattern(pattern: String) = {
// sat pattern decimals are optional
val SAT_PATTERN = "#,###,###,###,###,##0.###"
// bits pattern always shows 2 decimals (msat optional)
val BITS_PATTERN = "##,###,###,###,##0.00###"
// milli btc pattern always shows 5 decimals (msat optional)
val MILLI_BTC_PATTERN = "##,###,###,##0.00000###"
// btc pattern always shows 8 decimals (msat optional). This is the default pattern.
val BTC_PATTERN = "##,###,##0.00000000###"
var COIN_FORMAT: NumberFormat = new DecimalFormat(BTC_PATTERN)
def setCoinPattern(pattern: String): Unit = {
COIN_FORMAT = new DecimalFormat(pattern)
}
def getPatternFromUnit(unit: CoinUnit): String = {
unit match {
case MSatUnit => MILLI_SAT_PATTERN
case SatUnit => SAT_PATTERN
case BitUnit => BITS_PATTERN
case MBtcUnit => MILLI_BTC_PATTERN
case BtcUnit => BTC_PATTERN
case _ => throw new IllegalArgumentException("unhandled unit")
}
}
/**
* Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if
* it has too many decimals because MilliSatoshi only accepts Long amount.
*
* @param amount numeric String, can be decimal.
* @param unit bitcoin unit, can be milliSatoshi, Satoshi, milliBTC, BTC.
* @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC.
* @return amount as a MilliSatoshi object.
* @throws NumberFormatException if the amount parameter is not numeric.
* @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC.
@ -132,7 +157,7 @@ object CoinUtils extends Logging {
fr.acinq.bitcoin.millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit))
/**
* Only BtcUnit, MBtcUnit, SatUnit and MSatUnit codes or label are supported.
* Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported.
* @param unit
* @return
*/
@ -199,13 +224,13 @@ object CoinUtils extends Logging {
}
/**
* Converts the amount to the user preferred unit and returns the Long value.
* Converts the amount to the user preferred unit and returns the BigDecimal value.
* This method is useful to feed numeric text input without formatting.
*
* Returns -1 if the given amount can not be converted.
*
* @param amount BtcAmount
* @return Long value of the BtcAmount
* @return BigDecimal value of the BtcAmount
*/
def rawAmountInUnit(amount: BtcAmount, unit: CoinUnit): BigDecimal = Try(convertAmountToGUIUnit(amount, unit) match {
case a: BtcAmountGUILossless => BigDecimal(a.amount_msat) / a.unit.factorToMsat

View file

@ -27,6 +27,8 @@ class CoinUtilsSpec extends FunSuite {
assert(am_btc == MilliSatoshi(100000000000L))
val am_mbtc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MBtcUnit.code)
assert(am_mbtc == MilliSatoshi(100000000L))
val am_bits: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", BitUnit.code)
assert(am_bits == MilliSatoshi(100000))
val am_sat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", SatUnit.code)
assert(am_sat == MilliSatoshi(1000))
val am_msat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MSatUnit.code)
@ -42,6 +44,8 @@ class CoinUtilsSpec extends FunSuite {
assert(am_mbtc_dec_nozero == MilliSatoshi(25000000L))
val am_mbtc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", MBtcUnit.code)
assert(am_mbtc_dec == MilliSatoshi(123456789L))
val am_bits_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", BitUnit.code)
assert(am_bits_dec == MilliSatoshi(123456))
val am_sat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", SatUnit.code)
assert(am_sat_dec == MilliSatoshi(1234))
val am_msat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.234", MSatUnit.code)
@ -80,19 +84,23 @@ class CoinUtilsSpec extends FunSuite {
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), SatUnit) == BigDecimal(0.123))
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), MSatUnit) == BigDecimal(123))
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(12345678), BtcUnit) == BigDecimal(0.00012345678))
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(1234567), BitUnit) == BigDecimal(12.34567))
assert(CoinUtils.rawAmountInUnit(Satoshi(123), BtcUnit) == BigDecimal(0.00000123))
assert(CoinUtils.rawAmountInUnit(Satoshi(123), MBtcUnit) == BigDecimal(0.00123))
assert(CoinUtils.rawAmountInUnit(Satoshi(123), BitUnit) == BigDecimal(1.23))
assert(CoinUtils.rawAmountInUnit(Satoshi(123), SatUnit) == BigDecimal(123))
assert(CoinUtils.rawAmountInUnit(Satoshi(123), MSatUnit) == BigDecimal(123000))
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), BtcUnit) == BigDecimal(0.123456))
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), MBtcUnit) == BigDecimal(123.456))
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678), BitUnit) == BigDecimal(123456.78))
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456789), SatUnit) == BigDecimal(12345678.9))
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678987), MSatUnit) == BigDecimal(12345678987L))
assert(CoinUtils.rawAmountInUnit(Btc(123.456), BtcUnit) == BigDecimal(123.456))
assert(CoinUtils.rawAmountInUnit(Btc(123.45678987654), MBtcUnit) == BigDecimal(123456.78987654))
assert(CoinUtils.rawAmountInUnit(Btc(123.456789876), BitUnit) == BigDecimal(123456789.876))
assert(CoinUtils.rawAmountInUnit(Btc(1.22233333444), SatUnit) == BigDecimal(122233333.444))
assert(CoinUtils.rawAmountInUnit(Btc(0.00011111222), MSatUnit) == BigDecimal(11111222L))
}

View file

@ -15,10 +15,26 @@
-fx-font-weight: bold;
}
.text-xs {
-fx-font-size: 10px;
}
.text-sm {
-fx-font-size: 12px;
}
.text-md {
-fx-font-size: 14px;
}
.text-lg {
-fx-font-size: 16px;
}
.text-xl {
-fx-font-size: 18px;
}
.text-error {
-fx-text-fill: rgb(216,31,74);
-fx-font-size: 11px;
@ -115,15 +131,13 @@
/* ---------- Progress Bar ---------- */
.bar {
-fx-background-color: rgb(63,179,234);
-fx-background-color: rgb(114,193,229);
-fx-background-insets: 0;
-fx-background-radius: 0;
}
.track {
-fx-background-color: rgb(206,230,255);
-fx-background-color: rgb(211,227,234);
-fx-background-insets: 0;
-fx-background-radius: 0;
}
/* ---------- Forms ----------- */

View file

@ -1,8 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<!--
~ Copyright 2018 ACINQ SAS
~
@ -19,67 +28,66 @@
~ limitations under the License.
-->
<VBox fx:id="root" styleClass="channel" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
onContextMenuRequested="#openChannelContext">
<VBox fx:id="root" onContextMenuRequested="#openChannelContext" styleClass="channel" xmlns="http://javafx.com/javafx/8.0.172-ea" xmlns:fx="http://javafx.com/fxml/1">
<stylesheets>
<URL value="@../commons/globals.css"/>
<URL value="@./main.css"/>
<URL value="@../commons/globals.css" />
<URL value="@./main.css" />
</stylesheets>
<GridPane styleClass="grid" prefWidth="400.0">
<GridPane prefWidth="600.0" styleClass="grid">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="140.0" prefWidth="150.0" maxWidth="180.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="30.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="40.0" prefWidth="60.0" maxWidth="60.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="40.0"/>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="380.0" minWidth="100.0" prefWidth="100.0" />
<ColumnConstraints hgrow="NEVER" maxWidth="1.0" minWidth="1.0" prefWidth="1.0" />
<ColumnConstraints hgrow="NEVER" maxWidth="90.0" minWidth="90.0" prefWidth="90.0" />
<ColumnConstraints hgrow="SOMETIMES" prefWidth="100.0" />
<ColumnConstraints hgrow="NEVER" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
<ColumnConstraints hgrow="NEVER" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="4.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
</rowConstraints>
<HBox GridPane.columnSpan="4" GridPane.columnIndex="0" alignment="CENTER" spacing="5.0">
<TextField fx:id="channelId" text="N/A" editable="false" styleClass="noteditable, text-strong"
HBox.hgrow="ALWAYS" GridPane.valignment="BOTTOM" focusTraversable="false"/>
<HBox GridPane.columnIndex="4" GridPane.halignment="RIGHT" alignment="CENTER_RIGHT" HBox.hgrow="NEVER" spacing="5">
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close" visible="false"/>
<Button fx:id="forceclose" mnemonicParsing="false" styleClass="forceclose-channel" text="Force close" visible="false"/>
</HBox>
<VBox alignment="CENTER_RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.rowSpan="2147483647">
<Label styleClass="text-muted, text-xs" text="BALANCE" />
<Label fx:id="amountUs" alignment="CENTER_RIGHT" styleClass="text-lg, channel-balance" text="N/A" />
<ProgressBar fx:id="balanceBar" maxWidth="120.0" minHeight="4.0" prefHeight="4.0" progress="0.0" snapToPixel="false">
<VBox.margin>
<Insets top="3.0" />
</VBox.margin>
</ProgressBar>
</VBox>
<HBox alignment="BOTTOM_LEFT" spacing="5" GridPane.columnIndex="2" GridPane.columnSpan="3" GridPane.rowIndex="0" GridPane.valignment="BOTTOM">
<Label styleClass="text-strong, text-md" text="With" />
<Label fx:id="nodeAlias" maxWidth="120.0" styleClass="text-md, channel-peer-alias" visible="false"/>
<TextField fx:id="nodeId" editable="false" focusTraversable="false" styleClass="noteditable, text-strong, text-md" text="N/A" HBox.hgrow="ALWAYS" />
</HBox>
<ProgressBar fx:id="balanceBar" minHeight="4.0" prefHeight="4.0" maxWidth="1.7976931348623157E308"
progress="0.0" snapToPixel="false"
GridPane.columnSpan="4" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1"/>
<HBox alignment="CENTER_RIGHT" spacing="5" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="0" HBox.hgrow="NEVER">
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close" visible="false" />
<Button fx:id="forceclose" mnemonicParsing="false" styleClass="forceclose-channel" text="Force close" visible="false" />
</HBox>
<Label styleClass="text-muted" text="Funding tx id" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="txId" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="2"/>
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<TextField fx:id="state" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="1" />
<Label styleClass="text-muted" text="Remote node id" GridPane.columnIndex="0" GridPane.rowIndex="3"/>
<TextField fx:id="nodeId" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="3"/>
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
<TextField fx:id="funder" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="5" GridPane.rowIndex="4" />
<Label styleClass="text-muted" text="Your balance" GridPane.rowIndex="4"/>
<TextField fx:id="amountUs" text="N/A" focusTraversable="false" editable="false"
styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Channel id" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<TextField fx:id="channelId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="2" />
<Label styleClass="text-muted" text="Capacity" GridPane.rowIndex="5"/>
<TextField fx:id="capacity" text="N/A" focusTraversable="false" editable="false"
styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="5"/>
<Label styleClass="text-muted" text="Short channel id" GridPane.columnIndex="2" GridPane.rowIndex="3" />
<TextField fx:id="shortChannelId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="3" />
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
<TextField fx:id="funder" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Funding tx id" GridPane.columnIndex="2" GridPane.rowIndex="4" />
<TextField fx:id="txId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.rowIndex="4" />
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="5"/>
<TextField fx:id="state" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.rowIndex="5"/>
<HBox prefHeight="100.0" prefWidth="1.0" styleClass="channel-balance-separator" GridPane.columnIndex="1" GridPane.rowSpan="2147483647" />
</GridPane>
<HBox styleClass="channel-separator"/>
<HBox styleClass="channel-separator" />
</VBox>

View file

@ -1,7 +1,7 @@
/* ---------- Status Bar ---------- */
.status-bar {
-fx-padding: .5em 1em;
-fx-padding: .5em .7em;
-fx-background-color: rgb(221,221,221);
-fx-border-width: 1px 0 0 0;
-fx-border-color: rgb(181,181,181);
@ -50,18 +50,28 @@
}
.channel .grid {
-fx-padding: 10px;
-fx-padding: 14px;
-fx-vgap: 3px;
-fx-hgap: 5px;
-fx-hgap: 1em;
-fx-font-size: 12px;
}
.channel-separator {
-fx-background-color: rgb(220,220,220);
-fx-background-color: rgb(190,200,210);
-fx-pref-height: 1px;
-fx-padding: 0 -.25em;
}
.channel-balance-separator {
-fx-background-color: rgb(225,230,235);
}
.channel-peer-alias {
-fx-background-color: rgb(232,233,235);
-fx-background-radius: 2;
-fx-label-padding: 0 3px;
}
.channels-info {
-fx-padding: 4em 0 0 0;
}

View file

@ -209,51 +209,40 @@
</center>
<bottom>
<HBox fx:id="statusBarBox" styleClass="status-bar" spacing="10">
<children>
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
<children>
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true"
preserveRatio="true">
<image>
<Image url="@../commons/images/eclair-shape.png"/>
</image>
</ImageView>
<Label fx:id="labelNodeId" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="80.0">
<children>
<Separator orientation="VERTICAL"/>
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent"/>
<Label fx:id="labelAlias" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="HTTP" styleClass="badge, badge-http"/>
<Label fx:id="labelApi" styleClass="value" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="TCP" styleClass="badge, badge-tcp"/>
<Label fx:id="labelServer" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
<children>
<Separator orientation="VERTICAL"/>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="195.0">
<children>
<Label fx:id="bitcoinWallet" text="N/A" textAlignment="RIGHT" textOverrun="CLIP"/>
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP"/>
</children>
</HBox>
</children>
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true"
preserveRatio="true">
<Image url="@../commons/images/eclair-shape.png" />
</ImageView>
<Label fx:id="labelNodeId" text="N/A" />
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="160.0">
<Separator orientation="VERTICAL" />
<Label text="TOTAL" styleClass="badge" />
<Label fx:id="statusBalanceLabel" styleClass="value" text="N/A" />
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="85.0">
<Separator orientation="VERTICAL" />
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent" />
<Label fx:id="labelAlias" text="N/A" />
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<Separator orientation="VERTICAL" />
<Label text="HTTP" styleClass="badge, badge-http" />
<Label fx:id="labelApi" styleClass="value" text="N/A" />
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="80.0">
<Separator orientation="VERTICAL" />
<Label text="TCP" styleClass="badge, badge-tcp" />
<Label fx:id="labelServer" text="N/A" />
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
<Separator orientation="VERTICAL" />
</HBox>
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="155.0">
<Label fx:id="bitcoinWallet" text="N/A" textAlignment="RIGHT" textOverrun="CLIP" />
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP" />
</HBox>
</HBox>
</bottom>
<top>

View file

@ -89,10 +89,11 @@ class FxApp extends Application with Logging {
val unitConf = setup.config.getString("gui.unit")
FxApp.unit = Try(CoinUtils.getUnitFromString(unitConf)) match {
case Failure(_) =>
logger.warn(s"$unitConf is not a valid gui unit, must be msat, sat, mbtc or btc. Defaulting to btc.")
logger.warn(s"$unitConf is not a valid gui unit, must be msat, sat, bits, mbtc or btc. Defaulting to btc.")
BtcUnit
case Success(u) => u
}
CoinUtils.setCoinPattern(CoinUtils.getPatternFromUnit(FxApp.unit))
val guiUpdater = system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
@ -108,8 +109,8 @@ class FxApp extends Application with Logging {
override def run(): Unit = {
val scene = new Scene(mainRoot)
primaryStage.setTitle("Eclair")
primaryStage.setMinWidth(600)
primaryStage.setWidth(960)
primaryStage.setMinWidth(750)
primaryStage.setWidth(980)
primaryStage.setMinHeight(400)
primaryStage.setHeight(640)
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {

View file

@ -18,12 +18,6 @@ package fr.acinq.eclair.gui
import java.time.LocalDateTime
import java.util.function.Predicate
import javafx.application.Platform
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXMLLoader
import javafx.scene.control.Alert.AlertType
import javafx.scene.control.{Alert, ButtonType}
import javafx.scene.layout.VBox
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
@ -31,12 +25,18 @@ import fr.acinq.bitcoin._
import fr.acinq.eclair.CoinUtils
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDisconnected}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumDisconnected, ElectrumReady}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.{Data, _}
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.payment.PaymentLifecycle.{LocalFailure, PaymentFailed, PaymentSucceeded, RemoteFailure}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{NORMAL => _, _}
import fr.acinq.eclair.wire.NodeAnnouncement
import javafx.application.Platform
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXMLLoader
import javafx.scene.control.Alert.AlertType
import javafx.scene.control.{Alert, ButtonType}
import javafx.scene.layout.VBox
import scala.collection.JavaConversions._
@ -60,43 +60,23 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
def receive: Receive = main(Map())
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData): (ChannelPaneController, VBox) = {
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData): (ChannelPaneController, VBox) = {
log.info(s"new channel: $channel")
val loader = new FXMLLoader(getClass.getResource("/gui/main/channelPane.fxml"))
val channelPaneController = new ChannelPaneController(s"$remoteNodeId")
val channelPaneController = new ChannelPaneController(channel, remoteNodeId.toString())
loader.setController(channelPaneController)
val root = loader.load[VBox]
channelPaneController.nodeId.setText(remoteNodeId.toString())
channelPaneController.channelId.setText(temporaryChannelId.toString())
channelPaneController.channelId.setText(channelId.toString())
channelPaneController.funder.setText(if (isFunder) "Yes" else "No")
channelPaneController.close.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = channel ! CMD_CLOSE(scriptPubKey = None)
})
channelPaneController.forceclose.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = {
val alert = new Alert(AlertType.WARNING, "Careful: force-close is more expensive than a regular close and will incur a delay before funds are spendable.\n\nAre you sure you want to proceed?", ButtonType.YES, ButtonType.NO)
alert.showAndWait
if (alert.getResult eq ButtonType.YES) {
channel ! CMD_FORCECLOSE
}
}
})
// set the node alias if the node has already been announced
mainController.networkNodesList
.find(na => na.nodeId.toString.equals(remoteNodeId.toString))
.map(na => channelPaneController.updateRemoteNodeAlias(na.alias))
.foreach(na => channelPaneController.updateRemoteNodeAlias(na.alias))
(channelPaneController, root)
}
def updateBalance(channelPaneController: ChannelPaneController, commitments: Commitments) = {
val spec = commitments.localCommit.spec
channelPaneController.capacity.setText(CoinUtils.formatAmountInUnit(MilliSatoshi(spec.totalFunds), FxApp.getUnit, withUnit = true))
channelPaneController.amountUs.setText(CoinUtils.formatAmountInUnit(MilliSatoshi(spec.toLocalMsat), FxApp.getUnit, withUnit = true))
channelPaneController.balanceBar.setProgress(spec.toLocalMsat.toDouble / spec.totalFunds)
}
def main(m: Map[ActorRef, ChannelPaneController]): Receive = {
case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId) =>
@ -108,29 +88,32 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
case ChannelRestored(channel, peer, remoteNodeId, isFunder, channelId, currentData) =>
context.watch(channel)
val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, channelId)
currentData match {
case d: HasCommitments =>
updateBalance(channelPaneController, d.commitments)
channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
case _ => {}
}
runInGuiThread(() => mainController.channelBox.getChildren.addAll(root))
context.become(main(m + (channel -> channelPaneController)))
channelPaneController.updateBalance(currentData.commitments)
val m1 = m + (channel -> channelPaneController)
val totalBalance = MilliSatoshi(m1.values.map(_.getBalance.amount).sum)
runInGuiThread(() => {
channelPaneController.refreshBalance()
mainController.refreshTotalBalance(totalBalance)
channelPaneController.txId.setText(currentData.commitments.commitInput.outPoint.txid.toString())
mainController.channelBox.getChildren.addAll(root)
})
context.become(main(m1))
case ShortChannelIdAssigned(channel, channelId, shortChannelId) if m.contains(channel) =>
val channelPaneController = m(channel)
runInGuiThread(() => channelPaneController.shortChannelId.setText(shortChannelId.toString))
case ChannelIdAssigned(channel, _, _, channelId) if m.contains(channel) =>
val channelPaneController = m(channel)
runInGuiThread(() => channelPaneController.channelId.setText(s"$channelId"))
runInGuiThread(() => channelPaneController.channelId.setText(channelId.toString()))
case ChannelStateChanged(channel, _, _, _, currentState, currentData) if m.contains(channel) =>
case ChannelStateChanged(channel, _, remoteNodeId, _, currentState, currentData) if m.contains(channel) =>
val channelPaneController = m(channel)
runInGuiThread { () =>
(currentState, currentData) match {
case (WAIT_FOR_FUNDING_CONFIRMED, d: HasCommitments) =>
channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
case (WAIT_FOR_FUNDING_CONFIRMED, d: HasCommitments) => channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
case _ => {}
}
channelPaneController.close.setVisible(STATE_MUTUAL_CLOSE.contains(currentState))
channelPaneController.forceclose.setVisible(STATE_FORCE_CLOSE.contains(currentState))
channelPaneController.state.setText(currentState.toString)
@ -138,19 +121,30 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
case ChannelSignatureReceived(channel, commitments) if m.contains(channel) =>
val channelPaneController = m(channel)
runInGuiThread(() => updateBalance(channelPaneController, commitments))
channelPaneController.updateBalance(commitments)
val totalBalance = MilliSatoshi(m.values.map(_.getBalance.amount).sum)
runInGuiThread(() => {
channelPaneController.refreshBalance()
mainController.refreshTotalBalance(totalBalance)
})
case Terminated(actor) if m.contains(actor) =>
val channelPaneController = m(actor)
log.debug(s"channel=${channelPaneController.channelId.getText} to be removed from gui")
runInGuiThread(() => mainController.channelBox.getChildren.remove(channelPaneController.root))
val m1 = m - actor
val totalBalance = MilliSatoshi(m1.values.map(_.getBalance.amount).sum)
runInGuiThread(() => {
mainController.refreshTotalBalance(totalBalance)
})
context.become(main(m1))
case NodeDiscovered(nodeAnnouncement) =>
log.debug(s"peer node discovered with node id=${nodeAnnouncement.nodeId}")
runInGuiThread { () =>
if (!mainController.networkNodesList.exists(na => na.nodeId == nodeAnnouncement.nodeId)) {
mainController.networkNodesList.add(nodeAnnouncement)
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.peerNodeId)) {
f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
})
}
@ -170,7 +164,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
val idx = mainController.networkNodesList.indexWhere(na => na.nodeId == nodeAnnouncement.nodeId)
if (idx >= 0) {
mainController.networkNodesList.update(idx, nodeAnnouncement)
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.peerNodeId)) {
f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
})
}

View file

@ -16,48 +16,59 @@
package fr.acinq.eclair.gui.controllers
import akka.actor.ActorRef
import com.google.common.base.Strings
import fr.acinq.bitcoin.MilliSatoshi
import fr.acinq.eclair.CoinUtils
import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, Commitments}
import fr.acinq.eclair.gui.FxApp
import javafx.application.Platform
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.fxml.FXML
import javafx.scene.control._
import javafx.scene.input.{ContextMenuEvent, MouseEvent}
import javafx.scene.layout.VBox
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import grizzled.slf4j.Logging
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.control.Alert.AlertType
/**
* Created by DPA on 23/09/2016.
*/
class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
class ChannelPaneController(val channelRef: ActorRef, val peerNodeId: String) extends Logging {
@FXML var root: VBox = _
@FXML var channelId: TextField = _
@FXML var shortChannelId: TextField = _
@FXML var txId: TextField = _
@FXML var balanceBar: ProgressBar = _
@FXML var amountUs: TextField = _
@FXML var amountUs: Label = _
@FXML var nodeAlias: Label = _
@FXML var nodeId: TextField = _
@FXML var capacity: TextField = _
@FXML var funder: TextField = _
@FXML var state: TextField = _
@FXML var funder: TextField = _
@FXML var close: Button = _
@FXML var forceclose: Button = _
var contextMenu: ContextMenu = _
private var contextMenu: ContextMenu = _
private var balance: MilliSatoshi = MilliSatoshi(0)
private var capacity: MilliSatoshi = MilliSatoshi(0)
private def buildChannelContextMenu() = {
private def buildChannelContextMenu(): Unit = {
Platform.runLater(new Runnable() {
override def run() = {
contextMenu = ContextMenuUtils.buildCopyContext(List(
CopyAction("Copy Channel Id", channelId.getText),
CopyAction("Copy Peer Pubkey", theirNodeIdValue),
CopyAction("Copy Tx Id", txId.getText())
CopyAction("Copy channel id", channelId.getText),
CopyAction("Copy peer pubkey", peerNodeId),
CopyAction("Copy tx id", txId.getText())
))
contextMenu.getStyleClass.add("context-channel")
channelId.setContextMenu(contextMenu)
shortChannelId.setContextMenu(contextMenu)
txId.setContextMenu(contextMenu)
amountUs.setContextMenu(contextMenu)
nodeId.setContextMenu(contextMenu)
capacity.setContextMenu(contextMenu)
funder.setContextMenu(contextMenu)
state.setContextMenu(contextMenu)
}
@ -68,6 +79,38 @@ class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
channelId.textProperty.addListener(new ChangeListener[String] {
override def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = buildChannelContextMenu()
})
nodeId.setText(peerNodeId)
nodeAlias.managedProperty.bind(nodeAlias.visibleProperty)
close.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = {
val alert = new Alert(AlertType.CONFIRMATION,
s"""
|Are you sure you want to close this channel?
|
|$getChannelDetails
|""".stripMargin, ButtonType.YES, ButtonType.NO)
alert.showAndWait
if (alert.getResult eq ButtonType.YES) {
channelRef ! CMD_CLOSE(scriptPubKey = None)
}
}
})
forceclose.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = {
val alert = new Alert(AlertType.WARNING,
s"""
|Careful: force-close is more expensive than a regular close and will incur a delay before funds are spendable.
|
|Are you sure you want to forcibly close this channel?
|
|$getChannelDetails
""".stripMargin, ButtonType.YES, ButtonType.NO)
alert.showAndWait
if (alert.getResult eq ButtonType.YES) {
channelRef ! CMD_FORCECLOSE
}
}
})
buildChannelContextMenu()
}
@ -81,6 +124,31 @@ class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
}
def updateRemoteNodeAlias(alias: String) {
Option(nodeId).map((n: TextField) => n.setText(s"$theirNodeIdValue ($alias)"))
nodeAlias.setText(alias)
nodeAlias.setVisible(!Strings.isNullOrEmpty(alias))
}
def updateBalance(commitments: Commitments) {
balance = MilliSatoshi(commitments.localCommit.spec.toLocalMsat)
capacity = MilliSatoshi(commitments.localCommit.spec.totalFunds)
}
def refreshBalance(): Unit = {
amountUs.setText(s"${CoinUtils.formatAmountInUnit(balance, FxApp.getUnit)} / ${CoinUtils.formatAmountInUnit(capacity, FxApp.getUnit, withUnit = true)}")
balanceBar.setProgress(balance.amount.toDouble / capacity.amount)
}
def getBalance: MilliSatoshi = balance
def getCapacity: MilliSatoshi = capacity
def getChannelDetails: String =
s"""
|Channel details:
|---
|Id: ${channelId.getText().substring(0, 18)}...
|Peer: ${peerNodeId.toString().substring(0, 18)}...
|Balance: ${CoinUtils.formatAmountInUnit(getBalance, FxApp.getUnit, withUnit = true)}
|State: ${state.getText}
|"""
}

View file

@ -20,6 +20,17 @@ import java.text.NumberFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
import fr.acinq.eclair.gui.stages._
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import fr.acinq.eclair.gui.{FxApp, Handlers}
import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import fr.acinq.eclair.{CoinUtils, Setup}
import grizzled.slf4j.Logging
import javafx.animation.{FadeTransition, ParallelTransition, SequentialTransition, TranslateTransition}
import javafx.application.{HostServices, Platform}
import javafx.beans.property._
@ -36,21 +47,9 @@ import javafx.scene.layout.{AnchorPane, HBox, StackPane, VBox}
import javafx.scene.paint.Color
import javafx.scene.shape.Rectangle
import javafx.scene.text.Text
import javafx.stage.FileChooser.ExtensionFilter
import javafx.stage._
import javafx.util.{Callback, Duration}
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
import fr.acinq.eclair.gui.stages._
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import fr.acinq.eclair.gui.{FxApp, Handlers}
import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import fr.acinq.eclair.{CoinUtils, Setup}
import grizzled.slf4j.Logging
case class ChannelInfo(announcement: ChannelAnnouncement,
var feeBaseMsatNode1_opt: Option[Long], var feeBaseMsatNode2_opt: Option[Long],
var feeProportionalMillionthsNode1_opt: Option[Long], var feeProportionalMillionthsNode2_opt: Option[Long],
@ -82,6 +81,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
// status bar elements
@FXML var labelNodeId: Label = _
@FXML var statusBalanceLabel: Label = _
@FXML var rectRGB: Rectangle = _
@FXML var labelAlias: Label = _
@FXML var labelApi: Label = _
@ -240,16 +240,15 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
networkChannelsFeeBaseMsatNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
pc.getValue.feeBaseMsatNode2_opt.map(f => CoinUtils.formatAmountInUnit(MilliSatoshi(f), FxApp.getUnit, withUnit = true)).getOrElse("?"))
// CoinUtils.formatAmountInUnit(MilliSatoshi(pc.getValue.feeBaseMsatNode2), FxApp.getUnit, withUnit = true))
})
// feeProportionalMillionths is fee per satoshi in millionths of a satoshi
networkChannelsFeeProportionalMillionthsNode1Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
pc.getValue.feeProportionalMillionthsNode1_opt.map(f => s"${CoinUtils.COIN_FORMAT.format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
pc.getValue.feeProportionalMillionthsNode1_opt.map(f => s"${NumberFormat.getInstance().format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
})
networkChannelsFeeProportionalMillionthsNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
pc.getValue.feeProportionalMillionthsNode2_opt.map(f => s"${CoinUtils.COIN_FORMAT.format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
pc.getValue.feeProportionalMillionthsNode2_opt.map(f => s"${NumberFormat.getInstance().format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
})
networkChannelsCapacityColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
@ -572,4 +571,8 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
childStage.setX(getWindow.map(w => w.getX + w.getWidth / 2 - childStage.getWidth / 2).getOrElse(0))
childStage.setY(getWindow.map(w => w.getY + w.getHeight / 2 - childStage.getHeight / 2).getOrElse(0))
}
def refreshTotalBalance(total: MilliSatoshi): Unit = {
statusBalanceLabel.setText(CoinUtils.formatAmountInUnit(total, FxApp.getUnit, withUnit = true))
}
}

View file

@ -17,10 +17,9 @@
package fr.acinq.eclair.gui.utils
import javafx.collections.FXCollections
import fr.acinq.eclair.{BtcUnit, MBtcUnit, MSatUnit, SatUnit}
import fr.acinq.eclair._
object Constants {
val FX_UNITS_ARRAY_NO_MSAT = FXCollections.observableArrayList(SatUnit.label, MBtcUnit.label, BtcUnit.label)
val FX_UNITS_ARRAY = FXCollections.observableArrayList(MSatUnit.label, SatUnit.label, MBtcUnit.label, BtcUnit.label)
val FX_UNITS_ARRAY_NO_MSAT = FXCollections.observableArrayList(SatUnit.label, BitUnit.label, MBtcUnit.label, BtcUnit.label)
val FX_UNITS_ARRAY = FXCollections.observableArrayList(MSatUnit.label, SatUnit.label, BitUnit.label, MBtcUnit.label, BtcUnit.label)
}