Skip to content

Instantly share code, notes, and snippets.

@bgr
Created September 19, 2013 11:32
Show Gist options
  • Save bgr/6622108 to your computer and use it in GitHub Desktop.
Save bgr/6622108 to your computer and use it in GitHub Desktop.
Jython JTree demo
# Java Swing JTree demo in Jython
import java
from java.awt import Dimension, GridLayout, Toolkit, BorderLayout
from javax.swing.event import TreeModelListener
from javax.swing import (JPanel, JButton, SwingUtilities, JTree, JFrame,
JScrollPane, JTextField, ImageIcon, ToolTipManager,
JSplitPane, JEditorPane)
from javax.swing.event import TreeModelEvent
from javax.swing.tree import (DefaultTreeCellRenderer, DefaultTreeCellEditor,
TreeSelectionModel, TreePath, TreeModel)
# this is model value object that we'll use for tree nodes instead of String
class King(object):
def __init__(self, name, number, last_name):
self.name = name
self.number = number
self.last_name = last_name
self.parent = None
self.children = []
# note that it won't be this string representation that is used for JTree
# nodes, it is responsibility of the view to pull out and format the
# string on its own
def __repr__(self):
return "{0}({1}, {2}, {3})".format(self.__class__.__name__, self.name,
self.number, self.last_name)
# this will take care of tooltips and custom icons for tree nodes
class MyTreeCellRenderer(DefaultTreeCellRenderer):
def __init__(self, node_icon, leaf_icon):
super(MyTreeCellRenderer, self).__init__()
self.node_icon = node_icon
self.leaf_icon = leaf_icon
def getTreeCellRendererComponent(self, tree, node, is_sel,
is_expanded, is_leaf, row_num, has_focus):
# super doesn't work here for some reason, calling method directly
DefaultTreeCellRenderer.getTreeCellRendererComponent(
self, tree, node, is_sel, is_expanded, is_leaf, row_num, has_focus)
icon = self.leaf_icon if is_leaf else self.node_icon
self.icon = icon
self.setToolTipText(str(node))
return self
# this will show up when rename is triggered on tree node (F2 or tripple click)
class MyTreeCellEditor(DefaultTreeCellEditor):
def __init__(self, tree, renderer):
super(MyTreeCellEditor, self).__init__(tree, renderer)
# will have 3 text fields and OK button
tf_name = JTextField("", 8)
tf_number = JTextField("", 2)
tf_last_name = JTextField("", 8)
ok_button = JButton("OK")
# make enter work in textfields same as pressing OK button
ok_button.actionPerformed = \
tf_name.actionPerformed = \
tf_number.actionPerformed = \
tf_last_name.actionPerformed = lambda _: self.stopCellEditing()
editor_panel = JPanel()
editor_panel.add(tf_name)
editor_panel.add(tf_number)
editor_panel.add(tf_last_name)
editor_panel.add(ok_button)
self.tf_name = tf_name
self.tf_number = tf_number
self.tf_last_name = tf_last_name
self.editor_panel = editor_panel
def getCellEditorValue(self):
return (self.tf_name.text,
int(self.tf_number.text),
self.tf_last_name.text)
def getTreeCellEditorComponent(self, tree, value, is_selected,
is_expanded, is_leaf, row):
# pre-populate with node's current value
self.tf_name.text = value.name
self.tf_number.text = str(value.number)
self.tf_last_name.text = value.last_name
return self.editor_panel
# custom model will allow us to use our King value objects as tree nodes
class KingdomModel(TreeModel):
def __init__(self, roots):
self.listeners = []
self.root_king = King("Rooty", 0, "Unnamed") # will be invisible node
self.root_king.children = roots[:]
def increase_node(self, path, n=1):
""" Increases given node's number and its children's numbers and
notifies listeners.
"""
if path is None:
path = TreePath([self.root_king])
def bump(node):
node.number += n
[bump(ch) for ch in node.children]
bump(path.lastPathComponent)
self.fire_structure_changed(path) # this will collapse root node
def fire_structure_changed(self, path_to_root):
evt = TreeModelEvent(self, path_to_root)
for listener in self.listeners:
listener.treeStructureChanged(evt)
def fire_changed(self, path_to_child):
parent, child = path_to_child.path[-2], path_to_child.path[-1]
index = parent.children.index(child)
evt = TreeModelEvent(self, TreePath(path_to_child.path[:-1]),
[index], [child])
for listener in self.listeners:
listener.treeNodesChanged(evt)
def add_node(self, king_tuple, parent_path=None):
if parent_path is None:
parent_path = TreePath([self.root_king])
par = parent_path.lastPathComponent
if king_tuple is None:
name, number, last_name = par.name, par.number + 1, par.last_name
else:
name, number, last_name = king_tuple
child = King(name, number, last_name)
child.parent = par
par.children += [child]
# notify listeners, see API docs for why all this is as it is
index = par.children.index(child)
evt = TreeModelEvent(self, parent_path, [index], [child])
for listener in self.listeners:
listener.treeNodesInserted(evt)
# return path to added child node
return TreePath(list(parent_path.path) + [child])
def remove_node(self, path_to_child):
parent, child = path_to_child.path[-2], path_to_child.path[-1]
index = parent.children.index(child)
parent.children.remove(child)
evt = TreeModelEvent(self, TreePath(path_to_child.path[:-1]),
[index], [child])
for listener in self.listeners:
listener.treeNodesRemoved(evt)
def clear_tree(self):
self.root_king.children = []
self.fire_structure_changed(TreePath([self.root_king]))
# TreeModel interface implementation:
def addTreeModelListener(self, listener):
self.listeners += [listener]
def removeTreeModelListener(self, listener):
self.listeners.remove(listener)
def getChild(self, parent, index):
return parent.children[index]
def getChildCount(self, node):
return len(node.children)
def getIndexOfChild(self, parent, child):
return parent.children.index(child) # TODO: missing should return -1
def getRoot(self):
return self.root_king
def isLeaf(self, node):
return len(node.children) == 0
def valueForPathChanged(self, path, new_vals):
old_king = path.lastPathComponent
old_vals = (old_king.name, old_king.number, old_king.last_name)
if old_vals != new_vals: # notify only if value is actually changed
old_king.name, old_king.number, old_king.last_name = new_vals
self.fire_changed(path)
# this is our custom JTree
class KingdomTree(JTree):
def __init__(self, root_kings):
tree_model = KingdomModel(root_kings)
# tree_model will be exposed by self.model (which is self.getModel())
super(KingdomTree, self).__init__(tree_model)
self.expand()
self.selectionModel.\
selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
ToolTipManager.sharedInstance().registerComponent(self)
# we'll hide the real root to fake having multiple root kings
self.showsRootHandles = True
self.rootVisible = False
# make nodes editable and register our custom cell editor and renderer
self.editable = True
# change these to any 16x16 image you have
node_icon = ImageIcon("res/icons/folder.png", "node")
leaf_icon = ImageIcon("res/icons/elem.png", "leaf")
renderer = MyTreeCellRenderer(node_icon, leaf_icon)
editor = MyTreeCellEditor(self, renderer)
self.setCellEditor(editor)
self.setCellRenderer(renderer)
# overridden to properly format King as string
def convertValueToText(self, node, is_selected,
is_expanded, is_leaf, row, has_focus):
ends = { 1: 'st', 2: 'nd', 3: 'rd' } # incomplete but you get the idea
return '{name} {n}{th} {last}'.format(name=node.name,
n=node.number,
th=ends.get(node.number, 'th'),
last=node.last_name)
def add_node_to_selected(self, king_tuple=None):
return self.add_node(king_tuple, self.selectionPath)
def add_node(self, king_tuple, parent_path=None, scroll_to_added=True):
child_path = self.model.add_node(king_tuple, parent_path)
if self.rowCount == 0:
# for some reason new nodes don't show if the tree is empty (e.g.
# after calling clear_tree) as if our hidden root was collapsed but
# it can't be expanded while it's hidden, so we unhide and hide it
self.rootVisible = True
self.expandRow(0)
self.rootVisible = False
if scroll_to_added:
self.scrollPathToVisible(child_path)
return child_path
def remove_selected_node(self):
sel_path = self.selectionPath
if sel_path is None:
Toolkit.getDefaultToolkit().beep()
return
self.model.remove_node(sel_path)
def clear_tree(self):
self.model.clear_tree()
def increase_selected_node(self):
self.model.increase_node(self.selectionPath)
def expand(self, path=None):
if path:
self.expandPath(path)
else:
# expand all nodes (rowCount keeps changing, can't use for+range)
i = 0
while i < self.rowCount:
self.expandRow(i)
i += 1
# passive listener that listens for changes and logs them to textpane
class MyTreeModelListener(TreeModelListener):
def __init__(self, textpane):
self.textpane = textpane
def treeNodesChanged(self, e):
self.textpane.text += 'Nodes CHANGED:\n'
self.textpane.text += '{0}\n\n'.format(list(e.children))
def treeNodesInserted(self, e):
self.textpane.text += 'Nodes INSERTED:\n'
self.textpane.text += '{0}\n\n'.format(list(e.children))
def treeNodesRemoved(self, e):
self.textpane.text += 'Nodes REMOVED:\n'
self.textpane.text += '{0}\n\n'.format(list(e.children))
def treeStructureChanged(self, e):
self.textpane.text += 'Tree STRUCTURE CHANGED\n'
self.textpane.text += 'root: {0}\n\n'.format(e.path[0])
# main demo app panel
class Demo(JPanel):
def __init__(self):
super(Demo, self).__init__(BorderLayout())
self.preferredSize = Dimension(900, 700)
splitpane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT)
self.add(splitpane)
left = JPanel(BorderLayout())
right = JPanel(BorderLayout())
splitpane.leftComponent = left
splitpane.rightComponent = right
# left pane content (buttons and tree):
kingdom_tree = KingdomTree(self.create_fake_kings())
def add_node(e):
kingdom_tree.add_node_to_selected()
def remove_selected_node(e):
kingdom_tree.remove_selected_node()
def clear_tree(e):
kingdom_tree.clear_tree()
def increase_number(e):
kingdom_tree.increase_selected_node()
add_button = JButton("Add")
add_button.actionPerformed = add_node
remove_button = JButton("Remove")
remove_button.actionPerformed = remove_selected_node
clear_button = JButton("Clear")
clear_button.actionPerformed = clear_tree
inc_button = JButton("King++")
inc_button.actionPerformed = increase_number
buttons = JPanel(GridLayout(0, 4))
buttons.add(add_button)
buttons.add(remove_button)
buttons.add(clear_button)
buttons.add(inc_button)
tree_scrollpane = JScrollPane(kingdom_tree)
left.add(buttons, BorderLayout.PAGE_START)
left.add(tree_scrollpane, BorderLayout.CENTER)
# right pane content (log text pane):
log_pane = JEditorPane()
log_pane.editable = False
log_scrollpane = JScrollPane(log_pane)
right.add(log_scrollpane)
def log_selection_change(e):
king = e.path.lastPathComponent
log_pane.text += 'Selection changed:\n'
log_pane.text += 'name: {0}, number:{1}, last name: {2}\n'.format(
king.name, king.number, king.last_name)
log_pane.text += 'successors: {0}\n\n'.format(
len(king.children) if king.children else 'no')
kingdom_tree.valueChanged = log_selection_change
log_listener = MyTreeModelListener(log_pane)
kingdom_tree.model.addTreeModelListener(log_listener)
@staticmethod
def create_fake_kings():
data = [
("Jack", 1, "The Great", [
("Mike", 1, "Jackson", [
("Mickey", 2, "Jackson", []),
("Minnie", 1, "Jackson", []),
]),
("Jack", 2, "The Sub-par", [
("Jack", 3, "The Impostor", []),
("Stewart", 1, "Little", []),
]),
]),
("George", 2, "The Great", [
("Blake", 1, "Jackson", [
("Goofy", 2, "Jackson", []),
("Foobar", 1, "Jackson", []),
]),
("Eugene", 2, "The Sub-par", [
("Ralph", 3, "The Impostor", []),
("Zoidberg", 1, "Little", []),
]),
])
]
def link(data_tuple, parent_king=None):
name, number, last_name, children = data_tuple
k = King(name, number, last_name)
k.parent = parent_king
k.children = [link(ch, k) for ch in children]
return k
return [link(t) for t in data]
# these two go in your utils class:
class Runnable(java.lang.Runnable):
def __init__(self, func, *args, **kwargs):
"""Python wrapper for Java Runnable"""
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
assert SwingUtilities.isEventDispatchThread(), "not on EDT"
self.func(*self.args, **self.kwargs)
def invokeLater(func):
"""Decorator for running functions on Swing's Event Dispatch Thread"""
def wrapped(*args, **kwargs):
SwingUtilities.invokeLater(Runnable(func, *args, **kwargs))
return wrapped
@invokeLater
def run_demo():
frame = JFrame("Tree of Kings")
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
frame.contentPane = Demo()
frame.pack()
frame.visible = True
if __name__ == '__main__':
run_demo()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment