diff --git a/src/main/scala/scala/swing/CellView.scala b/src/main/scala/scala/swing/CellView.scala index a4f8814..34605e8 100644 --- a/src/main/scala/scala/swing/CellView.scala +++ b/src/main/scala/scala/swing/CellView.scala @@ -15,6 +15,7 @@ import collection._ */ trait CellView[+A] { this: Component => + def editable: Boolean def cellValues: Iterator[A] diff --git a/src/main/scala/scala/swing/EditableCellsCompanion.scala b/src/main/scala/scala/swing/EditableCellsCompanion.scala index 6eca894..710fecc 100644 --- a/src/main/scala/scala/swing/EditableCellsCompanion.scala +++ b/src/main/scala/scala/swing/EditableCellsCompanion.scala @@ -35,13 +35,13 @@ trait EditableCellsCompanion { val companion: CellEditorCompanion def peer: companion.Peer - protected def fireCellEditingCancelled() {publish(CellEditingCancelled(CellEditor.this))} - protected def fireCellEditingStopped() {publish(CellEditingStopped(CellEditor.this))} + protected def fireCellEditingCancelled() { publish(CellEditingCancelled(CellEditor.this)) } + protected def fireCellEditingStopped() { publish(CellEditingStopped(CellEditor.this)) } protected def listenToPeer(p: JCellEditor) { p.addCellEditorListener(new CellEditorListener { - override def editingCanceled(e: ChangeEvent) {fireCellEditingCancelled()} - override def editingStopped(e: ChangeEvent) {fireCellEditingStopped()} + override def editingCanceled(e: ChangeEvent) { fireCellEditingCancelled() } + override def editingStopped(e: ChangeEvent) { fireCellEditingStopped() } }) } diff --git a/src/main/scala/scala/swing/ListView.scala b/src/main/scala/scala/swing/ListView.scala index a06772a..d6911a5 100644 --- a/src/main/scala/scala/swing/ListView.scala +++ b/src/main/scala/scala/swing/ListView.scala @@ -10,11 +10,15 @@ package scala.swing -import event._ +import scala.swing._ +import scala.swing.{ListView => _} +import scala.swing.event._ import javax.swing._ import javax.swing.event._ -object ListView { +object ListView extends RenderableCellsCompanion { + type Owner = ListView[_] + /** * The supported modes of user selections. */ @@ -28,15 +32,19 @@ object ListView { override lazy val peer = c } - object Renderer { + object Renderer extends CellRendererCompanion { + type Peer = ListCellRenderer + case class CellInfo(isSelected: Boolean, focused: Boolean, index: Int) + object emptyCellInfo extends CellInfo(false, false, 0) def wrap[A](r: ListCellRenderer): Renderer[A] = new Wrapped[A](r) /** * Wrapper for javax.swing.ListCellRenderers */ class Wrapped[A](override val peer: ListCellRenderer) extends Renderer[A] { - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int) = { - Component.wrap(peer.getListCellRendererComponent(list.peer, a, index, isSelected, focused).asInstanceOf[JComponent]) + override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo) = { + Component.wrap(peer.getListCellRendererComponent(list.peer, a, cellInfo.index, + cellInfo.isSelected, cellInfo.focused).asInstanceOf[JComponent]) } } @@ -55,11 +63,18 @@ object ListView { * */ def apply[A,B](f: A => B)(implicit renderer: Renderer[B]): Renderer[A] = new Renderer[A] { - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int): Component = - renderer.componentFor(list, isSelected, focused, f(a), index) + override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component = + renderer.componentFor(list, f(a), cellInfo) } + + def default[A] = new DefaultRenderer[A] + + + def labelled[A](f: A => (Icon, String)) = new DefaultRenderer[A] with LabelRenderer[A] {val convert = f} } + import Renderer.CellInfo + /** * Item renderer for a list view. This is contravariant on the type of the * items, so a more general renderer can be used in place of a more specific @@ -68,12 +83,30 @@ object ListView { * * @see javax.swing.ListCellRenderer */ - abstract class Renderer[-A] { + trait Renderer[-A] extends CellRenderer[A] { + val companion = Renderer + + def dispatchToScalaRenderer(list: JList, a: Any, index: Int, isSelected: Boolean, focused: Boolean): JComponent = { + val wrappedList = ListView.wrap[A](list) + // Deal with deprecated method if necessary. + val comp = componentFor(wrappedList, isSelected, focused, a.asInstanceOf[A], index) + comp match { + case null => componentFor(wrappedList, a.asInstanceOf[A], CellInfo(isSelected, focused, index)).peer + case c => c.peer + } + } + def peer: ListCellRenderer = new ListCellRenderer { - def getListCellRendererComponent(list: JList, a: Any, index: Int, isSelected: Boolean, focused: Boolean) = - componentFor(ListView.wrap[A](list), isSelected, focused, a.asInstanceOf[A], index).peer + def getListCellRendererComponent(list: JList, a: Any, index: Int, isSelected: Boolean, focused: Boolean) = { + dispatchToScalaRenderer(list, a, index, isSelected, focused) + } } - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int): Component + + @deprecated("Override componentFor(list: List[_], a: A, cellInfo: CellInfo) instead.") + def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int): Component = null + + override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component = + componentFor(list, cellInfo.isSelected, cellInfo.focused, a, cellInfo.index) } /** @@ -110,23 +143,31 @@ object ListView { /** * Configures the component before returning it. */ - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int): Component = { - preConfigure(list, isSelected, focused, a, index) - configure(list, isSelected, focused, a, index) + override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component = { + preConfigure(list, cellInfo.isSelected, cellInfo.focused, a, cellInfo.index) + configure(list, cellInfo.isSelected, cellInfo.focused, a, cellInfo.index) component } } /** - * A generic renderer that uses Swing's built-in renderers. If there is no - * specific renderer for a type, this renderer falls back to a renderer - * that renders the string returned from an item's toString. - */ - implicit object GenericRenderer extends Renderer[Any] { - override lazy val peer: ListCellRenderer = new DefaultListCellRenderer - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: Any, index: Int): Component = { - val c = peer.getListCellRendererComponent(list.peer, a, index, isSelected, focused).asInstanceOf[JComponent] - Component.wrap(c) + * Default label-based renderer for a ListView. + */ + class DefaultRenderer[-A] extends Label with Renderer[A] { + override lazy val peer = new javax.swing.DefaultListCellRenderer with SuperMixin { peerThis => + override def getListCellRendererComponent(list: JList, value: AnyRef, index: Int, isSelected: Boolean, focused: Boolean): JComponent = { + dispatchToScalaRenderer(list, value, index, isSelected, focused) + peerThis + } + + def defaultRendererComponent(list: JList, value: AnyRef, index: Int, isSelected: Boolean, focused: Boolean) { + super.getListCellRendererComponent(list, value, index, isSelected, focused) + } + } + + override def componentFor(list: ListView[_], value: A, info: Renderer.CellInfo): Component = { + peer.defaultRendererComponent(list.peer, value.asInstanceOf[AnyRef], info.index, info.isSelected, info.focused) + this } } } @@ -140,9 +181,11 @@ object ListView { * * @see javax.swing.JList */ -class ListView[A] extends Component { +class ListView[A] extends Component with CellView[A] with RenderableCells[A] { import ListView._ override lazy val peer: JList = new JList with SuperMixin + override val companion = ListView + def this(items: Seq[A]) = { this() @@ -154,6 +197,10 @@ class ListView[A] extends Component { def getSize = items.size } + def cellValues = listData.iterator + + def editable = false + def listData: Seq[A] = peer.getModel match { case model: ModelWrapper => model.items case model @ _ => new Seq[A] { selfSeq => @@ -177,24 +224,22 @@ class ListView[A] extends Component { /** * The current item selection. */ - object selection extends Publisher { - protected abstract class Indices[A](a: =>Seq[A]) extends scala.collection.mutable.Set[A] { - def -=(n: A): this.type - def +=(n: A): this.type - def contains(n: A) = a.contains(n) - override def size = a.length - def iterator = a.iterator - } - + object selection extends CellSelection { + + @deprecated("Use SelectionSet[A] instead") + protected type Indices[A] = SelectionSet[A] + def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex /** * The indices of the currently selected items. */ - object indices extends Indices(peer.getSelectedIndices) { + object indices extends SelectionSet(peer.getSelectedIndices) { def -=(n: Int): this.type = { peer.removeSelectionInterval(n,n); this } def +=(n: Int): this.type = { peer.addSelectionInterval(n,n); this } + def --=(nn: Seq[Int]) = { nn foreach -=; this } + def ++=(nn: Seq[Int]) = { nn foreach +=; this } @deprecated("Use ListView.selection.leadIndex") def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex @deprecated("Use ListView.selection.anchorIndex") @@ -224,6 +269,10 @@ class ListView[A] extends Component { } }) + def cellValues = items.iterator + def count = items.size + def empty = items.isEmpty + def adjusting = peer.getSelectionModel.getValueIsAdjusting } @@ -249,6 +298,6 @@ class ListView[A] extends Component { peer.getModel.addListDataListener(new ListDataListener { def contentsChanged(e: ListDataEvent) { publish(ListChanged(ListView.this)) } def intervalRemoved(e: ListDataEvent) { publish(ListElementsRemoved(ListView.this, e.getIndex0 to e.getIndex1)) } - def intervalAdded(e: ListDataEvent) { publish(ListElementsAdded(ListView.this, e.getIndex0 to e.getIndex1)) } + def intervalAdded(e: ListDataEvent) { publish(ListElementsAdded(ListView.this, e.getIndex0 to e.getIndex1)) } }) } diff --git a/src/main/scala/scala/swing/RenderableCellsCompanion.scala b/src/main/scala/scala/swing/RenderableCellsCompanion.scala index ffc6e98..e0025f6 100644 --- a/src/main/scala/scala/swing/RenderableCellsCompanion.scala +++ b/src/main/scala/scala/swing/RenderableCellsCompanion.scala @@ -28,7 +28,7 @@ trait RenderableCellsCompanion { * specific renderer for a type, this renderer falls back to a renderer * that renders the string returned from an item's toString. */ - implicit val GenericRenderer: Renderer[Any] = Renderer.default[Any] + implicit val GenericRenderer: Renderer[Any] = Renderer.default /** * A default renderer implementation based on a Label. @@ -47,13 +47,14 @@ trait RenderableCellsCompanion { def default[A]: DefaultRenderer[A] /** - * Convenient default display of a tree node, which provides an Icon and label text for each item. + * Convenient default display of a cell node, which provides an Icon and label text for each item. */ def labelled[A](f: A => (Icon, String)): DefaultRenderer[A] protected trait LabelRenderer[-A] extends CellRenderer[A] { this: DefaultRenderer[A] => val convert: A => (Icon, String) + override abstract def componentFor(owner: Owner, a: A, info: companion.CellInfo): Component = { val c = super.componentFor(owner, a, info) val (labelIcon, labelText) = convert(a) diff --git a/src/main/scala/scala/swing/sample.xml b/src/main/scala/scala/swing/sample.xml new file mode 100644 index 0000000..cd0b067 --- /dev/null +++ b/src/main/scala/scala/swing/sample.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + au.com.fooproject + Foo + 1.0-SNAPSHOT + war + Foo + 2007 + + 2.7.3 + + + + + scala-tools.org + Scala-Tools Maven2 Repository + http://scala-tools.org/repo-releases + + + + + + scala-tools.org + Scala-Tools Maven2 Repository + http://scala-tools.org/repo-releases + + + + + + org.scala-lang + scala-library + ${scala.version} + + + net.liftweb + lift-util + 1.0 + + + net.liftweb + lift-webkit + 1.0 + + + net.liftweb + lift-mapper + 1.0 + + + org.apache.derby + derby + 10.5.3.0_1 + + + javax.servlet + servlet-api + 2.5 + provided + + + junit + junit + 4.5 + test + + + org.mortbay.jetty + jetty + [6.1.6,) + test + + + + + + org.scala-lang + scala-compiler + ${scala.version} + test + + + + + src/main/scala + src/test/scala + + + org.scala-tools + maven-scala-plugin + + + + compile + testCompile + + + + + ${scala.version} + + + + org.mortbay.jetty + maven-jetty-plugin + + / + 0 + + + + net.sf.alchim + yuicompressor-maven-plugin + + + + compress + + + + + true + + + + org.apache.maven.plugins + maven-eclipse-plugin + + true + + org.scala-lang:scala-library + + + ch.epfl.lamp.sdt.launching.SCALA_CONTAINER + + + ch.epfl.lamp.sdt.core.scalanature + org.eclipse.jdt.core.javanature + + + ch.epfl.lamp.sdt.core.scalabuilder + + + + + + + + + org.scala-tools + maven-scala-plugin + + ${scala.version} + + + + + diff --git a/src/main/scala/scala/swing/test/TreeDemo.scala b/src/main/scala/scala/swing/test/TreeDemo.scala index 3b8afe3..c749654 100644 --- a/src/main/scala/scala/swing/test/TreeDemo.scala +++ b/src/main/scala/scala/swing/test/TreeDemo.scala @@ -7,134 +7,306 @@ import swing.event._ import swing.tree._ import Tree._ import java.awt.Color +import java.awt.{event => jae} +import scala.collection.mutable.ListBuffer object TreeDemo extends SimpleSwingApplication { import Tree._ import java.io._ - object Data { - // Contrived class hierarchy - case class Customer(id: Int, title: String, firstName: String, lastName: String) - case class Product(id: String, name: String, price: Double) - case class Order(id: Int, customer: Customer, product: Product, quantity: Int) { - def price = product.price * quantity - } - // Contrived example data - val bob = Customer(1, "Mr", "Bob", "Baxter") - val fred = Customer(2, "Dr", "Fred", "Finkelstein") - val susan = Customer(3, "Ms", "Susan", "Smithers") - val powerSaw = Product("X-123", "Power Saw", 99.95) - val nailGun = Product("Y-456", "Nail gun", 299.95) - val boxOfNails = Product("Z-789", "Box of nails", 23.50) - val orders = List( - Order(1, fred, powerSaw, 1), - Order(2, fred, boxOfNails, 3), - Order(3, bob, boxOfNails, 44), - Order(4, susan, nailGun, 1)) + import ExampleData._ - lazy val xmlDoc: Node = try {XML load resourceFromClassloader("/scala/swing/test/sample.xml")} - catch {case _ => Error reading XML file. } - } - + // Use case 1: Show an XML document + lazy val xmlTree = new Tree[Node] { + model = TreeModel(xmlDoc)(_.child filterNot (_.text.trim.isEmpty)) + renderer = Renderer(n => + if (n.label startsWith "#") n.text.trim + else n.label) - // Common functionality for using files - trait FileTree { this: Tree[File] => - treeData = TreeModel(new File(".")) {f => + expandAll() + } + + + // Use case 2: Show the filesystem with filter + lazy val fileSystemTree = new Tree[File] { + model = TreeModel(new File(".")) {f => if (f.isDirectory) f.listFiles.toSeq else Seq() } - renderer = Renderer.labelled({f => - val iconFile = "/scala/swing/test/images/" + (if (f.isDirectory) "folder.png" else "file.png") - val iconURL = resourceFromClassloader(iconFile) ensuring (_ != null, "Couldn't find icon " + iconFile) - (Icon(iconURL), f.getName) + renderer = Renderer.labelled {f => + val icon = if (f.isDirectory) folderIcon + else fileIcon + (icon, f.getName) + } + expandRow(0) + } + + // Use case 3: Object graph containing diverse elements, reacting to clicks + lazy val objectGraphTree = new Tree[Any] { + model = TreeModel[Any](orders: _*) { + case o @ Order(_, cust, prod, qty) => Seq(cust, prod, "Qty" -> qty, "Cost" -> ("$" + o.price)) + case Product(id, name, price) => Seq("ID" -> id, "Name" -> name, "Price" -> ("$" + price)) + case Customer(id, _, first, last) => Seq("ID" -> id, "First name" -> first, "Last name" -> last) + case _ => Seq.empty + } + + renderer = Renderer({ + case Order(id, _, _, 1) => "Order #" + id + case Order(id, _, _, qty) => "Order #" + id + " x " + qty + case Product(id, _, _) => "Product " + id + case Customer(_, title, first, last) => title + " " + first + " " + last + case (field, value) => field + ": " + value + case x => x.toString }) + + expandAll() } + + // Use case 4: Infinitely deep structure + lazy val infiniteTree = new Tree(TreeModel(1000) {n => 1 to n filter (n % _ == 0)}) { + expandRow(0) + } + + + + // Use case 5: Mutable external tree model + val mutableExternalTree = new Tree[PretendFile] { - def top = new MainFrame { - title = "Scala Swing Tree Demo" + model = ExternalTreeModel(pretendFileSystem)(_.children).makeUpdatableWith { + (pathOfFile, updatedFile) => + val succeeded = pathOfFile.last.rename(updatedFile.name) + externalTreeStatusBar.text = "Updating file " + (if (succeeded) "succeeded" else "failed") + pathOfFile.last + + }.makeInsertableWith { + (parentPath, fileToInsert, index) => + val parentDir = parentPath.last + if (parentDir.children contains fileToInsert) false + else parentDir.insertChild(fileToInsert, index) + + }.makeRemovableWith { + (pathToRemove) => + if (pathToRemove.length >= 2) pathToRemove.last.delete() + else false + } + + listenTo(selection) + reactions += { + case TreeNodeSelected(node) => externalTreeStatusBar.text = "Selected: " + node + } + + renderer = Renderer.labelled {f => + val icon = if (f.isDirectory) folderIcon + else fileIcon + (icon, f.name) + } + editor = Editor((_: PretendFile).name, new PretendFile(_: String)) + expandRow(0) + } + + // Use case 6: Mutable internal tree model + val mutableInternalTree = new Tree[PretendFile] { + + model = InternalTreeModel(pretendFileSystem)(_.children) + + listenTo(selection) + reactions += { + case TreeNodeSelected(node) => internalTreeStatusBar.text = "Selected: " + node + } + + renderer = mutableExternalTree.renderer + editor = mutableExternalTree.editor + expandRow(0) + } - contents = new TabbedPane { - import Data._ + + class ButtonPanel(pretendFileTree: Tree[PretendFile], setStatus: String => Unit) extends GridPanel(10,1) { - // Use case 1: Show an XML document - val xmlTree = new Tree[Node] { - treeData = TreeModel(xmlDoc)(_.child filterNot (_.text.trim.isEmpty)) - renderer = Renderer(n => - if (n.label startsWith "#") n.text.trim - else n.label) - - expandAll() + val updateButton = new Button(Action("Directly update") { + val pathToRename = pretendFileTree.selection.paths.leadSelection + for (path <- pathToRename) { + val oldName = path.last.name + pretendFileTree.model.update(path, PretendFile("directly-updated-file")) + setStatus("Updated " + oldName) } - - - // Use case 2: Show the filesystem with filter - val fileSystemTree = new Tree[File] with FileTree { - expandRow(0) + }) + + val editButton = new Button(Action("Edit") { + val pathToEdit = pretendFileTree.selection.paths.leadSelection + for (path <- pathToEdit) { + pretendFileTree.startEditingAtPath(path) + setStatus("Editing... ") } - - // Use case 3: Object graph containing diverse elements, reacting to clicks - val objectGraphTree = new Tree[Any] { - treeData = TreeModel[Any](orders: _*)({ - case o @ Order(_, cust, prod, qty) => Seq(cust, prod, "Qty" -> qty, "Cost" -> ("$" + o.price)) - case Product(id, name, price) => Seq("ID" -> id, "Name" -> name, "Price" -> ("$" + price)) - case Customer(id, _, first, last) => Seq("ID" -> id, "First name" -> first, "Last name" -> last) - case _ => Seq.empty - }) - - renderer = Renderer({ - case Order(id, _, _, 1) => "Order #" + id - case Order(id, _, _, qty) => "Order #" + id + " x " + qty - case Product(id, _, _) => "Product " + id - case Customer(_, title, first, last) => title + " " + first + " " + last - case (field, value) => field + ": " + value - case x => x.toString - }) - - listenTo(selection) - reactions += { - case TreeNodeSelected(node) => println("Selected: " + node) - } - - expandAll() + }) + + val insertButton = new Button(Action("Insert under") { + val pathToInsertUnder = pretendFileTree.selection.paths.leadSelection + for (path <- pathToInsertUnder) { + val succeeded = pretendFileTree.model.insertUnder(path, PretendFile("new-under-" + path.last.name), 0) + setStatus("Inserting " + (if (succeeded) "succeeded" else "failed")) } - - // Use case 4: Infinitely deep structure - val infiniteTree = new Tree(TreeModel(1000) {n => 1 to n filter (n % _ == 0)}) - infiniteTree expandRow 0 - - // Use case 5: Editable file system - val editableFileSystemTree = new Tree[File] with FileTree { - treeData = treeData updatableWith { - (path, newValue) => val existing = path.last - val renamedFile = new File(existing.getParent + File.separator + newValue.getName) - existing renameTo renamedFile - renamedFile - } - - editor = Editor((_:File).getName, new File(_:String)) - expandRow(0) + }) + + val insertBeforeButton = new Button(Action("Insert before") { + val pathToInsertBefore = pretendFileTree.selection.paths.leadSelection + for (path <- pathToInsertBefore) { + val succeeded = pretendFileTree.model.insertBefore(path, PretendFile("new-before-" + path.last.name)) + setStatus("Inserting " + (if (succeeded) "succeeded" else "failed")) + } + }) + + val insertAfterButton = new Button(Action("Insert after") { + val pathToInsertAfter = pretendFileTree.selection.paths.leadSelection + for (path <- pathToInsertAfter) { + val succeeded = pretendFileTree.model.insertAfter(path, PretendFile("new-after-" + path.last.name)) + setStatus("Inserting " + (if (succeeded) "succeeded" else "failed")) + } + }) + + val removeButton = new Button(Action("Remove") { + val pathToRemove = pretendFileTree.selection.paths.leadSelection + for (path <- pathToRemove) { + val succeeded = pretendFileTree.model remove path + setStatus("Remove " + (if (succeeded) "succeeded" else "failed")) } + }) + + contents += editButton + contents += updateButton + contents += insertButton + contents += insertBeforeButton + contents += insertAfterButton + contents += removeButton + } + + val externalTreeStatusBar = new Label { + preferredSize = (100,12) + } + + val internalTreeStatusBar = new Label { + preferredSize = (100,12) + } + + // Other setup stuff + + + def top = new MainFrame { + title = "Scala Swing Tree Demo" + + contents = new TabbedPane { import TabbedPane.Page import BorderPanel.Position._ - def northAndCenter(north: Component, center: Component) = new BorderPanel { - layout(north) = North + def southCenterAndEast(north: Component, center: Component, east: Component) = new BorderPanel { + layout(north) = South layout(center) = Center + layout(east) = East } pages += new Page("1: XML file", new ScrollPane(xmlTree)) pages += new Page("2: File system", new ScrollPane(fileSystemTree)) pages += new Page("3: Diverse object graph", new ScrollPane(objectGraphTree)) pages += new Page("4: Infinite structure", new ScrollPane(infiniteTree)) - pages += new Page("5: Editable file system", northAndCenter( - new Label("Warning! Editing will actually rename files.") {foreground = Color.red}, - new ScrollPane(editableFileSystemTree))) + pages += new Page("5: Mutable external model", southCenterAndEast( + externalTreeStatusBar, + new ScrollPane(mutableExternalTree), + new ButtonPanel(mutableExternalTree, externalTreeStatusBar.text_=))) + + pages += new Page("6: Mutable internal model", southCenterAndEast( + internalTreeStatusBar, + new ScrollPane(mutableInternalTree), + new ButtonPanel(mutableInternalTree, internalTreeStatusBar.text_=))) + } + + size = (1024, 768): Dimension + } + object ExampleData { + + // File system icons + def getIconUrl(path: String) = resourceFromClassloader(path) ensuring (_ != null, "Couldn't find icon " + path) + val fileIcon = Icon(getIconUrl("/scala/swing/test/images/file.png")) + val folderIcon = Icon(getIconUrl("/scala/swing/test/images/folder.png")) + + // Contrived class hierarchy + case class Customer(id: Int, title: String, firstName: String, lastName: String) + case class Product(id: String, name: String, price: Double) + case class Order(id: Int, customer: Customer, product: Product, quantity: Int) { + def price = product.price * quantity + } + + // Contrived example data + val bob = Customer(1, "Mr", "Bob", "Baxter") + val fred = Customer(2, "Dr", "Fred", "Finkelstein") + val susan = Customer(3, "Ms", "Susan", "Smithers") + val powerSaw = Product("X-123", "Power Saw", 99.95) + val nailGun = Product("Y-456", "Nail gun", 299.95) + val boxOfNails = Product("Z-789", "Box of nails", 23.50) + val orders = List( + Order(1, fred, powerSaw, 1), + Order(2, fred, boxOfNails, 3), + Order(3, bob, boxOfNails, 44), + Order(4, susan, nailGun, 1)) + + lazy val xmlDoc: Node = try {XML load resourceFromClassloader("/scala/swing/test/sample.xml")} + catch {case _ => Error reading XML file. } + + + // Pretend file system, so we can safely add/edit/delete stuff + case class PretendFile(private var nameVar: String, private val childFiles: PretendFile*) { + var parent: Option[PretendFile] = None + childFiles foreach {_.parent = Some(this)} + private var childBuffer = ListBuffer(childFiles: _*) + + override def toString() = name + def name = nameVar + def rename(str: String): Boolean = if (siblingExists(str)) false + else { nameVar = str; true } + + def insertChild(child: PretendFile, index: Int): Boolean = { + if (!isDirectory) false + else if (childExists(child.name)) false + else { + child.parent = Some(this) + childBuffer.insert(index, child) + true + } + } + def delete(): Boolean = parent.map(_ removeChild this) getOrElse false + def removeChild(child: PretendFile): Boolean = if (children contains child) {childBuffer -= child; true} + else false + + def siblingExists(siblingName: String) = parent.map(_ childExists siblingName) getOrElse false + def childExists(childName: String) = children.exists(_.name == childName) + def children: Seq[PretendFile] = childBuffer + def isDirectory = children.nonEmpty } - size = (800, 600): Dimension + val pretendFileSystem = PretendFile("~", + PretendFile("lib", + PretendFile("coolstuff-1.1.jar"), + PretendFile("coolstuff-1.2.jar"), + PretendFile("robots-0.2.5.jar")), + PretendFile("bin", + PretendFile("cleanup"), + PretendFile("morestuff"), + PretendFile("dostuff")), + PretendFile("tmp", + PretendFile("log", + PretendFile("1.log"), + PretendFile("2.log"), + PretendFile("3.log"), + PretendFile("4.log")), + PretendFile("readme.txt"), + PretendFile("foo.bar"), + PretendFile("bar.foo"), + PretendFile("dingus")), + PretendFile("something.moo")) } -} \ No newline at end of file +} + + + diff --git a/src/main/scala/scala/swing/tree/ExternalTreeModel.scala b/src/main/scala/scala/swing/tree/ExternalTreeModel.scala new file mode 100644 index 0000000..79016e8 --- /dev/null +++ b/src/main/scala/scala/swing/tree/ExternalTreeModel.scala @@ -0,0 +1,255 @@ +package scala.swing.tree + +import Tree.Path +import scala.collection.mutable.ListBuffer +import javax.swing.{tree => jst} +import javax.swing.event.TreeModelEvent +import javax.swing.event.TreeModelListener +import scala.collection.mutable.ArrayBuffer + + +object ExternalTreeModel { + def empty[A]: ExternalTreeModel[A] = new ExternalTreeModel[A](Seq.empty, _ => Seq.empty) + def apply[A](roots: A*)(children: A => Seq[A]): ExternalTreeModel[A] = + new ExternalTreeModel(roots, children) +} + +/** + * Represents tree data as a sequence of root nodes, and a function that can retrieve child nodes. + */ +class ExternalTreeModel[A](rootItems: Seq[A], + children: A => Seq[A]) extends TreeModel[A] { + self => + + import TreeModel._ + + private var rootsVar = List(rootItems: _*) + + def roots: Seq[A] = rootsVar + + def getChildrenOf(parentPath: Path[A]): Seq[A] = if (parentPath.isEmpty) roots + else children(parentPath.last) + + def filter(p: A => Boolean): ExternalTreeModel[A] = new ExternalTreeModel[A](roots filter p, a => children(a) filter p) + + def toInternalModel: InternalTreeModel[A] = InternalTreeModel(roots: _*)(children) + + def isExternalModel = true + + def map[B](f: A=>B): InternalTreeModel[B] = toInternalModel map f + + def pathToTreePath(path: Tree.Path[A]): jst.TreePath = { + val array = (hiddenRoot +: path).map(_.asInstanceOf[AnyRef]).toArray(ClassManifest.Object) + new jst.TreePath(array) + } + + def treePathToPath(tp: jst.TreePath): Tree.Path[A] = { + if (tp == null) null + else tp.getPath.map(_.asInstanceOf[A]).tail.toIndexedSeq + } + + /** + * A function to update a value in the model, at a given path. By default this will throw an exception; to + * make a TreeModel updatable, call makeUpdatable() to provide a new TreeModel with the specified update method. + */ + protected[tree] val updateFunc: (Path[A], A) => A = { + (_,_) => error("Update is not supported on this tree") + } + + /** + * A function to insert a value in the model at a given path, returning whether the operation succeeded. + * By default this will throw an exception; to allow insertion on a TreeModel, + * call insertableWith() to provide a new TreeModel with the specified insert method. + */ + protected[tree] val insertFunc: (Path[A], A, Int) => Boolean = { + (_,_,_) => error("Insert is not supported on this tree") + } + + /** + * A function to remove a item in the model at a given path, returning whether the operation succeeded. + * By default this will throw an exception; to allow removal from a TreeModel, + * call removableWith() to provide a new TreeModel with the specified remove method. + */ + protected[tree] val removeFunc: Path[A] => Boolean = { + _ => error("Removal is not supported on this tree") + } + + /** + * Returns a new VirtualTreeModel that knows how to modify the underlying representation, + * using the given function to replace one value with another. + *

+ * Calling update() on a model returned from makeUpdatable() will perform the update. + */ + def makeUpdatableWith(effectfulUpdate: (Path[A], A) => A): ExternalTreeModel[A] = new ExternalTreeModel(roots, children) { + override val updateFunc = effectfulUpdate + override val insertFunc = self.insertFunc + override val removeFunc = self.removeFunc + this.peer copyListenersFrom self.peer + } + + def makeInsertableWith(effectfulInsert: (Path[A], A, Int) => Boolean): ExternalTreeModel[A] = new ExternalTreeModel(roots, children) { + override val updateFunc = self.updateFunc + override val insertFunc = effectfulInsert + override val removeFunc = self.removeFunc + this.peer copyListenersFrom self.peer + } + + def makeRemovableWith(effectfulRemove: Path[A] => Boolean): ExternalTreeModel[A] = new ExternalTreeModel(roots, children) { + override val updateFunc = self.updateFunc + override val insertFunc = self.insertFunc + override val removeFunc = effectfulRemove + this.peer copyListenersFrom self.peer + } + + /** + * Replaces one value with another, mutating the underlying structure. + * If a way to modify the external tree structure has not been provided with makeUpdatableWith(), then + * an exception will be thrown. + */ + def update(path: Path[A], newValue: A) { + if (path.isEmpty) throw new IllegalArgumentException("Cannot update an empty path") + + val existing = path.last + val result = updateFunc(path, newValue) + + val replacingWithDifferentReference = existing.isInstanceOf[AnyRef] && + (existing.asInstanceOf[AnyRef] ne result.asInstanceOf[AnyRef]) + + + // If the result is actually replacing the node with a different reference object, then + // fire "tree structure changed". + if (replacingWithDifferentReference) { + if (path.size == 1) { + rootsVar = rootsVar.updated(roots indexOf newValue, newValue) + } + + peer.fireTreeStructureChanged(pathToTreePath(path.init), result) + } + // If the result is a value type or is a modification of the same node reference, then + // just fire "nodes changed". + else { + peer.fireNodesChanged(pathToTreePath(path.init), result) + } + } + + def insertUnder(parentPath: Path[A], newValue: A, index: Int): Boolean = { + val succeeded = if (parentPath.nonEmpty) { + insertFunc(parentPath, newValue, index) + } + else { + val (before, after) = rootsVar splitAt index + rootsVar = before ::: newValue :: after + true + } + + if (succeeded) { + val actualIndex = siblingsUnder(parentPath) indexOf newValue + if (actualIndex == -1) return false + + peer.fireNodesInserted(pathToTreePath(parentPath), newValue, actualIndex) + } + succeeded + } + + def remove(pathToRemove: Path[A]): Boolean = { + if (pathToRemove.isEmpty) return false + + val parentPath = pathToRemove.init + val index = siblingsUnder(parentPath) indexOf pathToRemove.last + if (index == -1) return false + + val succeeded = if (pathToRemove.size == 1) { + rootsVar = rootsVar.filterNot(pathToRemove.last ==) + true + } + else { + removeFunc(pathToRemove) + } + + if (succeeded) { + + peer.fireNodesRemoved(pathToTreePath(parentPath), pathToRemove.last, index) + } + succeeded + } + + + class ExternalTreeModelPeer extends jst.TreeModel { + private val treeModelListenerList = ListBuffer[TreeModelListener]() + + def getChildrenOf(parent: Any) = parent match { + case `hiddenRoot` => roots + case a: A => children(a) + } + + def getChild(parent: Any, index: Int): AnyRef = { + val ch = getChildrenOf(parent) + if (index >= 0 && index < ch.size) + ch(index).asInstanceOf[AnyRef] + else + error("No child of \"" + parent + "\" found at index " + index) + } + def getChildCount(parent: Any): Int = getChildrenOf(parent).size + def getIndexOfChild(parent: Any, child: Any): Int = getChildrenOf(parent) indexOf child + def getRoot(): AnyRef = hiddenRoot + def isLeaf(node: Any): Boolean = getChildrenOf(node).isEmpty + + + private[tree] def copyListenersFrom(otherPeer: ExternalTreeModel[A]#ExternalTreeModelPeer) { + otherPeer.treeModelListeners foreach addTreeModelListener + } + + def treeModelListeners: Seq[TreeModelListener] = treeModelListenerList + + def addTreeModelListener(tml: TreeModelListener) { + treeModelListenerList += tml + } + + def removeTreeModelListener(tml: TreeModelListener) { + treeModelListenerList -= tml + } + + def valueForPathChanged(path: jst.TreePath, newValue: Any) { + update(treePathToPath(path), newValue.asInstanceOf[A]) + } + + private def createEvent(parentPath: jst.TreePath, newValue: Any) = { + val index = getChildrenOf(parentPath.getPath.last) indexOf newValue + createEventWithIndex(parentPath, newValue, index) + } + + private def createEventWithIndex(parentPath: jst.TreePath, newValue: Any, index: Int) = { + new TreeModelEvent(this, parentPath, Array(index), Array(newValue.asInstanceOf[AnyRef])) + } + + def fireTreeStructureChanged(parentPath: jst.TreePath, newValue: Any) { + treeModelListenerList foreach { _ treeStructureChanged createEvent(parentPath, newValue) } + } + + def fireNodesChanged(parentPath: jst.TreePath, newValue: Any) { + treeModelListenerList foreach { _ treeNodesChanged createEvent(parentPath, newValue) } + } + + def fireNodesInserted(parentPath: jst.TreePath, newValue: Any, index: Int) { + def createEvent = createEventWithIndex(parentPath, newValue, index) + treeModelListenerList foreach { _ treeNodesInserted createEvent } + } + + def fireNodesRemoved(parentPath: jst.TreePath, removedValue: Any, index: Int) { + def createEvent = createEventWithIndex(parentPath, removedValue, index) + treeModelListenerList foreach { _ treeNodesRemoved createEvent } + } + } + + + + /** + * Underlying tree model that exposes the tree structure to Java Swing. + * + * This implementation of javax.swing.tree.TreeModel takes advantage of its abstract nature, so that it respects + * the tree shape of the underlying structure provided by the user. + */ + lazy val peer = new ExternalTreeModelPeer + +} + diff --git a/src/main/scala/scala/swing/tree/InternalTreeModel.scala b/src/main/scala/scala/swing/tree/InternalTreeModel.scala new file mode 100644 index 0000000..94a6155 --- /dev/null +++ b/src/main/scala/scala/swing/tree/InternalTreeModel.scala @@ -0,0 +1,118 @@ +package scala.swing.tree + +import javax.swing.{tree => jst} +import Tree.Path +import TreeModel.hiddenRoot +import scala.collection.JavaConversions.enumerationAsScalaIterator +import scala.sys.error +import InternalTreeModel.{PeerModel, PeerNode} + +object InternalTreeModel { + + def empty[A] = new InternalTreeModel[A](new PeerModel(new jst.DefaultMutableTreeNode(hiddenRoot))) + + def apply[A](roots: A*)(children: A => Seq[A]): InternalTreeModel[A] = { + def createNode(a: A): PeerNode = { + val node = new PeerNode(a) + children(a) map createNode foreach node.add + node + } + + val rootNode = new PeerNode(hiddenRoot) + roots map createNode foreach rootNode.add + + new InternalTreeModel(new PeerModel(rootNode)) + } + + private[tree] type PeerModel = jst.DefaultTreeModel + private[tree] type PeerNode = jst.DefaultMutableTreeNode +} + + +class InternalTreeModel[A] private (val peer: PeerModel) extends TreeModel[A] { + + self => + + def this() = this(new PeerModel(new PeerNode(hiddenRoot))) + + def pathToTreePath(path: Tree.Path[A]): jst.TreePath = { + + val nodePath = path.foldLeft(List(rootPeerNode)) { (pathList, a) => + val childNodes = getNodeChildren(pathList.head) + val node = childNodes.find(_.getUserObject == a) getOrElse error("Couldn't find internal node for " + a) + node :: pathList + }.reverse + + val array = nodePath.toArray(ClassManifest.Object) + new jst.TreePath(array) + } + + def treePathToPath(tp: jst.TreePath): Tree.Path[A] = { + if (tp == null) null + else (tp.getPath map unpackNode).tail.toIndexedSeq + } + + private def rootPeerNode = peer.getRoot.asInstanceOf[PeerNode] + + def roots: Seq[A] = getNodeChildren(rootPeerNode) map unpackNode + + def update(path: Path[A], newValue: A) { + peer.valueForPathChanged(pathToTreePath(path), newValue) + } + + private def getPeerNodeAt(path: Path[A]): PeerNode = { + pathToTreePath(path).getLastPathComponent.asInstanceOf[PeerNode] + } + + def insertUnder(parentPath: Path[A], newValue: A, index: Int): Boolean = { + peer.insertNodeInto(new PeerNode(newValue), getPeerNodeAt(parentPath), index) + true + } + + def remove(pathToRemove: Path[A]): Boolean = { + peer.removeNodeFromParent(getPeerNodeAt(pathToRemove)) + true + } + + def map[B](f: A=>B): InternalTreeModel[B] = new InternalTreeModel[B] { + override val peer = copyFromModel(self, f) + } + + protected[tree] def copyFromModel[B](otherModel: TreeModel[B], f: B => A): jst.DefaultTreeModel = { + def copyNodeAt(bPath: Path[B]): PeerNode = { + val copiedNode = new PeerNode(f(bPath.last)) + val otherChildren = otherModel.getChildrenOf(bPath) + val copiedChildren = otherChildren map { b => copyNodeAt(bPath :+ b) } + copiedChildren foreach copiedNode.add + copiedNode + } + + val rootNode = new PeerNode(hiddenRoot) + val children = otherModel.roots map { b => copyNodeAt(Path(b)) } + children foreach rootNode.add + new jst.DefaultTreeModel(rootNode) + } + + private def getNodeChildren(node: PeerNode): Seq[PeerNode] = node.children.toSeq.asInstanceOf[Seq[PeerNode]] + + def getChildrenOf(parentPath: Path[A]): Seq[A] = { + val lastNode = pathToTreePath(parentPath).getLastPathComponent.asInstanceOf[PeerNode] + getNodeChildren(lastNode) map unpackNode + } + + def filter(p: A => Boolean): InternalTreeModel[A] = { + def filterChildren(node: PeerNode): PeerNode = { + val newNode = new PeerNode(node.getUserObject) + val okChildren = getNodeChildren(node) filter { n => p(unpackNode(n)) } + okChildren map filterChildren foreach newNode.add + newNode + } + new InternalTreeModel(new PeerModel(filterChildren(rootPeerNode))) + } + + def toInternalModel: InternalTreeModel[A] = this + + def isExternalModel = false + + override def unpackNode(node: Any): A = node.asInstanceOf[PeerNode].getUserObject.asInstanceOf[A] +} diff --git a/src/main/scala/scala/swing/tree/ListView.scala b/src/main/scala/scala/swing/tree/ListView.scala deleted file mode 100644 index 8a6a7fb..0000000 --- a/src/main/scala/scala/swing/tree/ListView.scala +++ /dev/null @@ -1,302 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ Scala API ** -** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** -** /____/\___/_/ |_/____/_/ | | ** -** |/ ** -\* */ - - - -package scala.swing.tree - -import scala.swing._ -import scala.swing.{ListView => _} -import scala.swing.event._ -import javax.swing._ -import javax.swing.event._ - -object ListView extends RenderableCellsCompanion { - type Owner = ListView[_] - - /** - * The supported modes of user selections. - */ - object IntervalMode extends Enumeration { - val Single = Value(ListSelectionModel.SINGLE_SELECTION) - val SingleInterval = Value(ListSelectionModel.SINGLE_INTERVAL_SELECTION) - val MultiInterval = Value(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) - } - - def wrap[A](c: JList) = new ListView[A] { - override lazy val peer = c - } - - object Renderer extends CellRendererCompanion { - type Peer = ListCellRenderer - case class CellInfo(isSelected: Boolean, focused: Boolean, index: Int) - object emptyCellInfo extends CellInfo(false, false, 0) - def wrap[A](r: ListCellRenderer): Renderer[A] = new Wrapped[A](r) - - /** - * Wrapper for javax.swing.ListCellRenderers - */ - class Wrapped[A](override val peer: ListCellRenderer) extends Renderer[A] { - override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo) = { - Component.wrap(peer.getListCellRendererComponent(list.peer, a, cellInfo.index, - cellInfo.isSelected, cellInfo.focused).asInstanceOf[JComponent]) - } - } - - /** - * Returns a renderer for items of type A. The given function - * converts items of type A to items of type B - * for which a renderer is implicitly given. This allows chaining of - * renderers, e.g.: - * - * - * case class Person(name: String, email: String) - * val persons = List(Person("John", "j.doe@a.com"), Person("Mary", "m.jane@b.com")) - * new ListView(persons) { - * renderer = ListView.Renderer(_.name) - * } - * - */ - def apply[A,B](f: A => B)(implicit renderer: Renderer[B]): Renderer[A] = new Renderer[A] { - def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component = - renderer.componentFor(list, f(a), cellInfo) - } - - def default[A] = new DefaultRenderer[A] - - def labelled[A](f: A => (Icon, String)) = new DefaultRenderer[A] with LabelRenderer[A] {val convert = f} - } - - import Renderer.CellInfo - - /** - * Item renderer for a list view. This is contravariant on the type of the - * items, so a more general renderer can be used in place of a more specific - * one. For instance, an Any renderer can be used for a list view - * of strings. - * - * @see javax.swing.ListCellRenderer - */ - trait Renderer[-A] extends CellRenderer[A] { - val companion = Renderer - - def dispatchToScalaRenderer(list: JList, a: Any, index: Int, isSelected: Boolean, focused: Boolean): JComponent = { - val wrappedList = ListView.wrap[A](list) - // Deal with deprecated method if necessary. - val comp = componentFor(wrappedList, isSelected, focused, a.asInstanceOf[A], index) - comp match { - case null => componentFor(wrappedList, a.asInstanceOf[A], CellInfo(isSelected, focused, index)).peer - case c => c.peer - } - } - - - def peer: ListCellRenderer = new ListCellRenderer { - def getListCellRendererComponent(list: JList, a: Any, index: Int, isSelected: Boolean, focused: Boolean) = { - dispatchToScalaRenderer(list, a, index, isSelected, focused) - } - } - - @deprecated("Override componentFor(list: List[_], a: A, cellInfo: CellInfo) instead.") - def componentFor(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int): Component = null - override def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component - } - - /** - * A default renderer that maintains a single component for item rendering - * and preconfigures it to sensible defaults. It is polymorphic on the - * component's type so clients can easily use component specific attributes - * during configuration. - */ - abstract class AbstractRenderer[-A, C<:Component](protected val component: C) extends Renderer[A] { - // The renderer component is responsible for painting selection - // backgrounds. Hence, make sure it is opaque to let it draw - // the background. - component.opaque = true - - /** - * Standard preconfiguration that is commonly done for any component. - * This includes foreground and background colors, as well as colors - * of item selections. - */ - def preConfigure(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int) { - if (isSelected) { - component.background = list.selectionBackground - component.foreground = list.selectionForeground - } else { - component.background = list.background - component.foreground = list.foreground - } - } - /** - * Configuration that is specific to the component and this renderer. - */ - def configure(list: ListView[_], isSelected: Boolean, focused: Boolean, a: A, index: Int) - - /** - * Configures the component before returning it. - */ - def componentFor(list: ListView[_], a: A, cellInfo: CellInfo): Component = { - preConfigure(list, cellInfo.isSelected, cellInfo.focused, a, cellInfo.index) - configure(list, cellInfo.isSelected, cellInfo.focused, a, cellInfo.index) - component - } - } - - /** - * Default label-based renderer for a ListView. - */ - class DefaultRenderer[-A] extends Label with Renderer[A] { - override lazy val peer = new javax.swing.DefaultListCellRenderer with SuperMixin { peerThis => - override def getListCellRendererComponent(list: JList, value: AnyRef, index: Int, isSelected: Boolean, focused: Boolean): JComponent = { - dispatchToScalaRenderer(list, value, index, isSelected, focused) - peerThis - } - - def defaultRendererComponent(list: JList, value: AnyRef, index: Int, isSelected: Boolean, focused: Boolean) { - super.getListCellRendererComponent(list, value, index, isSelected, focused) - } - } - - override def componentFor(list: ListView[_], value: A, info: Renderer.CellInfo): Component = { - peer.defaultRendererComponent(list.peer, value.asInstanceOf[AnyRef], info.index, info.isSelected, info.focused) - this - } - } -} - -/** - * A component that displays a number of elements in a list. A list view does - * not support inline editing of items. If you need it, use a table view instead. - * - * Named ListView to avoid a clash with the frequently used - * scala.List - * - * @see javax.swing.JList - */ -class ListView[A] extends Component with CellView[A] with RenderableCells[A] { - import ListView._ - override lazy val peer: JList = new JList with SuperMixin - override val companion = ListView - - - def this(items: Seq[A]) = { - this() - listData = items - } - - protected class ModelWrapper(val items: Seq[A]) extends AbstractListModel { - def getElementAt(n: Int) = items(n).asInstanceOf[AnyRef] - def getSize = items.size - } - - def cellValues = listData.iterator - - def editable = false - - def listData: Seq[A] = peer.getModel match { - case model: ModelWrapper => model.items - case model @ _ => new Seq[A] { selfSeq => - def length = model.getSize - def iterator = new Iterator[A] { - var idx = 0 - def next = { idx += 1; apply(idx-1) } - def hasNext = idx < selfSeq.length - } - def apply(n: Int) = model.getElementAt(n).asInstanceOf[A] - } - } - - def listData_=(items: Seq[A]) { - peer.setModel(new AbstractListModel { - def getElementAt(n: Int) = items(n).asInstanceOf[AnyRef] - def getSize = items.size - }) - } - - /** - * The current item selection. - */ - object selection extends CellSelection { - - @deprecated("Use SelectionSet[A] instead") - protected type Indices[A] = SelectionSet[A] - - def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex - def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex - - /** - * The indices of the currently selected items. - */ - object indices extends SelectionSet(peer.getSelectedIndices) { - def -=(n: Int): this.type = { peer.removeSelectionInterval(n,n); this } - def +=(n: Int): this.type = { peer.addSelectionInterval(n,n); this } - def --=(nn: Seq[Int]) = {nn foreach -=; this} - def ++=(nn: Seq[Int]) = {nn foreach +=; this} - @deprecated("Use ListView.selection.leadIndex") - def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex - @deprecated("Use ListView.selection.anchorIndex") - def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex - } - - @deprecated("Use ListView.selectIndices") - def selectIndices(ind: Int*) = peer.setSelectedIndices(ind.toArray) - - /** - * The currently selected items. - */ - object items extends scala.collection.SeqProxy[A] { - def self = peer.getSelectedValues.map(_.asInstanceOf[A]) - @deprecated("Use ListView.selection.leadIndex") - def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex - @deprecated("Use ListView.selection.anchorIndex") - def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex - } - - def intervalMode: IntervalMode.Value = IntervalMode(peer.getSelectionModel.getSelectionMode) - def intervalMode_=(m: IntervalMode.Value) { peer.getSelectionModel.setSelectionMode(m.id) } - - peer.getSelectionModel.addListSelectionListener(new ListSelectionListener { - def valueChanged(e: javax.swing.event.ListSelectionEvent) { - // TODO Reinstate for the real ListView - //publish(new ListSelectionChanged(ListView.this, e.getFirstIndex to e.getLastIndex, e.getValueIsAdjusting)) - } - }) - - def cellValues = items.iterator - def count = items.size - def empty = items.isEmpty - - def adjusting = peer.getSelectionModel.getValueIsAdjusting - } - - def renderer: ListView.Renderer[A] = ListView.Renderer.wrap(peer.getCellRenderer) - def renderer_=(r: ListView.Renderer[A]) { peer.setCellRenderer(r.peer) } - - def fixedCellWidth = peer.getFixedCellWidth - def fixedCellWidth_=(x: Int) = peer.setFixedCellWidth(x) - - def fixedCellHeight = peer.getFixedCellHeight - def fixedCellHeight_=(x: Int) = peer.setFixedCellHeight(x) - - def prototypeCellValue: A = peer.getPrototypeCellValue.asInstanceOf[A] - def prototypeCellValue_=(a: A) { peer.setPrototypeCellValue(a) } - - def selectionForeground: Color = peer.getSelectionForeground - def selectionForeground_=(c: Color) = peer.setSelectionForeground(c) - def selectionBackground: Color = peer.getSelectionBackground - def selectionBackground_=(c: Color) = peer.setSelectionBackground(c) - - def selectIndices(ind: Int*) = peer.setSelectedIndices(ind.toArray) - - peer.getModel.addListDataListener(new ListDataListener { - def contentsChanged(e: ListDataEvent) {} // { publish(ListChanged(ListView.this)) } // TODO Reinstate for the real ListView - def intervalRemoved(e: ListDataEvent) {} //{ publish(ListElementsRemoved(ListView.this, e.getIndex0 to e.getIndex1)) } // TODO Reinstate for the real ListView - def intervalAdded(e: ListDataEvent) {} //{ publish(ListElementsAdded(ListView.this, e.getIndex0 to e.getIndex1)) } // TODO Reinstate for the real ListView - }) -} diff --git a/src/main/scala/scala/swing/tree/Tree.scala b/src/main/scala/scala/swing/tree/Tree.scala index 65e5f74..8ad23a8 100644 --- a/src/main/scala/scala/swing/tree/Tree.scala +++ b/src/main/scala/scala/swing/tree/Tree.scala @@ -58,16 +58,19 @@ sealed trait TreeEditors extends EditableCellsCompanion { def apply[A, B](toB: A => B, toA: B => A)(implicit editor: Editor[B]): Editor[A] = new Editor[A] { override lazy val peer = new jst.TreeCellEditor { + override def getTreeCellEditorComponent(tree: JTree, value: Any, isSelected: Boolean, isExpanded: Boolean, isLeaf: Boolean, row: Int) = { - editor.peer.getTreeCellEditorComponent(tree, toB(value.asInstanceOf[A]), isSelected, isExpanded, isLeaf, row) + val treeWrapper = getTreeWrapper(tree) + val a = treeWrapper.model unpackNode value + editor.peer.getTreeCellEditorComponent(tree, toB(a), isSelected, isExpanded, isLeaf, row) } - def addCellEditorListener(cel: jse.CellEditorListener) {editor.peer.addCellEditorListener(cel)} - def cancelCellEditing() {editor.peer.cancelCellEditing()} + def addCellEditorListener(cel: jse.CellEditorListener) { editor.peer.addCellEditorListener(cel) } + def cancelCellEditing() { editor.peer.cancelCellEditing() } def getCellEditorValue(): AnyRef = toA(editor.peer.getCellEditorValue.asInstanceOf[B]).asInstanceOf[AnyRef] def isCellEditable(e: java.util.EventObject) = editor.peer.isCellEditable(e) - def removeCellEditorListener(cel: jse.CellEditorListener) {editor.peer.removeCellEditorListener(cel)} - def shouldSelectCell(e: java.util.EventObject) = {editor.peer.shouldSelectCell(e)} + def removeCellEditorListener(cel: jse.CellEditorListener) { editor.peer.removeCellEditorListener(cel) } + def shouldSelectCell(e: java.util.EventObject) = { editor.peer.shouldSelectCell(e) } def stopCellEditing() = editor.peer.stopCellEditing() } @@ -89,19 +92,23 @@ sealed trait TreeEditors extends EditableCellsCompanion { import Editor._ val companion = Editor + protected[tree] def getTreeWrapper(peerTree: JTree) = peerTree match { + case t: JTreeMixin[A] => t.treeWrapper + case _ => throw new IllegalArgumentException( + "This javax.swing.JTree does not mix in JTreeMixin, and so cannot be used by scala.swing.Tree#Editor") + } + protected class TreeEditorPeer extends EditorPeer with jst.TreeCellEditor { override def getTreeCellEditorComponent(tree: js.JTree, value: Any, selected: Boolean, expanded: Boolean, leaf: Boolean, rowIndex: Int) = { - def treeWrapper(tree: js.JTree) = tree match { - case t: JTreeMixin[A] => t.treeWrapper - case _ => assert(false); null - } - componentFor(treeWrapper(tree), value.asInstanceOf[A], CellInfo(isSelected=selected, + val treeWrapper = getTreeWrapper(tree) + val a = treeWrapper.model unpackNode value + componentFor(treeWrapper, a, CellInfo(isSelected=selected, isExpanded=expanded, isLeaf=leaf, row=rowIndex)).peer } } - private[this] lazy val _peer: jst.TreeCellEditor = new TreeEditorPeer - def peer = _peer // We can't use a lazy val directly, as Wrapped wouldn't be able to override with a non-lazy val. + private[this] lazy val lazyPeer: jst.TreeCellEditor = new TreeEditorPeer + def peer = lazyPeer // We can't use a lazy val directly, as Wrapped wouldn't be able to override with a non-lazy val. } } @@ -127,8 +134,8 @@ sealed trait TreeRenderers extends RenderableCellsCompanion { */ class Wrapped[-A](override val peer: Peer) extends Renderer[A] { override def componentFor(tree: Tree[_], value: A, info: CellInfo): Component = { - Component.wrap(peer.getTreeCellRendererComponent(tree.peer, value, - info.isSelected, info.isExpanded, info.isLeaf, info.row, info.hasFocus).asInstanceOf[js.JComponent]) + Component.wrap(peer.getTreeCellRendererComponent(tree.peer, value, info.isSelected, + info.isExpanded, info.isLeaf, info.row, info.hasFocus).asInstanceOf[js.JComponent]) } } @@ -154,18 +161,21 @@ sealed trait TreeRenderers extends RenderableCellsCompanion { protected def dispatchToScalaRenderer(tree: JTree, value: AnyRef, selected: Boolean, expanded: Boolean, leaf: Boolean, rowIndex: Int, focus: Boolean): js.JComponent = { + + def treeWrapper = tree match { + case t: JTreeMixin[A] => t.treeWrapper + case _ => throw new IllegalArgumentException( + "This javax.swing.JTree does not mix in JTreeMixin, and so cannot be used by scala.swing.Tree#Renderer") + } value match { // JTree's TreeModel property change will indirectly cause the Renderer // to be activated on the root node, even if it is permanently hidden; since our underlying root node // is not a suitably-typed A, we need to intercept it and return a harmless component. case TreeModel.hiddenRoot => new js.JTextField - case a: A => - componentFor(tree match { - case t: JTreeMixin[A] => t.treeWrapper - case _ => assert(false); null - }, a, CellInfo(isSelected=selected, isExpanded=expanded, isLeaf=leaf, row=rowIndex, hasFocus=focus)).peer - + case _ => + val a = treeWrapper.model unpackNode value + componentFor(treeWrapper, a, CellInfo(isSelected=selected, isExpanded=expanded, isLeaf=leaf, row=rowIndex, hasFocus=focus)).peer } } @@ -253,33 +263,26 @@ sealed trait TreeRenderers extends RenderableCellsCompanion { this } } - +} -} object Tree extends TreeRenderers with TreeEditors { - // TODO - // The trouble with List is that the most useful element, the last one, can only be accessed in O(n) time. - // Furthermore, using a type alias here instead of defining a separate type locks us into the List API, and denies - // us any future flexibility in adjusting the functionality behind this API. - // - // This probably should be a custom class Path, backed by an IndexedSeq, with a peer object j.s.t.TreePath. - // - // What I like about List is the appealing syntax of root :: branch :: leaf. We can still - // get this with an implicit conversion, but that won't apply to pattern matching. - val Path = List - type Path[+A] = List[A] + val Path = IndexedSeq + type Path[+A] = IndexedSeq[A] /** * The style of lines drawn between tree nodes. */ - object LineStyle extends Enumeration("Angled", "None") { - val Angled, None = Value + object LineStyle extends Enumeration { + val Angled = Value("Angled") + val None = Value("None") + // "Horizontal" is omitted; it does not display as expected, because of the hidden root; it only shows lines // for the top level. + // val Horizontal = Value("Horizontal") } object SelectionMode extends Enumeration { @@ -308,11 +311,9 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) import Tree._ - def this(roots: Seq[A], children: A => Seq[A]) = this(new TreeModel(roots, children)) - override val companion = Tree - override lazy val peer: js.JTree = new js.JTree(treeData.peer) with JTreeMixin[A] { + override lazy val peer: js.JTree = new js.JTree(model.peer) with JTreeMixin[A] { def treeWrapper = thisTree // We keep the true root node as an invisible and empty value; the user's data will @@ -327,13 +328,13 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) * Implicitly converts Tree.Path[A] lists to TreePath objects understood by the underlying peer JTree. * In addition to the objects in the list, the JTree's hidden root node must be prepended. */ - implicit def pathToTreePath(p: Path[A]): jst.TreePath = treeDataModel pathToTreePath p + implicit def pathToTreePath(p: Path[A]): jst.TreePath = model pathToTreePath p /** * Implicitly converts javax.swing.tree.TreePath objects to Tree.Path[A] lists recognised in Scala Swing. * TreePaths will include the underlying JTree's hidden root node, which is omitted for Tree.Paths. */ - implicit def treePathToPath(tp: jst.TreePath): Path[A] = treeDataModel treePathToPath tp + implicit def treePathToPath(tp: jst.TreePath): Path[A] = model treePathToPath tp /** * Implicit method to produce a generic editor. @@ -388,19 +389,22 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) def +=(p: Path[A]) = { peer.addSelectionPath(p); this } def --=(ps: Seq[Path[A]]) = { peer.removeSelectionPaths(ps map pathToTreePath toArray); this } def ++=(ps: Seq[Path[A]]) = { peer.addSelectionPaths(ps map pathToTreePath toArray); this } - def leadSelection = peer.getLeadSelectionPath + def leadSelection: Option[Path[A]] = Option(peer.getLeadSelectionPath) } peer.getSelectionModel.addTreeSelectionListener(new TreeSelectionListener { - def valueChanged(e: javax.swing.event.TreeSelectionEvent) { - val (newPath, oldPath) = e.getPaths.map(treePathToPath).toList.partition(e.isAddedPath(_)) - publish(new TreePathSelected(thisTree, newPath, oldPath, + def valueChanged(e: jse.TreeSelectionEvent) { + val (newPath, oldPath) = e.getPaths.toList.partition(e.isAddedPath) + + publish(new TreePathSelected(thisTree, + newPath map treePathToPath, + oldPath map treePathToPath, Option(e.getNewLeadSelectionPath: Path[A]), Option(e.getOldLeadSelectionPath: Path[A]))) } }) - def cellValues: Iterator[A] = paths.iterator map (_.last) + def cellValues: Iterator[A] = paths.iterator.map(_.last) def mode = Tree.SelectionMode(peer.getSelectionModel.getSelectionMode) @@ -413,19 +417,19 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) protected val modelListener = new TreeModelListener { override def treeStructureChanged(e: jse.TreeModelEvent) { - publish(TreeStructureChanged[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toList, + publish(TreeStructureChanged[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toIndexedSeq, e.getChildIndices.toList, e.getChildren.asInstanceOf[Array[A]].toList)) } override def treeNodesInserted(e: jse.TreeModelEvent) { - publish(TreeNodesInserted[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toList, + publish(TreeNodesInserted[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toIndexedSeq, e.getChildIndices.toList, e.getChildren.asInstanceOf[Array[A]].toList)) } override def treeNodesRemoved(e: jse.TreeModelEvent) { - publish(TreeNodesRemoved[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toList, + publish(TreeNodesRemoved[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toIndexedSeq, e.getChildIndices.toList, e.getChildren.asInstanceOf[Array[A]].toList)) } def treeNodesChanged(e: jse.TreeModelEvent) { - publish(TreeNodesChanged[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toList, + publish(TreeNodesChanged[A](Tree.this, e.getPath.asInstanceOf[Array[A]].toIndexedSeq, e.getChildIndices.toList, e.getChildren.asInstanceOf[Array[A]].toList)) } } @@ -448,9 +452,9 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) def collapsePath(path: Path[A]) {peer collapsePath path} def collapseRow(row: Int) {peer collapseRow row} - def treeData = treeDataModel + def model = treeDataModel - def treeData_=(tm: TreeModel[A]) = { + def model_=(tm: TreeModel[A]) = { if (treeDataModel != null) treeDataModel.peer.removeTreeModelListener(modelListener) @@ -459,7 +463,7 @@ class Tree[A](private var treeDataModel: TreeModel[A] = TreeModel.empty[A]) treeDataModel.peer.addTreeModelListener(modelListener) } - override def cellValues: Iterator[A] = treeData.depthFirstIterator + override def cellValues: Iterator[A] = model.depthFirstIterator /** * Collapses all visible rows. diff --git a/src/main/scala/scala/swing/tree/TreeModel.scala b/src/main/scala/scala/swing/tree/TreeModel.scala index 0d4a3b8..0db1cde 100644 --- a/src/main/scala/scala/swing/tree/TreeModel.scala +++ b/src/main/scala/scala/swing/tree/TreeModel.scala @@ -1,12 +1,16 @@ package scala.swing package tree -import javax.swing.event.{TreeModelListener, TreeModelEvent} -import javax.swing.tree.{MutableTreeNode, TreeNode, DefaultTreeModel, TreePath, TreeModel => JTreeModel} -import scala.reflect.ClassManifest +import scala.Array.fallbackCanBuildFrom import scala.collection.mutable.ListBuffer +import scala.reflect.ClassManifest +import scala.swing.tree.Tree.Path + +import TreeModel.hiddenRoot import Tree.Path -import scala.swing.event._ +import javax.swing.event.TreeModelEvent +import javax.swing.event.TreeModelListener +import javax.swing.{tree => jst} object TreeModel { @@ -16,164 +20,85 @@ object TreeModel { */ private[tree] case object hiddenRoot - /* - class Node[A](private var userObj: A) { - def children = ListBuffer[A]() - def userObject = userObj - def userObject_=(a: A) {userObj = a} - }*/ - - - def empty[A] = new TreeModel[A](Seq.empty, _ => Seq.empty) // Needs to be a method rather than a val, because A is invariant. - def apply[A](roots: A*)(children: A => Seq[A]) = new TreeModel(roots, children) + def empty[A]: TreeModel[A] = new ExternalTreeModel[A](Seq.empty, _ => Seq.empty) + def apply[A](roots: A*)(children: A => Seq[A]): TreeModel[A] = new ExternalTreeModel(roots, children) } -/** - * Represents tree data as a sequence of root nodes, and a function that can retrieve child nodes. - */ -class TreeModel[A](val roots: Seq[A], - children: A => Seq[A]) { - self => - - import TreeModel._ - - def filter(p: A => Boolean) = new TreeModel[A](roots filter p, a => children(a) filter p) - - def foreach[U](f: A => U): Unit = depthFirstIterator foreach f - - /** - * A function to update a value in the model, at a given path. By default this will throw an exception; to - * make a TreeModel updatable, call updatableWith() to provide a new TreeModel with the specified update method. - */ - protected val updateFunc: (Path[A], A) => A = { - (_,_) => error("Update is not supported on this tree") - } - - def getChildrenOf(parent: A): Seq[A] = children(parent) - - def update(path: Path[A], newValue: A) { - val existing = path.last - val result = updateFunc(path, newValue) - - // If the result is actually replacing the node with a different reference object, then - // fire "tree structure changed". - if (existing.isInstanceOf[AnyRef] && (existing.asInstanceOf[AnyRef] ne result.asInstanceOf[AnyRef])) { - peer.fireTreeStructureChanged(pathToTreePath(path), result) - } - // If the result is a value type or is a modification of the same node reference, then - // just fire "nodes changed". - else { - peer.fireNodesChanged(pathToTreePath(path), result) - } - } +trait TreeModel[A] { + + def roots: Seq[A] + val peer: jst.TreeModel + def getChildrenOf(parentPath: Path[A]): Seq[A] + def getChildPathsOf(parentPath: Path[A]): Seq[Path[A]] = getChildrenOf(parentPath).map(parentPath :+ _) + def filter(p: A => Boolean): TreeModel[A] + def map[B](f: A=>B): TreeModel[B] + def foreach[U](f: A=>U): Unit = depthFirstIterator foreach f + def isExternalModel: Boolean + def toInternalModel: InternalTreeModel[A] + + + def pathToTreePath(path: Tree.Path[A]): jst.TreePath + def treePathToPath(tp: jst.TreePath): Tree.Path[A] + /** - * Returns a new TreeModel that is updatable with the given function. The returned TreeModel will have - * the same roots and child function. + * Replace the item at the given path in the tree with a new value. + * Events are fired as appropriate. */ - def updatableWith(updater: (Path[A], A) => A): TreeModel[A] = new TreeModel(roots, children) { - override val updateFunc = updater - this.peer.treeModelListeners foreach self.peer.addTreeModelListener - } - + def update(path: Path[A], newValue: A): Unit + def remove(pathToRemove: Path[A]): Boolean + def insertUnder(parentPath: Path[A], newValue: A, index: Int): Boolean - /** - * Underlying tree model that exposes the tree structure to Java Swing. - * - * This implementation of javax.swing.tree.TreeModel takes advantage of its abstract nature, so that it respects - * the tree shape of the underlying structure provided by the user. - */ - lazy val peer = new JTreeModel { - private val treeModelListenerList = ListBuffer[TreeModelListener]() - - private def getChildrenOf(parent: Any) = parent match { - case `hiddenRoot` => roots - case a: A => children(a) - } - - def getChild(parent: Any, index: Int): AnyRef = { - val ch = getChildrenOf(parent) - if (index >= 0 && index < ch.size) - ch(index).asInstanceOf[AnyRef] - else - error("No child of \"" + parent + "\" found at index " + index) - } - def getChildCount(parent: Any): Int = getChildrenOf(parent).size - def getIndexOfChild(parent: Any, child: Any): Int = getChildrenOf(parent) indexOf child - def getRoot(): AnyRef = hiddenRoot - def isLeaf(node: Any): Boolean = getChildrenOf(node).isEmpty - - - def treeModelListeners: Seq[TreeModelListener] = treeModelListenerList - - def addTreeModelListener(tml: TreeModelListener) { - treeModelListenerList += tml - } - - def removeTreeModelListener(tml: TreeModelListener) { - treeModelListenerList -= tml - } - - def valueForPathChanged(path: TreePath, newValue: Any) { - update(treePathToPath(path), newValue.asInstanceOf[A]) - } + def insertBefore(path: Path[A], newValue: A): Boolean = { + if (path.isEmpty) throw new IllegalArgumentException("Cannot insert before empty path") - private def createEvent(path: TreePath, newValue: Any) = new TreeModelEvent(this, path, - Array(getChildrenOf(path.getPath.last) indexOf newValue), - Array(newValue.asInstanceOf[AnyRef])) - - def fireTreeStructureChanged(path: TreePath, newValue: Any) { - treeModelListenerList foreach (_.treeStructureChanged(createEvent(path, newValue))) - } - - def fireNodesChanged(path: TreePath, newValue: Any) { - treeModelListenerList foreach (_.treeNodesChanged(createEvent(path, newValue))) - } - - def fireNodesInserted(path: TreePath, newValue: Any, index: Int) { - treeModelListenerList foreach (_.treeNodesInserted(createEvent(path, newValue))) - } + val parentPath = path.init + val index = siblingsUnder(parentPath) indexOf path.last + insertUnder(parentPath, newValue, index) } - - def pathToTreePath(path: Tree.Path[A]) = { - val array = (hiddenRoot :: path).map(_.asInstanceOf[AnyRef]).toArray(ClassManifest.Object) - new TreePath(array) + + def insertAfter(path: Path[A], newValue: A): Boolean = { + if (path.isEmpty) throw new IllegalArgumentException("Cannot insert after empty path") + + val parentPath = path.init + val index = siblingsUnder(parentPath) indexOf path.last + insertUnder(parentPath, newValue, index+1) } - def treePathToPath(tp: TreePath): Tree.Path[A] = { - if (tp == null) null - else tp.getPath.map(_.asInstanceOf[A]).toList.tail - } + protected def siblingsUnder(parentPath: Path[A]) = if (parentPath.isEmpty) roots + else getChildrenOf(parentPath) + /** * Iterates sequentially through each item in the tree, either in breadth-first or depth-first ordering, * as decided by the abstract pushChildren() method. */ private trait TreeIterator extends Iterator[A] { - protected var openNodes: Iterator[A] = roots.iterator + protected var openNodes: Iterator[Path[A]] = roots.map(Path(_)).iterator - def pushChildren(item: A): Unit + def pushChildren(path: Path[A]): Unit def hasNext = openNodes.nonEmpty def next() = if (openNodes.hasNext) { - val item = openNodes.next - pushChildren(item) - item + val path = openNodes.next + pushChildren(path) + path.last } else error("No more items") } def breadthFirstIterator: Iterator[A] = new TreeIterator { - override def pushChildren(item: A) {openNodes ++= children(item).iterator} + override def pushChildren(path: Path[A]) {openNodes ++= getChildPathsOf(path).toIterator} } def depthFirstIterator: Iterator[A] = new TreeIterator { - override def pushChildren(item: A) { + override def pushChildren(path: Path[A]) { val open = openNodes - openNodes = children(item).iterator ++ open // ++'s argument is by-name, and should not directly pass in a var + openNodes = getChildPathsOf(path).toIterator ++ open // ++'s argument is by-name, and should not directly pass in a var } } def size = depthFirstIterator.size + def unpackNode(node: Any): A = node.asInstanceOf[A] }