Last active
April 27, 2021 15:17
-
-
Save JokerMartini/4a78b3c5db1dff8b7ed8 to your computer and use it in GitHub Desktop.
PySide + Python: Demonstrates a way to handle a hierarchy of nodes along with instancing.
This file contains 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
# Imports | |
# ------------------------------------------------------------------------------ | |
import sys | |
import uuid | |
import json | |
from random import randint | |
from PySide import QtCore, QtGui | |
NODES = [] | |
HIERARCHY = {} | |
INSTANCED_NODES = [] | |
FONTS = None | |
# Fonts | |
# ------------------------------------------------------------------------------ | |
class Fonts(object): | |
def __init__(self): | |
self._fonts = {} | |
def add_font(self, name="", bold=False, italic=False): | |
font = QtGui.QFont() | |
font.setBold(bold) | |
font.setItalic(italic) | |
self._fonts.update({name: font}) | |
def get_font(self, name): | |
return self._fonts.get(name) | |
# Class Object | |
# ------------------------------------------------------------------------------ | |
class Person(): | |
def __init__(self, name="", age=0): | |
self.name = name | |
self.uid = str( uuid.uuid4( ) ) | |
self.age = (randint(0,20)) | |
NODES.append( self ) | |
# Custom QTreeWidgetItem | |
# ------------------------------------------------------------------------------ | |
class CustomTreeNode( QtGui.QTreeWidgetItem ): | |
def __init__( self, parent, person ): | |
super( CustomTreeNode, self ).__init__( parent ) | |
self.person = person | |
@property | |
def person(self): | |
return self._person | |
@person.setter | |
def person(self, value): | |
self._person = value | |
self.setText( 0, self.person.name ) | |
def update(self): | |
if self.person: | |
self.setText(0, self.person.name) | |
# Custom QTreeWidgetItem | |
# ------------------------------------------------------------------------------ | |
class CustomTreeWidget( QtGui.QTreeWidget ): | |
def __init__(self, parent=None): | |
QtGui.QTreeWidget.__init__(self, parent) | |
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) | |
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) | |
self.setItemsExpandable(True) | |
self.setAnimated(True) | |
self.setDragEnabled(True) | |
self.setDropIndicatorShown(True) | |
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove) | |
self.setAlternatingRowColors(True) | |
self._hierarchy = {} | |
self._instances = {} | |
# signals | |
self.itemExpanded.connect(self.on_item_expanded) | |
self.itemCollapsed.connect(self.on_item_collapsed) | |
self.itemSelectionChanged.connect(self.on_selection_changed) | |
def dropEvent(self, event): | |
results = super( CustomTreeWidget, self ).dropEvent( event ) | |
print "Get Hierarhcy" | |
self.get_node_hierarchy() | |
return results | |
def set_headers(self, headers=[]): | |
self.setColumnCount( len(headers) ) | |
self.setHeaderLabels( headers ) | |
def keyPressEvent(self, event): | |
if (event.key() == QtCore.Qt.Key_Escape and | |
event.modifiers() == QtCore.Qt.NoModifier): | |
self.clearSelection() | |
else: | |
QtGui.QTreeWidget.keyPressEvent(self, event) | |
def mousePressEvent(self, event): | |
item = self.itemAt(event.pos()) | |
if item is None: | |
self.clearSelection() | |
QtGui.QTreeWidget.mousePressEvent(self, event) | |
def on_item_expanded(self): | |
key_mod = QtGui.QApplication.keyboardModifiers() | |
if key_mod == QtCore.Qt.ControlModifier: | |
self.expandAll() | |
def on_item_collapsed(self): | |
key_mod = QtGui.QApplication.keyboardModifiers() | |
if key_mod == QtCore.Qt.ControlModifier: | |
self.collapseAll() | |
def get_tree_nodes(self, root=None): | |
results = [] | |
for i in range( root.childCount() ): | |
item = root.child(i) | |
results.append( item ) | |
results.extend ( self.get_tree_nodes(root=item) ) | |
return results | |
def get_instances(self): | |
nodes = self.get_tree_nodes( root=self.invisibleRootItem() ) | |
self._instances = {} | |
for n in nodes: | |
if n.person.uid not in self._instances.keys(): | |
self._instances.update( {n.person.uid : [n]} ) | |
else: | |
self._instances[n.person.uid].append( n ) | |
def accent_instances(self): | |
print "highilight" | |
self.get_instances() | |
# reset accent highlighting | |
for key, values in self._instances.items(): | |
for item in values: | |
item.setFont(0, FONTS.get_font("regular")) | |
# bold instances | |
uids = [x.person.uid for x in self.selectedItems()] | |
for uid in uids: | |
items = self._instances.get(uid, []) | |
if len(items) > 1: | |
for item in items: | |
item.setFont(0, FONTS.get_font("accent")) | |
def on_selection_changed(self): | |
print "selection changed?" | |
self.accent_instances() | |
def build_node_hierarchy(self, root=None): | |
results = [] | |
for i in range( root.childCount() ): | |
item = root.child(i) | |
data = {} | |
data["node"] = item.person.uid | |
data["children"] = [self.build_node_hierarchy(root=item)] | |
results.append( data ) | |
return results | |
def get_node_hierarchy(self): | |
global HIERARCHY | |
self._hierarchy = self.build_node_hierarchy( root=self.invisibleRootItem() ) | |
HIERARCHY = self._hierarchy | |
return self._hierarchy | |
# UI | |
# ------------------------------------------------------------------------------ | |
class ExampleWidget(QtGui.QWidget): | |
def __init__( self, parent=None ): | |
super(ExampleWidget, self).__init__() | |
self.init_fonts() | |
self.initUI() | |
def init_fonts(self): | |
global FONTS | |
FONTS = Fonts() | |
FONTS.add_font(name="accent", bold=True, italic=True) | |
FONTS.add_font(name="regular", bold=False, italic=False) | |
def initUI(self): | |
# widgets | |
self.treewidget = CustomTreeWidget() | |
self.treewidget.set_headers( ["items"] ) | |
self.btn_add_tree_nodes = QtGui.QPushButton("Add") | |
self.btn_instance_tree_nodes = QtGui.QPushButton("Instance") | |
self.btn_delete_tree_nodes = QtGui.QPushButton("Delete") | |
# layout | |
self.mainLayout = QtGui.QGridLayout(self) | |
self.mainLayout.addWidget(self.btn_add_tree_nodes, 0,0) | |
self.mainLayout.addWidget(self.btn_instance_tree_nodes, 0,1) | |
self.mainLayout.addWidget(self.btn_delete_tree_nodes, 0,2) | |
self.mainLayout.addWidget(self.treewidget, 1,0,1,3) | |
# signals | |
self.btn_add_tree_nodes.clicked.connect(self.add_tree_nodes_clicked) | |
self.btn_delete_tree_nodes.clicked.connect(self.delete_tree_nodes_clicked) | |
self.btn_instance_tree_nodes.clicked.connect(self.instance_tree_nodes_clicked) | |
self.treewidget.itemDoubleClicked.connect(self.item_doubleclicked) | |
# self.treewidget.itemSelectionChanged.connect(self.item_selection_changed) | |
# display | |
self.resize(200, 400) | |
self.show() | |
self.center_window(self, True) | |
# Functions | |
# -------------------------------------------------------------------------- | |
def closeEvent(self, event): | |
print "saving" | |
self.save_nodes() | |
def showEvent(self, event): | |
print "loading" | |
def serialize_node(self, node): | |
data = {} | |
if node: | |
# base attributes | |
data["class_name"] = node.__class__.__name__ | |
data["name"] = node.name | |
data["age"] = node.age | |
data["uid"] = node.uid | |
return data | |
def save_nodes(self): | |
print "Saving serialized nodes..." | |
data = {} | |
items = [self.serialize_node(x) for x in NODES] | |
data.update( {"nodes" : items} ) | |
data.update( {"hierarchy" : HIERARCHY} ) | |
json.dump(data, open("test.json",'w'), indent=4) | |
def add_tree_nodes_clicked(self): | |
roots = [self.treewidget] if self.treewidget.selectedItems() == [] else self.treewidget.selectedItems() | |
text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter your name:') | |
if ok: | |
print "Created new!" | |
for root in roots: | |
person = Person( text ) | |
node = CustomTreeNode( root, person ) | |
node.setExpanded(True) | |
self.treewidget.get_node_hierarchy() | |
self.treewidget.itemSelectionChanged.emit() | |
def instance_tree_nodes_clicked(self): | |
print "Instanced!" | |
roots = self.treewidget.selectedItems() | |
for root in roots: | |
parent = self.treewidget if not root.parent() else root.parent() | |
node = CustomTreeNode( parent, root.person ) | |
node.setExpanded(True) | |
self.treewidget.get_node_hierarchy() | |
self.treewidget.itemSelectionChanged.emit() | |
def delete_tree_nodes_clicked(self): | |
global NODES | |
root = self.treewidget.invisibleRootItem() | |
# delete treewidget items from gui | |
for item in self.treewidget.selectedItems(): | |
(item.parent() or root).removeChild(item) | |
# collect all uids used in GUI | |
uids = [x.person.uid for x in self.treewidget.get_tree_nodes( root=self.treewidget.invisibleRootItem() )] | |
for n in reversed(NODES): | |
if n.uid not in uids: | |
NODES.remove(n) | |
self.treewidget.get_node_hierarchy() | |
def update_nodes(self, root=None): | |
for i in range( root.childCount() ): | |
item = root.child(i) | |
item.update() | |
self.update_nodes(root=item) | |
def item_doubleclicked(self): | |
item = self.treewidget.selectedItems()[0] | |
text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter your name:') | |
if ok: | |
item.person.name = text | |
item.update() | |
# update - update all GUI items who are instances of the modified item | |
self.update_nodes( root=self.treewidget.invisibleRootItem() ) | |
def center_window(self, window , cursor=False): | |
qr = window.frameGeometry() | |
cp = QtGui.QCursor.pos() if cursor else QtGui.QDesktopWidget().availableGeometry().center() | |
qr.moveCenter(cp) | |
window.move(qr.topLeft()) | |
# __name__ | |
# ------------------------------------------------------------------------------ | |
if __name__ == '__main__': | |
app = QtGui.QApplication(sys.argv) | |
ex = ExampleWidget() | |
sys.exit(app.exec_()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment