kivyblocks/kivyblocks/tree.py
2021-07-13 16:14:31 +08:00

644 lines
16 KiB
Python

import traceback
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ListProperty, BooleanProperty
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.image import AsyncImage
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 appPublic.registerfunction import getRegisterFunctionByName
from .baseWidget import PressableLabel, Text, HBox, VBox
from .color_definitions import getColors
from .widget_css import WidgetCSS
from .utils import alert,absurl
from .toggleitems import PressableBox
from .threadcall import HttpClient
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):
open_status = BooleanProperty(False)
color = ListProperty([1,0,0,1])
def __init__(self, color=[1,0,0,1],**kw):
# super(NodeTrigger, self).__init__(**kw)
EmptyBox.__init__(self, **kw)
ButtonBehavior.__init__(self)
self.countPoints()
self.color = color
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, *args):
self.open_status = False if self.open_status else True
# self.showOpenStatus()
def on_open_status(self, *largs):
self.showOpenStatus()
def on_color(self, *largs):
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.color)
Triangle(points=points)
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.
}
"""
super(TreeNode, self).__init__(orientation='vertical',size_hint_y=None)
self.treeObj = tree
self.parentNode = parentNode
self.node_level = 0
if self.parentNode:
self.node_level = self.parentNode.node_level + 1
self.data = data
self.content = None
self.children_open = False
self.nodes = []
self.initChildren = False
self.node_box = HBox(spacing=CSize(0.5),
csscls=self.treeObj.normal_css,
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(color=self.node_box.fgcolor)
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 sibling(self):
if self.parentNode is None:
return [ i for i in self.treeObj.nodes if i != self ]
return [ i for i in self.parentNode.nodes if i != self ]
def selected(self):
self.content.selected()
def unselected(self):
self.content.unselected()
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._trigger_layout()
if self.parentNode:
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.treeObj.rowheight
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:
return
if self.data.children == []:
self.treeObj.getUrlData(self.addChildren,self.data)
return
if len(self.nodes) == 0:
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.treeObj.single_expand:
if self.parentNode:
self.parentNode.collapseall()
else:
self.treeObj.collapseall()
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",
"normal_css",
"selected_css",
"single_expand",
"select_leaf_only",
"bgcolor",
"checkbox",
"multplecheck",
"idField",
"textFiled",
"data" # array of {children:{},...}
}
"""
class Tree(WidgetCSS, ScrollWidget):
data = ListProperty([])
def __init__(self,
url=None,
params={},
single_expand=False,
select_leaf_only=True,
bgcolor=[0.2,0.2,0.2,1],
normal_css="default",
row_height=2,
selected_css="selected",
checkbox=False,
multiplecheck=False,
idField='id',
textField='text',
data=None,
**options):
self.url = url
self.params = params
self.data = data
self.single_expand=single_expand
self.select_leaf_only=select_leaf_only
self.row_height=row_height
self.rowheight = CSize(self.row_height)
self.bgcolor = bgcolor
self.normal_css = normal_css
self.selected_css = selected_css
self.checkbox = checkbox
self.multiplecheck = multiplecheck
self.idField = idField
self.textField = textField
print('options=',options)
super(Tree, self).__init__(**options)
self.options = DictObject(**options)
self.nodes = []
self.initflag = False
self.selected_node = None
self.buildTree()
self.bind(size=self.onSize,pos=self.onSize)
self.register_event_type('on_press')
def on_press(self,*larg):
pass
def select_row(self, node):
self.unselect_row()
self.selected_node = node
node.selected()
if not self.select_leaf_only or not node.hasChildren_nodes:
self.dispatch('on_press', node)
if node.hasChildren_nodes:
node.toggleChildren(node)
node.trigger.on_press()
if node.children_open and self.single_expand:
for n in node.sibling():
if n.hasChildren_nodes and n.children_open:
n.collapse()
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):
for n in self.nodes:
n.setMinWidth(self.width)
def setNodeKlass(self,klass):
self.NodeKlass = klass
def getUrlData(self,kv=None):
hc = HttpClient()
params = self.params.copy()
if kv:
for k,v in kv.items():
if k in params.keys():
params[k] = v
params['id'] = kv[self.idField]
config = getConfig()
app = App.get_running_app()
url = app.absurl(self.url)
Logger.info('Tree: getUrlData(),url=%s',url)
hc.get(url,params=params,
callback=self.dataLoaded,
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 on_data(self, *largs):
self.addNodes()
def buildTree(self,kv=None):
if not hasattr(self,'NodeKlass'):
self.NodeKlass = TreeNode
if self.url:
return self.getUrlData(kv)
if len(self.data) > 0:
self.addNodes()
def dataLoaded(self, o, d):
self.data = d
def addNodes(self):
Logger.info("Tree: addNodes()")
for c in self.data:
self.addNode(c)
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 ]
def collapseall(self):
for n in self.nodes:
n.collapse()
def expandall(self):
for n in self.nodes:
n.expand()
class TextContent(PressableLabel):
def __init__(self,**options):
PressableLabel.__init__(self,**options)
class TextTreeNode(TreeNode):
def buildContent(self):
txt = self.data.get(self.treeObj.textField,
self.data.get(self.treeObj.idField,'defaulttext'))
self.content = TextContent(csscls=self.treeObj.normal_css,
text=txt,
size_hint_y=None,
font_size=CSize(1),
text_size=CSize(len(txt)-1,1),
halign='left',
height=CSize(2))
self.content.bind(on_press=self.onPress)
return
def onPress(self,o,v=None):
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,nodeklass=TextTreeNode,**options):
self.NodeKlass = nodeklass
super().__init__(**options)
class MenuTreeNode(TextTreeNode):
def on_size(self, *args):
self.node_box.width = self.width
self.content.width = self.node_box.width - self.trigger.width
self.text_widget.width = self.content.width - CSize(1)
def selected(self):
self.content.active(True)
def unselected(self):
self.content.active(False)
def buildContent(self):
txt = self.data.get(self.treeObj.textField,
self.data.get(self.treeObj.idField,'defaulttext'))
icon = self.data.get('icon')
self.content = PressableBox(normal_css=self.treeObj.normal_css,
actived_css=self.treeObj.selected_css,
size_hint_y=None,
height=self.treeObj.rowheight
)
self.content.orientation = 'horizontal'
if icon:
img = AsyncImage(source=icon,
size_hint=(None,None),
height=CSize(1),
width=CSize(1))
else:
img = EmptyBox()
self.content.add_widget(img)
textw = Text( text=txt,
size_hint=(None,None),
font_size=CSize(1),
text_size=CSize(len(txt)-1,1),
halign='left',
wrap=True,
height=CSize(2),
width=self.width)
self.content.add_widget(textw)
self.text_widget = textw
self.content.bind(on_press=self.onPress)
print('****',self.content,'w,y=',self.content.width,
self.content.height, 'tree width=', self.treeObj.width)
return
"""
{
...
target:kkk,
data:[
{
id:
text:
icon:
url:
rfname
children
}
]
}
"""
class MenuTree(TextTree):
def __init__(self, target=None, **kw):
self.target = target
super().__init__(nodeklass=MenuTreeNode, **kw)
def select_row(self, node):
super().select_row(node)
if not node.hasChildren_nodes:
self.menucall(node)
def menucall(self, node):
data = {}
dw = node.data.get('datawidget')
if dw:
data_widget = Factory.Blocks.getWidgetById(dw)
if data_widget:
vn = node.data.get('datamethod', 'getValue')
if hasattr(data_widget, vn):
f = getattr(data_widget, vn)
data = f()
if not isinstance(data, dict):
data = {}
url = node.data.get('url')
target = Factory.Blocks.getWidgetById(node.data.get('target',self.target),self)
if url:
params = node.data.get('params',{})
params.update(data)
blocks = Factory.Blocks()
desc = {
"widgettype":"urlwidget",
"options":{
"url":url,
"params":params
}
}
print('menucall(), params=', params)
w = blocks.widgetBuild(desc)
if w and target:
target.add_widget(w)
return
rfname = node.data.get('rfname')
if rfname:
f = getRegisterFunctionByName(rfname)
if f:
f(self, **data)
return
script = node.data.get('script')
if script:
target_name = node.data.get('target', self.target)
target = Factory.Blocks.getWidgetById(target_name, self)
data.update({'self':target})
if target:
eval(script,data)
return
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):
data = {
'target':self.treeObj.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