PageRenderTime 65ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/scala/acumen/ui/tl/FileTree.scala

https://bitbucket.org/effective/acumen-graveyard
Scala | 298 lines | 212 code | 36 blank | 50 comment | 57 complexity | 71b8148340d6937f3ad6e9aaef87d485 MD5 | raw file
  1. package acumen.ui.tl
  2. import java.awt.event.ActionEvent
  3. import java.io.File
  4. import scala.Array.canBuildFrom
  5. import scala.collection.mutable.ArrayBuffer
  6. import scala.collection.mutable.Buffer
  7. import scala.swing.BorderPanel
  8. import scala.swing.Component
  9. import scala.swing.ScrollPane
  10. import acumen.ui.GraphicalMain
  11. import acumen.ui.Icons
  12. import javax.swing.AbstractAction
  13. import javax.swing.JButton
  14. import javax.swing.JToggleButton
  15. import javax.swing.JToolBar
  16. import javax.swing.JTree
  17. import javax.swing.event.ChangeEvent
  18. import javax.swing.event.ChangeListener
  19. import javax.swing.event.TreeModelEvent
  20. import javax.swing.event.TreeModelListener
  21. import javax.swing.tree.TreeModel
  22. import javax.swing.tree.TreePath
  23. import java.awt.event.MouseAdapter
  24. import java.awt.event.MouseEvent
  25. import javax.swing.text.Position
  26. import javax.swing.JComponent
  27. import scala.swing.event.Key
  28. import java.awt.event.KeyAdapter
  29. import java.awt.event.KeyEvent
  30. import javax.swing.JCheckBox
  31. /** Wrapper for FileTree, provides navigation and configuration buttons. */
  32. class FileBrowser(initialPath: File, editor: CodeArea) extends BorderPanel {
  33. val fileTree = new FileTree(initialPath)
  34. /* When synchronization is not enabled, double-clicking a model opens it in the editor */
  35. fileTree.peer.addMouseListener(new MouseAdapter {
  36. override def mousePressed(e: MouseEvent) {
  37. val clicked = fileTree.peer.getPathForLocation(e.getX, e.getY)
  38. if (!GraphicalMain.syncEditorWithBrowser && e.getClickCount == 2 &&
  39. clicked != null && clicked.getLastPathComponent != null) {
  40. val f = clicked.getLastPathComponent.asInstanceOf[File]
  41. if (f.isFile) editor.loadFile(f)
  42. }
  43. }
  44. })
  45. val syncButton = new JCheckBox()
  46. syncButton.setAction(new AbstractAction("Synchronize with editor") {
  47. override def actionPerformed(e: ActionEvent) {
  48. GraphicalMain.syncEditorWithBrowser = !GraphicalMain.syncEditorWithBrowser
  49. if (GraphicalMain.syncEditorWithBrowser)
  50. editor.currentFile match {
  51. case Some(file) => fileTree.focus(file)
  52. case None => fileTree.refresh
  53. }
  54. }
  55. })
  56. syncButton.setSelected(GraphicalMain.syncEditorWithBrowser)
  57. syncButton.setToolTipText("Synchronize editor with file browser")
  58. val toolbar = new JToolBar()
  59. toolbar.setFloatable(false)
  60. syncButton.setFocusable(false)
  61. syncButton.setBorderPainted(false)
  62. toolbar.add(syncButton)
  63. add(Component.wrap(toolbar), BorderPanel.Position.North)
  64. add(new ScrollPane(fileTree), BorderPanel.Position.Center)
  65. }
  66. /**
  67. * Through a listener defined in CodeArea, this view (the FileTree component)
  68. * is used to select files for editing in Acumen. Selecting a file (called
  69. * README or with suffix .acm) will either open it directly or, if there is
  70. * unsaved work, propmt the user.
  71. * GraphicalMain.syncEditorWithBrowser can be toggled to disable the
  72. * synchronization of the editor an the file tree.
  73. */
  74. class FileTree(initialPath: File) extends Component with ChangeListener {
  75. override lazy val peer = init(initialPath)
  76. private def init(path: File): JTree with SuperMixin = {
  77. val fileSystemModel = new FileSystemModel(initialPath)
  78. val tree = new JTree(fileSystemModel) with SuperMixin
  79. tree setEditable false
  80. tree setRootVisible false
  81. /* Override behavior of the and right keyboard arrows.
  82. * When a top level folder is selected, pressing the left arrow
  83. * causes the root to be reset to the parent of the current root.
  84. * If an expanded folder is selected, pressing the right arrow
  85. * causes the root to be reset to selected node.
  86. */
  87. tree addKeyListener new KeyAdapter {
  88. override def keyPressed(e: KeyEvent) = {
  89. val leftPressed = e.getKeyCode == KeyEvent.VK_LEFT
  90. val selectedFile = getSelectedFile
  91. selectedFile match {
  92. case Some(f) =>
  93. if (leftPressed &&
  94. (f == tree.getModel.getRoot || // Directory is empty (no visible node is selected)
  95. (!peer.isExpanded(peer.getSelectionPath) && // A top-level node is selected
  96. f.getParentFile.getCanonicalFile == tree.getModel.getRoot.asInstanceOf[File].getCanonicalFile)))
  97. goUp
  98. else if (e.getKeyCode == KeyEvent.VK_RIGHT && peer.isExpanded(peer.getSelectionPath))
  99. goInto
  100. case None => if (leftPressed) goUp
  101. }
  102. }
  103. }
  104. tree
  105. }
  106. /**
  107. * Focus the file tree on a path.
  108. * Current node selection and expansion state will be restored.
  109. * Unlike reset(), this will not reset the root if the path is
  110. * a descendant of the root. In this case the tree node
  111. * corresponding to the node will be expanded instead.
  112. */
  113. def focus(path: File) {
  114. val didExpand = expandDescendant(peer.getModel.getRoot.asInstanceOf[File], path)
  115. if (!didExpand) reset(path)
  116. }
  117. /**
  118. * If the path is a descendant of some root node, a tree node
  119. * corresponding to path will be expanded. Returns true if the
  120. * path is a descendant of the root and false otherwise.
  121. */
  122. def expandDescendant(root: File, path: File): Boolean = {
  123. descendantOf(root, path) match {
  124. case Some(treePath) => // Path is a descendant of the root
  125. peer.expandPath(treePath)
  126. selectPath(treePath)
  127. true
  128. case None =>
  129. false
  130. }
  131. }
  132. /** Selects and scrolls the path into view */
  133. def selectPath(treePath: TreePath) {
  134. peer.getSelectionModel.setSelectionPath(treePath)
  135. peer.scrollPathToVisible(treePath)
  136. }
  137. /**
  138. * Reset the file tree on a path.
  139. * Current node selection and expansion state will be restored.
  140. */
  141. def reset(path: File) {
  142. // save expanded/selected nodes
  143. val expanded = ArrayBuffer[TreePath]()
  144. val expDescendants = peer.getExpandedDescendants(new TreePath(peer.getModel.getRoot))
  145. if (expDescendants != null)
  146. while (expDescendants.hasMoreElements) {
  147. val nextPath = expDescendants.nextElement.asInstanceOf[TreePath]
  148. if (nextPath.getLastPathComponent != peer.getModel.getRoot)
  149. expanded += nextPath
  150. }
  151. val selected = peer.getSelectionModel.getSelectionPaths
  152. // focus model on path
  153. peer.setModel(new FileSystemModel(
  154. if (path.isDirectory) path
  155. else path.getParentFile))
  156. // restore expanded/selected nodes
  157. for (i <- (peer.getRowCount - 1) to 0 by -1)
  158. peer collapseRow i
  159. for (path <- expanded)
  160. peer expandPath path
  161. if (selected != null)
  162. for (s <- selected)
  163. selectPath(s)
  164. }
  165. /**
  166. * If path is a sub-directory of the current tree's root, returns a TreePath to
  167. * this sub-directory wrapped in Some, otherwise returns None
  168. */
  169. private def descendantOf(someRoot: File, path: File): Option[TreePath] = {
  170. val r = someRoot.getCanonicalFile.getAbsolutePath
  171. val p = path.getCanonicalFile.getAbsolutePath
  172. if (r != p && p.startsWith(r)) {
  173. val pathElems = p.substring(r.length).split("/").tail //FIXME Make sure this work on Windows
  174. var treePath: TreePath = null
  175. for (e <- pathElems) {
  176. val row = if (treePath == null) 0 else peer.getRowForPath(treePath)
  177. treePath = peer.getNextMatch(e, row, Position.Bias.Forward)
  178. peer.expandPath(treePath)
  179. }
  180. Some(treePath)
  181. }
  182. else None
  183. }
  184. /** Reload the contents of the model's folder into the tree view */
  185. def refresh(): Unit = focus(peer.getModel.getRoot.asInstanceOf[File])
  186. /** Focus the browser on the parent folder */
  187. def goUp(): Unit = {
  188. val oldRoot = peer.getModel.getRoot.asInstanceOf[File]
  189. val parent = oldRoot.getAbsoluteFile.getParentFile
  190. if (parent != null) {
  191. reset(parent)
  192. expandDescendant(parent, oldRoot)
  193. }
  194. }
  195. /** Focus the browser on the currently selected folder */
  196. def goInto(): Unit = getSelectedFile.foreach(reset(_))
  197. /** Returns the file currently selected in the tree */
  198. def getSelectedFile(): Option[File] = {
  199. val selectionPath = peer.getSelectionModel.getSelectionPath
  200. if (selectionPath != null && selectionPath.getPathCount > 0 && selectionPath.getLastPathComponent != null)
  201. Some(selectionPath.getLastPathComponent.asInstanceOf[TreeFile])
  202. else None
  203. }
  204. /*
  205. * If the user opened or saved the current file to a new name, check if the
  206. * path needs to be adjusted.
  207. */
  208. override def stateChanged(e: ChangeEvent) {
  209. val ca = e.getSource.asInstanceOf[CodeArea]
  210. val f = peer.getModel.getRoot.asInstanceOf[File]
  211. // Check what was selected through File > Open
  212. ca.currentFile.foreach { file =>
  213. if (GraphicalMain.syncEditorWithBrowser) focus(file)
  214. if (file.isDirectory) peer.clearSelection
  215. }
  216. }
  217. }
  218. private class TreeFile(parent: File, child: String) extends File(parent, child) {
  219. /** Show file name (and not the absolute path) in the tree */
  220. override def toString() = getName
  221. }
  222. class FileSystemModel(rootDirectory: File) extends TreeModel {
  223. private val listeners: Buffer[TreeModelListener] = new ArrayBuffer
  224. override def getRoot() = rootDirectory
  225. override def getChild(parent: Object, index: Int): Object = {
  226. val directory = parent.asInstanceOf[File]
  227. val children = directory.listFiles.filter(CodeArea.acumenFileFilter.accept(_)).map(_.getName)
  228. new TreeFile(directory, children(index))
  229. }
  230. def getChildCount(parent: Object): Int = {
  231. val file = parent.asInstanceOf[File]
  232. if (file.isDirectory) {
  233. val fileList = file.listFiles.filter(CodeArea.acumenFileFilter.accept(_))
  234. if (fileList == null) 0
  235. else fileList.length
  236. }
  237. else 0
  238. }
  239. def isLeaf(node: Object) = node.asInstanceOf[File].isFile;
  240. def getIndexOfChild(parent: Object, child: Object): Int = {
  241. val directory = parent.asInstanceOf[File]
  242. val file = child.asInstanceOf[File]
  243. directory.listFiles.filter(CodeArea.acumenFileFilter.accept(_)).indexWhere(_ == file.getName)
  244. }
  245. def valueForPathChanged(path: TreePath, value: Object) {
  246. val oldFile = path.getLastPathComponent.asInstanceOf[File]
  247. val fileParentPath = oldFile.getParent
  248. val newFileName = value.asInstanceOf[String]
  249. val targetFile = new File(fileParentPath, newFileName)
  250. oldFile.renameTo(targetFile)
  251. val parent = new File(fileParentPath)
  252. val changedChildrenIndices = List(getIndexOfChild(parent, targetFile)).toArray
  253. val changedChildren = List(targetFile.asInstanceOf[Object]).toArray
  254. fireTreeNodesChanged(path.getParentPath, changedChildrenIndices, changedChildren);
  255. }
  256. private def fireTreeNodesChanged(parentPath: TreePath, indices: Array[Int], children: Array[Object]) = {
  257. val event = new TreeModelEvent(this, parentPath, indices, children)
  258. for (l <- listeners) l.treeNodesChanged(event)
  259. }
  260. def addTreeModelListener(listener: TreeModelListener) { listeners += listener }
  261. def removeTreeModelListener(listener: TreeModelListener) { listeners -= listener }
  262. }