644 lines
16 KiB
Python
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
|