diff --git a/.idea/runConfigurations/explorer.xml b/.idea/runConfigurations/explorer.xml
index 393a176dc6..df90329e1c 100644
--- a/.idea/runConfigurations/explorer.xml
+++ b/.idea/runConfigurations/explorer.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/client/src/main/kotlin/net/corda/client/fxutils/ObservableUtilities.kt b/client/src/main/kotlin/net/corda/client/fxutils/ObservableUtilities.kt
index 81d75f6f75..2ee4ce3b90 100644
--- a/client/src/main/kotlin/net/corda/client/fxutils/ObservableUtilities.kt
+++ b/client/src/main/kotlin/net/corda/client/fxutils/ObservableUtilities.kt
@@ -275,3 +275,7 @@ fun ObservableList.last(): ObservableValue {
}
}, arrayOf(this))
}
+
+fun ObservableList.unique(): ObservableList {
+ return associateByAggregation { it }.getObservableValues().map { Bindings.valueAt(it, 0) }.flatten()
+}
\ No newline at end of file
diff --git a/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt b/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
index 4192750277..8eb3c6bb5d 100644
--- a/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
+++ b/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
@@ -1,14 +1,14 @@
package net.corda.client.model
+import javafx.collections.ObservableList
+import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.fxutils.foldToObservableList
-import net.corda.client.fxutils.recordInSequence
+import net.corda.client.fxutils.map
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.node.services.Vault
-import javafx.collections.ObservableList
-import kotlinx.support.jdk8.collections.removeIf
import rx.Observable
data class Diff(
@@ -22,10 +22,10 @@ data class Diff(
class ContractStateModel {
private val vaultUpdates: Observable by observable(NodeMonitorModel::vaultUpdates)
- val contractStatesDiff: Observable> = vaultUpdates.map {
+ private val contractStatesDiff: Observable> = vaultUpdates.map {
Diff(it.produced, it.consumed)
}
- val cashStatesDiff: Observable> = contractStatesDiff.map {
+ private val cashStatesDiff: Observable> = contractStatesDiff.map {
// We can't filter removed hashes here as we don't have type info
Diff(it.added.filterCashStateAndRefs(), it.removed)
}
@@ -35,6 +35,7 @@ class ContractStateModel {
observableList.addAll(statesDiff.added)
}
+ val cash = cashStates.map { it.state.data.amount }
companion object {
private fun Collection>.filterCashStateAndRefs(): List> {
diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle
index 258fc0c1e8..bb373a2346 100644
--- a/tools/explorer/build.gradle
+++ b/tools/explorer/build.gradle
@@ -72,6 +72,9 @@ dependencies {
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
compile 'org.controlsfx:controlsfx:8.40.12'
+ compile 'commons-lang:commons-lang:2.6'
+ // This provide com.apple.eawt stub for non-mac system.
+ compile 'com.yuvimasory:orange-extensions:1.3.0'
}
task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
index 2de65e7d12..9dd8dbbf5a 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
@@ -1,41 +1,98 @@
package net.corda.explorer
+import com.apple.eawt.Application
+import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
+import javafx.embed.swing.SwingFXUtils
+import javafx.scene.control.Alert
+import javafx.scene.control.ButtonType
+import javafx.scene.image.Image
import javafx.stage.Stage
+import jfxtras.resources.JFXtrasFontRoboto
import net.corda.client.mock.EventGenerator
import net.corda.client.model.Models
import net.corda.client.model.NodeMonitorModel
import net.corda.core.node.services.ServiceInfo
-import net.corda.explorer.views.runInFxApplicationThread
+import net.corda.explorer.model.CordaViewModel
+import net.corda.explorer.views.*
+import net.corda.explorer.views.cordapps.CashViewer
import net.corda.node.driver.PortAllocation
import net.corda.node.driver.driver
import net.corda.node.services.User
import net.corda.node.services.config.FullNodeConfiguration
+import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.startProtocol
import net.corda.node.services.startProtocolPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.protocols.CashProtocol
+import org.apache.commons.lang.SystemUtils
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
+import tornadofx.addStageIcon
+import tornadofx.find
import java.util.*
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
*/
class Main : App() {
- override val primaryView = MainWindow::class
+ override val primaryView = MainView::class
+ private val loginView by inject()
override fun start(stage: Stage) {
+ // Login to Corda node
+ loginView.login { hostAndPort, username, password ->
+ Models.get(MainView::class).register(hostAndPort, configureTestSSL(), username, password)
+ }
+ super.start(stage)
+ stage.minHeight = 600.0
+ stage.minWidth = 800.0
+ stage.setOnCloseRequest {
+ val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
+ initOwner(stage.scene.window)
+ }.showAndWait().get()
+ if (button != ButtonType.OK) it.consume()
+ }
+ }
+
+ init {
+ // Shows any uncaught exception in exception dialog.
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace()
- // Show exceptions in exception dialog.
+ // Show exceptions in exception dialog. Ensure this runs in application thread.
runInFxApplicationThread {
- // [showAndWait] need to be in the FX thread
+ // [showAndWait] need to be in the FX thread.
ExceptionDialog(throwable).showAndWait()
System.exit(1)
}
}
- super.start(stage)
+ // Do this first before creating the notification bar, so it can autosize itself properly.
+ loadFontsAndStyles()
+ // Add Corda logo to OSX dock and windows icon.
+ val cordaLogo = Image(javaClass.getResourceAsStream("images/Logo-03.png"))
+ if (SystemUtils.IS_OS_MAC_OSX) {
+ Application.getApplication().dockIconImage = SwingFXUtils.fromFXImage(cordaLogo, null)
+ }
+ addStageIcon(cordaLogo)
+ // Register views.
+ Models.get(Main::class).apply {
+ // TODO : This could block the UI thread when number of views increase, maybe we can make this async and display a loading screen.
+ // Stock Views.
+ registerView()
+ registerView()
+ // CordApps Views.
+ registerView()
+ // Tools.
+ registerView()
+ registerView()
+ // Default view to Dashboard.
+ selectedView.set(find())
+ }
+ }
+
+ private fun loadFontsAndStyles() {
+ JFXtrasFontRoboto.loadAll()
+ FontAwesomeIconFactory.get() // Force initialisation.
}
}
@@ -57,12 +114,11 @@ fun main(args: Array) {
arrayOf(notaryNode, aliceNode, bobNode).forEach {
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
}
-
// Register with alice to use alice's RPC proxy to create random events.
Models.get(Main::class).register(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config), user.username, user.password)
val rpcProxy = Models.get(Main::class).proxyObservable.get()
- for (i in 0..10000) {
+ for (i in 0..10) {
Thread.sleep(500)
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/MainWindow.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/MainWindow.kt
deleted file mode 100644
index 1f04460659..0000000000
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/MainWindow.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package net.corda.explorer
-
-import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
-import jfxtras.resources.JFXtrasFontRoboto
-import net.corda.client.model.Models
-import net.corda.client.model.NodeMonitorModel
-import net.corda.explorer.views.LoginView
-import net.corda.explorer.views.TopLevel
-import net.corda.node.services.config.configureTestSSL
-import tornadofx.View
-import tornadofx.importStylesheet
-
-/**
- * The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
- */
-class MainWindow : View() {
- private val toplevel: TopLevel by inject()
- override val root = toplevel.root
- private val loginView by inject()
-
- init {
- // Do this first before creating the notification bar, so it can autosize itself properly.
- loadFontsAndStyles()
- loginView.login { hostAndPort, username, password ->
- Models.get(MainWindow::class).register(hostAndPort, configureTestSSL(), username, password)
- }
- }
-
- private fun loadFontsAndStyles() {
- JFXtrasFontRoboto.loadAll()
- importStylesheet("/net/corda/explorer/css/wallet.css")
- FontAwesomeIconFactory.get() // Force initialisation.
- root.styleClass += "root"
- }
-}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt
new file mode 100644
index 0000000000..6899e7f8b3
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt
@@ -0,0 +1,33 @@
+package net.corda.explorer.model
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import javafx.beans.property.SimpleObjectProperty
+import javafx.scene.Node
+import tornadofx.View
+import tornadofx.find
+import tornadofx.observable
+
+class CordaViewModel {
+ val selectedView = SimpleObjectProperty()
+ val registeredViews = mutableListOf().observable()
+
+ inline fun registerView() where T : CordaView {
+ // Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
+ // point to the top level observable/stream wiring! Any events sent before this init may be lost!
+ registeredViews.add(find().apply { root })
+ }
+}
+
+/**
+ * Contain methods to construct various UI component used by the explorer UI framework.
+ * TODO : Implement views with this interface and register in [CordaViewModel] when UI start up. We can use the [CordaViewModel] to dynamically create sidebar and dashboard without manual wiring.
+ * TODO : "goto" functionality?
+ */
+abstract class CordaView(title: String? = null) : View(title) {
+ abstract val widget: Node?
+ abstract val icon: FontAwesomeIcon
+
+ init {
+ if (title == null) super.title = javaClass.simpleName
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/TopLevelModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/TopLevelModel.kt
deleted file mode 100644
index d7e95c997d..0000000000
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/TopLevelModel.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.corda.explorer.model
-
-import javafx.beans.property.SimpleObjectProperty
-import javafx.scene.image.Image
-
-enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array = emptyArray()) {
- Home("Home", getImage("home.png")),
- Transaction("Transaction", getImage("tx.png")),
- Setting("Setting", getImage("settings_lrg.png")),
- NewTransaction("New Transaction", getImage("cash.png")),
- Cash("Cash", getImage("cash.png"), arrayOf(Transaction, NewTransaction)),
- NetworkMap("Network Map", getImage("cash.png")),
- Vault("Vault", getImage("cash.png"), arrayOf(Cash)),
- Network("Network", getImage("inst.png"), arrayOf(NetworkMap, Transaction))
-}
-
-private fun getImage(imageName: String): Image {
- val basePath = "/net/corda/explorer/images"
- return Image("$basePath/$imageName")
-}
-
-class TopLevelModel {
- val selectedView = SimpleObjectProperty(SelectedView.Home)
-}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/CordaView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/CordaView.kt
deleted file mode 100644
index 001a28350d..0000000000
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/CordaView.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.corda.explorer.views
-
-import javafx.scene.Node
-
-/**
- * Corda view interface, provides methods to construct various UI component used by the explorer UI framework.
- * TODO : Implement this interface on all views and register the views with ViewModel when UI start up, then we can use the ViewModel to dynamically create sidebar and dashboard without manual wiring.
- * TODO : Sidebar icons.
- */
-interface CordaView {
- val widget: Node?
- val viewName: String
-}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt
new file mode 100644
index 0000000000..a9f8df9fe8
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt
@@ -0,0 +1,51 @@
+package net.corda.explorer.views
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import javafx.beans.binding.Bindings
+import javafx.scene.Node
+import javafx.scene.Parent
+import javafx.scene.control.TitledPane
+import javafx.scene.input.MouseButton
+import javafx.scene.layout.TilePane
+import net.corda.client.fxutils.filterNotNull
+import net.corda.client.fxutils.map
+import net.corda.client.model.observableList
+import net.corda.client.model.writableValue
+import net.corda.explorer.model.CordaView
+import net.corda.explorer.model.CordaViewModel
+
+class Dashboard : CordaView() {
+ override val root: Parent by fxml()
+ override val icon = FontAwesomeIcon.DASHBOARD
+ override val widget: Node? = null
+ private val tilePane: TilePane by fxid()
+ private val template: TitledPane by fxid()
+
+ private val selectedView by writableValue(CordaViewModel::selectedView)
+ private val registeredViews by observableList(CordaViewModel::registeredViews)
+
+ init {
+ val widgetPanes = registeredViews.map { view ->
+ view.widget?.let {
+ TitledPane(view.title, it).apply {
+ styleClass.addAll(template.styleClass)
+ collapsibleProperty().bind(template.collapsibleProperty())
+ setOnMouseClicked {
+ if (it.button == MouseButton.PRIMARY) {
+ selectedView.value = view
+ }
+ }
+ }
+ }
+ }.filterNotNull()
+
+ Bindings.bindContent(tilePane.children, widgetPanes)
+
+ // Dynamically change column count and width according to the window size.
+ tilePane.widthProperty().addListener { e ->
+ val prefWidth = 350
+ val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
+ tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Formatter.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Formatter.kt
new file mode 100644
index 0000000000..5c5af069e0
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Formatter.kt
@@ -0,0 +1,33 @@
+package net.corda.explorer.views
+
+import javafx.scene.control.TextFormatter
+import javafx.util.converter.BigDecimalStringConverter
+import javafx.util.converter.ByteStringConverter
+import javafx.util.converter.IntegerStringConverter
+import java.math.BigDecimal
+import java.util.regex.Pattern
+
+// BigDecimal text Formatter, restricting text box input to decimal values.
+fun bigDecimalFormatter(): TextFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run {
+ TextFormatter(BigDecimalStringConverter(), null) { change ->
+ val newText = change.controlNewText
+ if (matcher(newText).matches()) change else null
+ }
+}
+
+// Byte text Formatter, restricting text box input to decimal values.
+fun byteFormatter(): TextFormatter = Pattern.compile("\\d*").run {
+ TextFormatter(ByteStringConverter(), null) { change ->
+ val newText = change.controlNewText
+ if (matcher(newText).matches()) change else null
+ }
+}
+
+// Short text Formatter, restricting text box input to decimal values.
+fun intFormatter(): TextFormatter = Pattern.compile("\\d*").run {
+ TextFormatter(IntegerStringConverter(), null) { change ->
+ val newText = change.controlNewText
+ if (matcher(newText).matches()) change else null
+ }
+}
+
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt
index 11c7070b5c..6c0818abf4 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt
@@ -1,7 +1,15 @@
package net.corda.explorer.views
import javafx.application.Platform
+import javafx.event.EventTarget
+import javafx.geometry.Pos
+import javafx.scene.Parent
+import javafx.scene.layout.GridPane
+import javafx.scene.layout.Priority
+import javafx.scene.text.TextAlignment
import javafx.util.StringConverter
+import tornadofx.gridpane
+import tornadofx.label
/**
* Helper method to reduce boiler plate code
@@ -24,8 +32,9 @@ fun stringConverter(fromStringFunction: ((String?) -> T)? = null, toStringFu
*/
fun Number.toStringWithSuffix(precision: Int = 1): String {
if (this.toDouble() < 1000) return "$this"
- val exp = (Math.log(this.toDouble()) / Math.log(1000.0)).toInt()
- return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)} ${"kMGTPE"[exp - 1]}"
+ val scales = "kMBT"
+ val exp = Math.min(scales.length, (Math.log(this.toDouble()) / Math.log(1000.0)).toInt())
+ return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)}${scales[exp - 1]}"
}
fun Double.format(precision: Int) = String.format("%.${precision}f", this)
@@ -40,3 +49,15 @@ fun runInFxApplicationThread(block: () -> Unit) {
Platform.runLater(block)
}
}
+
+fun EventTarget.underConstruction(): Parent {
+ return gridpane {
+ label("Under Construction...") {
+ maxWidth = Double.MAX_VALUE
+ textAlignment = TextAlignment.CENTER
+ alignment = Pos.CENTER
+ GridPane.setVgrow(this, Priority.ALWAYS)
+ GridPane.setHgrow(this, Priority.ALWAYS)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Header.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Header.kt
deleted file mode 100644
index 2ba7d54a74..0000000000
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Header.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.corda.explorer.views
-
-import net.corda.client.fxutils.map
-import net.corda.client.model.NetworkIdentityModel
-import net.corda.client.model.observableValue
-import net.corda.explorer.model.TopLevelModel
-import javafx.scene.control.Label
-import javafx.scene.control.SplitMenuButton
-import javafx.scene.image.ImageView
-import javafx.scene.layout.GridPane
-import tornadofx.View
-
-class Header : View() {
- override val root: GridPane by fxml()
-
- private val sectionLabel: Label by fxid()
- private val userButton: SplitMenuButton by fxid()
- private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
- private val selectedView by observableValue(TopLevelModel::selectedView)
-
- init {
- sectionLabel.textProperty().bind(selectedView.map { it.displayableName })
- sectionLabel.graphicProperty().bind(selectedView.map {
- ImageView(it.image).apply {
- fitHeight = 30.0
- fitWidth = 30.0
- }
- })
- userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
- }
-}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Home.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Home.kt
deleted file mode 100644
index e79922589d..0000000000
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Home.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.corda.explorer.views
-
-import net.corda.client.fxutils.map
-import net.corda.client.model.GatheredTransactionData
-import net.corda.client.model.GatheredTransactionDataModel
-import net.corda.client.model.observableListReadOnly
-import net.corda.client.model.writableValue
-import net.corda.explorer.model.SelectedView
-import net.corda.explorer.model.TopLevelModel
-import javafx.beans.binding.Bindings
-import javafx.beans.value.WritableValue
-import javafx.collections.ObservableList
-import javafx.scene.Node
-import javafx.scene.Parent
-import javafx.scene.control.Label
-import javafx.scene.control.TitledPane
-import javafx.scene.input.MouseButton
-import javafx.scene.input.MouseEvent
-import javafx.scene.layout.TilePane
-import tornadofx.View
-import tornadofx.find
-
-class Home : View() {
- override val root: Parent by fxml()
- private val tilePane: TilePane by fxid()
- private val ourCashPane: TitledPane by fxid()
- private val ourTransactionsLabel: Label by fxid()
-
- private val selectedView: WritableValue by writableValue(TopLevelModel::selectedView)
- private val gatheredTransactionDataList: ObservableList
- by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
-
- init {
- // TODO: register views in view model and populate the dashboard dynamically.
- ourTransactionsLabel.textProperty().bind(
- Bindings.size(gatheredTransactionDataList).map { it.toString() }
- )
-
- ourCashPane.apply {
- content = find(CashViewer::class).widget
- }
-
- tilePane.widthProperty().addListener { e ->
- val prefWidth = 350
- val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
- tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
- }
- }
-
- fun changeView(event: MouseEvent) {
- if (event.button == MouseButton.PRIMARY) {
- selectedView.value = SelectedView.valueOf((event.source as Node).id)
- }
- }
-}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt
index eee74fcaad..95d2a11a0a 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt
@@ -3,14 +3,12 @@ package net.corda.explorer.views
import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.*
-import javafx.util.converter.IntegerStringConverter
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View
-import java.util.regex.Pattern
import kotlin.system.exitProcess
class LoginView : View() {
- override val root: DialogPane by fxml()
+ override val root by fxml()
private val host by fxid()
private val port by fxid()
@@ -19,46 +17,42 @@ class LoginView : View() {
private val portProperty = SimpleIntegerProperty()
fun login(loginFunction: (HostAndPort, String, String) -> Unit) {
- val loggedIn = Dialog().apply {
+ val status = Dialog().apply {
dialogPane = root
- var exception = false
setResultConverter {
- exception = false
when (it?.buttonData) {
ButtonBar.ButtonData.OK_DONE -> try {
// TODO : Run this async to avoid UI lockup.
loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text)
- true
+ LoginStatus.loggedIn
} catch (e: Exception) {
- ExceptionDialog(e).showAndWait()
- exception = true
- false
+ // TODO : Handle this in a more user friendly way.
+ ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
+ LoginStatus.exception
}
- else -> false
+ else -> LoginStatus.exited
}
}
setOnCloseRequest {
- if (!result && !exception) {
- when (Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
- }.showAndWait().get()) {
- ButtonType.OK -> exitProcess(0)
+ if (result == LoginStatus.exited) {
+ val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
+ initOwner(root.scene.window)
+ }.showAndWait().get()
+ if (button == ButtonType.OK) {
+ exitProcess(0)
}
}
}
}.showAndWait().get()
-
- if (!loggedIn) login(loginFunction)
+ if (status != LoginStatus.loggedIn) login(loginFunction)
}
init {
// Restrict text field to Integer only.
- val integerFormat = Pattern.compile("-?(\\d*)").run {
- TextFormatter(IntegerStringConverter(), null) { change ->
- val newText = change.controlNewText
- if (matcher(newText).matches()) change else null
- }
- }
- port.textFormatter = integerFormat
- portProperty.bind(integerFormat.valueProperty())
+ port.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
}
-}
+
+ private enum class LoginStatus {
+ loggedIn, exited, exception
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/MainView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/MainView.kt
new file mode 100644
index 0000000000..edb9862fcb
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/MainView.kt
@@ -0,0 +1,89 @@
+package net.corda.explorer.views
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
+import javafx.beans.binding.Bindings
+import javafx.geometry.Insets
+import javafx.geometry.Pos
+import javafx.scene.Parent
+import javafx.scene.control.ContentDisplay
+import javafx.scene.control.MenuButton
+import javafx.scene.input.MouseButton
+import javafx.scene.layout.BorderPane
+import javafx.scene.layout.StackPane
+import javafx.scene.layout.VBox
+import javafx.scene.text.Font
+import javafx.scene.text.TextAlignment
+import net.corda.client.fxutils.ChosenList
+import net.corda.client.fxutils.map
+import net.corda.client.model.NetworkIdentityModel
+import net.corda.client.model.objectProperty
+import net.corda.client.model.observableList
+import net.corda.client.model.observableValue
+import net.corda.explorer.model.CordaViewModel
+import tornadofx.*
+
+/**
+ * The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
+ */
+class MainView : View() {
+ override val root by fxml()
+
+ // Inject components.
+ private val userButton by fxid()
+ private val sidebar by fxid()
+ private val selectionBorderPane by fxid()
+
+ // Inject data.
+ private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
+ private val selectedView by objectProperty(CordaViewModel::selectedView)
+ private val registeredViews by observableList(CordaViewModel::registeredViews)
+
+ private val menuItemCSS = "sidebar-menu-item"
+ private val menuItemArrowCSS = "sidebar-menu-item-arrow"
+ private val menuItemSelectedCSS = "$menuItemCSS-selected"
+
+ init {
+ // Header
+ userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
+ // Sidebar
+ val menuItems = registeredViews.map {
+ // This needed to be declared val or else it will get GCed and listener unregistered.
+ val buttonStyle = ChosenList(selectedView.map { selected->
+ if(selected == it) listOf(menuItemCSS, menuItemSelectedCSS).observable() else listOf(menuItemCSS).observable()
+ })
+ stackpane {
+ button(it.title) {
+ graphic = FontAwesomeIconView(it.icon).apply {
+ glyphSize = 30
+ textAlignment = TextAlignment.CENTER
+ fillProperty().bind(this@button.textFillProperty())
+ }
+ Bindings.bindContent(styleClass, buttonStyle)
+ setOnMouseClicked { e ->
+ if (e.button == MouseButton.PRIMARY) {
+ selectedView.value = it
+ }
+ }
+ // Transform to smaller icon layout when sidebar width is below 150.
+ val smallIconProperty = widthProperty().map { (it.toDouble() < 150) }
+ contentDisplayProperty().bind(smallIconProperty.map { if (it) ContentDisplay.TOP else ContentDisplay.LEFT })
+ textAlignmentProperty().bind(smallIconProperty.map { if (it) TextAlignment.CENTER else TextAlignment.LEFT })
+ alignmentProperty().bind(smallIconProperty.map { if (it) Pos.CENTER else Pos.CENTER_LEFT })
+ fontProperty().bind(smallIconProperty.map { if (it) Font.font(10.0) else Font.font(12.0) })
+ wrapTextProperty().bind(smallIconProperty)
+ }
+ // Small triangle indicator to make selected view more obvious.
+ add(FontAwesomeIconView(FontAwesomeIcon.CARET_LEFT).apply {
+ StackPane.setAlignment(this, Pos.CENTER_RIGHT)
+ StackPane.setMargin(this, Insets(0.0, -5.0, 0.0, 0.0))
+ styleClass.add(menuItemArrowCSS)
+ visibleProperty().bind(selectedView.map { selected -> selected == it })
+ })
+ }
+ }
+ Bindings.bindContent(sidebar.children, menuItems)
+ // Main view
+ selectionBorderPane.centerProperty().bind(selectedView.map { it?.root })
+ }
+}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt
new file mode 100644
index 0000000000..fc9caf3728
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt
@@ -0,0 +1,13 @@
+package net.corda.explorer.views
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import javafx.scene.Node
+import net.corda.explorer.model.CordaView
+
+// TODO : Construct a node map using node info and display hem on a world map.
+// TODO : Allow user to see transactions between nodes on a world map.
+class Network : CordaView() {
+ override val root = underConstruction()
+ override val widget: Node? = null
+ override val icon = FontAwesomeIcon.GLOBE
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/NewTransaction.kt
index c5bd008964..df59c322b9 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/NewTransaction.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/NewTransaction.kt
@@ -2,65 +2,114 @@ package net.corda.explorer.views
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
+import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
-import javafx.collections.ObservableList
-import javafx.scene.Node
-import javafx.scene.Parent
import javafx.scene.control.*
-import javafx.util.converter.BigDecimalStringConverter
+import javafx.stage.Window
import net.corda.client.fxutils.map
-import net.corda.client.model.NetworkIdentityModel
-import net.corda.client.model.NodeMonitorModel
-import net.corda.client.model.observableList
-import net.corda.client.model.observableValue
+import net.corda.client.fxutils.unique
+import net.corda.client.model.*
import net.corda.core.contracts.*
+import net.corda.core.crypto.Party
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes
import net.corda.explorer.model.CashTransaction
-import net.corda.node.services.messaging.CordaRPCOps
import net.corda.node.services.messaging.startProtocol
import net.corda.protocols.CashCommand
import net.corda.protocols.CashProtocol
import net.corda.protocols.CashProtocolResult
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View
+import tornadofx.observable
import java.math.BigDecimal
import java.util.*
-import java.util.regex.Pattern
class NewTransaction : View() {
- override val root: Parent by fxml()
+ override val root by fxml()
- private val partyATextField: TextField by fxid()
- private val partyBChoiceBox: ChoiceBox by fxid()
- private val partyALabel: Label by fxid()
- private val partyBLabel: Label by fxid()
- private val amountLabel: Label by fxid()
+ // Components
+ private val transactionTypeCB by fxid>()
+ private val partyATextField by fxid()
+ private val partyALabel by fxid
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/SearchField.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/SearchField.fxml
index 69a64340ba..6a39e0db50 100644
--- a/tools/explorer/src/main/resources/net/corda/explorer/views/SearchField.fxml
+++ b/tools/explorer/src/main/resources/net/corda/explorer/views/SearchField.fxml
@@ -1,23 +1,26 @@
+
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
-
-
+
-
+
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/Sidebar.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/Sidebar.fxml
deleted file mode 100644
index 355c7caf3e..0000000000
--- a/tools/explorer/src/main/resources/net/corda/explorer/views/Sidebar.fxml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/TopLevel.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/TopLevel.fxml
deleted file mode 100644
index cfa177630a..0000000000
--- a/tools/explorer/src/main/resources/net/corda/explorer/views/TopLevel.fxml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/TransactionViewer.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/TransactionViewer.fxml
index 1f5febab64..72f286e061 100644
--- a/tools/explorer/src/main/resources/net/corda/explorer/views/TransactionViewer.fxml
+++ b/tools/explorer/src/main/resources/net/corda/explorer/views/TransactionViewer.fxml
@@ -5,9 +5,9 @@
-
+
-
+
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/CashStateViewer.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/CashStateViewer.fxml
similarity index 100%
rename from tools/explorer/src/main/resources/net/corda/explorer/views/CashStateViewer.fxml
rename to tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/CashStateViewer.fxml
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/CashViewer.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/CashViewer.fxml
similarity index 83%
rename from tools/explorer/src/main/resources/net/corda/explorer/views/CashViewer.fxml
rename to tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/CashViewer.fxml
index 34d7c38ae5..0f4c0ba604 100644
--- a/tools/explorer/src/main/resources/net/corda/explorer/views/CashViewer.fxml
+++ b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/CashViewer.fxml
@@ -3,10 +3,13 @@
-
+
-
+
+
+
+
diff --git a/tools/explorer/src/test/kotlin/net/corda/explorer/views/GuiUtilitiesKtTest.kt b/tools/explorer/src/test/kotlin/net/corda/explorer/views/GuiUtilitiesKtTest.kt
new file mode 100644
index 0000000000..7e830bc914
--- /dev/null
+++ b/tools/explorer/src/test/kotlin/net/corda/explorer/views/GuiUtilitiesKtTest.kt
@@ -0,0 +1,16 @@
+package net.corda.explorer.views
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class GuiUtilitiesKtTest {
+ @Test
+ fun `test to string with suffix`() {
+ assertEquals("10.5k", 10500.toStringWithSuffix())
+ assertEquals("100", 100.toStringWithSuffix())
+ assertEquals("5.0M", 5000000.toStringWithSuffix())
+ assertEquals("1.0B", 1000000000.toStringWithSuffix())
+ assertEquals("1.5T", 1500000000000.toStringWithSuffix())
+ assertEquals("1000.0T", 1000000000000000.toStringWithSuffix())
+ }
+}
\ No newline at end of file