scalismo.ui.view.NodesPanel.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2016 University of Basel, Graphics and Vision Research Group
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package scalismo.ui.view
import java.awt.event._
import javax.swing.event.{TreeSelectionEvent, TreeSelectionListener}
import javax.swing.plaf.basic.BasicTreeUI
import javax.swing.tree._
import javax.swing.{Icon, JPopupMenu, JTree}
import scalismo.ui.model._
import scalismo.ui.model.capabilities.{CollapsableView, Removeable}
import scalismo.ui.model.properties.ColorProperty
import scalismo.ui.resources.icons.{BundledIcon, FontIcon, ScalableIcon}
import scalismo.ui.util.NodeListFilters
import scalismo.ui.view.NodesPanel.{SceneNodeCellRenderer, ViewNode}
import scalismo.ui.view.action.popup.{PopupAction, PopupActionWithOwnMenu}
import scala.collection.JavaConverters._
import scala.collection.immutable
import scala.swing.{BorderPanel, Component, ScrollPane}
import scala.util.Try
object NodesPanel {
class ViewNode(backend: SceneNode) extends DefaultMutableTreeNode(backend) {
override def getUserObject: SceneNode = {
super.getUserObject.asInstanceOf[SceneNode]
}
}
class SceneNodeCellRenderer extends DefaultTreeCellRenderer {
class Icons(open: Icon, closed: Icon, leaf: Icon) {
// the invocation context is a call to getTreeCellRendererComponent().
def apply(): Unit = {
setOpenIcon(open)
setClosedIcon(closed)
setLeafIcon(leaf)
}
}
object Icons {
/* note: this uses the "closed" icon for leaves. */
private def closedIcon(node: SceneNode): Option[ScalableIcon] = {
node match {
case _: Scene => Some(BundledIcon.Scene)
case _: GroupNode => Some(BundledIcon.Group)
case n: TriangleMeshNode => Some(BundledIcon.Mesh.colored(n.color.value.darker))
case n: TetrahedralMeshNode => Some(BundledIcon.VolumeMesh.colored(n.color.value.darker))
case _: ScalarTetrahedralMeshFieldNode => Some(BundledIcon.VolumeMesh.colored(FontIcon.RainbowColor))
case n: PointCloudNode => Some(BundledIcon.PointCloud.colored(n.color.value.darker))
case n: LandmarkNode => Some(BundledIcon.Landmark.colored(n.color.value.darker))
case _: ScalarMeshFieldNode => Some(BundledIcon.Mesh.colored(FontIcon.RainbowColor))
case _: ImageNode => Some(BundledIcon.Image)
case _: TransformationNode[_] => Some(BundledIcon.Transformation)
case _: SceneNodeCollection[_] => Some(BundledIcon.FolderClosed)
case _ => None
}
}
private def openIcon(node: SceneNode): Option[ScalableIcon] = {
node match {
case _: SceneNodeCollection[_] => Some(BundledIcon.FolderOpen)
case _ => None
}
}
def forNode(node: SceneNode): Icons = {
val closed = closedIcon(node).map(_.standardSized()).getOrElse(BundledIcon.Fallback)
val open = openIcon(node).map(_.standardSized()).getOrElse(closed)
new Icons(open, closed, closed)
}
}
private var recursingInGetRendererComponent = false
override def getTreeCellRendererComponent(tree: JTree,
value: scala.Any,
sel: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
hasFocus: Boolean): java.awt.Component = {
val sceneNode = value.asInstanceOf[ViewNode].getUserObject
Icons.forNode(sceneNode).apply()
val component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
/* Next,we try to set the width of the component to extend (almost) to the right edge of the containing tree.
* This has two advantages: first, it should get rid of the annoying ellipses when renaming a node (e.g. "A" -> "ABC"
* would result in the tree showing "..." instead). Second, it allows to right-click anywhere in the row to get the popup menu.
*/
if (component == this) {
// the tree.getPathBounds() method used below results in another call to this method, which will create a stack overflow if we don't handle it.
if (!recursingInGetRendererComponent) {
recursingInGetRendererComponent = true
val Margin = 3
val bounds = tree.getPathBounds(tree.getPathForRow(row))
val treeWidth = tree.getWidth
setPreferredSize(null)
if (bounds != null && treeWidth - Margin > bounds.x) {
val pref = getPreferredSize
val alternativeWidth = treeWidth - Margin - bounds.x
if (alternativeWidth > pref.width) {
pref.width = alternativeWidth
setPreferredSize(pref)
}
}
recursingInGetRendererComponent = false
}
}
component
}
}
}
class NodesPanel(val frame: ScalismoFrame) extends BorderPanel with NodeListFilters {
private val scene = frame.scene
val rootNode = new ViewNode(scene)
// indicator that a synchronization between model and view is currently
// being performed (i.e. tree is being programmatically modified)
private var synchronizing = false
val mouseListener: MouseAdapter = new MouseAdapter() {
override def mousePressed(event: MouseEvent): Unit = handle(event)
override def mouseReleased(event: MouseEvent): Unit = handle(event)
def handle(event: MouseEvent): Unit = {
if (event.isPopupTrigger) {
val (x, y) = (event.getX, event.getY)
pathToSceneNode(tree.getPathForLocation(x, y)).foreach { node =>
val selected = getSelectedSceneNodes
// the action will always affect the node that was clicked. However,
// if the clicked node is part of a multi-selection, then it will also
// affect all other selected elements.
val affected = if (selected.contains(node)) selected else List(node)
val actions = PopupAction(affected)(frame)
if (actions.nonEmpty) {
val pop = new JPopupMenu()
actions.foreach {
case menu: PopupActionWithOwnMenu => pop.insert(menu.menuItem, pop.getComponentCount)
case a: PopupAction => pop.add(a.peer)
}
pop.show(tree, x, y)
// needed because otherwise the popup is sometimes (partly) hidden by the renderer window
frame.peer.revalidate()
}
}
}
}
}
private val selectionListener = new TreeSelectionListener {
override def valueChanged(e: TreeSelectionEvent): Unit = {
if (!synchronizing) {
frame.selectedNodes = getSelectedSceneNodes
}
}
}
private val componentListener = new ComponentAdapter {
override def componentResized(e: ComponentEvent): Unit = {
repaintTree()
}
}
private val keyListener = new KeyAdapter {
override def keyTyped(event: KeyEvent): Unit = {
if (event.getKeyChar == '\u007f') {
// delete
allMatch[Removeable](getSelectedSceneNodes).foreach(_.remove())
}
}
}
val treeModel = new DefaultTreeModel(rootNode)
val tree: JTree = new JTree(treeModel) {
setCellRenderer(new SceneNodeCellRenderer)
getSelectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION)
addTreeSelectionListener(selectionListener)
addKeyListener(keyListener)
addMouseListener(mouseListener)
addComponentListener(componentListener)
setExpandsSelectedPaths(true)
setLargeModel(true)
}
val scroll = new ScrollPane(Component.wrap(tree))
def pathToSceneNode(path: TreePath): Option[SceneNode] = {
Option(path).flatMap { path =>
Try {
path.getLastPathComponent.asInstanceOf[ViewNode].getUserObject
}.toOption
}
}
def sceneNodeToPath(node: SceneNode): Option[TreePath] = {
def findRecursive(currentNode: ViewNode): Option[ViewNode] = {
if (currentNode.getUserObject eq node) Some(currentNode)
else {
currentNode.children().asScala.foreach { child =>
findRecursive(child.asInstanceOf[ViewNode]) match {
case found @ Some(_) => return found
case _ =>
}
}
None
}
}
val viewNode: Option[ViewNode] = findRecursive(rootNode)
viewNode.map { defined =>
val pathAsArray = treeModel.getPathToRoot(defined).asInstanceOf[Array[Object]]
new TreePath(pathAsArray)
}
}
// helper function for collect(), to turn e.g. a List[Option[T]] into a (purged) List[T]
def definedOnly[T]: PartialFunction[Option[T], T] = {
case option if option.isDefined => option.get
}
// currently selected nodes
def getSelectedSceneNodes: List[SceneNode] = {
tree.getSelectionPaths match {
case null => Nil
case paths => paths.toList.map(pathToSceneNode).collect(definedOnly)
}
}
def setSelectedSceneNodes(nodes: immutable.Seq[SceneNode]): Unit = {
val paths = nodes.map(sceneNodeToPath).collect(definedOnly)
if (paths.nonEmpty) {
tree.setSelectionPaths(paths.toArray)
} else {
tree.setSelectionRow(0)
}
}
def repaintTree(): Unit = {
// try to force the tree to invalidate cached node sizes
tree.getUI match {
case ui: BasicTreeUI => ui.setLeftChildIndent(ui.getLeftChildIndent)
case _ => //don't know how to handle
}
tree.treeDidChange()
if (preferredSize.width > size.width) {
frame.peer.revalidate()
}
}
def synchronizeWholeTree(): Unit = {
synchronizing = true
// save user's selection for later
val selecteds = getSelectedSceneNodes
synchronizeSingleNode(scene, rootNode)
repaintTree()
synchronizing = false
setSelectedSceneNodes(selecteds)
}
def synchronizeSingleNode(model: SceneNode, view: ViewNode): Unit = {
// this method operates at the level of a single node, and synchronizes the view
// of that node's children.
// don't replace this with a val, it has to be freshly evaluated every time
def viewChildren = view.children.asScala.map(_.asInstanceOf[ViewNode]).toList
def nodeOrChildrenIfCollapsed(node: SceneNode): Seq[SceneNode] = {
node match {
case c: CollapsableView if c.isViewCollapsed => node.children.flatMap(nodeOrChildrenIfCollapsed)
case group: GroupNode if group.hidden => Nil
case _ => List(node)
}
}
val modelChildren = model.children.flatMap(nodeOrChildrenIfCollapsed)
// remove (obsolete) children that are in view, but not in model
// Note: don't replace the exists with contains: we're using object identity, not "normal" equality
viewChildren
.filterNot({ n =>
modelChildren.exists(_ eq n.getUserObject)
})
.foreach(treeModel.removeNodeFromParent(_))
val existingNodesInView = viewChildren.map(_.getUserObject)
val nodesToAddToView = modelChildren.zipWithIndex.filterNot {
case (o, _) => existingNodesInView.exists(_ eq o)
}
nodesToAddToView.foreach({
case (obj, idx) =>
val node = new ViewNode(obj)
treeModel.insertNodeInto(node, view, idx)
// this ensures the tree gets expanded to show newly added nodes
val p = node.getPath.map(_.asInstanceOf[Object])
tree.setSelectionPath(new TreePath(p))
})
// recurse
modelChildren.zip(viewChildren).foreach {
case (m, v) => synchronizeSingleNode(m, v)
}
}
//constructor logic
layout(scroll) = BorderPanel.Position.Center
synchronizeWholeTree()
listenTo(scene, frame, ColorProperty)
reactions += {
case ScalismoFrame.event.SelectedNodesChanged(_) => setSelectedSceneNodes(frame.selectedNodes)
case Scene.event.SceneChanged(_) => synchronizeWholeTree()
case ColorProperty.event.SomeColorPropertyChanged => repaintTree()
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy