Created
September 19, 2013 11:32
-
-
Save bgr/6622108 to your computer and use it in GitHub Desktop.
Jython JTree demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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