kivyblocks/kivyblocks/tree.py
2020-07-12 23:02:07 +08:00

465 lines
12 KiB
Python

import traceback
from kivy.app import App
from kivy.core.window import Window
from kivy.logger import Logger
from kivy.graphics import Color, Rectangle, Triangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.uix.button import ButtonBehavior
from kivyblocks.widgetExt import ScrollWidget
from kivyblocks.utils import CSize
from appPublic.dictObject import DictObject
from appPublic.jsonConfig import getConfig
from .baseWidget import PressableLabel
from .color_definitions import getColors
from .bgcolorbehavior import BGColorBehavior
from .utils import alert,absurl
class EmptyBox(Label):
def __init__(self,size_cnt=1):
self.size_cnt = size_cnt
siz=CSize(self.size_cnt)
super().__init__(text=' ',font_size=siz,
text_size=(siz,siz),
halign='center',
size_hint=[None,None],size=[siz,siz])
def onSize(self,o=None,v=None):
return
class NodeTrigger(ButtonBehavior, EmptyBox):
def __init__(self, size_cnt=1,open_status=False,color=[1,0,0,1]):
super().__init__(size_cnt=size_cnt)
self.open_status = open_status
self.line_color = color
self.countPoints()
self.showOpenStatus()
self.bind(size=self.onSize,pos=self.onSize)
def countPoints(self):
self.open_points = [0,self.height,
self.width,self.height,
self.width/2,0]
self.close_points = [0,self.height,
0,0, self.width,
self.height/2]
def pointsShift(self,points):
x = [ p + self.pos[0] if i % 2 == 0 else p + self.pos[1] \
for i,p in enumerate(points) ]
return x
def onSize(self,o=None,v=None):
self.countPoints()
self.showOpenStatus()
def on_press(self):
self.open_status = False if self.open_status else True
self.showOpenStatus()
def showOpenStatus(self):
points = self.close_points
if self.open_status:
points = self.open_points
self.canvas.clear()
points = self.pointsShift(points)
with self.canvas:
Color(*self.line_color)
Triangle(points=points)
# print('pos=',self.pos,'size=',self.size)
class TreeNode(BoxLayout):
def __init__(self,data,tree=None,
parentNode=None,
):
"""
base TreeNode
data:{
if children miss, it is a leaf node,if children is a empty array, is mean need load at it is first expanded.
}
"""
BoxLayout.__init__(self,orientation='vertical',size_hint=(None,None))
self.treeObj = tree
self.parentNode = parentNode
self.data = data
self.content = None
self.children_open = False
self.nodes = []
self.initChildren = False
self.node_box = BoxLayout(orientation='horizontal',
spacing=CSize(0.5),
size_hint_y=None)
self.node_box1 = BoxLayout(orientation='horizontal')
n = data.get('children')
self.hasChildren_nodes = True if not n is None else False
self.children_nodes = n
self.children_loaded = False
self.hasChildren_nodes = data.get('children')
if self.hasChildren_nodes:
self.trigger = NodeTrigger()
self.trigger.bind(on_press=self.toggleChildren)
self.buildChildrenContainer()
else:
self.trigger = EmptyBox()
self.node_box.add_widget(self.trigger)
self.add_widget(self.node_box)
self.addContent()
self.setSize()
def setSize(self):
if self.children_open:
self.height = self.node_box.height + self.node_box1.height
else:
self.height = self.node_box.height
self.width = self.node_box.width
self._trigger_layout()
if self.parentNode:
self.parentNode._trigger_layout()
self.parentNode._trigger_layout()
else:
self.treeObj._inner._trigger_layout()
def buildChildrenContainer(self):
self.node_box1.add_widget(EmptyBox())
self.children_box = BoxLayout(orientation='vertical')
self.node_box1.add_widget(self.children_box)
def setMinWidth(self,width):
if self.node_box.width < width:
self.node_box.width = width
if self.node_box1.width < width:
self.node_box1.width = width
for n in self.nodes:
n.setMinWidth(width)
def buildContent(self):
pass
def addContent(self):
self.buildContent()
self.node_box.add_widget(self.content)
self.node_box.height = self.content.height
self.node_box.width = self.trigger.width + \
self.content.width
Logger.info('Tree : content=(%d,%d),box=(%d,%d)', \
self.content.width,self.content.height,
self.node_box.width,self.node_box.height)
def buildChildren(self):
if self.initChildren == True:
return
if self.data.children is None:
Logger.info('Tree : is a leaf node')
return
if self.data.children == []:
self.treeObj.getUrlData(self.addChildren,self.data)
return
if len(self.nodes) == 0:
Logger.info('Tree : add subnodes')
self.addChildren(self.data.children)
else:
Logger.info('Tree : not need add subnodes')
self.initChildren = True
def childrenSizeChange(self,tn,v):
h = 0
w = 0
for n in self.nodes:
h += n.height
w = max(w,n.width)
self.children_box.height = h
self.children_box.width = w
self.node_box1.height = self.children_box.height
self.node_box1.width = self.trigger.width + self.children_box.width
self.setSize()
def addChildren(self,children):
self.data.children = children
self.children_box.height = 0
self.children_box.width = 0
for c in children:
options = {}
options['tree'] = self.treeObj
options['parentNode'] = self
options['data'] = c
tn = self.treeObj.NodeKlass(**options)
tn.bind(size=self.childrenSizeChange)
self.nodes.append(tn)
self.children_box.add_widget(tn)
self.children_box.height += tn.height
self.children_box.width = max(self.children_box.width,tn.width)
self.node_box1.height = self.children_box.height
self.node_box1.width = self.trigger.width + self.children_box.width
def addNodeByData(self,data):
options = {}
options['tree'] = self.treeObj
options['parentNode'] = self
if isinstance(data,dict):
options['data'] = DictObject(**data)
else:
options['data'] = data
w = self.treeObj.NodeKlass(**options)
self.nodes.append(w)
self.children_box.add_widget(w)
self.childrenSizeChange(None,None)
def addNode(self,node):
node.parentNode = self
self.nodes.append(node)
self.children_box.add_widget(node)
self.childrenSizeChange(None,None)
def deleteNode(self,node):
self.children_box.remove_widget(node)
self.nodes = [ i for i in self.nodes if i != node ]
self.childrenSizeChange(None,None)
def delete(self):
parentNode = self.parentNode if self.parentNode else self.treeObj
parentNode.deleteNode(self)
def expand(self):
if self.children_open:
return
self.toggleChildren(None)
def collapse(self):
if not self.children_open:
return
self.toggleChildren(None)
def expandall(self):
if not self.children_open:
self.toggleChildren(None)
for n in self.nodes:
n.expandall()
def collapseall(self):
if self.children_open:
self.toggleChildren(None)
for n in self.nodes:
n.collapseall(None)
def moveNode(self,node,newParent=None):
old_parent = node.parent
if old_parent == None:
old_parent = node.treeObj
old_parent.deleteNode(node)
if newParent == None:
newParent = node.treeObj
newParent.addNode(node)
def toggleChildren(self,o):
self.treeObj.unselect_row()
self.children_open = True if not self.children_open else False
if self.children_open:
self.buildChildren()
self.add_widget(self.node_box1)
else:
self.remove_widget(self.node_box1)
self.setSize()
# when a widget remove from its parent, the get_parent_window()
# will return a None
# w1 = self.children_box.get_parent_window()
# Logger.info('Tree :get_parent_window() return=%s',str(type(w1)))
"""
tree options
{
"url":
"params",
"color_level",
"checkbox",
"multplecheck",
"idField",
"textFiled",
"data" # array of {children:{},...}
}
"""
class Tree(BGColorBehavior, ScrollWidget):
def __init__(self,**options):
self.color_level = options.get('color_level',0)
ScrollWidget.__init__(self)
BGColorBehavior.__init__(self,color_level=self.color_level)
self.options = DictObject(**options)
self.nodes = []
self.initflag = False
self.selected_node = None
self.bind(size=self.onSize,pos=self.onSize)
def select_row(self, node):
self.unselect_row()
self.selected_node = node
node.selected()
def unselect_row(self):
if self.selected_node:
Logger.info('selected node unselected')
self.selected_node.unselected()
self.selected_node = None
def onSize(self,o,v=None):
if not self.initflag:
self.initflag = True
self.buildTree()
for n in self.nodes:
n.setMinWidth(self.width)
def setNodeKlass(self,klass):
self.NodeKlass = klass
def getUrlData(self,callback,kv=None):
hc = App.get_running_app().hc
params = self.options.get('params',{}).copy()
if not kv is None:
for k,v in kv.items():
if k in params.keys():
params[k] = v
params['id'] = kv[self.options.idField]
config = getConfig()
url = absurl(self.options.url,None)
Logger.info('Tree: getUrlData(),url=%s',url)
hc.get(url,params=params,
callback=callback,
errback=self.showError)
def showError(self,o,e):
traceback.print_exc()
Logger.info('Tree: showError() o=%s,e=%s',o,e)
alert(e,title='error')
def buildTree(self,kv=None):
if not hasattr(self,'NodeKlass'):
self.NodeKlass = TreeNode
if self.options.url:
return self.getUrlData(self.dataLoaded,kv)
data = self.options.data or []
Logger.info("Tree : buildTree,data=%s",data)
self.dataLoaded(None,data)
self.color, self.bgcolor = getColors(self.color_level)
def dataLoaded(self,o,d):
Logger.info("Tree: dataLoaded,d=%s",d)
self.data = d
self.addNodes()
def addNodes(self):
Logger.info("Tree: addNodes()")
for c in self.data:
options = {}
options['tree'] = self
options['parentNode'] = None
options['data'] = DictObject(**c)
w = self.NodeKlass(**options)
self.nodes.append(w)
self.add_widget(w)
Logger.info('Tree : node=%s',type(w))
def addNode(self,data,parentNode=None):
options = {}
options['tree'] = self
options['parentNode'] = None
options['data'] = DictObject(**data)
w = self.NodeKlass(**options)
self.nodes.append(w)
self.add_widget(w)
def deleteNode(self,node):
if self.selected_node == node:
self.selected_node = None
self.remove_widget(node)
self.nodes = [ i for i in self.nodes if i != node ]
class TextContent(PressableLabel):
def __init__(self,color_level=0,**options):
PressableLabel.__init__(self,color_level=color_level,**options)
class TextTreeNode(TreeNode):
def buildContent(self):
txt = self.data.get(self.treeObj.options.textField,
self.data.get(self.treeObj.options.idField,'defaulttext'))
self.content = TextContent(color_level=self.treeObj.color_level,
text=txt,
size_hint=(None,None),
font_size=CSize(1),
text_size=CSize(len(txt)-1,1),
halign='left',
height=CSize(2),
width=CSize(len(txt)))
self.content.color, self.content.bgcolor = getColors(self.treeObj.color_level,
selected=False)
self.content.bind(on_press=self.onPress)
return
def onPress(self,o,v=None):
if self.hasChildren_nodes:
self.toggleChildren(self)
self.trigger.on_press()
return
Logger.info('select the leaf node')
self.treeObj.select_row(self)
def selected(self):
if hasattr(self.content,'selected'):
self.content.selected()
def unselected(self):
if hasattr(self.content,'unselected'):
self.content.unselected()
class TextTree(Tree):
def __init__(self,**options):
self.NodeKlass = TextTreeNode
super().__init__(**options)
self.register_event_type('on_press')
def select_row(self, node):
super().select_row(node)
self.dispatch('on_press',node.data)
def on_press(self,o,v=None):
print('TextTree():on_press(),o=',o,'v=',v)
class PopupMenu(BoxLayout):
def __init__(self,target,menudesc,**opts):
self.target = target
self.menudesc = menudesc
BoxLayout.__init__(self, size_hint=(0.5,0.5), **opts)
self.menu_tree = TextTree(**menudesc)
self.add_widget(self.menu_tree)
self.menu_tree.bind(on_press=self.onMenuItemTouch)
self.register_event_type('on_press')
def on_press(self,o,v=None):
Logger.info('PopupMenu: on_press fired')
def onMenuItemTouch(self,o,d=None,v=None):
Logger.info('MenuTree: on_press fired,o=%s,d=%s,v=%s',o,d,v)
data = {
'target':self.target,
'menudata':d
}
self.dispatch('on_press',data)
self.dismiss()
def open(self):
Window.add_widget(self)
self.center = Window.center
def dismiss(self):
Window.remove_widget(self)
def on_touch_down(self,touch):
if not self.collide_point(*touch.pos):
self.dismiss()
return False
super().on_touch_down(touch)
return True