diff --git a/kivyblocks/blocks.py b/kivyblocks/blocks.py index da4d020..3391e0e 100755 --- a/kivyblocks/blocks.py +++ b/kivyblocks/blocks.py @@ -33,6 +33,7 @@ from .orientationlayout import OrientationLayout from .threadcall import HttpClient from .register import * from .script import Script, set_script_env +from .mixin import filter_mixin, get_mixins, mixin_behaviors class WidgetNotFoundById(Exception): def __init__(self, id): @@ -237,18 +238,22 @@ class Blocks(EventDispatcher): return self.dictValueExpr(obj,localnamespace) return obj - def w_build(self,desc) -> Widget: + def w_build(self, desc:dict) -> Widget: widgetClass = desc.get('widgettype',None) if not widgetClass: Logger.info("Block: w_build(), desc invalid", desc) raise Exception(desc) widgetClass = desc['widgettype'] - opts = self.valueExpr(desc.get('options',{}).copy()) + opts_org = self.valueExpr(desc.get('options',{}).copy()) + opts = filter_mixin(opts_org) + bopts = get_mixins(opts_org) widget = None try: klass = Factory.get(widgetClass) widget = klass(**opts) + mixin_behaviors(widget, bopts) + except Exception as e: print('Error:',widgetClass,'contructon error') print_exc() diff --git a/kivyblocks/mixin.py b/kivyblocks/mixin.py new file mode 100644 index 0000000..67a2707 --- /dev/null +++ b/kivyblocks/mixin.py @@ -0,0 +1,27 @@ + +from kivy.factory import Factory + +_mix_ins = {} + +def filter_mixin(kwargs): + return {k:v for k, v in kwargs.items() if not k.endswith('behavior')} + +def get_mixins(kwargs): + return {k:v for k, v in kwargs.items() if k.endswith('behavior')} + +def register_mixin(name, klass): + _mix_ins[name] = klass + +def mix_other(inst, other): + for k,v in other.__dict__.items(): + setattr(inst, k, v) + +def mixin_behaviors(inst, kwargs): + behaviors_kw = get_mixins(kwargs) + for name, dic in behaviors_kw.items(): + klass = _mix_ins.get(name) + if klass: + other = Factory.Blocks().widgetBuild(dic) + if other: + mix_other(inst, other) + diff --git a/kivyblocks/modalbehavior.py b/kivyblocks/modalbehavior.py new file mode 100644 index 0000000..a0db041 --- /dev/null +++ b/kivyblocks/modalbehavior.py @@ -0,0 +1,118 @@ + +from kivy.properties import DictProperty, BooleanProperty, \ + StringProperty, OptionProperty, \ + NumericProperty + +from mixin import register_mixin + +class ModalBehavior(object): + auto_open = BooleanProperty(True) + auto_dismiss = BooleanProperty(True) + target = StringProperty(None) + show_time = NumericProperty(0) + position = OptionProperty('cc',options=['tl', 'tc', 'tr', + 'cl', 'cc', 'cr', + 'bl', 'bc', 'br']) + def __init__(self, **kw): + self.time_task = None + self._target = None + super(Modal, self).__init__(**kw) + self.set_size_position() + self._target.bind(size=self.set_size_position) + self.register_event_type('on_open') + self.register_event_type('on_pre_open') + self.register_event_type('on_pre_dismiss') + self.register_event_type('on_dismiss') + self.bind(on_touch_down=self.on_touchdown) + if self.auto_open: + self.open() + + def on_touchdown(self, o, touch): + if not self.collide_point(touch.x, touch.y): + if self.auto_dismiss: + self.dispatch('on_pre_dismiss') + self.dismiss() + return True + + def on_target(self, *args): + self._target = None + self.set_target(self) + + def set_target(self): + if self._target is None: + if self.target is None: + w = Window + else: + w = Factory.Blocks.getWidgetById(self.target, from_target=self) + if w is None: + w = Window + self._target = w + + def set_size_position(self, *args): + self.set_target() + if self.size_hint_x: + self.width = self.size_hint_x * self._target.width + if self.size_hint_y: + self.height = self.size_hint_y * self._target.height + self.set_modal_position() + + def set_modal_position(self): + self.set_target() + xn = self.position[1] + yn = self.position[0] + x, y = 0, 0 + if xn == 'c': + x = (self._target.width - self.width) / 2 + elif xn == 'r': + x = self._target.width - self.width + if x < 0: + x = 0 + if yn == 'c': + y = (self._target.height - self.height) / 2 + elif yn == 't': + y = self._target.height - self.height + if y < 0: + y = 0 + if self._target == Window: + self.pos = x, y + else: + self.pos = self._target.pos[0] + x, self._target.pos[1] + y + + def open(self): + if self.time_task is not None: + self.time_task.cancel() + self.time_task = None + if self.show_time > 0: + self.time_task = \ + Clock.schedule_once(self.dismiss, self.show_time) + if self.parent: + self.parent.remove_widget(self) + self.dispatch('on_pre_open') + Window.add_widget(self) + self.dispatch('on_open') + if self._target != Window: + self._target.disabled = True + + def dismiss(self, *args): + if self.time_task: + self.time_task.cancel() + self.time_task = None + self.dispatch('on_pre_dismiss') + self.dispatch('on_dismiss') + Window.remove_widget(self) + if self._target != Window: + self._target.enabled = False + + def on_open(self, *args): + pass + + def on_dismiss(self, *args): + pass + + def on_pre_open(self, *args): + pass + + def on_pre_dismiss(self, *args): + pass + +register_mixin('modalbehavior', ModalBehavior) diff --git a/kivyblocks/utils.py b/kivyblocks/utils.py index 8f45d4a..1413fd8 100644 --- a/kivyblocks/utils.py +++ b/kivyblocks/utils.py @@ -1,235 +1,241 @@ -import os -from traceback import print_exc -from traceback import print_exc -from kivy.app import App -from kivy.logger import Logger -from appPublic.jsonConfig import getConfig -from kivy.uix.popup import Popup -from kivy.uix.button import Button -from kivy.uix.label import Label -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.modalview import ModalView -from kivy.uix.image import AsyncImage -from appPublic.dictObject import DictObject - -from .kivysize import KivySizes - -class NeedLogin(Exception): - pass - -class InsufficientPrivilege(Exception): - pass - -class HTTPError(Exception): - def __init__(self,resp_code): - self.resp_code = resp_code - Exception.__init__(self) - - def __expr__(self): - return f'Exception:return code={self.resp_code}' - - def __str__(self): - return f'Exception:return code={self.resp_code}' - -alert_widget= None - -def kwarg_pop(obj, kw): - keys = [k for k in kw.keys()] - for k in keys: - if hasattr(obj, k): - setattr(obj, k, kw.pop(k)) - -def SUPER(klass, obj, kw): - keys = [ k for k in kw.keys() ] - dic = { k:kw.pop(k) for k in keys if hasattr(obj, k) } - super(klass, obj).__init__(**kw) - for k,v in dic.items(): - try: - setattr(obj, k, v) - except Exception as e: - print(f'obj={obj}, setattr(obj, "{k}","{v}") error') - print_exc() - raise e - -def blockImage(name): - p = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(p,'imgs',name) - -def loaded(widget): - widget.loadingwidget.dismiss() - # widget.remove_widget(widget.loadingwidget) - del widget.loadingwidget - widget.loadingwidget = None - -def loading(parent): - fp = os.path.join(os.path.dirname(__file__),'imgs','loading1.gif') - image = AsyncImage(source=blockImage('loading1.gif'), \ - width=CSize(2), height=CSize(2), - size_hint=(None,None)) - view = ModalView(auto_dismiss=False) - view.add_widget(image) - view.center = parent.center - parent.loadingwidget = view - # parent.add_widget(view) - view.open() - return view - -def set_widget_width(self, width): - if width <= 1: - self.size_hint_x = width - else: - self.size_hint_x = None - self.width = width - -def set_widget_height(self, height): - if height <= 1: - self.size_hint_y = height - else: - self.size_hint_y = None - self.height = height - -def setSizeOptions(desc,kw): - """ - desc's width, and height to setup a widget's size options - if width or height is not set, kw add not thing - if width or height <= 1, using present rate of size - else use CSize to tims width or height - """ - if not isinstance(kw, DictObject): - kw = DictObject(**kw) - - width = desc.get('width',0) - if width > 1.01: - kw.width = CSize(width) - kw.size_hint_x = None - elif width > 0.00: - kw.size_hint_x = width - - height = desc.get('height',0) - if height > 1.01: - kw.height = CSize(height) - kw.size_hint_y = None - elif height > 0.00: - kw.size_hint_y = height - return kw - -def alert(text,title='alert'): - global alert_widget - def close_alert(obj): - alert_widget.dismiss() - - charsize = CSize(1) - if alert_widget is None: - bl = BoxLayout(orientation='horizontal') - msg = Label(font_size=charsize) - bl.add_widget(msg) - button = Button(size_hint_y=None,height=1.4*charsize,font_size=charsize,text='OK') - button.bind(on_press=close_alert) - bl.add_widget(button) - alert_widget = Popup(content=bl, size_hint=(0.9,0.6)) - alert_widget.msg_widget = msg - alert_widget.msg_widget.text = str(text) - alert_widget.title = str(title) - alert_widget.open() - -def StrConvert(s): - if not s.startswith('py::'): - return s - s = s[4:] - try: - ns = {} - exec('_n_=' + s,globals(),ns) - return ns['_n_'] - except Exception as e: - print('----e=',e,'------------s=',s) - return s - -def ArrayConvert(a): - s = [] - for i in a: - s.append(JDConvert(i)) - return s - -def DictConvert(dic): - d = {} - for k,v in dic.items(): - if k == 'widgettype': - d[k] = v - else: - d[k] = JDConvert(v) - return d - -def JDConvert(dic): - nd = {} - if type(dic) == type(''): - return StrConvert(dic) - if type(dic) == type([]): - return ArrayConvert(dic) - if type(dic) == type({}): - return DictConvert(dic) - return dic - -def getWidgetById(w,id): - if id[0] == '/': - app = App.get_running_ap() - if not hasattr('ids'): - return None - return app.ids.get(id[1:]) - if id in ['self', '.' ]: - return w - if not hasattr(w,'ids'): - return None - return w.ids.get(id) - -def CSize(x,y=None,name=None): - ks = KivySizes() - return ks.CSize(x,y=y,name=name) - -def screenSize(): - ks = KivySizes() - return ks.getScreenSize() - -def screenPhysicalSize(): - ks = KivySizes() - return ks.getScreenPhysicalSize() - -def isHandHold(): - ks = KivySizes() - return ks.isHandHold() - -def absurl(url,parent): - if parent is None: - parent = '' - config = getConfig() - if url.startswith('http://'): - return url - if url.startswith('https://'): - return url - if url.startswith('file:///'): - return url - if url.startswith('/'): - return config.uihome + url - if url.startswith(config.uihome): - return url - if parent == '': - print('url=',url) - raise Exception('related url(%s) need a parent url' % url) - - if parent.startswith(config.uihome): - parent = parent[len(config.uihome):] - paths = parent.split('/') - paths.pop() - for i in url.split('/'): - if i in [ '.', '' ]: - continue - if i == '..': - if len(paths) > 1: - paths.pop() - continue - paths.append(i) - return config.uihome + '/'.join(paths) - -def show_widget_info(w, tag='DEBUG'): - id = getattr(w, 'widget_id', 'null') - msg=f"""{tag}:size_hint={w.size_hint},size={w.size},pos={w.pos},widget_id={id},{w}""" - Logger.info(msg) +import os +from traceback import print_exc +from traceback import print_exc +from kivy.app import App +from kivy.logger import Logger +from appPublic.jsonConfig import getConfig +from kivy.uix.popup import Popup +from kivy.uix.button import Button +from kivy.uix.label import Label +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.modalview import ModalView +from kivy.uix.image import AsyncImage +from appPublic.dictObject import DictObject +from .mixin import filter_mixin, get_mixins, mixin_behaviors + +from .kivysize import KivySizes + +class NeedLogin(Exception): + pass + +class InsufficientPrivilege(Exception): + pass + +class HTTPError(Exception): + def __init__(self,resp_code): + self.resp_code = resp_code + Exception.__init__(self) + + def __expr__(self): + return f'Exception:return code={self.resp_code}' + + def __str__(self): + return f'Exception:return code={self.resp_code}' + +alert_widget= None + +def kwarg_pop(obj, kw): + keys = [k for k in kw.keys()] + for k in keys: + if hasattr(obj, k): + setattr(obj, k, kw.pop(k)) + +def SUPER(klass, obj, kw): + mixins_kw = get_mixins(kw) + kw = filter_mixin(kw) + keys = [ k for k in kw.keys() ] + dic = { k:kw.pop(k) for k in keys if hasattr(obj, k) } + super(klass, obj).__init__(**kw) + for k,v in dic.items(): + try: + if v is not None: + setattr(obj, k, v) + except Exception as e: + print(f'obj={obj}, setattr(obj, "{k}","{v}") error') + print_exc() + raise e + + mixin_behaviors(obj, mixins_kw) + +def blockImage(name): + p = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(p,'imgs',name) + +def loaded(widget): + widget.loadingwidget.dismiss() + # widget.remove_widget(widget.loadingwidget) + del widget.loadingwidget + widget.loadingwidget = None + +def loading(parent): + fp = os.path.join(os.path.dirname(__file__),'imgs','loading1.gif') + image = AsyncImage(source=blockImage('loading1.gif'), \ + width=CSize(2), height=CSize(2), + size_hint=(None,None)) + view = ModalView(auto_dismiss=False) + view.add_widget(image) + view.center = parent.center + parent.loadingwidget = view + # parent.add_widget(view) + view.open() + return view + +def set_widget_width(self, width): + if width <= 1: + self.size_hint_x = width + else: + self.size_hint_x = None + self.width = width + +def set_widget_height(self, height): + if height <= 1: + self.size_hint_y = height + else: + self.size_hint_y = None + self.height = height + +def setSizeOptions(desc,kw): + """ + desc's width, and height to setup a widget's size options + if width or height is not set, kw add not thing + if width or height <= 1, using present rate of size + else use CSize to tims width or height + """ + if not isinstance(kw, DictObject): + kw = DictObject(**kw) + + width = desc.get('width',0) + if width > 1.01: + kw.width = CSize(width) + kw.size_hint_x = None + elif width > 0.00: + kw.size_hint_x = width + + height = desc.get('height',0) + if height > 1.01: + kw.height = CSize(height) + kw.size_hint_y = None + elif height > 0.00: + kw.size_hint_y = height + return kw + +def alert(text,title='alert'): + global alert_widget + def close_alert(obj): + alert_widget.dismiss() + + charsize = CSize(1) + if alert_widget is None: + bl = BoxLayout(orientation='horizontal') + msg = Label(font_size=charsize) + bl.add_widget(msg) + button = Button(size_hint_y=None,height=1.4*charsize,font_size=charsize,text='OK') + button.bind(on_press=close_alert) + bl.add_widget(button) + alert_widget = Popup(content=bl, size_hint=(0.9,0.6)) + alert_widget.msg_widget = msg + alert_widget.msg_widget.text = str(text) + alert_widget.title = str(title) + alert_widget.open() + +def StrConvert(s): + if not s.startswith('py::'): + return s + s = s[4:] + try: + ns = {} + exec('_n_=' + s,globals(),ns) + return ns['_n_'] + except Exception as e: + print('----e=',e,'------------s=',s) + return s + +def ArrayConvert(a): + s = [] + for i in a: + s.append(JDConvert(i)) + return s + +def DictConvert(dic): + d = {} + for k,v in dic.items(): + if k == 'widgettype': + d[k] = v + else: + d[k] = JDConvert(v) + return d + +def JDConvert(dic): + nd = {} + if type(dic) == type(''): + return StrConvert(dic) + if type(dic) == type([]): + return ArrayConvert(dic) + if type(dic) == type({}): + return DictConvert(dic) + return dic + +def getWidgetById(w,id): + if id[0] == '/': + app = App.get_running_ap() + if not hasattr('ids'): + return None + return app.ids.get(id[1:]) + if id in ['self', '.' ]: + return w + if not hasattr(w,'ids'): + return None + return w.ids.get(id) + +def CSize(x,y=None,name=None): + ks = KivySizes() + return ks.CSize(x,y=y,name=name) + +def screenSize(): + ks = KivySizes() + return ks.getScreenSize() + +def screenPhysicalSize(): + ks = KivySizes() + return ks.getScreenPhysicalSize() + +def isHandHold(): + ks = KivySizes() + return ks.isHandHold() + +def absurl(url,parent): + if parent is None: + parent = '' + config = getConfig() + if url.startswith('http://'): + return url + if url.startswith('https://'): + return url + if url.startswith('file:///'): + return url + if url.startswith('/'): + return config.uihome + url + if url.startswith(config.uihome): + return url + if parent == '': + print('url=',url) + raise Exception('related url(%s) need a parent url' % url) + + if parent.startswith(config.uihome): + parent = parent[len(config.uihome):] + paths = parent.split('/') + paths.pop() + for i in url.split('/'): + if i in [ '.', '' ]: + continue + if i == '..': + if len(paths) > 1: + paths.pop() + continue + paths.append(i) + return config.uihome + '/'.join(paths) + +def show_widget_info(w, tag='DEBUG'): + id = getattr(w, 'widget_id', 'null') + msg=f"""{tag}:size_hint={w.size_hint},size={w.size},pos={w.pos},widget_id={id},{w}""" + Logger.info(msg) diff --git a/test/script/scripts/index.ui b/test/script/scripts/index.ui new file mode 100644 index 0000000..55d3c13 --- /dev/null +++ b/test/script/scripts/index.ui @@ -0,0 +1,30 @@ +{ + "id":"mainctrl", + "widgettype":"PagePanel", + "options":{ + "bar_autohide":true, + "i18n":true, + "bar_size":2, + "bar_at":"top", + "csscls":"mainctrl", + "singlepage":true, + "left_menu":{ + "widgettype":"urlwidget", + "options":{ + "params":{ + "userid":"testuser" + }, + "url":"{{entire_url('mainmenu.ui')}}" + } + } + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('t.ui')}}" + } + } + ] +} + diff --git a/test/script/scripts/input_m3u.ui b/test/script/scripts/input_m3u.ui new file mode 100644 index 0000000..e41c1df --- /dev/null +++ b/test/script/scripts/input_m3u.ui @@ -0,0 +1,106 @@ +{ + "id":"m3u_modal", + "widgettype":"Modal", + "options":{ + "auto_open":true, + "position":"tl", + "csscls":"modal_panel", + "size_hint":[0.7,0.7], + "content":{ + "id":"modal_content", + "widgettype":"VBox", + "options":{ + }, + "subwidgets":[ + { + "id":"m3u_form", + "widgettype":"Form", + "options":{ + "cols":1, + "labelwidth":0.3, + "inputheight":2, + "fields":[ + { + "name":"url", + "label":"Url", + "required":true, + "datatype":"str" + }, + { + "name":"name", + "label":"Name", + "datatype":"str", + "required":true, + "uitype":"str" + } + ] + } + }, + { + "widgettype":"HBox", + "options":{}, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"total channels", + "size_hint_x":null, + "width":"py::CSize(14)", + "wrap":true, + "halign":"right" + } + }, + { + "id":"tc_w", + "widgettype":"Text", + "options":{ + "text":"", + "size_hint_x":null, + "width":"py::CSize(14)", + "wrap":true, + "halign":"left" + } + } + ] + }, + { + "widgettype":"HBox", + "options":{}, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"good channels", + "size_hint_x":null, + "width":"py::CSize(14)", + "wrap":true, + "halign":"right" + } + }, + { + "id":"gc_w", + "widgettype":"Text", + "options":{ + "text":"", + "size_hint_x":null, + "width":"py::CSize(14)", + "wrap":true, + "halign":"left" + } + } + ] + } + ] + } + }, + "binds":[ + { + "wid":"m3u_form", + "event":"on_submit", + "actiontype":"script", + "datawidget":"m3u_form", + "target":"app", + "script":"self.open_m3u(**kwargs)" + } + ] +} diff --git a/test/script/scripts/input_url.ui b/test/script/scripts/input_url.ui new file mode 100644 index 0000000..c261617 --- /dev/null +++ b/test/script/scripts/input_url.ui @@ -0,0 +1,64 @@ +{ + "widgettype":"Modal", + "options":{ + "auto_open":true, + "position":"tl", + "csscls":"modal_panel", + "size_hint":[0.7,0.7], + "content":{ + "id":"url_form", + "widgettype":"Form", + "options":{ + "cols":1, + "labelwidth":0.3, + "inputheight":2, + "fields":[ + { + "name":"url", + "label":"Url", + "required":true, + "datatype":"str" + }, + { + "name":"urltype", + "label":"urltype", + "datatype":"str", + "uitype":"code", + "uiparams":{ + "valueField":"ut", + "textField":"ut", + "data":[ + { + "ut":"video" + }, + { + "ut":"m3u" + }, + { + "ut":"ebook" + } + ] + } + } + ] + } + } + }, + "binds":[ + { + "wid":"url_form", + "event":"on_submit", + "actiontype":"script", + "target":"self", + "script":"self.dismiss()" + }, + { + "wid":"url_form", + "event":"on_submit", + "actiontype":"script", + "datawidget":"url_form", + "target":"app", + "script":"self.open_url(**kwargs)" + } + ] +} diff --git a/test/script/scripts/mainmenu.ui b/test/script/scripts/mainmenu.ui new file mode 100644 index 0000000..1d9b7a0 --- /dev/null +++ b/test/script/scripts/mainmenu.ui @@ -0,0 +1,20 @@ +{ + "widgettype":"Menu", + "options":{ + "single_expand":true, + "idField":"id", + "font_size_c":1, + "node_height":2.5, + "textField":"label", + "bgcolor":[0.2,0.2,0.2,1], + "target":"root.mainctrl", + "data":[ + { + "id":"setting", + "label":"Setting", + "icon":"{{entire_url('/imgs/setting.png')}}", + "url":"{{entire_url('t.ui')}}" + } + ] + } +} diff --git a/test/script/scripts/python.ui b/test/script/scripts/python.ui new file mode 100644 index 0000000..8522e0b --- /dev/null +++ b/test/script/scripts/python.ui @@ -0,0 +1,5 @@ +{ + "widgettype":"PyInterpreter", + "options":{ + } +} diff --git a/test/script/scripts/t.m3u8 b/test/script/scripts/t.m3u8 new file mode 100644 index 0000000..87aa3d8 --- /dev/null +++ b/test/script/scripts/t.m3u8 @@ -0,0 +1,4 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-STREAM-INF:BANDWIDTH=898222,RESOLUTION=1047x576,CODECS="avc1.77.30,mp4a.40.2" +chunklist_w891111828.m3u8 diff --git a/test/script/scripts/t.ui b/test/script/scripts/t.ui new file mode 100644 index 0000000..02e457d --- /dev/null +++ b/test/script/scripts/t.ui @@ -0,0 +1,6 @@ +{ + "widgettype":"Text", + "options":{ + "text":"To Be Implements" + } +} diff --git a/test/script/scripts/welcome.ui b/test/script/scripts/welcome.ui new file mode 100644 index 0000000..00384b4 --- /dev/null +++ b/test/script/scripts/welcome.ui @@ -0,0 +1,7 @@ +{ + "widgettype":"Text", + "options":{ + "text":"Welcome to Kivyblocks", + "i18n":true + } +}