DemoBench: UI improvements, part uno

This commit is contained in:
Mike Hearn 2017-04-13 17:31:13 +02:00
parent 87dd99d968
commit a90b2ba839
11 changed files with 194 additions and 70 deletions

View File

@ -48,17 +48,20 @@ data class PhysicalLocation(val coordinate: WorldCoordinate, val description: St
* A simple lookup table of city names to their coordinates. Lookups are case insensitive. * A simple lookup table of city names to their coordinates. Lookups are case insensitive.
*/ */
object CityDatabase { object CityDatabase {
private val cityMap = HashMap<String, PhysicalLocation>() private val caseInsensitiveLookups = HashMap<String, PhysicalLocation>()
val cityMap = HashMap<String, PhysicalLocation>()
init { init {
javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines -> javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines ->
for (line in lines) { for (line in lines) {
if (line.startsWith("#")) continue if (line.startsWith("#")) continue
val (name, lng, lat) = line.split('\t') val (name, lng, lat) = line.split('\t')
cityMap[name.toLowerCase()] = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), name) val location = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), name)
caseInsensitiveLookups[name.toLowerCase()] = location
cityMap[name] = location
} }
} }
} }
operator fun get(name: String) = cityMap[name.toLowerCase()] operator fun get(name: String) = caseInsensitiveLookups[name.toLowerCase()]
} }

View File

@ -5,6 +5,8 @@ import net.corda.demobench.views.DemoBenchView
import tornadofx.* import tornadofx.*
import java.io.InputStreamReader import java.io.InputStreamReader
import java.nio.charset.StandardCharsets.UTF_8 import java.nio.charset.StandardCharsets.UTF_8
import javax.swing.SwingUtilities
import javax.swing.UIManager
/** /**
* README! * README!

View File

@ -25,6 +25,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar") private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar")
//private val command = arrayOf("/usr/local/bin/fish")
private val command = jvm.commandFor(cordaPath).toTypedArray() private val command = jvm.commandFor(cordaPath).toTypedArray()
private val nodes = LinkedHashMap<String, NodeConfig>() private val nodes = LinkedHashMap<String, NodeConfig>()

View File

@ -15,7 +15,6 @@ import tornadofx.*
import java.util.* import java.util.*
class DemoBenchView : View("Corda DemoBench") { class DemoBenchView : View("Corda DemoBench") {
override val root by fxml<Parent>() override val root by fxml<Parent>()
private val profileController by inject<ProfileController>() private val profileController by inject<ProfileController>()
@ -26,6 +25,7 @@ class DemoBenchView : View("Corda DemoBench") {
private val menuSaveAs by fxid<MenuItem>() private val menuSaveAs by fxid<MenuItem>()
init { init {
importStylesheet("/net/corda/demobench/r3-style.css")
importStylesheet("/net/corda/demobench/style.css") importStylesheet("/net/corda/demobench/style.css")
configureShutdown() configureShutdown()

View File

@ -1,27 +1,34 @@
package net.corda.demobench.views package net.corda.demobench.views
import javafx.application.Platform import javafx.application.Platform
import javafx.scene.control.ComboBox
import javafx.scene.control.SelectionMode.MULTIPLE import javafx.scene.control.SelectionMode.MULTIPLE
import javafx.scene.control.SelectionMode.values
import javafx.scene.input.KeyCode import javafx.scene.input.KeyCode
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.scene.layout.Priority
import javafx.stage.FileChooser import javafx.stage.FileChooser
import javafx.util.converter.NumberStringConverter import javafx.util.converter.NumberStringConverter
import net.corda.core.node.CityDatabase
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.demobench.model.* import net.corda.demobench.model.*
import net.corda.demobench.ui.CloseableTab import net.corda.demobench.ui.CloseableTab
import org.controlsfx.control.textfield.TextFields
import tornadofx.* import tornadofx.*
import java.nio.file.Path import java.nio.file.Path
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.* import java.util.*
class NodeTabView : Fragment() { class NodeTabView : Fragment() {
private val cityList = CityDatabase.cityMap.keys.toList().sorted()
override val root = stackpane {} override val root = stackpane {}
private val main by inject<DemoBenchView>() private val main by inject<DemoBenchView>()
private val showConfig by param(true) private val showConfig by param(true)
private companion object : Component() { private companion object : Component() {
const val textWidth = 200.0 const val textWidth = 465.0
const val numberWidth = 100.0 const val numberWidth = 100.0
const val maxNameLength = 15 const val maxNameLength = 15
@ -46,32 +53,28 @@ class NodeTabView : Fragment() {
private val nodeTerminalView = find<NodeTerminalView>() private val nodeTerminalView = find<NodeTerminalView>()
private val nodeConfigView = stackpane { private val nodeConfigView = stackpane {
isVisible = showConfig isVisible = showConfig
styleClass += "config-view"
form { form {
fieldset("Configuration") { fieldset("Configuration") {
isFillWidth = false isFillWidth = false
field("Node Name", op = { nodeNameField() }) field("Legal name") { nodeNameField() }
field("Nearest City", op = { nearestCityField() }) field("Nearest city") { nearestCityField() }
}
/*fieldset("Ports") {
field("P2P Port", op = { p2pPortField() }) field("P2P Port", op = { p2pPortField() })
field("RPC port", op = { rpcPortField() }) field("RPC port", op = { rpcPortField() })
field("Web Port", op = { webPortField() }) field("Web Port", op = { webPortField() })
field("Database Port", op = { databasePortField() }) field("Database Port", op = { databasePortField() })
} }*/
hbox { hbox {
styleClass.addAll("node-panel") styleClass.addAll("node-panel")
fieldset("Services") {
styleClass.addAll("services-panel")
listview(availableServices.observable()) {
selectionModel.selectionMode = MULTIPLE
model.item.extraServices.set(selectionModel.selectedItems)
}
}
fieldset("CorDapps") { fieldset("CorDapps") {
hboxConstraints { hGrow = Priority.ALWAYS }
styleClass.addAll("cordapps-panel") styleClass.addAll("cordapps-panel")
listview(cordapps) { listview(cordapps) {
@ -82,23 +85,41 @@ class NodeTabView : Fragment() {
key.consume() key.consume()
} }
} }
button("Add CorDapp") { }
setOnAction {
val app = (chooser.showOpenDialog(null) ?: return@setOnAction).toPath() fieldset("Services") {
if (!cordapps.contains(app)) { styleClass.addAll("services-panel")
cordapps.add(app)
} listview(availableServices.observable()) {
} selectionModel.selectionMode = MULTIPLE
model.item.extraServices.set(selectionModel.selectedItems)
} }
} }
} }
button("Create Node") { hbox {
setOnAction { button("Add CorDapp") {
if (model.validate()) { setOnAction {
launch() val app = (chooser.showOpenDialog(null) ?: return@setOnAction).toPath()
main.enableAddNodes() if (!cordapps.contains(app)) {
main.enableSaveProfile() cordapps.add(app)
}
}
}
// Spacer pane.
pane {
hboxConstraints { hGrow = Priority.ALWAYS }
}
button("Start node") {
styleClass += "start-button"
setOnAction {
if (model.validate()) {
launch()
main.enableAddNodes()
main.enableSaveProfile()
}
} }
} }
} }
@ -117,6 +138,7 @@ class NodeTabView : Fragment() {
root.add(nodeTerminalView) root.add(nodeTerminalView)
model.legalName.value = if (nodeController.hasNetworkMap()) "" else DUMMY_NOTARY.name model.legalName.value = if (nodeController.hasNetworkMap()) "" else DUMMY_NOTARY.name
model.nearestCity.value = if (nodeController.hasNetworkMap()) "" else "London"
model.p2pPort.value = nodeController.nextPort model.p2pPort.value = nodeController.nextPort
model.rpcPort.value = nodeController.nextPort model.rpcPort.value = nodeController.nextPort
model.webPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort
@ -147,13 +169,29 @@ class NodeTabView : Fragment() {
} }
} }
private fun Pane.nearestCityField() = textfield(model.nearestCity) { // private fun Pane.nearestCityField() = textfield(model.nearestCity) {
// minWidth = textWidth
// TextFields.bindAutoCompletion(this, CityDatabase.cityMap.keys)
// validator(trigger = ValidationTrigger.OnBlur) {
// if (it == null || it.trim().isEmpty()) {
// error("Nearest city is required")
// } else if (CityDatabase[it] == null) {
// error("$it is not in our database, sorry")
// } else {
// null
// }
// }
// }
private fun Pane.nearestCityField() = combobox(model.nearestCity, cityList) {
minWidth = textWidth minWidth = textWidth
validator { isEditable = true
if (it == null) { TextFields.bindAutoCompletion(editor, cityList)
error("Nearest city is required") validator(trigger = ValidationTrigger.OnBlur) {
} else if (it.trim().isEmpty()) { if (it == null || it.trim().isEmpty()) {
error("Nearest city is required") error("Nearest city is required")
} else if (CityDatabase[it] == null) {
error("$it is not in our database, sorry")
} else { } else {
null null
} }

View File

@ -31,7 +31,6 @@ class NodeTerminalView : Fragment() {
private val webServerController by inject<WebServerController>() private val webServerController by inject<WebServerController>()
private val nodeName by fxid<Label>() private val nodeName by fxid<Label>()
private val p2pPort by fxid<PropertyLabel>()
private val states by fxid<PropertyLabel>() private val states by fxid<PropertyLabel>()
private val transactions by fxid<PropertyLabel>() private val transactions by fxid<PropertyLabel>()
private val balance by fxid<PropertyLabel>() private val balance by fxid<PropertyLabel>()
@ -49,7 +48,6 @@ class NodeTerminalView : Fragment() {
fun open(config: NodeConfig, onExit: () -> Unit) { fun open(config: NodeConfig, onExit: () -> Unit) {
nodeName.text = config.legalName nodeName.text = config.legalName
p2pPort.value = config.p2pPort.toString()
launchWebButton.text = "Launch\nWeb Server\n(Port ${config.webPort})" launchWebButton.text = "Launch\nWeb Server\n(Port ${config.webPort})"
val swingTerminal = SwingNode() val swingTerminal = SwingNode()
@ -177,8 +175,8 @@ class NodeTerminalView : Fragment() {
} }
class TerminalSettingsProvider : DefaultSettingsProvider() { class TerminalSettingsProvider : DefaultSettingsProvider() {
override fun getDefaultStyle() = TextStyle(TerminalColor.WHITE, TerminalColor.BLACK) override fun getDefaultStyle() = TextStyle(TerminalColor.WHITE, TerminalColor.rgb(50, 50, 50))
override fun emulateX11CopyPaste() = true override fun emulateX11CopyPaste() = true
} }
} }

View File

@ -0,0 +1,55 @@
/** Styles that follow the corporate branding guidelines, and which give a web/bootstrap look'n'feel */
.root-pane {
-fx-font-family: sans-serif;
-fx-font-size: 14pt;
-db-grey: #ddd;
}
.button {
-fx-padding: 12px;
-fx-border-radius: 4px;
}
.info-button {
-fx-base: #5bc0de;
-fx-text-fill: white;
-fx-border-color: #298dff;
-fx-background-color: linear-gradient(to bottom, #70b3ff 0%, #3392ff 100%);
}
.info-button:hover {
-fx-background-color: #3392ff;
}
.tab-pane:top > .tab-header-area > .tab-header-background {
-fx-background-color: white;
-fx-border-color: -db-grey;
-fx-border-width: 0 0 1 0;
}
.tab-pane:top > .tab-header-area {
-fx-padding: 0.816667em 0 0 8em;
}
.tab .focus-indicator {
-fx-border-width: 0, 0, 0;
}
.tab, .tab:selected:hover {
-fx-background-color: white;
-fx-background-radius: 0;
-fx-border-radius: 4px 4px 0 0;
-fx-padding: 10px 15px;
}
.tab:selected {
-fx-border-width: 1;
-fx-border-color: -db-grey -db-grey white -db-grey;
}
.tab:hover {
-fx-background-color: -db-grey;
-fx-background-radius: 4px 4px 0 0;
-fx-cursor: hand;
}

View File

@ -4,27 +4,38 @@
*/ */
.header { .header {
-fx-background-color: #505050; -fx-background-color: white;
-fx-padding: 15px; -fx-padding: 1.5em;
-fx-spacing: 20px;
} }
.property-label .label { .property-label .label {
-fx-font-size: 14pt; -fx-font-size: 14pt;
-fx-text-fill: white; -fx-text-fill: black;
}
.add-node-button {
-fx-base: red;
} }
.big-button { .big-button {
-fx-base: #009759; -fx-font-size: 12pt;
-fx-background-radius: 5px; }
-fx-opacity: 80%;
.start-button {
-fx-base: #26b426;
-fx-text-fill: white;
}
.terminal-gradient-pane {
-fx-border-color: black;
-fx-border-width: 1 0 0 0;
-fx-background-color: linear-gradient(to bottom, black, #323232);
}
.terminal-vbox {
-fx-background-color: #323232;
} }
.node-panel { .node-panel {
-fx-spacing: 20px; -fx-spacing: 1em;
-fx-background-color: white;
} }
.services-panel { .services-panel {
@ -39,5 +50,10 @@
.list-cell { .list-cell {
-fx-control-inner-background: white; -fx-control-inner-background: white;
-fx-control-inner-background-alt: gainsboro; -fx-control-inner-background-alt: #f7f7f7;
}
.config-view {
-fx-background-color: white;
-fx-padding: 1em;
} }

View File

@ -1,22 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.Button?>
<?import javafx.scene.layout.*?> <?import javafx.scene.control.Menu?>
<VBox xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1"> <?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
<MenuBar useSystemMenuBar="true"> <MenuBar useSystemMenuBar="true">
<Menu text="File"> <Menu text="File">
<MenuItem fx:id="menuOpen" text="Open"/> <MenuItem fx:id="menuOpen" text="Open" />
<MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/> <MenuItem fx:id="menuSaveAs" disable="true" text="Save As" />
</Menu> </Menu>
</MenuBar> </MenuBar>
<StackPane VBox.vgrow="ALWAYS"> <StackPane styleClass="root-pane" VBox.vgrow="ALWAYS">
<TabPane fx:id="nodeTabPane" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" <TabPane fx:id="nodeTabPane" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0" />
tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0"/> <ImageView fitHeight="40.0" preserveRatio="true" StackPane.alignment="TOP_LEFT">
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" <Image url="@cordalogo-full-trans.png"/>
StackPane.alignment="TOP_RIGHT">
<StackPane.margin> <StackPane.margin>
<Insets right="5.0" top="5.0"/> <Insets left="20" top="15.0"/>
</StackPane.margin>
</ImageView>
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="info-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
<StackPane.margin>
<Insets right="25.0" top="10.0" />
</StackPane.margin> </StackPane.margin>
</Button> </Button>
</StackPane> </StackPane>

View File

@ -3,19 +3,17 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import net.corda.demobench.ui.*?> <?import net.corda.demobench.ui.*?>
<VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.102" <VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1" styleClass="terminal-vbox">
xmlns:fx="http://javafx.com/fxml/1"> <HBox prefHeight="95.0" prefWidth="800.0" styleClass="header">
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header"> <VBox prefHeight="66.0" HBox.hgrow="ALWAYS">
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0"> <Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" minWidth="NaN"/>
<Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;"/>
<PropertyLabel fx:id="p2pPort" name="P2P port: "/>
</VBox> </VBox>
<VBox prefHeight="93.0" prefWidth="267.0"> <VBox prefHeight="93.0" prefWidth="267.0">
<PropertyLabel fx:id="states" name="States in vault: "/> <PropertyLabel fx:id="states" name="States in vault: "/>
<PropertyLabel fx:id="transactions" name="Known transactions: "/> <PropertyLabel fx:id="transactions" name="Known transactions: "/>
<PropertyLabel fx:id="balance" name="Balance: "/> <PropertyLabel fx:id="balance" name="Balance: "/>
</VBox> </VBox>
<Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS"/> <!--<Pane prefHeight="200.0" prefWidth="200.0"/>-->
<Button fx:id="viewDatabaseButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" <Button fx:id="viewDatabaseButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0"
styleClass="big-button" text="View&#10;Database" textAlignment="CENTER"/> styleClass="big-button" text="View&#10;Database" textAlignment="CENTER"/>
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" <Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0"
@ -23,4 +21,5 @@
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" <Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0"
styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER"/> styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER"/>
</HBox> </HBox>
<Pane minHeight="8" mouseTransparent="true" styleClass="terminal-gradient-pane"/>
</VBox> </VBox>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB