mirror of
https://github.com/corda/corda.git
synced 2025-01-14 08:49:47 +00:00
First iteration of the IntelliJ plugin for the flow snapshots tool (#20)
* First iteration of the IntelliJ plugin for the flow snapshots tool * Addressing review comments * Addressing review comments round 2
This commit is contained in:
parent
3ae53683ea
commit
f5fc4ceeb8
@ -25,6 +25,11 @@ intellij {
|
||||
buildDir = "$projectDir/build"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// For JSON
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:2.8.5"
|
||||
}
|
||||
|
||||
group 'net.corda'
|
||||
version '0.1.0' // Plugin version
|
||||
|
||||
|
@ -13,6 +13,11 @@ Corda Intellij Plugin is a plugin for Intellij Idea IDE which aid Corda applicat
|
||||
###Running the project
|
||||
You can run or debug the project using provided Intellij Run Configuration `CordaPlugin` or by using the gradle command
|
||||
`./gradlew runIde` IDE's log file is located in `build/idea-sandbox/system/log/idea.log`
|
||||
|
||||
###Corda Flow Tool
|
||||
After the IntelliJ (with the Corda plugin) is started, go to View/Tool Windows in the menu bar and in the list of
|
||||
available tools you should see the 'Corda Flow Tool'. In the tool window, point to the flow snapshot location
|
||||
(or node's base directory) and you will be able to see and examine all available flow snapshots.
|
||||
|
||||
## TODOs
|
||||
* Create a higher quality Corda icon.
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.ideaPlugin.module
|
||||
package net.corda.ideaplugin.module
|
||||
|
||||
import com.intellij.ide.fileTemplates.FileTemplateManager
|
||||
import com.intellij.ide.projectWizard.ProjectSettingsStep
|
@ -1,4 +1,4 @@
|
||||
package net.corda.ideaPlugin.module
|
||||
package net.corda.ideaplugin.module
|
||||
|
||||
import com.intellij.openapi.module.ModuleType
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
@ -6,13 +6,14 @@ import com.intellij.openapi.util.IconLoader
|
||||
class CordaModuleType : ModuleType<CordaModuleBuilder>(ID) {
|
||||
companion object {
|
||||
val ID = "CORDA_MODULE"
|
||||
val CORDA_ICON = IconLoader.getIcon("/images/corda-icon.png")
|
||||
val CORDA_ICON = IconLoader.getIcon("/images/corda-module-icon.png")
|
||||
val CORDA_ICON_BIG = IconLoader.getIcon("/images/corda-module-icon@2x.png")
|
||||
val instance = CordaModuleType()
|
||||
}
|
||||
|
||||
override fun createModuleBuilder() = CordaModuleBuilder()
|
||||
override fun getNodeIcon(p0: Boolean) = CORDA_ICON
|
||||
override fun getBigIcon() = CORDA_ICON
|
||||
override fun getBigIcon() = CORDA_ICON_BIG
|
||||
override fun getName() = "CorDapp"
|
||||
override fun getDescription() = "Corda DLT Platform Application"
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.ideaPlugin.module
|
||||
package net.corda.ideaplugin.module
|
||||
|
||||
import com.intellij.ide.util.projectWizard.ModuleWizardStep
|
||||
import com.intellij.openapi.options.ConfigurationException
|
@ -1,4 +1,4 @@
|
||||
package net.corda.ideaPlugin.module
|
||||
package net.corda.ideaplugin.module
|
||||
|
||||
import com.intellij.openapi.util.Version
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
@ -1,4 +1,4 @@
|
||||
package net.corda.ideaPlugin.module
|
||||
package net.corda.ideaplugin.module
|
||||
|
||||
object CordaTemplateProvider {
|
||||
fun getTemplateFiles(cordaTemplate: CordaTemplate, language: Language): List<TemplateFile> {
|
@ -0,0 +1,212 @@
|
||||
package net.corda.ideaplugin.toolwindow
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowFactory
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.content.Content
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import com.intellij.ui.treeStructure.Tree
|
||||
import net.corda.ideaplugin.module.CordaModuleType
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.io.File
|
||||
import javax.swing.*
|
||||
import javax.swing.tree.*
|
||||
|
||||
/**
|
||||
* GUI class for the Corda Flow tool
|
||||
*/
|
||||
class CordaFlowToolWindow : ToolWindowFactory {
|
||||
// Left-hand side tree view
|
||||
private val flowTree = Tree()
|
||||
// Right-hand side tree view
|
||||
private val snapshotTree = Tree()
|
||||
// Main panel
|
||||
private val panel = JPanel(GridBagLayout())
|
||||
private val textField = JTextField()
|
||||
private val browseButton = JButton()
|
||||
private val refreshButton = JButton()
|
||||
|
||||
private val snapshotDataManager = FlowSnapshotTreeDataManager(snapshotTree)
|
||||
private val flowDataManager = FlowTreeDataManager(flowTree, snapshotDataManager)
|
||||
|
||||
init {
|
||||
setUpSnapshotTree()
|
||||
setUpFlowTree()
|
||||
setUpBrowseButton()
|
||||
setUpRefreshButton()
|
||||
setUpTextField()
|
||||
|
||||
// Laying-out left-hand side panel
|
||||
val flowPanel = JPanel(BorderLayout(3, 3))
|
||||
flowPanel.add(BorderLayout.NORTH, JLabel("Flows"))
|
||||
val flowScrollPane = JBScrollPane(flowTree)
|
||||
flowScrollPane.border = BorderFactory.createEmptyBorder()
|
||||
flowPanel.add(BorderLayout.CENTER, flowScrollPane)
|
||||
|
||||
// Laying-out right-hand side panel
|
||||
val snapshotPanel = JPanel(BorderLayout(3, 3))
|
||||
snapshotPanel.add(BorderLayout.NORTH, JLabel("Snapshots"))
|
||||
val snapshotScrollPane = JBScrollPane(snapshotTree)
|
||||
snapshotScrollPane.border = BorderFactory.createEmptyBorder()
|
||||
snapshotPanel.add(BorderLayout.CENTER, snapshotScrollPane)
|
||||
|
||||
// Horizontal divider
|
||||
val splitPane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, flowPanel, snapshotPanel)
|
||||
splitPane.dividerSize = 2
|
||||
splitPane.resizeWeight = 0.5
|
||||
splitPane.border = BorderFactory.createEmptyBorder()
|
||||
|
||||
// Container for the text field and buttons
|
||||
val topPanel = JPanel()
|
||||
topPanel.layout = BoxLayout(topPanel, BoxLayout.LINE_AXIS)
|
||||
topPanel.add(textField)
|
||||
topPanel.add(browseButton)
|
||||
topPanel.add(refreshButton)
|
||||
|
||||
GridBagConstraints().apply {
|
||||
gridwidth = 1
|
||||
weightx = 0.05
|
||||
weighty = 0.05
|
||||
fill = GridBagConstraints.HORIZONTAL // Fill cell in both direction
|
||||
insets = Insets(3, 3, 3, 3)
|
||||
gridy = 0
|
||||
panel.add(topPanel, this)
|
||||
}
|
||||
GridBagConstraints().apply {
|
||||
gridwidth = 1
|
||||
weightx = 1.0 // Cell takes up all extra horizontal space
|
||||
weighty = 1.0 // Cell takes up all extra vertical space
|
||||
fill = GridBagConstraints.BOTH // Fill cell in both direction
|
||||
insets = Insets(3, 3, 3, 3)
|
||||
gridy = 1
|
||||
panel.add(splitPane, this)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the tool window content.
|
||||
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
||||
val contentFactory: ContentFactory = ContentFactory.SERVICE.getInstance()
|
||||
val content: Content = contentFactory.createContent(panel, "", false)
|
||||
toolWindow.contentManager.addContent(content)
|
||||
}
|
||||
|
||||
private fun setUpSnapshotTree() {
|
||||
snapshotTree.isRootVisible = false
|
||||
snapshotTree.cellRenderer = SnapshotTreeRenderer()
|
||||
}
|
||||
|
||||
private fun setUpFlowTree() {
|
||||
flowTree.isRootVisible = false
|
||||
flowTree.selectionModel = LeafOnlyTreeSelectionModel()
|
||||
flowTree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
|
||||
flowTree.cellRenderer = FlowTreeRenderer()
|
||||
flowTree.addTreeSelectionListener {
|
||||
val node = flowTree.lastSelectedPathComponent
|
||||
if (node is DefaultMutableTreeNode) {
|
||||
val dir = node.userObject as File
|
||||
if (dir.exists()) {
|
||||
snapshotDataManager.loadSnapshots(dir.listFiles().filter {
|
||||
!it.isDirectory && it.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
snapshotDataManager.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpBrowseButton() {
|
||||
browseButton.icon = AllIcons.General.Ellipsis
|
||||
browseButton.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent?) {
|
||||
val chooser = JFileChooser()
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
val chooserValue = chooser.showOpenDialog(null)
|
||||
if (chooserValue == JFileChooser.APPROVE_OPTION) {
|
||||
var selectedFile = chooser.selectedFile
|
||||
if (selectedFile.name != FlowTreeDataManager.FLOWS_DIRECTORY) {
|
||||
val subDirectory = File(selectedFile, FlowTreeDataManager.FLOWS_DIRECTORY)
|
||||
if (subDirectory.exists()) {
|
||||
selectedFile = subDirectory
|
||||
} else {
|
||||
textField.text = "Could not find the ${FlowTreeDataManager.FLOWS_DIRECTORY} directory"
|
||||
clear()
|
||||
return
|
||||
}
|
||||
}
|
||||
textField.text = selectedFile.canonicalPath
|
||||
flowDataManager.loadFlows(selectedFile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setUpRefreshButton() {
|
||||
refreshButton.icon = AllIcons.Actions.Refresh
|
||||
refreshButton.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent?) {
|
||||
clear()
|
||||
flowDataManager.loadFlows()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
flowTree.clearSelection()
|
||||
flowDataManager.clear()
|
||||
snapshotTree.clearSelection()
|
||||
snapshotDataManager.clear()
|
||||
}
|
||||
|
||||
private fun setUpTextField() {
|
||||
textField.isEditable = false
|
||||
textField.isFocusable = false
|
||||
textField.background = Color(0, 0, 0, 0)
|
||||
textField.text = "Choose flow data location (i.e. node base directory)"
|
||||
}
|
||||
|
||||
private class FlowTreeRenderer : DefaultTreeCellRenderer() {
|
||||
override fun getTreeCellRendererComponent(tree: JTree?, value: Any?, sel: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean): Component {
|
||||
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||
val node = value as DefaultMutableTreeNode
|
||||
if (node.isLeaf) {
|
||||
icon = AllIcons.Debugger.Frame
|
||||
}
|
||||
val userObject = node.userObject
|
||||
if (userObject is File) {
|
||||
text = userObject.name
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private class SnapshotTreeRenderer : DefaultTreeCellRenderer() {
|
||||
override fun getTreeCellRendererComponent(tree: JTree?, value: Any?, sel: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean): Component {
|
||||
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
|
||||
val descriptor = (value as DefaultMutableTreeNode).userObject as SnapshotDataDescriptor
|
||||
icon = descriptor.icon
|
||||
if (!descriptor.key.isNullOrEmpty()) {
|
||||
text = "${descriptor.key}: ${descriptor.data.toString()}"
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private class LeafOnlyTreeSelectionModel : DefaultTreeSelectionModel() {
|
||||
override fun addSelectionPaths(paths: Array<out TreePath>?) {
|
||||
super.addSelectionPaths(filterPaths(paths))
|
||||
}
|
||||
|
||||
override fun setSelectionPaths(paths: Array<out TreePath>?) {
|
||||
super.setSelectionPaths(filterPaths(paths))
|
||||
}
|
||||
|
||||
private fun filterPaths(paths: Array<out TreePath>?): Array<TreePath>? {
|
||||
return paths?.filter { (it.lastPathComponent as DefaultMutableTreeNode).isLeaf }?.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package net.corda.ideaplugin.toolwindow
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.intellij.icons.AllIcons
|
||||
import java.io.File
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JTree
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
|
||||
/**
|
||||
* Snapshot tree data descriptor. It is used as userObject in the [DefaultMutableTreeNode] class.
|
||||
*/
|
||||
class SnapshotDataDescriptor(val data: Any?, val icon: Icon, val key: String? = null) {
|
||||
override fun toString(): String = data?.toString() ?: "null"
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager class for flow snapshots. It is responsible for parsing data read from a snapshot file and constructing
|
||||
* tree model from it.
|
||||
*/
|
||||
class FlowSnapshotTreeDataManager(tree: JTree) {
|
||||
companion object {
|
||||
const val SNAPSHOT_FILE_PREFIX = "flowStackSnapshot"
|
||||
}
|
||||
|
||||
// Root node for the snapshot hierarchy, which is an empty node.
|
||||
private val root = DefaultMutableTreeNode(SnapshotDataDescriptor(null, AllIcons.Json.Object))
|
||||
// Snapshot tree model
|
||||
private val snapshotModel = DefaultTreeModel(root)
|
||||
|
||||
private val mapper = ObjectMapper()
|
||||
|
||||
init {
|
||||
tree.model = snapshotModel
|
||||
snapshotModel.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all current data from the snapshot hierarchy and refreshes the model.
|
||||
*/
|
||||
fun clear() {
|
||||
root.removeAllChildren()
|
||||
snapshotModel.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs tree model from snapshot files
|
||||
*/
|
||||
fun loadSnapshots(snapshots: List<File>) {
|
||||
root.removeAllChildren()
|
||||
snapshots.forEach {
|
||||
insertNodeToSnapshotModel(it)
|
||||
}
|
||||
snapshotModel.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds snapshot file to the snapshot hierarchy. The content of the file is processed and
|
||||
* the model is updated accordingly.
|
||||
*/
|
||||
fun addNodeToSnapshotModel(snapshotFile: File) {
|
||||
val insertionIndex = -(root.childNodes().map {
|
||||
(it.userObject as SnapshotDataDescriptor).key
|
||||
}.binarySearch(extractFileName(snapshotFile))) - 1
|
||||
insertNodeToSnapshotModel(snapshotFile, insertionIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the snapshot file from the snapshot hierarchy. The model is also updated after this operation.
|
||||
*/
|
||||
fun removeNodeFromSnapshotModel(snapshotFile: File) {
|
||||
val node = root.childNodes().find {
|
||||
(it.userObject as SnapshotDataDescriptor).data == extractFileName(snapshotFile)
|
||||
} as MutableTreeNode?
|
||||
if (node != null) {
|
||||
snapshotModel.removeNodeFromParent(node)
|
||||
snapshotModel.nodesWereRemoved(root, intArrayOf(), arrayOf(node))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertNodeToSnapshotModel(snapshotFile: File, insertionIndex: Int = -1) {
|
||||
buildChildrenModel(mapper.readTree(snapshotFile), root, extractFileName(snapshotFile), insertionIndex)
|
||||
}
|
||||
|
||||
private fun extractFileName(file: File): String {
|
||||
return file.name.substring(0, file.name.lastIndexOf("."))
|
||||
}
|
||||
|
||||
private fun buildChildrenModel(
|
||||
node: JsonNode?,
|
||||
parent: DefaultMutableTreeNode, key: String? = null,
|
||||
insertionIndex: Int = -1) {
|
||||
val child: DefaultMutableTreeNode
|
||||
if (node == null || !node.isContainerNode) {
|
||||
child = DefaultMutableTreeNode(SnapshotDataDescriptor(node, AllIcons.Debugger.Db_primitive, key))
|
||||
addToModelAndRefresh(snapshotModel, child, parent, insertionIndex)
|
||||
} else {
|
||||
if (node.isArray) {
|
||||
child = DefaultMutableTreeNode(SnapshotDataDescriptor(key, AllIcons.Debugger.Db_array))
|
||||
addToModelAndRefresh(snapshotModel, child, parent, insertionIndex)
|
||||
node.mapIndexed { index: Int, item: JsonNode? ->
|
||||
buildChildrenModel(item, child, index.toString())
|
||||
}
|
||||
} else {
|
||||
child = DefaultMutableTreeNode(SnapshotDataDescriptor(key, AllIcons.Json.Object))
|
||||
addToModelAndRefresh(snapshotModel, child, parent, insertionIndex)
|
||||
node.fields().forEach {
|
||||
buildChildrenModel(it.value, child, it.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package net.corda.ideaplugin.toolwindow
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.*
|
||||
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
||||
import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE
|
||||
import javax.swing.JTree
|
||||
import javax.swing.SwingWorker
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
|
||||
/**
|
||||
* Manager class for flow list data.
|
||||
*/
|
||||
class FlowTreeDataManager(val tree: JTree, val snapshotModel: FlowSnapshotTreeDataManager) {
|
||||
companion object {
|
||||
const val FLOWS_DIRECTORY = "flowStackSnapshots"
|
||||
}
|
||||
|
||||
// Root node (i.e. [FLOWS_DIRECTORY]) of the flow directory hierarchy.
|
||||
private val root = DefaultMutableTreeNode()
|
||||
// Flow tree model
|
||||
private val flowModel = DefaultTreeModel(root)
|
||||
|
||||
// Watcher service used to monitor directory contents addition and deletion.
|
||||
private val watcher: WatchService = FileSystems.getDefault().newWatchService()
|
||||
// Registered watch keys for the corresponding directories
|
||||
private val watchKeys = hashMapOf<WatchKey, File>()
|
||||
// Watch keys polling thread. Responsible for processing changes ot the contents of the monitored directories
|
||||
private val dirObserver: DirObserver = DirObserver()
|
||||
|
||||
init {
|
||||
tree.model = flowModel
|
||||
flowModel.reload()
|
||||
dirObserver.execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the flow directory hierarchy with the root being associated with the passed [flowsDirectory].
|
||||
* If the parameter is missing the function rebuilds current hierarchy and reloads (refreshes) current model.
|
||||
*/
|
||||
fun loadFlows(flowsDirectory: File? = root.userObjectAsFile()) {
|
||||
root.userObject = flowsDirectory ?: return
|
||||
root.removeAllChildren()
|
||||
|
||||
// Invalidate all current watch keys
|
||||
invalidateWatchKeys()
|
||||
|
||||
// We need to add a watch key to the parent
|
||||
startWatching(flowsDirectory)
|
||||
|
||||
// We expect 2-level directory nesting. The first level are dates and the second level are flow IDs.
|
||||
// Dates directories
|
||||
flowsDirectory.listFiles().filter { it.isDirectory }.forEach {
|
||||
insertDateDirectory(it)
|
||||
}
|
||||
flowModel.reload()
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserts date directory to the node hierarchy. A date directory is considered to be on the first hierarchy level.
|
||||
*/
|
||||
private fun insertDateDirectory(dateDir: File, insertionIndex: Int = -1) {
|
||||
startWatching(dateDir)
|
||||
val dateNode = DefaultMutableTreeNode(dateDir)
|
||||
addToModelAndRefresh(flowModel, dateNode, root, insertionIndex)
|
||||
// Flows directories
|
||||
dateDir.listFiles().filter { it.isDirectory }.forEach {
|
||||
startWatching(it)
|
||||
addToModelAndRefresh(flowModel, DefaultMutableTreeNode(it), dateNode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes current hierarchy.
|
||||
*/
|
||||
fun clear() {
|
||||
invalidateWatchKeys()
|
||||
root.removeAllChildren()
|
||||
flowModel.reload()
|
||||
}
|
||||
|
||||
private fun isSelected(dir: File?): Boolean {
|
||||
val node = tree.selectedNode()
|
||||
return node != null && dir == node.userObjectAsFile()
|
||||
}
|
||||
|
||||
private fun startWatching(dir: File) {
|
||||
val key = dir.toPath().register(watcher, ENTRY_CREATE, ENTRY_DELETE)
|
||||
watchKeys.put(key, dir)
|
||||
}
|
||||
|
||||
private fun invalidateWatchKeys() {
|
||||
watchKeys.keys.forEach { it.cancel() }
|
||||
watchKeys.clear()
|
||||
}
|
||||
|
||||
private fun addNodeToFlowModel(dir: File) {
|
||||
// We consider addition only of either a date or flow directory
|
||||
val parent = dir.parentFile
|
||||
if (parent.name == FLOWS_DIRECTORY) {
|
||||
// if a date directory has been added this means that that its parent is [FLOWS_DIRECTORY]
|
||||
insertDateDirectory(dir, findInsertionIndex(root.childNodes(), dir.name))
|
||||
} else if (parent.parentFile.name == FLOWS_DIRECTORY) {
|
||||
// if a flow directory has been added this means that that the parent of its parent is [FLOWS_DIRECTORY]
|
||||
val parentNode = root.childNodes().findByFile(parent) ?: return
|
||||
flowModel.insertNodeInto(DefaultMutableTreeNode(dir), parentNode, findInsertionIndex(parentNode.childNodes(), dir.name))
|
||||
startWatching(dir)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeNodeFromFlowModel(dir: File) {
|
||||
val selectedNode = tree.selectedNode()
|
||||
if (selectedNode != null && selectedNode.userObjectAsFile() == dir) {
|
||||
// Reload flows if the [dir] is currently selected
|
||||
loadFlows()
|
||||
return
|
||||
}
|
||||
val parent = dir.parentFile
|
||||
var parentNode: DefaultMutableTreeNode? = null
|
||||
if (parent.name == FLOWS_DIRECTORY) {
|
||||
// if a date directory has been added this means that that its parent is [FLOWS_DIRECTORY]
|
||||
parentNode = root
|
||||
} else if (parent.parentFile.name == FLOWS_DIRECTORY) {
|
||||
// if a flow directory has been added this means that that the parent of its parent is [FLOWS_DIRECTORY]
|
||||
parentNode = root.childNodes().findByFile(parent)
|
||||
}
|
||||
if (parentNode != null) {
|
||||
val node = parentNode.childNodes().findByFile(dir) ?: return
|
||||
flowModel.removeNodeFromParent(node)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findInsertionIndex(nodes: List<DefaultMutableTreeNode>, name: String): Int {
|
||||
return -nodes.toList().map { (it.userObject as File).name }.binarySearch(name) - 1
|
||||
}
|
||||
|
||||
/*
|
||||
* Swing thread polling the watcher service for any changes (i.e. entry creation or deletion).
|
||||
* Once a change is detected the event is processed accordingly.
|
||||
*/
|
||||
private inner class DirObserver : SwingWorker<Void, WatchKey>() {
|
||||
override fun doInBackground(): Void? {
|
||||
while (true) {
|
||||
// wait for key to be signaled
|
||||
val key = try {
|
||||
watcher.take()
|
||||
} catch (x: InterruptedException) {
|
||||
return null
|
||||
}
|
||||
|
||||
publish(key)
|
||||
|
||||
if (!key.reset()) {
|
||||
watchKeys.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun process(keys: MutableList<WatchKey>?) {
|
||||
keys?.forEach {
|
||||
processKey(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processKey(key: WatchKey) {
|
||||
key.pollEvents().forEach {
|
||||
val kind = it.kind()
|
||||
|
||||
// The OVERFLOW event can occur regardless if events are lost or discarded.
|
||||
if (kind == StandardWatchEventKinds.OVERFLOW) {
|
||||
return
|
||||
}
|
||||
processEvent(it as WatchEvent<*>, key)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEvent(event: WatchEvent<*>, key: WatchKey) {
|
||||
val parent = getParent(key)
|
||||
val file = File(parent, (event.context() as Path).toString())
|
||||
when (event.kind()) {
|
||||
ENTRY_CREATE -> {
|
||||
if (!file.isDirectory) {
|
||||
// We only need to do anything if the flow snapshots are being currently examined
|
||||
if (isSelected(parent) && file.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX)) {
|
||||
snapshotModel.addNodeToSnapshotModel(file)
|
||||
}
|
||||
} else {
|
||||
addNodeToFlowModel(file)
|
||||
}
|
||||
}
|
||||
ENTRY_DELETE -> {
|
||||
// We only need to do anything if the flow snapshots are being currently examined
|
||||
if (isSelected(parent) && file.name.startsWith(FlowSnapshotTreeDataManager.SNAPSHOT_FILE_PREFIX)) {
|
||||
snapshotModel.removeNodeFromSnapshotModel(file)
|
||||
} else {
|
||||
removeNodeFromFlowModel(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getParent(key: WatchKey): File? {
|
||||
return watchKeys[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DefaultMutableTreeNode.userObjectAsFile() = userObject as? File
|
||||
internal fun DefaultMutableTreeNode.childNodes() = children().toList().mapNotNull { it as? DefaultMutableTreeNode }
|
||||
private fun List<DefaultMutableTreeNode>.findByFile(file: File) = find { it.userObjectAsFile() == file }
|
||||
private fun JTree.selectedNode() = lastSelectedPathComponent as? DefaultMutableTreeNode
|
||||
|
||||
internal fun addToModelAndRefresh(model: DefaultTreeModel,
|
||||
child: DefaultMutableTreeNode,
|
||||
parent: DefaultMutableTreeNode,
|
||||
insertionIndex: Int = -1) {
|
||||
val indices = if (insertionIndex < 0) {
|
||||
parent.add(child)
|
||||
intArrayOf(parent.childCount - 1)
|
||||
} else {
|
||||
parent.insert(child, insertionIndex)
|
||||
intArrayOf(insertionIndex)
|
||||
}
|
||||
model.nodesWereInserted(parent, indices)
|
||||
}
|
@ -26,7 +26,8 @@
|
||||
<depends>org.jetbrains.plugins.gradle</depends>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<moduleType id="CORDA_MODULE" implementationClass="net.corda.ideaPlugin.module.CordaModuleType"/>
|
||||
<moduleType id="CORDA_MODULE" implementationClass="net.corda.ideaplugin.module.CordaModuleType"/>
|
||||
<toolWindow id="Corda Flow Tool" icon="/images/corda-toolwindow-icon.png" anchor="right" factoryClass="net.corda.ideaplugin.toolwindow.CordaFlowToolWindow" />
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 293 B |
Binary file not shown.
After Width: | Height: | Size: 689 B |
Binary file not shown.
After Width: | Height: | Size: 284 B |
Loading…
Reference in New Issue
Block a user