commit f8c06c9e8e48a15060b4454a89e262641dbee473 Author: yumoqing Date: Thu Dec 19 11:13:47 2019 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..36879b9 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# KivyBlocks +Can you ever image build a gui application like play Lego blocks? kivyblocks it try to provide programmer a tool to build a application + +## Requirement +see the [requirements.txt]('./requirements.txt') + +## Principle + +There is a blocksapp(inherited from App) contains a toppage widget to hold all the top widget create by Blocks, and the Blocks can create widgets which is specified in a dictionary data, and a HttpClient to get data or weiget spec file for web server. + +the widget specifiction file +## How to use +see the simple example below: +``` +``` + diff --git a/kivyblocks/__init__.py b/kivyblocks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivyblocks/aplayer.py b/kivyblocks/aplayer.py new file mode 100644 index 0000000..b5a4aae --- /dev/null +++ b/kivyblocks/aplayer.py @@ -0,0 +1,215 @@ +import sys +import os +from kivy.app import App +from kivy.core.audio import SoundLoader +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.progressbar import ProgressBar +from kivy.uix.label import Label +from kivy.uix.popup import Popup +from kivy.clock import Clock +from kivy.uix.filechooser import FileChooserListView +from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, \ + NumericProperty, DictProperty, OptionProperty + +class APlayer(BoxLayout): + def __init__(self,afile=None,loop=False, + can_openfile=False, + can_changevolume=False, + can_cut = False, + can_replay = False, + can_move=False): + super().__init__(orientation='vertical') + self.loop = loop + self.old_path = os.getcwd() + self.can_openfile = can_openfile + self.can_cut = can_cut + self.can_replay = can_replay + self.can_move = can_move + self.can_changevolume = can_changevolume + self._popup = None + self.pb = None + self.ap = None + if afile is None: + self.playlist = [] + else: + if type(afile) == type([]): + self.playlist = afile + else: + self.playlist = [afile] + self.curplay = -1 + self.addMenu() + if len(self.playlist) > 0: + self.curplay = 0 + self.createSound(self.playlist[self.curplay]) + + def totime(self,dur): + h = dur / 3600 + m = dur % 3600 / 60 + s = dur % 60 + return '%02d:%02d:%02d' % (h,m,s) + + def createSound(self,source): + self.ap = SoundLoader.load(source) + self.ap.bind(on_play=self.begin_play) + self.ap.bind(on_stop=self.end_play) + self.ap.play() + + def begin_play(self,obj): + if self.pb: + return + self.pb = BoxLayout(orientation='horizontal', + size_hint = (0.99,None),height=40) + self.curposition = Label(text=self.totime(self.ap.get_pos()),width=60, + size_hint_x=None) + self.curposition.align='right' + self.maxposition = Label(text=self.totime(self.ap.length), + width=60,size_hint_x=None) + self.maxposition.align = 'left' + max = int(self.ap.length*1000) + self.progressbar = ProgressBar(value=0,max=max) + self.add_widget(self.pb) + self.pb.add_widget(self.curposition) + self.pb.add_widget(self.progressbar) + self.pb.add_widget(self.maxposition) + self.pb.pos = (0,0) + Clock.schedule_interval(self.update_progressbar,1) + + def update_progressbar(self,t): + pos = self.ap.get_pos() + max = self.ap.length + self.curposition.text = self.totime(pos) + self.progressbar.value = int(pos*1000) + self.progressbar.max = int(max*1000) + self.maxposition.text = self.totime(max) + + + def end_play(self,obj): + self.curplay += 1 + if not self.loop and self.curplay >= len(self.playlist): + self.parent.remove_widget(self) + return + del self.ap + self.curplay = self.curplay % len(self.playlist) + self.createSound(self.playlist[self.curplay]) + + def addMenu(self): + self.menubar = BoxLayout(orientation='horizontal', + size_hint_y=None,height=40) + if self.can_openfile: + btn_open = Button(text='open') + btn_open.bind(on_press=self.openfile) + self.menubar.add_widget(btn_open) + if self.can_move: + btn_back = Button(text='<<') + btn_forward = Button(text='>>') + btn_forward.bind(on_press=self.moveforward) + btn_back.bind(on_press=self.moveback) + self.menubar.add_widget(btn_back) + self.menubar.add_widget(btn_forward) + if self.can_changevolume: + btn_volumeinc = Button(text='+') + btn_volumedec = Button(text='-') + btn_volumeinc.bind(on_press=self.volumeinc) + btn_volumedec.bind(on_press=self.volumedec) + self.menubar.add_widget(btn_volumedec) + self.menubar.add_widget(btn_volumeinc) + if self.can_cut: + btn_cut = Button(text='>>|') + btn_cut.bind(on_press=self.endplay) + self.menubar.add_widget(btn_cut) + if self.can_replay: + btn_replay = Button(text='replay') + btn_replay.bind(on_press=self.replay) + self.menubar.add_widget(btn_replay) + self.menubar.pos = 0,45 + self.add_widget(self.menubar) + + def openfile(self,t): + def afilter(path,filename): + aexts = [ + '.wav', + '.mp3', + '.ape', + '.flac' + + ] + for ext in aexts: + if filename.endswith(ext): + return True + return False + + if self._popup is None: + c = BoxLayout(orientation='vertical') + self.file_chooser = FileChooserListView() + self.file_chooser.filters = [afilter] + self.file_chooser.multiselect = True + self.file_chooser.path = self.old_path + #self.file_chooser.bind(on_submit=self.loadFilepath) + c.add_widget(self.file_chooser) + b = BoxLayout(size_hint_y=None,height=35) + c.add_widget(b) + #cancel = Button(text='Cancel') + #cancel.bind(on_press=self.cancelopen) + load = Button(text='load') + load.bind(on_press=self.playfile) + b.add_widget(load) + #b.add_widget(cancel) + self._popup = Popup(title='Open file',content=c,size_hint=(0.9,0.9)) + self._popup.open() + + def endplay(self,btn): + self.ap.seek(self.ap.length - 0.01 ) + + def replay(self,btn): + self.ap.seek(0) + + def volumeinc(self,btn): + self.ap.volume += 0.05 + if self.ap.volume > 1.0: + self.ap.volume = 1.0 + + def volumedec(self,btn): + self.ap.volume -= 0.05 + if self.ap.volume < 0.0: + self.ap.volume = 0.0 + + def moveback(self,btn): + f = self.ap.get_pos() + self.ap.seek(f - 2) + + def moveforward(self,btn): + f = self.ap.get_pos() + self.ap.seek(f + 2) + + def playfile(self,object): + self.playlist = [] + for f in self.file_chooser.selection: + fp = os.path.join(self.file_chooser.path,f) + self.playlist.append(fp) + if len(self.playlist) == 0: + return + self._popup.dismiss() + if self.ap is not None: + self.ap.stop() + return + self.curplay = 0 + self.createSound(self.playlist[self.curplay]) + + + +if __name__ == '__main__': + class MyApp(App): + def build(self): + af = None + if len(sys.argv) > 1: + af = sys.argv[1:] + return APlayer(afile=af, + loop=True, + can_openfile=True, + can_move = True, + can_cut=True, + can_replay=True, + can_changevolume = True + ) + MyApp().run() diff --git a/kivyblocks/baseWidget.py b/kivyblocks/baseWidget.py new file mode 100755 index 0000000..b5ff064 --- /dev/null +++ b/kivyblocks/baseWidget.py @@ -0,0 +1,86 @@ +import sys +from kivy.utils import platform +from kivy.uix.button import Button, ButtonBehavior +from kivy.uix.accordion import Accordion,AccordionItem +from kivy.uix.label import Label + +from kivy.uix.actionbar import ActionBar,ActionView,ActionPrevious,ActionItem,ActionButton +from kivy.uix.actionbar import ActionToggleButton, ActionCheck,ActionSeparator,ActionGroup + +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.pagelayout import PageLayout +from kivy.uix.scatterlayout import ScatterLayout +from kivy.uix.recycleboxlayout import RecycleBoxLayout +from kivy.uix.relativelayout import RelativeLayout +from kivy.uix.stacklayout import StackLayout + +from kivy.uix.checkbox import CheckBox +from kivy.uix.switch import Switch +from kivy.uix.textinput import TextInput +from kivy.uix.dropdown import DropDown +from kivy.uix.tabbedpanel import TabbedPanel,TabbedPanelContent,TabbedPanelHeader,TabbedPanelItem +from kivy.uix.treeview import TreeView +from kivy.uix.image import AsyncImage,Image +from kivy.uix.togglebutton import ToggleButton +from kivy.uix.scrollview import ScrollView +from kivy.uix.splitter import Splitter +from kivy.uix.spinner import Spinner +from kivy.uix.slider import Slider + +from kivy.uix.screenmanager import ScreenManager +from kivy.uix.sandbox import Sandbox +from kivy.uix.progressbar import ProgressBar +from kivy.uix.popup import Popup +from kivy.uix.modalview import ModalView +from kivy.uix.filechooser import FileChooser +from kivy.uix.effectwidget import EffectWidget +from kivy.uix.colorpicker import ColorPicker +from kivy.uix.carousel import Carousel +from kivy.uix.camera import Camera +from kivy.uix.bubble import Bubble +from kivy.uix.codeinput import CodeInput +from kivy.graphics import Color, Rectangle +from kivy.properties import ListProperty +from kivycalendar import DatePicker + +from appPublic.dictObject import DictObject + +from .widgetExt.scrollwidget import ScrollWidget +from .widgetExt.binstateimage import BinStateImage +from .widgetExt.jsoncodeinput import JsonCodeInput +from .widgetExt.inputext import FloatInput,IntegerInput, \ + StrInput,SelectInput, BoolInput, AmountInput +from .widgetExt.messager import Messager + +if platform == 'android': + from .widgetExt.phonebutton import PhoneButton + from .widgetExt.androidwebview import AWebView + +class PressableImage(ButtonBehavior,AsyncImage): + def on_press(self): + print + +class Text(Label): + bgColor = ListProperty([0.5,0.5,0.5,1]) + def __init__(self,**kw): + self.options = DictObject(**kw) + kwargs = kw.copy() + self.bind(pos=self._update,size=self._update) + if kwargs.get('bgColor'): + self.bgColor = kwargs['bgColor'] + del kwargs['bgColor'] + super().__init__(**kwargs) + self.bind(pos=self.sizeChange,size=self.sizeChange) + + def _update(self,t,v): + self.pos = t.pos + self.size = t.size + + def sizeChange(self,o,t=None): + self.canvas.before.clear() + with self.canvas.before: + Color(*self.bgColor) + Rectangle(pos=self.pos,size=self.size) diff --git a/kivyblocks/blocks.py b/kivyblocks/blocks.py new file mode 100755 index 0000000..27d7067 --- /dev/null +++ b/kivyblocks/blocks.py @@ -0,0 +1,505 @@ +import os +import sys +import codecs +import json +from traceback import print_exc + +from functools import partial + +from appPublic.dictExt import dictExtend +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath +from appPublic.dictObject import DictObject +from appPublic.Singleton import SingletonDecorator + +from kivy.config import Config +from kivy.metrics import sp,dp,mm +from kivy.core.window import WindowBase +from kivy.properties import BooleanProperty +from kivy.uix.widget import Widget +from kivy.app import App +from .baseWidget import * +from .widgetExt import Messager +from .externalwidgetmanager import ExternalWidgetManager +from .threadcall import HttpClient +from .toolbar import * +from .dg import DataGrid +from .utils import * +from .ready import WidgetReady +from .serverImageViewer import ServerImageViewer +from .vplayer import VPlayer +from .form import InputBox, Form, StrSearchForm +from .boxViewer import BoxViewer + +def showError(e): + print('error',e) + +class WidgetNotFoundById(Exception): + def __init__(self, id): + super().__init__() + self.idstr = id + + def __str__(self): + return "Widget not found by id:" + self.idstr + ':' + + def __expr__(self): + return str(self) + +class ClassMethodNotFound(Exception): + def __init__(self,k,m): + super().__init__() + self.kname = k + self.mname = m + + def __str__(self): + s = 'Method(%s) not found in class(%s)' % (self.mname, + str(self.kname.__classname__)) + return s + + def __expr__(self): + return self.__str__() + + +class NotExistsObject(Exception): + def __init__(self,name): + super().__init__() + self.name = name + + def __str__(self): + s = 'not exists widget(%s)' % self.name + return s + + def __expr__(self): + return self.__str__() + +class ArgumentError(Exception): + def __init__(self,argument,desc): + super().__init__() + self.argument = argument + self.desc = desc + + def __str__(self): + s = 'argument(%s) missed:%s' % (self.argument,self.desc) + return s + + def __expr__(self): + return self.__str__() + + +class NotRegistedWidget(Exception): + def __init__(self,name): + super().__init__() + self.widget_name = name + + def __str__(self): + s = 'not reigsted widget(%s)' % self.name + return s + + def __expr__(self): + return self.__str__() + +@SingletonDecorator +class RegistedFunction: + def __init__(self): + self.rf_list = {} + + def register(self,name,func): + self.rf_list[name] = func + + def get(self,name): + return self.rf_list.get(name) + +def registerWidget(name,widget): + globals()[name] = widget + + +class Blocks: + registedWidgets = {} + def __init__(self): + self.action_id = 0 + + def register_widget(self,name,widget): + globals()[name] = widget + + def buildAction(self,widget,desc): + self.action_id += 1 + fname = 'action%d' % self.action_id + l = { + } + body="""def %s(widget,obj=None, v=None): + jsonstr='''%s''' + print(type(widget), type(obj),v,'action():desc=',jsonstr) + desc = json.loads(jsonstr) + app = App.get_running_app() + app.blocks.uniaction(widget, desc) + print('finished') +""" % (fname, json.dumps(desc)) + exec(body,globals(),l) + f = l.get(fname,None) + if f is None: + raise Exception('None Function') + func =partial(f,widget) + return func + setattr(widget,fname,f) + return getattr(widget,fname) + + def eval(self,s,l): + g = {} + forbidens = [ + "os", + "sys", + "codecs", + "json", + ] + + for k,v in globals().copy().items(): + if k not in forbidens: + g[k] = v + + g['__builtins__'] = globals()['__builtins__'].copy() + g['__builtins__']['__import__'] = None + g['__builtins__']['__loader__'] = None + g['__builtins__']['open'] = None + + return eval(s,g,l) + + def getUrlData(self,kw,parenturl=None): + url = kw.get('url') + method = kw.get('method','GET') + params = kw.get('params',{}) + if url is None: + print('kw=',kw) + raise ArgumentError('url','getUrlData() miss a url argument') + url = absurl(url,parenturl) + if url.startswith('file://'): + filename = url[7:] + print(filename) + with codecs.open(filename,'r','utf-8') as f: + b = f.read() + dic = json.loads(b) + else: + dic = App.get_running_app().hc(url,method=method,params=params) + return dic, url + + def register(self,name : str ,base:str=None, + defaultOpts:dict={},optkeys:list=[], + properties:list=[]): + """ + """ + self.registedWidgets[name] = { + "defaultOpts":defaultOpts, + "base":base, + "properties":properties, # property: 0 name, 1 type + "optkeys":optkeys, + "infos":{} + } + + def buildRegistedWidgetInfo(self, widgettype:str): + """ + setup registed widget defaultOpts and optkeys + by merge its and it's bases widgets + """ + rw = self.registedWidgets.get(widgettype) + if not rw: + raise NotRegistedWidget(widgettype) + rw['infos']['defaultOpts'] = rw['defaultOpts'] + rw['infos']['optkeys'] = rw['optkeys'] + for b in base: + brw = self.registedWidgets.get(b) + if not brw: + raise NotRegistedWidget(b) + opts = self.getWidgetOptions(b) + opts.update(rw['defaultOpts']) + rw['infos']['defaultOpts'].update({'defaultOpts':opts}) + rw['infos']['optkeys'] = list(set(self.getWidgetOptkeys(b) + rw['infos']['optkeys'])) + return rw['infos'] + + def getWidgetOptkeys(self,widgettype:str): + """ + """ + if widgettype not in self.registedWidgets.keys(): + raise NotRegistedWidget(widgettype) + infos = self.registedWidgets.get(widgettype)['infos'] + if infos == {}: + infos = self.buildRegistedWidgetInfo(widgettype) + return infos['optkeys'] + + def getWidgetOptions(self,widgettype:str, opts:dict={}): + """ + get widget type valid options, from it's registed info + and it given argument:opts + """ + if widgettype not in self.registedWidgets.keys(): + raise NotRegistedWidget(widgettype) + infos = self.registedWidgets.get(widgettype)['infos'] + if infos == {}: + infos = self.buildRegistedWidgetInfo(widgettype) + opt = infos['defaultOpts'] + keys = opts.keys() + for k in infos['optkeys']: + if k in keys: + opt.update({k:opts[k]}) + return opt + + def getObject(self,klass:str ,bases:list=[], + properties:list=[]): + pstrs = [ "%s = %s\n" % (n,t) for n, t in properties ] + + if klass not in globals().keys(): + script=""" +class %s(%s): + %s + def __init__(self): + super().__init__() + """ % (klass, '\t'.join(pstrs),','.join(bases)) + exec(script,globals(),globals()) + + return globals().get(klass)() + + def isBaseWidget(self,widgettype): + rw = self.registedWidgets.get(widgettype) + if not rw: + raise NotRegistedWidget(widgettype) + return rw['bases'] == [] + + def getBases(self, widgettype:str): + rw = self.registedWidgets.get(widgettype) + if not rw: + raise NotRegistedWidget(widgettype) + return rw['bases'] + + def strValueExpr(self,s:str,localnamespace:dict={}): + if not s.startswith('py::'): + return s + try: + v = self.eval(s[4:],localnamespace) + return v + except Exception as e: + if s.startswith('CSize'): + print('Exception .... ',e,s) + print_exc() + return s + + def arrayValueExpr(self,arr:list,localnamespace:dict={}): + d = [] + for v in arr: + if type(v) == type(''): + d.append(self.strValueExpr(v,localnamespace)) + continue + if type(v) == type([]): + d.append(self.arrayValueExpr(v,localnamespace)) + continue + if type(v) == type({}): + d.append(self.dictValueExpr(v,localnamespace)) + continue + if type(v) == type(DictObject): + d.append(self.dictValueExpr(v,localnamespace)) + continue + d.append(v) + return d + + def dictValueExpr(self,dic:dict,localnamespace:dict={}): + d = {} + for k,v in dic.items(): + if type(v) == type(''): + d[k] = self.strValueExpr(v,localnamespace) + continue + if type(v) == type([]): + d[k] = self.arrayValueExpr(v,localnamespace) + continue + if type(v) == type({}): + d[k] = self.dictValueExpr(v,localnamespace) + continue + if type(v) == type(DictObject): + d[k] = self.dictValueExpr(v,localnamespace) + continue + d[k] = v + return d + def valueExpr(self,obj,localnamespace={}): + if type(obj) == type(''): + return self.strValueExpr(obj,localnamespace) + if type(obj) == type([]): + return self.arrayValueExpr(obj,localnamespace) + if type(obj) in [ type({}), type(DictObject) ]: + return self.dictValueExpr(obj,localnamespace) + return obj + + def __build(self,desc:dict,ancestor=None): + def checkReady(w,o): + print('checkReady():w=',w,'o=',o) + w.children_ready[o] = True + if all(w.children_ready.values()): + w.ready = True + widgetClass = desc.get('widgettype',None) + if not widgetClass: + print("__build(), desc invalid", desc) + raise Exception(desc) + + widgetClass = desc['widgettype'] + opts = desc.get('options',{}) + klass = globals().get(widgetClass) + if not klass: + raise NotExistsObject(widgetClass) + widget = klass(**opts) + if desc.get('parenturl'): + widget.parenturl = desc.get('parenturl') + ancestor = widget + + if desc.get('id'): + myid = desc.get('id') + holder = ancestor + if ancestor == widget: + app = App.get_running_app() + holder = app.root + if not hasattr(holder,'widget_ids'): + setattr(holder,'widget_ids',{}) + holder.widget_ids[myid] = widget + + widget.build_desc = desc + self.build_rest(widget,desc,ancestor) + # f = partial(self.build_rest,widget,desc,ancestor) + # event = Clock.schedule_once(f,0) + return widget + + def build_rest(self, widget,desc,ancestor,t=None): + for sw in desc.get('subwidgets',[]): + w = self.widgetBuild(sw, ancestor=ancestor) + if w is None: + continue + w.parenturl = widget.parenturl + widget.add_widget(w) + if hasattr(widget,'addChild'): + widget.addChild(w) + for b in desc.get('binds',[]): + self.buildBind(widget,b) + if hasattr(widget, 'built'): + widget.built() + + def buildBind(self,widget,desc): + body=""" + + """ + wid = desc.get('wid','self') + w = self.getWidgetByIdPath(widget,wid) + event = desc.get('event') + if event is None: + return + f = self.buildAction(widget,desc) + w.bind(**{event:f}) + + def uniaction(self,widget,desc): + acttype = desc.get('actiontype') + if acttype=='urlwidget': + return self.urlwidgetAction(widget,desc) + if acttype == 'registedfunction': + return self.registedfunctionAction(widget,desc) + if acttype == 'script': + return self.scriptAction(widget, desc) + if acttype == 'method': + return self.methodAction(widget, desc) + alert(msg="actiontype invalid",title=acttype) + + def urlwidgetAction(self,widget,desc): + target = self.getWidgetByIdPath(widget, desc.get('target','self')) + add_mode = desc.get('mode','replace') + opts = desc.get('options') + d = self.getActionData(widget,desc) + p = opts.get('params',{}) + p.update(d) + opts['params'] = p + d = { + 'widgettype' : 'urlwidget' + } + d['options'] = opts + x = self.widgetBuild(d,ancestor=widget) + if x is None: + return + if add_mode == 'replace': + target.clear_widgets() + target.add_widget(x) + + def getActionData(self,widget,desc): + data = {} + if desc.get('datawidget',False): + dwidget = self.getWidgetByIdPath(widget, + desc.get('datawidget')) + data = dwidget.getData() + return data + + def registedfunctionAction(self, widget, desc): + rf = RegistedFunction() + name = desc.get('rfname') + func = rf.get(name) + params = desc.get(params,{}) + d = self.getActionData(widget,desc) + params.update(d) + func(params) + + def scriptAction(self, widget, desc): + script = desc.get('script') + if script: + self.eval(script, {'self': widget}) + + def methodAction(self, widget, desc): + method = desc.get('method') + target = self.getWidgetByIdPath(widget, desc.get('target','self')) + if hasattr(target,method): + f = getattr(target, method) + kwargs = desc.get('options',{}) + d = self.getActionData(widget,desc) + kwargs.update(d) + f(**kwargs) + else: + alert('%s method not found' % method) + + def widgetBuild(self,desc,ancestor=None): + """ + desc format: + { + widgettype:, + id:widget id, + options: + subwidgets:[ + ] + binds:[ + ] + } + """ + name = desc['widgettype'] + if name == 'urlwidget': + opts = desc.get('options') + parenturl = None + if ancestor: + parenturl = ancestor.parenturl + desc, parenturl = self.getUrlData(opts,parenturl) + if not isinstance(desc,dict): + print("getUrlData() return error data",opts,parenturl) + alert(str(dict) + 'format error') + return None + desc = self.valueExpr(desc) + desc['parenturl'] = parenturl + + try: + widget = self.__build(desc,ancestor=ancestor) + return widget + except: + print('widgetBuild() Error',desc) + print_exc() + alert(str(desc)) + return None + + def getWidgetByIdPath(self,widget,path): + ids = path.split('/') + if ids[0] == '': + app = App.get_running_app() + widget = app.root + ids = ids[1:] + for id in ids: + if id == 'self': + return widget + widget = widget.widget_ids.get(id,None) + if widget is None: + print('widget not found,path=',path,'id=',id,'ids=',ids) + raise WidgetNotFoundById(id) + return widget + diff --git a/kivyblocks/blocksapp.py b/kivyblocks/blocksapp.py new file mode 100644 index 0000000..14d502e --- /dev/null +++ b/kivyblocks/blocksapp.py @@ -0,0 +1,77 @@ + +import os +import sys +import signal + +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import ProgramPath + +from kivy.config import Config +from kivy.metrics import sp,dp,mm +from kivy.core.window import WindowBase, Window +from kivy.clock import Clock + +import kivy +from kivy.resources import resource_add_path +resource_add_path(os.path.join(os.path.dirname(__file__),'./ttf')) +Config.set('kivy', 'default_font', [ + 'msgothic', + 'DroidSansFallback.ttf']) + +from kivy.app import App +# from .baseWidget import baseWidgets +# from .widgetExt import Messager +# from .externalwidgetmanager import ExternalWidgetManager +from .threadcall import HttpClient,Workers +# from .derivedWidget import buildWidget, loadUserDefinedWidget +from .utils import * +from .pagescontainer import PageContainer +from .widgetExt.messager import Messager +from .blocks import Blocks + +def signal_handler(signal, frame): + app = App.get_running_app() + app.workers.running = False + app.stop() + print('Singal handled .........') + +signal.signal(signal.SIGINT, signal_handler) + +class BlocksApp(App): + def build(self): + x = PageContainer() + self.title = 'Test Title' + self.blocks = Blocks() + config = getConfig() + self.config = config + self.workers = Workers(maxworkers=config.maxworkers or 80) + Window.bind(on_request_close=self.on_close) + self.workers.start() + self.hc = HttpClient() + WindowBase.softinput_mode='below_target' + Clock.schedule_once(self.build1) + return x + + def build1(self,t): + x = None + x = self.blocks.widgetBuild(self.config.root) + if x is None: + alert(str(self.config.root)+': cannt build widget') + return + self.root.add_widget(x) + return + + def on_close(self,o,v=None): + """ + catch the "x" button's event of window + """ + self.workers.running = False + + def on_pause(self,o,v=None): + """ + to avoid app start from beginening when user exit and reenter this app + """ + return True + + def __del__(self): + self.workers.running = False diff --git a/kivyblocks/boxViewer.py b/kivyblocks/boxViewer.py new file mode 100644 index 0000000..c64616d --- /dev/null +++ b/kivyblocks/boxViewer.py @@ -0,0 +1,106 @@ +""" +BoxViewer options: +{ + "dataloader":{ + "url", + "method", + "params" + "filter":{ + } + } + "boxwidth", + "boxheight", + "viewer" + "toolbar":{ + } +} +""" +from kivy.app import App +from kivy.utils import platform +from kivy.uix.boxlayout import BoxLayout +from .responsivelayout import VResponsiveLayout +from .toolbar import Toolbar +from .paging import Paging, RelatedLoader +from .utils import CSize + +class BoxViewer(BoxLayout): + def __init__(self, **options): + self.toolbar = None + self.parenturl = None + self.dataloader = None + self.initflag = False + BoxLayout.__init__(self, orientation='vertical') + self.selected_data = None + self.options = options + self.box_width = CSize(options['boxwidth']) + self.box_height = CSize(options['boxheight']) + self.viewContainer = VResponsiveLayout(cols=2,box_width=self.box_width) + if options.get('toolbar'): + self.toolbar = Toolbar(options['toolbar']) + lopts = options['dataloader'].copy() + lopts['options']['adder'] = self.showObject + lopts['options']['remover'] = self.viewContainer.remove_widget + lopts['options']['clearer'] = self.viewContainer.clear_widgets + lopts['options']['target'] = self + lopts['options']['locater'] = self.locater + LClass = RelatedLoader + if lopts['widgettype'] == 'paging': + LClass = Paging + + self.dataloader = LClass(**lopts['options']) + if self.toolbar: + self.add_widget(self.toolbar) + if self.dataloader.widget: + self.add_widget(self.dataloader.widget) + self.add_widget(self.viewContainer) + self.register_event_type('on_selected') + self.viewContainer.bind(size=self.resetCols, + pos=self.resetCols) + self.viewContainer.bind(on_scroll_stop=self.on_scroll_stop) + + def on_selected(self, o, v=None): + print('BoxViewer(): on_selected fired....') + + def locater(self, pos): + self.viewContainer.scroll_y = pos + + def resetCols(self,o=None, v=None): + h = 0 + if self.toolbar: + h += self.toolbar.height + if self.dataloader.widget: + h += self.dataloader.widget.height + self.viewContainer.height = self.height - h + + self.viewContainer.setCols() + if not self.initflag: + self.dataloader.loadPage(1) + self.initflag = True + + def showObject(self, rec,**kw): + blocks = App.get_running_app().blocks + options = self.options['viewer'].copy() + options['options']['record'] = rec + options['options']['ancestor'] = self + options['options']['size_hint'] = None,None + options['options']['width'] = self.box_width + options['options']['height'] = self.box_height + w = blocks.widgetBuild(options, ancestor=self) + if w is None: + return None + w.bind(on_press=self.select_record) + self.viewContainer.add_widget(w,**kw) + return w + + def on_scroll_stop(self,o,v=None): + if o.scroll_y <= 0.001: + self.dataloader.loadNextPage() + if o.scroll_y >= 0.999: + self.dataloader.loadPreviousPage() + + def select_record(self,o,v=None): + self.selected_data = o.getRecord() + self.dispatch('on_selected',o.getRecord()) + + def getData(self): + return self.selected_data diff --git a/kivyblocks/dg.py b/kivyblocks/dg.py new file mode 100644 index 0000000..00a75e5 --- /dev/null +++ b/kivyblocks/dg.py @@ -0,0 +1,414 @@ +import time +from kivy.uix.gridlayout import GridLayout +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.scrollview import ScrollView +from kivy.uix.label import Label +from kivy.uix.button import ButtonBehavior +from kivy.clock import Clock +from kivy.properties import BooleanProperty +from kivy.properties import ListProperty +from kivy.graphics import Color, Rectangle +from kivy.app import App + +from appPublic.dictObject import DictObject +from appPublic.timecost import TimeCost +from appPublic.uniqueID import getID + +from .utils import CSize, setSizeOptions, loading, loaded, absurl, alert +from .baseWidget import Text +from .widgetExt import ScrollWidget +from .paging import Paging, RelatedLoader +from .ready import WidgetReady +from .toolbar import Toolbar +from .i18n import I18nText + +class BLabel(ButtonBehavior, Text): + def __init__(self, **kw): + ButtonBehavior.__init__(self) + Text.__init__(self,**kw) + +class Cell(BoxLayout): + def __init__(self,row,desc): + """ + desc:{ + width, + datatype:fff + value: + format: + on_press: + } + """ + self.desc = desc + self.row = row + super().__init__(size_hint=(None,None), + width = self.desc['width'], + height = self.row.part.datagrid.rowHeight() + ) + if not self.row.header and self.desc.get('viewer'): + viewer = self.desc.get('viewer') + blocks = App.get_running_app().blocks + if isinstance(viewer,str): + l = self.desc.copy() + l['row'] = self.row + viewer = blocks.eval(viewer,l) + if isinstance(viewer,dict): + print('viewer = ', viewer) + w = blocks.widgetBuild(viewer,ancestor=self.row.part.datagrid) + self.add_widget(w) + return + if desc['header']: + bl = I18nText(otext=str(desc['value']), + font_size=CSize(1), + halign='left', + bgColor=self.row.part.datagrid.header_bgcolor + ) + else: + bl = BLabel(text = str(desc['value']), + font_size=CSize(1), + halign='left', + bgColor=self.row.part.datagrid.body_bgcolor + ) + self.add_widget(bl) + bl.bind(on_press=self.cell_press) + + def cell_press(self,obj): + self.row.selected() + +class Row(GridLayout): + def __init__(self,part, rowdesc,header=False,data=None): + """ + rowdesc=[ + { + width, + name + value + on_press + } + + """ + self.part = part + self.header = header + self.row_data = data + self.row_id = None + self.linewidth = 1 + self.rowdesc = rowdesc + super().__init__(cols=len(self.rowdesc),spacing=self.linewidth) + # Clock.schedule_once(self.init,0) + self.init(0) + + def init(self,t): + w = 0 + h = 0 + for c in self.rowdesc: + c['header'] = self.header + cell = Cell(self,c) + self.add_widget(cell) + w += cell.width + if cell.height > h: + h = cell.height + self.size_hint = None,None + self.width = w + self.linewidth * 2 * len(self.rowdesc) + self.height = h #self.part.datagrid.rowHeight() + + def selected(self): + if not hasattr(self,'row_data'): + return # header + print('row selected',self.row_id, self.row_data) + + self.part.datagrid.row_selected = True + self.part.datagrid.select_rowid = self.row_id + self.part.datagrid.dispatch('on_selected',self) + + +class Header(WidgetReady, ScrollWidget): + def __init__(self,part,**kw): + self.part = part + ScrollWidget.__init__(self,**kw) + WidgetReady.__init__(self) + self.init(1) + self.bind(on_scroll_stop=self.part.datagrid.on_scrollstop) + + def init(self,t): + rd = [ f.copy() for f in self.part.rowdesc ] + [ f.update({'value':self.part.fields[i]['label']}) for i,f in enumerate(rd) ] + self.header = Row(self.part,rd,header=True) + self.add_widget(self.header) + self.addChild(self.header) + self.height = self.header.height + self.built() + +class Body(ScrollWidget): + def __init__(self,part,**kw): + self.part = part + ScrollWidget.__init__(self,**kw) + self.idRow = {} + self.bind(on_scroll_stop=self.part.datagrid.on_scrollstop) + + def addRow(self,id, data,index=0): + rd = [ f.copy() for f in self.part.rowdesc ] + [ f.update({'value':data.get(f['name'])}) for f in rd ] + row = Row(self.part,rd,data=data) + row.row_id = id + self.add_widget(row,index=index) + self.idRow[id] = row + + def clearRows(self): + self.idRow = {} + self.clear_widgets() + + def delRowById(self,id): + row = self.idRow[id] + self.remove_widget(row) + del self.idRow[id] + + def getRowData(self,rowid): + return self.idRow[rowid].row_data + + def getRowHeight(self): + return self.part.datagrid.rowHeight() + +class DataGridPart(WidgetReady, BoxLayout): + def __init__(self,dg, freeze_flag, fields): + self.datagrid = dg + self.fields = fields + self.freeze_flag = freeze_flag + self.fields_width = None + BoxLayout.__init__(self, orientation='vertical') + WidgetReady.__init__(self) + # Clock.schedule_once(self.init,0.1) + self.init(0) + + def setWidth(self): + if self.freeze_flag: + self.size_hint_x = None + self.width = self.getFieldsWidth() + + def getFieldsWidth(self): + if not self.fields_width: + width = 0 + for f in self.rowdesc: + width += f['width'] + self.fields_width = width + return self.fields_width + + def init(self,t): + rd = [] + for f in self.fields: + r = f.copy() + r['width'] = CSize(f.get('width',10)) + rd.append(r) + self.rowdesc = rd + self.setWidth() + kw = {} + if self.freeze_flag: + kw['width'] = self.fields_width + kw['size_hint'] = (None,None) + else: + kw['size_hint'] = (1,None) + + kw['height'] = self.datagrid.rowHeight() + + if not self.datagrid.noheader: + self.header = Header(self,**kw) + self.add_widget(self.header) + self.addChild(self.header) + self.body = Body(self) + self.add_widget(self.body) + self.addChild(self.body) + if not self.freeze_flag: + self.body.bind(pos=self.datagrid.bodyOnSize, + size=self.datagrid.bodyOnSize) + self.built() + + def clearRows(self): + return self.body.clearRows() + + def addRow(self,id, data): + self.body.addRow(id, data) + +class DataGrid(WidgetReady, BoxLayout): + row_selected = BooleanProperty(False) + def __init__(self,**options): + kw = DictObject() + kw = setSizeOptions(options,kw) + kw.orientation = 'vertical' + WidgetReady.__init__(self) + BoxLayout.__init__(self,**kw) + self.parenturl = options.get('parenturl',None) + self.options = options + self.noheader = options.get('noheader',False) + self.header_bgcolor = options.get('header_bgcolor',[0.29,0.29,0.29,1]) + self.body_bgcolor = options.get('body_bgcolor',[0.25,0.25,0.25,1]) + self.color = options.get('color',[0.91,0.91,0.91,1]) + self.widget_ids = {} + self.row_height = None + self.selected_rowid = None + self.dataUrl = self.options.get('dataurl') + self.show_rows = 0 + self.toolbar = None + self.freeze_part = None + self.normal_part = None + self.page_rows = self.options.get('page_rows', 60) + self.params = self.options.get('params',{}) + self.total_cnt = 0 + self.max_row = 0 + self.curpage = 0 + self.loading = False + self.freeze_fields = self.getPartFields(freeze_flag=True) + self.normal_fields = self.getPartFields(freeze_flag=False) + self.createDataGridPart() + self.createToolbar() + if self.toolbar: + self.add_widget(self.toolbar) + self.addChild(self.toolbar) + + b = BoxLayout(orientation='horizontal') + if self.freeze_part: + b.add_widget(self.freeze_part) + self.addChild(self.freeze_part) + if self.normal_part: + b.add_widget(self.normal_part) + self.addChild(self.normal_part) + self.add_widget(b) + if self.options.get('paging',False): + self.loader = Paging(adder=self.addRow, + clearer=self.clearRows, + dataurl=self.dataUrl, + target=self, + params=self.params, + method=self.options.get('method','GET') + ) + self.add_widget(self.loader.widget) + else: + self.loader = RelatedLoader(adder=self.addRow, + remover=self.delRow, + locater=self.setScrollPosition, + page_rows=self.page_rows, + dataurl=self.dataUrl, + target=self, + params=self.params, + method=self.options.get('method','GET') + ) + self.on_sizeTask = None + self.register_event_type('on_selected') + self.register_event_type('on_scrollstop') + self.built() + + def setScrollPosition(self,pos): + self.normal_part.body.scroll_y = pos + if self.freeze_part: + self.freeze_part.body.scroll_y = pos + + def on_scrollstop(self,o,v=None): + if not self.noheader and o == self.normal_part.header: + self.normal_part.body.scroll_x = o.scroll_x + return + if o == self.normal_part.body: + if not self.noheader: + self.normal_part.header.scroll_x = o.scroll_x + if self.freeze_part: + self.freeze_part.body.scroll_y = o.scroll_y + if self.freeze_part and o == self.freeze_part.body: + self.normal_part.body.scroll_y = o.scroll_y + + if self.options.get('paging'): + return + if o.scroll_y <= 0.001: + self.loader.loadNextPage() + if o.scroll_y >= 0.999: + self.loader.loadPreviousPage() + + def getData(self): + if not self.row_selected: + return None + return self._getRowData(self.select_rowid) + + def _getRowData(self, rowid): + d = {} + if self.freeze_part: + d.update(self.freeze_part.body.getRowData(rowid)) + d.update(self.normal_part.body.getRowData(rowid)) + print('getData() return=',d) + return DictObject(**d) + + def bodyOnSize(self,o,s): + if self.on_sizeTask is not None: + self.on_sizeTask.cancel() + self.on_sizeTask = Clock.schedule_once(self.calculateShowRows,0.3) + + def rowHeight(self): + if not self.row_height: + self.row_height = CSize(self.options.get('row_height',1.8)) + + return self.row_height + + def calculateShowRows(self,t): + print('body height=',self.normal_part.body.height + ,'row_height=',self.rowHeight() + ) + self.show_rows = int(self.normal_part.body.height/self.rowHeight()) + self.loader.setPageRows(self.show_rows) + + def getShowRows(self): + if self.show_rows == 0: + return 60 + return self.show_rows + + def init(self,t): + if self.options.get('dataurl'): + self.loader.loadPage(1) + else: + data = self.options.get('data',[]) + if len(data) > 0: + loading(self) + for d in data: + self.addRow() + loaded(self) + + def clearRows(self): + if self.freeze_part: + self.freeze_part.body.clearRows() + self.normal_part.body.clearRows() + + def addRow(self,data, **kw): + id = getID() + if self.freeze_part: + self.freeze_part.body.addRow(id, data, **kw) + self.normal_part.body.addRow(id, data, **kw) + return id + + def delRow(self,id,**kw): + if self.freeze_part: + self.freeze_part.body.delRowById(id) + self.normal_part.body.delRowById(id) + + def createToolbar(self): + if 'toolbar' in self.options.keys(): + tb = self.options['toolbar'] + self.toolbar = Toolbar(ancestor=self,**tb) + + def on_selected(self,row): + self.selected_row = row + + def createDataGridPart(self): + self.freeze_part = None + self.normal_part = None + if self.freeze_fields: + self.freeze_part = DataGridPart(self,True, self.freeze_fields) + if self.normal_fields: + self.normal_part = DataGridPart(self, False, self.normal_fields) + + def getPartFields(self,freeze_flag:bool=False) -> list: + fs = [] + for f in self.options['fields']: + if freeze_flag: + if f.get('freeze',False): + fs.append(f) + else: + if not f.get('freeze',False): + fs.append(f) + return fs + + def on_ready(self,o,v=None): + print('***********onRadey*************') + diff --git a/kivyblocks/externalwidgetmanager.py b/kivyblocks/externalwidgetmanager.py new file mode 100755 index 0000000..14b3df4 --- /dev/null +++ b/kivyblocks/externalwidgetmanager.py @@ -0,0 +1,63 @@ +# -*- utf-8 -*- +""" +模板文件以wrdesc结尾 +界面描述文件以uidesc结尾 +""" +import os +import sys +import codecs +from kivy.app import App + +from appPublic.folderUtils import ProgramPath,listFile +from appPublic.jsonConfig import getConfig + +import json + +class ExternalWidgetManager: + def __init__(self): + #self.register_root = os.path.join(ProgramPath(),'widgets') + #self.ui_root = os.path.join(ProgramPath(),'ui') + self.ui_root = './ui' + self.register_root = './widgets' + + def loadJson(self,filepath): + with codecs.open(filepath,'r','utf-8') as f: + return json.load(f) + + def travalRegisterDesc(self,func): + return + for f in listFile(self.register_root,suffixs=['.wrdesc'],rescursive=True): + desc = self.loadJson(f) + return func(desc) + + def loadWidgetDesc(self,desc): + def text2Json(d): + j = json.loads(d) + return j + + # print(desc) + if desc.get('filename'): + path = desc.get('filename') + if path.endswith('.uidesc'): + f = FileDataLoader() + f.bind(on_dataloaded=text2Json) + if path.startswith('/'): + path = path[1:] + fn = os.path.join(self.ui_root,path) + return text2Json(f.loadData(fn)) + raise Exception('file error',path) + + if desc.get('url'): + url = desc.get('url') + headers = desc.get('headers',{}) + params = desc.get('params',{}) + app = App.get_running_app() + resp = app.hc.sync_get(url,params=params,headers=headers) + if resp.status_code == 200: + d = resp.json() + if d.get('status') == 'OK': + return d['data'] + raise Exception('ui desc loaded failed %s' % url) + + raise Exception('ui desc loaded failed' , desc) + diff --git a/kivyblocks/form.py b/kivyblocks/form.py new file mode 100644 index 0000000..356a4d9 --- /dev/null +++ b/kivyblocks/form.py @@ -0,0 +1,260 @@ +from kivy.uix.textinput import TextInput +from kivy.uix.boxlayout import BoxLayout +from kivy.core.window import Window +from .widgetExt.inputext import FloatInput,IntegerInput, \ + StrInput,SelectInput, BoolInput, AmountInput, Password +from .baseWidget import * +from kivycalendar import DatePicker +from .utils import * +from .i18n import getI18n +""" +form options +{ + "toolbar": + "dataloader":{ + "dataurl": + "params": + "method" + } + "cols":"1" + "labelwidth": + "textsize": + "inputheight": + "inline":True,False + "fields":[ + { + "name": + "label": + "datatype", + "uitype", + "uiparams", + "default", + "required" + }, + ] + "binds":[ + ] +} +""" + +uitypes = { + "string":{ + "orientation":"horizontal", + "wclass":StrInput, + }, + "number":{ + "orientation":"horizontal", + "wclass":IntegerInput, + }, + "float":{ + "orientation":"horizontal", + "wclass":FloatInput, + }, + "amount":{ + "orientation":"horizontal", + "wclass":AmountInput, + }, + "date":{ + "orientation":"horizontal", + "wclass":DatePicker, + }, + "time":{ + "orientation":"horizontal", + "wclass":StrInput, + }, + "bool":{ + "orientation":"horizontal", + "wclass":BoolInput, + }, + "code":{ + "orientation":"horizontal", + "wclass":SelectInput, + }, + "text":{ + "orientation":"vertical", + "wclass":StrInput, + "options":{ + "multiline":True, + "height":CSize(6), + } + }, + "teleno":{ + "orientation":"horizontal", + "wclass":StrInput, + }, + "email":{ + "orientation":"horizontal", + "wclass":StrInput, + }, +} +class InputBox(BoxLayout): + def __init__(self, form, **options): + self.form = form + self.options = options + self.uitype = options.get('uityoe','string') + self.uidef = uitypes[self.uitype] + orientation = self.uidef['orientation'] + width = self.form.inputwidth + height = self.form.inputheight + self.labelwidth = self.form.options['labelwidth'] + kwargs = { + "orientation":orientation, + "size_hint_y":None, + "height":CSzie(height) + } + if width <= 1: + kwargs['size_hint_x'] = width + else: + kwargs['size_hint_x'] = None + kwargs['width'] = CSize(width) + super().__init__(**kwargs) + self.initflag = False + self.bind(on_size=self.setSize, + pos=self.setSize) + self.register_event_type("on_datainput") + + def init(self): + i18n = getI18n() + if self.initflag: + return + label = self.options.get('label',self.options.get('name')) + if self.options.get('required'): + label = label + '*' + kwargs = { + "otext":label, + "font_size":CSize(1) + } + if self.labelwidth<=1: + kwargs['size_hint_x'] = self.labelwidth + else: + kwargs['size_hint_x'] = None + kwargs['width'] = self.labelwidth + + self.labeltext = I18nText(**kwargs) + self.add_widget(self.labeltext) + options = self.uidef.get('options',{}).copy() + options.update(self.options.get('uiparams',{})) + options['allow_copy'] = True + if self.options.get('tip'): + options['hint_text'] = i18n(self.options.get('tip')) + self.input_widget = self.uidef['wclass'](**options) + self.form[self.options['name']] = self.input_widget + self.add_widget(self.input_widget) + self.initflag = True + self.input_widget.bind(on_focus=self.on_focus) + if self.options.get('default'): + self.input_widget.setValue(self.options.get('default')) + + def on_focus(self,o,v): + if v: + self.old_value = o.text + else: + if self.old_value != o.text: + self.dispatch('on_datainput',o.text) + + def setSize(self,o,v=None): + self.init() + + def setValue(self,v): + self.input_widget.setValue(v) + + def getValue(self): + return self.input_widget.getValue() + +def defaultToolbar(): + return { + img_size:1.5, + text_size:0.7, + tools:[ + { + "name":"__submit", + "img_src":blockImage("clear.png"), + "label":"submit" + }, + { + "name":"__clear", + "img_src":blockImage("clear.png"), + "label":"clear" + } + ] + + } + +class Form(BoxLayout): + def __init__(self, **options): + self.options = options + BoxLayout.__init__(self, orientation='vertical') + self.widget_ids = {} + self.cols = self.options_cols = self.options['cols'] + if isHandHold() and Window.width < Window.height: + self.cols = 1 + self.inputwidth = Window.width / self.cols + self.inputheight = CSize(self.options.get('inputheight',3)) + self.toolbar = Toolbar(options.get('toolbar',defaultToolbar())) + self.fsc = VResponsiveLayout(cols=self.cols, + box_width=self.inputwidth) + self.add_widget(self.toolbar) + self.add_widget(self.fsc) + self.register_event_type('on_submit') + self.initflag = False + self.bind(size=self.on_size, + pos=self.on_size) + + def init(self): + if self.initflag: + return + self.fieldWidgets=[] + for f in self.options['fields']: + w = InputBox(self, self, **f) + self.fsc.add_widget(w) + self.fieldWidgets.append(w) + blocks = App.get_running_app().blocks + wid = getWidgetById(self,'__submit') + wid.bind(on_press=self.on_submit_button) + wid = getWidgetById(self,'__clear') + wid.bind(on_press=self.on_clear_button) + self.initflag = True + + def on_submit_button(self,o,v=None): + self.dispatch('on_submit') + + def on_clear_button(self,o,v=None): + for fw in self.fieldWidgets: + fw.clear() + + def on_size(self,o, v=None): + self.init() + +class StrSearchForm(BoxLayout): + def __init__(self,img_url=None,**options): + print('here ---------------') + self.name = options.get('name','search_string') + BoxLayout.__init__(self,orientation='horizontal',size_hint_y=None,height=CSize(3)) + self.inputwidget = TextInput( + multiline=False, + font_size=CSize(1), + size_hint_y=None, + height=CSize(3)) + self.add_widget(self.inputwidget) + imgsrc = img_url if img_url else blockImage('search.png') + self.search = PressableImage(source=imgsrc, + size_hint=(None,None), + size=CSize(3,3) + ) + self.add_widget(self.search) + self.register_event_type('on_submit') + self.search.bind(on_press=self.submit_input) + self.inputwidget.bind(on_text_validate=self.submit_input) + + def submit_input(self,o,v=None): + print('StrSearchForm():submit_input() called') + text = self.inputwidget.text + if text != '': + d = { + self.name:text + } + self.dispatch('on_submit',d) + + def on_submit(self,o,v=None): + print('StrSearchForm():on_submit fired ..........') + diff --git a/kivyblocks/httpvplayer.py b/kivyblocks/httpvplayer.py new file mode 100644 index 0000000..79da318 --- /dev/null +++ b/kivyblocks/httpvplayer.py @@ -0,0 +1,31 @@ +import requests + +headers = { + 'User-Agent': "Mozilla/5.0 (Windows NT 6.1; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/72.0.3626.109 Safari/537.36", +} + +def read_m3u8(m3u8Url): + baseurl = '/'.join(m3u8Url.split('/')[:-1]) + m3u8_res = requests.get(m3u8Url,headers) + if m3u8_res.status_code != 200: + print('URL不能正常访问', m3u8_res.status_code) + m3u8_content = m3u8_res.content.decode('utf8') + print(m3u8_content) + '''获取m3u8中的下载连接''' + media_url_list = [] + lines_list = m3u8_content.strip().split('\r\n') + if len(lines_list) < 3: + lines_list = m3u8_content.strip().split('\n') + if '#EXTM3U' not in m3u8_content: + raise BaseException('非M3U8连接') + for index,line in enumerate(lines_list): + # print(index,line) + if '#EXTINF' in line: + media_url_list.append('%s/%s' %(baseurl,lines_list[index+1])) + return media_url_list + +if __name__ == '__main__': + vfile = read_m3u8("http://pili-live-hdl.qhmywl.com/dsdtv/6d735caeaf70f8901aa00f86aefc4dde.m3u8") + print(vfile) diff --git a/kivyblocks/i18n.py b/kivyblocks/i18n.py new file mode 100644 index 0000000..d330922 --- /dev/null +++ b/kivyblocks/i18n.py @@ -0,0 +1,68 @@ +import locale +from kivy.app import App +from kivy.properties import StringProperty +from appPublic.Singleton import SingletonDecorator +from appPublic.jsonConfig import getConfig +from .baseWidget import Text + +@SingletonDecorator +class I18n: + def __init__(self,url): + self.kvlang={} + self.loadUrl = url + self.lang = locale.getdefaultlocale()[0] + self.loadI18n(self.lang) + self.i18nWidgets = [] + + def addI18nWidget(self,w): + self.i18nWidgets.append(w) + + def loadI18n(self,lang): + if not self.loadUrl: + self.kvlang[lang] = {} + return + app = App.get_running_app() + d = app.hc.get('%s?lang=%s' % (self.loadUrl,lang)) + self.kvlang[lang] = d + + def __call__(self,msg,lang=None): + if lang is None: + lang = self.lang + d = self.kvlang.get(lang,{}) + return d.get(msg,msg) + + def changeLang(self,lang): + d = self.kvlang.get(lang) + if not d: + self.loadI18n(lang) + self.lang = lang + for w in self.i18nWidgets: + w.changeLang(lang) + +def getI18n(url=None): + i18n=I18n(url) + return i18n + +class I18nText(Text): + lang=StringProperty('') + otext=StringProperty('') + + def __init__(self,**kw): + self.options = kw.copy() + otext = kw.get('otext',kw.get('text')) + if kw.get('otext'): + del kw['otext'] + super().__init__(**kw) + self.i18n = getI18n() + self.i18n.addI18nWidget(self) + self.otext = otext + + def on_otext(self,o,v=None): + self.text = self.i18n(self.otext) + + def changeLang(self,lang): + self.lang = lang + + def on_lang(self,o,lang): + self.text = self.i18n(self.otext) + diff --git a/kivyblocks/imgs/Cycle.jpg b/kivyblocks/imgs/Cycle.jpg new file mode 100644 index 0000000..40cc92c Binary files /dev/null and b/kivyblocks/imgs/Cycle.jpg differ diff --git a/kivyblocks/imgs/Mute64x64.png b/kivyblocks/imgs/Mute64x64.png new file mode 100644 index 0000000..789f4b6 Binary files /dev/null and b/kivyblocks/imgs/Mute64x64.png differ diff --git a/kivyblocks/imgs/Next64x64.png b/kivyblocks/imgs/Next64x64.png new file mode 100644 index 0000000..c75014c Binary files /dev/null and b/kivyblocks/imgs/Next64x64.png differ diff --git a/kivyblocks/imgs/Play64X64.png b/kivyblocks/imgs/Play64X64.png new file mode 100644 index 0000000..412db44 Binary files /dev/null and b/kivyblocks/imgs/Play64X64.png differ diff --git a/kivyblocks/imgs/Sound64x64.png b/kivyblocks/imgs/Sound64x64.png new file mode 100644 index 0000000..98029a1 Binary files /dev/null and b/kivyblocks/imgs/Sound64x64.png differ diff --git a/kivyblocks/imgs/bullet_arrow_down.png b/kivyblocks/imgs/bullet_arrow_down.png new file mode 100755 index 0000000..9b23c06 Binary files /dev/null and b/kivyblocks/imgs/bullet_arrow_down.png differ diff --git a/kivyblocks/imgs/bullet_arrow_right.png b/kivyblocks/imgs/bullet_arrow_right.png new file mode 100755 index 0000000..5943ebe Binary files /dev/null and b/kivyblocks/imgs/bullet_arrow_right.png differ diff --git a/kivyblocks/imgs/clear.png b/kivyblocks/imgs/clear.png new file mode 100644 index 0000000..9991e9b Binary files /dev/null and b/kivyblocks/imgs/clear.png differ diff --git a/kivyblocks/imgs/doing.gif b/kivyblocks/imgs/doing.gif new file mode 100644 index 0000000..de73ef4 Binary files /dev/null and b/kivyblocks/imgs/doing.gif differ diff --git a/kivyblocks/imgs/folder.png b/kivyblocks/imgs/folder.png new file mode 100755 index 0000000..784e8fa Binary files /dev/null and b/kivyblocks/imgs/folder.png differ diff --git a/kivyblocks/imgs/loading.gif b/kivyblocks/imgs/loading.gif new file mode 100644 index 0000000..de70d8f Binary files /dev/null and b/kivyblocks/imgs/loading.gif differ diff --git a/kivyblocks/imgs/loading1.gif b/kivyblocks/imgs/loading1.gif new file mode 100644 index 0000000..3979a68 Binary files /dev/null and b/kivyblocks/imgs/loading1.gif differ diff --git a/kivyblocks/imgs/musictrack.jpg b/kivyblocks/imgs/musictrack.jpg new file mode 100644 index 0000000..061c90a Binary files /dev/null and b/kivyblocks/imgs/musictrack.jpg differ diff --git a/kivyblocks/imgs/mute.jpg b/kivyblocks/imgs/mute.jpg new file mode 100644 index 0000000..70da9c1 Binary files /dev/null and b/kivyblocks/imgs/mute.jpg differ diff --git a/kivyblocks/imgs/next.jpg b/kivyblocks/imgs/next.jpg new file mode 100644 index 0000000..23320aa Binary files /dev/null and b/kivyblocks/imgs/next.jpg differ diff --git a/kivyblocks/imgs/origin.jpg b/kivyblocks/imgs/origin.jpg new file mode 100644 index 0000000..8f2cb6e Binary files /dev/null and b/kivyblocks/imgs/origin.jpg differ diff --git a/kivyblocks/imgs/pause.jpg b/kivyblocks/imgs/pause.jpg new file mode 100644 index 0000000..d93c235 Binary files /dev/null and b/kivyblocks/imgs/pause.jpg differ diff --git a/kivyblocks/imgs/picture_empty.png b/kivyblocks/imgs/picture_empty.png new file mode 100755 index 0000000..abd2b9b Binary files /dev/null and b/kivyblocks/imgs/picture_empty.png differ diff --git a/kivyblocks/imgs/play.jpg b/kivyblocks/imgs/play.jpg new file mode 100644 index 0000000..63232fa Binary files /dev/null and b/kivyblocks/imgs/play.jpg differ diff --git a/kivyblocks/imgs/power64x64.png b/kivyblocks/imgs/power64x64.png new file mode 100644 index 0000000..bfd234c Binary files /dev/null and b/kivyblocks/imgs/power64x64.png differ diff --git a/kivyblocks/imgs/previous.jpg b/kivyblocks/imgs/previous.jpg new file mode 100644 index 0000000..1a73c79 Binary files /dev/null and b/kivyblocks/imgs/previous.jpg differ diff --git a/kivyblocks/imgs/replay.jpg b/kivyblocks/imgs/replay.jpg new file mode 100644 index 0000000..a60a719 Binary files /dev/null and b/kivyblocks/imgs/replay.jpg differ diff --git a/kivyblocks/imgs/running.gif b/kivyblocks/imgs/running.gif new file mode 100644 index 0000000..3cf6a78 Binary files /dev/null and b/kivyblocks/imgs/running.gif differ diff --git a/kivyblocks/imgs/save.png b/kivyblocks/imgs/save.png new file mode 100644 index 0000000..6aef032 Binary files /dev/null and b/kivyblocks/imgs/save.png differ diff --git a/kivyblocks/imgs/search.png b/kivyblocks/imgs/search.png new file mode 100644 index 0000000..a72d11a Binary files /dev/null and b/kivyblocks/imgs/search.png differ diff --git a/kivyblocks/imgs/stop64x64.png b/kivyblocks/imgs/stop64x64.png new file mode 100644 index 0000000..a631c36 Binary files /dev/null and b/kivyblocks/imgs/stop64x64.png differ diff --git a/kivyblocks/imgs/volume.jpg b/kivyblocks/imgs/volume.jpg new file mode 100644 index 0000000..355aa6e Binary files /dev/null and b/kivyblocks/imgs/volume.jpg differ diff --git a/kivyblocks/kivysize.py b/kivyblocks/kivysize.py new file mode 100644 index 0000000..5f4df69 --- /dev/null +++ b/kivyblocks/kivysize.py @@ -0,0 +1,59 @@ +from kivy.app import App +from kivy.metrics import mm +from kivy.core.window import Window + +from appPublic.Singleton import SingletonDecorator +from appPublic.jsonConfig import getConfig + +@SingletonDecorator +class KivySizes: + myFontSizes = { + "smallest":1.5, + "small":2.0, + "normal":2.5, + "large":3.0, + "huge":3.5, + "hugest":4.0, + } + separatorsize = 2 + + def getFontSize(self,name=None): + config = getConfig() + if config.font_sizes: + self.myFontSizes = config.font_sizes + if name is None: + name = getConfig().font_name + x = self.myFontSizes.get(name,None) + if x == None: + x = self.myFontSizes.get('normal') + return x + + def namedSize(self,cnt=1,name=None): + return mm(cnt * self.getFontSize(name=name)) + + def unitedSize(self,x,y=None,name=None): + xr = self.namedSize(cnt=x,name=name) + if y is None: + return xr + return (xr,self.namedSize(cnt=y,name=name)) + + def CSize(self,x,y=None,name=None): + return self.unitedSize(x,y=y,name=name) + + def getScreenSize(self): + #root = App.get_running_app().root + #return root.width, root.height + return Window.width, Window.height + + def getWindowPhysicalSize(self,w): + h_phy = float(w.height) / mm(1) + w_phy = float(w.width) / mm(1) + return w_phy, h_phy + + def getScreenPhysicalSize(self): + return self.getWindowPhysicalSize(App.get_running_app().root) + return self.getWindowPhysicalSize(Window) + + def isHandHold(self): + return min(self.getScreenPhysicalSize()) <= 75.0 + diff --git a/kivyblocks/login.py b/kivyblocks/login.py new file mode 100644 index 0000000..1a6cff2 --- /dev/null +++ b/kivyblocks/login.py @@ -0,0 +1,76 @@ +from kivy.app import App +from kivy.uix.popup import Popup +from appPublic.Singleton import SingletonDecorator +from appPublic.registerfunction import RegisterFunction + +logformdesc = { + "widgettype":"Form", + "options":{ + "cols":1, + "labelwidth":0.3, + "textsize":1, + "inputheight":3, + "fields":[ + { + "name":"userid", + "label":"user name", + "datatype":"str", + "uitype":"str" + }, + { + "name":"passwd", + "label":"Password", + "datatype":"str", + "uitype":"password" + } + ] + } + "binds":[ + "wid":"self", + "event":"on_submit", + "datawidegt":"self", + "actiontype":"registedfunction", + "rfname":"setupUserInfo" + } +} + +@SingletonDecorator +class LoginForm(Popup): + def __init__(self): + super().__init__() + app = App.get_running_app() + self.content = app.blocks.widgetBuild(logformdesc) + self.title = 'login' + self.blockCalls = [] + self.open_status = False + self.content.bind(on_submit=self.on_submit) + + def needlogin(self, url,method,params,headers,callback=None,errback=None): + self.blockCalls.append([url, method, params,headers,callback,errback]) + if self.open_status: + return + self.open_status = True + self.open() + + def recall(self): + app = App.get_running_app() + for url, method, params, headers,callback, errback in self.blockCalls: + app.hc(url, method=pethod,params=params,headers=headers, + callback=callback,errback=errback) + + def close(self): + self.open_status = False + self.dismiss() + + def on_submit(self,o,v): + self.dismiss() + +def setupUserInfo(userinfo): + app = App.get_running_app() + app.setUserInfo(userinfo) + lf = LoginForm() + lf.recall() + +rf = RegisterFunction() +rf.register('setupUserInfo',setupUserInfo) + diff --git a/kivyblocks/objectViewer.py b/kivyblocks/objectViewer.py new file mode 100644 index 0000000..f10caa9 --- /dev/null +++ b/kivyblocks/objectViewer.py @@ -0,0 +1,71 @@ +from kivy.utils import platform +from .responsivelayout import VResponsiveLayout +from .utils import isHandHold +from .paging import RelatedLoader + +""" +options format +{ + box_width:optional, if set, the child width + dataurl: url to get data + page_rows:records per read + viewer:{ + widgettype:an class name to show the object + options:{ + } + }, + binds:[ + bind info + ] +} + +""" + +class ObjectViewer(VResponsiveLayout): + def __init__(self, dataurl=None, viewer={}, + page_rows=25,params={},**options): + super().__init__(**options) + self.options = options + self.dataUrl = dataurl + self.params = params + self.viewer = viewer + self.page_rows = page_rows + self.initflag = False + + + def setScrollPos(self,pos): + print('setScrollPos(),pos=',pos) + self.scroll_y = pos + + def sizeChangedWork(self,v=None): + super().sizeChangedWork() + if not self.initflag: + self.initflag = True + self.loader = RelatedLoader( + adder=self.showObject, + remover=self.remove_widget, + locater=self.setScrollPos, + page_rows=self.page_rows, + dataurl=self.dataUrl, + target=self, + params=self.params, + method=self.options.get('method','GET') + ) + self.loader.loadPage(1) + print('ObjectViewer:::::::::::::self.size=',self.size) + + def on_scroll_stop(self,o,v=None): + return + print('on_scroll_stop(), self.scroll_y=', self.scroll_y, + self._inner.pos, + self._inner.size, + self.size) + if self.scroll_y >=0.999: + print('on_scroll_stop(), move_up') + self.loader.loadPreviousPage() + if self.scroll_y <= 0.001: + print('on_scroll_stop(), move_down') + self.loader.loadNextPage() + + def showObject(self,rec,**kw): + pass diff --git a/kivyblocks/pagescontainer.py b/kivyblocks/pagescontainer.py new file mode 100644 index 0000000..69bf9cd --- /dev/null +++ b/kivyblocks/pagescontainer.py @@ -0,0 +1,97 @@ +from kivy.core.window import Window +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.button import Button +from kivy.uix.label import Label +from kivy.app import App +from .utils import CSize + +class PageContainer(FloatLayout): + def __init__(self,**kw): + super().__init__(**kw) + self.show_back = True + self.pageWidgets = [] + self.backButton = Button(text='<',size_hint=(None,None), + font_size=CSize(1), + height=CSize(1.8),width=CSize(1.8)) + self.backButton.bind(on_press=self.previous) + Window.bind(size=self.on_window_size) + # self.bind(size=self.reshowBackButton, + # on_orientation=self.reshowBackButton) + + def on_window_size(self,o,v=None): + print('on_window_size event fired ....',Window.size) + self.size = Window.size + self.reshowBackButton() + + def reshowBackButton(self,o=None,v=None): + print(self.size) + self.showBackButton() + + def hideBack(self): + if not self.show_back: + return + super(PageContainer,self).remove_widget(self.backButton) + self.show_back = False + + def showBack(self): + if self.show_back: + return + super(PageContainer,self).add_widget(self.backButton) + self.show_back = True + + def showLastPage(self): + super(PageContainer,self).clear_widgets() + if len(self.pageWidgets) < 1: + return + w = self.pageWidgets[-1] + w.pos = 0,0 + w.size = self.size + super(PageContainer,self).add_widget(w) + self.showBackButton() + + def previous(self,v=None): + if len(self.pageWidgets) <= 1: + return + w = self.pageWidgets[-1] + if hasattr(w, 'beforeDestroy'): + x = w.beforeDestroy() + if not x: + return + self.pageWidgets = self.pageWidgets[:-1] + del w + self.showLastPage() + + def add_widget(self,widget): + self.pageWidgets.append(widget) + self.showLastPage() + + def showBackButton(self): + if len(self.pageWidgets) <= 1: + return + print('size event fired .....',self.size) + super(PageContainer,self).remove_widget(self.backButton) + self.show_back = False + self.backButton.pos = (4,self.height - 4 - self.backButton.height) + self.showBack() + + +if __name__ == '__main__': + class Page(Button): + def __init__(self,container,page_cnt = 1): + self.container = container + self.page_cnt = page_cnt + Button.__init__(self,text = 'page' + str(page_cnt)) + self.bind(on_press=self.newpage) + + def newpage(self,v): + p = Page(self.container,self.page_cnt + 1) + self.container.add(p) + + class MyApp(App): + def build(self): + x = PageContainer() + p = Page(x) + x.add(p) + return x + + MyApp().run() diff --git a/kivyblocks/paging.py b/kivyblocks/paging.py new file mode 100644 index 0000000..a3e1579 --- /dev/null +++ b/kivyblocks/paging.py @@ -0,0 +1,286 @@ +import traceback +import math + +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.clock import Clock +from kivy.app import App +from functools import partial +from appPublic.dictObject import DictObject +from appPublic.jsonConfig import getConfig +from .baseWidget import Text +from .utils import CSize, absurl, alert +from .form import StrSearchForm + +class PagingButton(Button): + def __init__(self, **kw): + super().__init__(**kw) + self.size_hint = (None,None) + self.size = CSize(2,1.8) + self.font_size = CSize(1) + +""" +{ + dataurl + params + method + locater + filter +} +""" +class PageLoader: + def __init__(self, **options): + self.loading = False + self.filter = None + if options.get('filter'): + self.filter = StrSearchForm(**options['filter']) + self.filter.bind(on_submit=self.do_search) + + self.locater = options.get('locater') + self.params = options.get('params') + self.method = options.get('method','GET') + self.url = options.get('dataurl') + if not self.url: + raise Exception('dataurl must be present:options=%s' % str(options)) + self.total_cnt = 0 + self.total_page = 0 + self.page_rows = options.get('page_rows',0) + self.curpage = 0 + + def do_search(self,o,params): + print('PageLoader().do_search(), on_submit handle....',params) + self.clearer() + self.params.update(params) + print('do_search():,params=',self.params) + self.loadPage(1) + + def calculateTotalPage(self): + self.total_page = math.floor(self.total_cnt / self.page_rows) + if self.total_cnt % self.page_rows != 0: + self.total_page += 1 + + def setPageRows(self,row_cnt): + if row_cnt == 0: + return + self.page_rows = row_cnt + if self.total_cnt != 0: + self.calculateTotalPage() + self.reload() + + def reload(self): + if self.curpage == 0: + self.curpage = 1 + self.loadPage(self.curpage) + + def setDir(self): + pages = self.objectPages.keys() + if len(pages)==0: + self.dir = 'down' + return + if self.curpage >= max(self.objectPages.keys()): + self.dir = 'down' + else: + self.dir = 'up' + + def show_page(self,o,d): + pass + + def loadPage(self,p): + self.curpage = p + self.loading = True + params = self.params.copy() + params.update({ + "page":self.curpage, + "rows":self.page_rows + }) + hc = App.get_running_app().hc + url = absurl(self.url,self.target.parenturl) + # print('loadPage():url=',url,'params=',params) + x = hc(url,method=self.method, + params=params,callback=self.show_page, + errback=self.httperror) + + def httperror(self,o,e): + traceback.print_exc() + alert(str(e),title='alert') + +""" +{ + adder, + remover + target, + locater, + dataurl, + params, + method, + filter +} +""" +class RelatedLoader(PageLoader): + def __init__(self, **options): + super().__init__(**options) + self.adder = options.get('adder') + self.remover = options.get('remover') + self.clearer = options.get('clearer') + self.target = options.get('target') + self.objectPages = {} + self.totalObj = 0 + self.MaxbufferPage = 3 + if self.filter: + self.widget = self.filter + else: + self.widget = None + + def setPageRows(self,row_cnt): + if self.total_cnt != 0: + self.calculateTotalPage() + self.reload() + + pass + + def doBufferMaintain(self): + siz = len(self.objectPages.keys()) + if siz >= self.MaxbufferPage: + if self.dir == 'up': + p = max(self.objectPages.keys()) + else: + p = min(self.objectPages.keys()) + self.deleteBuffer(p) + + def deleteBuffer(self,page): + for w in self.objectPages[page]: + self.remover(w) + self.totalObj -= len(self.objectPages[page]) + del self.objectPages[page] + + def show_page(self,o,data): + if self.objectPages.get(self.curpage): + self.deleteBuffer(self.curpage) + else: + self.setDir() + self.doBufferMaintain() + self.total_cnt = data['total'] + self.calculateTotalPage() + widgets = [] + rows = data['rows'] + if self.dir == 'up': + rows.reverse() + for r in rows: + if self.dir == 'up': + w = self.adder(r,index=self.totalObj) + else: + w = self.adder(r) + widgets.append(w) + self.totalObj += 1 + self.objectPages[self.curpage] = widgets + pages = len(self.objectPages.keys()) + loc = 1.0 / float(pages) + if pages == 1: + self.locater(1) + elif self.dir == 'up': + self.locater(1 - loc) + else: + self.locater(loc) + + self.loading = False + print('buffer pages=',len(self.objectPages.keys()),'pages=',self.objectPages.keys()) + + def loadPreviousPage(self): + if self.loading: + return + + pages = self.objectPages.keys() + if len(pages) < 1: + return + + self.curpage = min(pages) + if self.curpage <= 1: + return + + self.curpage -= 1 + self.loadPage(self.curpage) + + def loadNextPage(self): + if self.loading: + return + pages = self.objectPages.keys() + if len(pages) == 0: + return + self.curpage = max(pages) + if self.curpage>=self.total_page: + return + self.curpage += 1 + self.loadPage(self.curpage) + +""" +{ + adder, + clearer + target, + dataurl + params, + method +} +""" +class Paging(PageLoader): + def __init__(self,**options): + PageLoader.__init__(self,**options) + self.adder = options.get('adder') + self.clearer = options.get('clearer') + self.target = options.get('target') + self.init() + + def init(self): + kwargs = {} + kwargs['size_hint_y'] = None + kwargs['height'] = CSize(2) + kwargs['orientation'] = 'horizontal' + self.widget = BoxLayout(**kwargs) + self.b_f = PagingButton(text="|<") + self.b_p = PagingButton(text="<") + self.b_n = PagingButton(text=">") + self.b_l = PagingButton(text=">|") + self.b_f.bind(on_press=self.loadFirstPage) + self.b_p.bind(on_press=self.loadPreviousPage) + self.b_n.bind(on_press=self.loadNextPage) + self.b_l.bind(on_press=self.loadLastPage) + self.widget.add_widget(self.b_f) + self.widget.add_widget(self.b_p) + self.widget.add_widget(self.b_n) + self.widget.add_widget(self.b_l) + if self.filter: + self.widget.add_widget(self.filter) + + def show_page(self,o,d): + d = DictObject(**d) + self.total_cnt = d.total + self.calculateTotalPage() + self.clearer() + for r in d.rows: + self.adder(r) + self.loading = False + + def loadFirstPage(self,o=None): + if self.curpage == 1: + print('return not loading') + return + self.loadPage(1) + + def loadPreviousPage(self,o=None): + if self.curpage < 2: + print('return not loading') + return + self.loadPage(self.curpage-1) + + def loadNextPage(self,o=None): + if self.curpage >= self.total_page: + print('return not loading') + return + self.loadPage(self.curpage+1) + + def loadLastPage(self,o=None): + if self.curpage >= self.total_page: + print('return not loading') + return + self.loadPage(self.total_page) + diff --git a/kivyblocks/ready.py b/kivyblocks/ready.py new file mode 100644 index 0000000..28a1d98 --- /dev/null +++ b/kivyblocks/ready.py @@ -0,0 +1,60 @@ +from kivy.uix.widget import Widget +from kivy.properties import BooleanProperty +from kivy.clock import Clock +from kivy.graphics import Color, Rectangle + +class WidgetReady(Widget): + ready = BooleanProperty(False) + kwargs_keys = ['bg_color'] + def __init__(self, **kw): + self.bg_color = kw.get('bg_color',None) + self.all_ready = {} + self.all_ready[self] = False + self.backgroundcolorTask = None + if self.bg_color is not None: + self.setupBackgroundColor() + + def setupBackgroundColor(self): + self.bind(pos=self.backgroundcolorfunc) + self.bind(size=self.backgroundcolorfunc) + + def backgroundcolorfunc(self,o,v): + if self.backgroundcolorTask is not None: + self.backgroundcolorTask.cancel() + self.backgroundcolorTask = Clock.schedule_once(self._setBackgroundColor) + + def _setBackgroundColor(self,t=0): + self.canvas.before.clear() + with self.canvas.before: + Color(*self.bg_color) + Rectangle(size=self.size,pos=self.pos) + + def addChild(self,w): + """ + need to call addChild to make widget to wait the subwidget on_ready event + w the subwidget of self + """ + if hasattr(w,'ready'): + self.all_ready[w] = False + w.bind(on_ready=self.childReady) + + def childReady(self,c,v=None): + self.all_ready[c] = True + if all(self.all_ready.values()): + self.ready = True + + def on_ready(self,o,v=None): + print(self,'on_ready') + + def built(self): + """ + when self built, call it at end to issue the on_ready event + """ + self.all_ready[self] = True + if all(self.all_ready.values()): + self.ready = True + + def setBackgroundColor(self,bgcolor): + self.bg_color = bgcolor + self._setBackgroundColor(0) + diff --git a/kivyblocks/responsivelayout.py b/kivyblocks/responsivelayout.py new file mode 100644 index 0000000..9f80d98 --- /dev/null +++ b/kivyblocks/responsivelayout.py @@ -0,0 +1,62 @@ + +from kivy.utils import platform +from kivy.uix.scrollview import ScrollView +from kivy.uix.gridlayout import GridLayout +from kivy.graphics import Color, Ellipse,Rectangle +from kivy.clock import Clock +from .utils import CSize, isHandHold + +class VResponsiveLayout(ScrollView): + def __init__(self,box_width,cols, **kw): + self.org_box_width = box_width + self.org_cols = cols + self.box_width = box_width + self.box_cols = cols + super(VResponsiveLayout, self).__init__(**kw) + self.options = kw + self._inner = GridLayout(cols=self.org_cols, padding=2, + spacing=2,size_hint=(1,None)) + super(VResponsiveLayout,self).add_widget(self._inner) + self._inner.bind( + minimum_height=self._inner.setter('height')) + self.sizeChangedTask = None + self.bind(pos=self.sizeChanged,size=self.sizeChanged) + + def sizeChanged(self,o,v=None): + if self.sizeChangedTask: + self.sizeChangedTask.cancel() + self.sizeChangedTask = Clock.schedule_once(self.sizeChangedWork,0.1) + + def sizeChangedWork(self,t=None): + self.setCols() + + def on_orientation(self,o): + self.setCols() + + def add_widget(self,widget,**kw): + width = self.box_width + if hasattr(widget, 'cols'): + width = widget.cols * self.box_width + widget.width = width + a = self._inner.add_widget(widget,**kw) + return a + + def clear_widgets(self,**kw): + a = self._inner.clear_widgets(**kw) + + def remove_widget(self,widget,**kw): + a = self._inner.remove_widget(widget,**kw) + return a + + def setCols(self,t=None): + self.box_width = self.org_box_width + self.cols = int(self.width / self.box_width) + if isHandHold(): + w,h = self.size + if w < h: + self.box_width = w / self.org_cols - 2 + self.cols = self.org_cols + self._inner.cols = self.cols + for w in self._inner.children: + w.width = self.box_width + diff --git a/kivyblocks/serverImageViewer.py b/kivyblocks/serverImageViewer.py new file mode 100644 index 0000000..7c1ddbb --- /dev/null +++ b/kivyblocks/serverImageViewer.py @@ -0,0 +1,29 @@ +from appPublic.jsonConfig import getConfig +from kivy.uix.button import ButtonBehavior +from kivy.app import App +from kivy.uix.boxlayout import BoxLayout +from .baseWidget import * +from .objectViewer import ObjectViewer + +class ServerImageViewer(ObjectViewer): + def showObject(self,rec,**kw): + blocks = App.get_running_app().blocks + config = getConfig() + url = '%s/%s/%s' % ( config.uihome,'thumb',rec['id']) + desc = self.viewer.copy() + desc['options'].update({ + "size_hint_x":None, + "width":self.box_width, + "keep_ratio":True, + "source":url + }) + w = blocks.widgetBuild(desc,ancestor=self) + if w is None: + print('Error desc=',desc) + return + bl = BoxLayout(size_hint=(None,None),size=(self.box_width,self.box_width)) + bl.add_widget(w) + self.add_widget(bl,**kw) + print('showObject():add widget',desc,w.size) + return bl + diff --git a/kivyblocks/threadcall.py b/kivyblocks/threadcall.py new file mode 100644 index 0000000..a58b89c --- /dev/null +++ b/kivyblocks/threadcall.py @@ -0,0 +1,196 @@ +import time +from threading import Thread, Lock, BoundedSemaphore +import requests + +from kivy.event import EventDispatcher +from kivy.clock import Clock +from kivy.app import App + +class NeedLogin(Exception): + pass + +class ThreadCall(Thread,EventDispatcher): + def __init__(self,target, args=(), kwargs={}): + Thread.__init__(self) + EventDispatcher.__init__(self) + self.register_event_type('on_result') + self.register_event_type('on_error') + self.rez = None + self.daemon = False + self.target = target + self.args = args + self.timing = None + self.kwargs = kwargs + + def start(self): + Thread.start(self) + self.timing = Clock.schedule_once(self.checkStop,0) + + def run(self): + try: + self.rez = self.target(*self.args,**self.kwargs) + self.dispatch('on_result',self.rez) + except NeedLogin as e: + lf = LoginForm() + lf.needlogin(url,method,params,headers,callback,errback) + + except Exception as e: + self.dispatch('on_error',e) + + def on_result(self, v): + pass + + def on_error(self,e): + pass + + def checkStop(self,timestamp): + x = self.join(timeout=0.0001) + if self.is_alive(): + self.timing = Clock.schedule_once(self.checkStop,0) + return + +class Workers(Thread): + def __init__(self,maxworkers): + super().__init__() + self.max_workers = maxworkers + self.tasks = [] + # task = [callee,callback,kwargs] + self.lock = Lock() + self.work_sema = BoundedSemaphore(value=self.max_workers) + self.running = False + def run(self): + self.running = True + while self.running: + if len(self.tasks) == 0: + time.sleep(0.001) + continue + task = None + with self.lock: + task = self.tasks.pop() + if task is None: + continue + with self.work_sema: + callee,callback,errback,kwargs = task + x = ThreadCall(callee,kwargs=kwargs) + x.bind(on_result=callback) + if errback: + x.bind(on_error=errback) + x.start() + + def add(self,callee,callback,errback=None,kwargs={}): + with self.lock: + self.tasks.insert(0,[callee,callback,errback,kwargs]) + +class HttpClient: + def __init__(self): + self.s = requests.Session() + self.workers = App.get_running_app().workers + + def webcall(self,url,method="GET",params={},headers={}): + if method in ['GET']: + req = requests.Request(method,url, + params=params,headers=headers) + else: + req = requests.Request(method,url, + data=params,headers=headers) + prepped = self.s.prepare_request(req) + resp = self.s.send(prepped) + if resp.status_code == 200: + try: + data = resp.json() + if type(data) != type({}): + return data + status = data.get('status',None) + if status is None: + return data + if status == 'OK': + return data['data'] + return data + except: + return resp.text + if resp.status_code == 401: + raise NeedLogin + + print('Error', url, method, params, resp.status_code,type(resp.status_code)) + raise Exception('error:%s' % url) + + def __call__(self,url,method="GET",params={},headers={},callback=None,errback=None): + def cb(t,resp): + return resp + + if callback is None: + try: + resp = self.webcall(url, method=method, + params=params, headers=headers) + return cb(None,resp) + except NeedLogin as e: + lf = LoginForm() + lf.needlogin(url,method,params,headers,callback,errback) + return None + except Exception as e: + if errback is not None: + errback(e) + + kwargs = { + "url":url, + "method":method, + "params":params, + "headers":headers + } + self.workers.add(self.webcall,callback,errback,kwargs=kwargs) + + def get(self, url, params={}, headers={}, callback=None, errback=None): + return self.__call__(url,method='GET',params=params, + headers=headers, callback=callback, + errback=errback) + def post(self, url, params={}, headers={}, callback=None, errback=None): + return self.__call__(url,method='POST',params=params, + headers=headers, callback=callback, + errback=errback) + + def put(self, url, params={}, headers={}, callback=None, errback=None): + return self.__call__(url,method='PUT',params=params, + headers=headers, callback=callback, + errback=errback) + + def delete(self, url, params={}, headers={}, callback=None, errback=None): + return self.__call__(url,method='DELETE',params=params, + headers=headers, callback=callback, + errback=errback) + + def option(self, url, params={}, headers={}, callback=None, errback=None): + return self.__call__(url,method='OPTION',params=params, + headers=headers, callback=callback, errback=errback) + +if __name__ == '__main__': + from kivy.uix.textinput import TextInput + from kivy.app import App + from kivy.uix.boxlayout import BoxLayout + from kivy.uix.button import Button + class MyApp(App): + def build(self): + self.hc = HttpClient() + x = BoxLayout(orientation='vertical') + y = BoxLayout(orientation='horizontal',size_hint_y=0.07) + self.ti = TextInput(size_hint_x=0.95,multiline=False) + btn = Button(size_hint_x=0.05,text='go') + y.add_widget(self.ti) + y.add_widget(btn) + btn.bind(on_press=self.getHtml) + self.texti = TextInput(multiline=True,readonly=True) + x.add_widget(y) + x.add_widget(self.texti) + + return x + + def getHtml(self,v=None): + url = self.ti.text + self.hc.get(url,callback=self.showResult) + self.texti.text = 'loading...' + + def showResult(self,target,resp): + if resp.status_code==200: + self.texti.text = resp.text + else: + print(reps.status_code,'...............') + MyApp().run() diff --git a/kivyblocks/toolbar.py b/kivyblocks/toolbar.py new file mode 100644 index 0000000..cae7aa2 --- /dev/null +++ b/kivyblocks/toolbar.py @@ -0,0 +1,206 @@ +from kivy.graphics import Color, Rectangle +from kivy.uix.button import ButtonBehavior +from kivy.uix.image import AsyncImage +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.label import Label +from kivy.app import App +from kivy.clock import Clock + +from appPublic.dictObject import DictObject + +from .widgetExt.scrollwidget import ScrollWidget +from .utils import * +from .kivysize import KivySizes +from .ready import WidgetReady +from .i18n import I18nText +""" +toobar={ + "mode":"icon", "icontext","text" + img_size=1.5, + text_size=0.7, + "tools":[ + ] +} + +tool options +{ + name:'', + label:'' + img='' +} +""" +class Tool(ButtonBehavior, WidgetReady,BoxLayout): + normal_bgColor=[0.1,0,0,1] + active_bgColor=[0.4,0.4,0.4,1] + def __init__(self,ancestor=None,**opts): + if ancestor is None: + ancestor = App.get_running_app().root + ancestor.widget_ids[opts['name']] = self + ButtonBehavior.__init__(self) + bc = opts.get('bg_color',self.normal_bgColor) + WidgetReady.__init__(self,bg_color=self.normal_bgColor) + BoxLayout.__init__(self, + orientation='vertical',size_hint=(None,None)) + self.opts = DictObject(**opts) + if not self.opts.img_size: + self.opts.img_size = 2 + if not self.opts.text_size: + self.opts.text_size = 0.7 + app = App.get_running_app() + ks = KivySizes() + size = ks.unitedSize(self.opts.img_size or 2) + img = AsyncImage(source=self.opts.img_src,size_hint=(None,None), + size=(size,size)) + tsize = ks.unitedSize(self.opts.text_size) + label = self.opts.label or self.opts.name + lbl = I18nText(otext=label,font_size=int(tsize)) + lbl.text_size = (size, 1.3 * tsize) + self.add_widget(img) + self.add_widget(lbl) + self.size = (size * 1.1, (size + 2 * tsize)*1.1) + + def on_size(self,obj,size): + if self.parent: + print('********center*dd**********') + self.center = self.parent.center + + def on_press(self): + print('Tool(). pressed ...............') + + def setActive(self,flag): + if flag: + self.setBackgroundColor(self.active_bgColor) + else: + self.setBackgroundColor(self.normal_bgColor) + + +""" +toolbar options +{ + img_size=1.5, + text_size=0.7, + tools:[ + { + "name":"myid", + "img_src":"gggggg", + "label":"gggggg" + }, + ... + ] +} +""" +class Toolbar(GridLayout): + def __init__(self, ancestor=None,**opts): + self.opts = DictObject(**opts) + self.tool_widgets={} + super().__init__(cols = len(self.opts.tools)) + self.size_hint = (1,None) + first = True + for opt in self.opts.tools: + opt.img_size = self.opts.img_size + opt.text_size = self.opts.text_size + purl = None + if ancestor: + purl = ancestor.parenturl + opt.img_src = absurl(opt.img_src,purl) + tool = Tool(ancestor=ancestor, **opt) + if first: + first = False + h = ancestor + if not ancestor: + h = App.get_runnung_app().root + h.widget_ids['_home_'] = tool + self.tool_widgets[opt.name] = tool + box = BoxLayout() + box.add_widget(tool) + self.add_widget(box) + tool.bind(on_press=self.tool_press) + self.height = tool.height * 1.1 + + def on_size(self,obj,size): + with self.canvas.before: + Color(0.3,0.3,0.3,1) + Rectangle(pos=self.pos,size=self.size) + + def tool_press(self,o,v=None): + o.background_color = [0.3,1,1,0.5] + for n,w in self.tool_widgets.items(): + active = False + if w == o: + active = True + w.setActive(active) + +""" +Toolpage options +{ + img_size=1.5, + text_size=0.7, + tool_at:"left","right","top","bottom", + tools:[ + { + "name":"myid", + "img_src":"gggggg", + "text":"gggggg" + "url":"ggggggggg" + }, + ... + ] + +""" +class ToolPage(BoxLayout): + def __init__(self,**opts): + self.opts = DictObject(**opts) + self.parenturl = opts.get('parenturl',None) + self.widget_ids = {} + if self.opts.tool_at in [ 'top','bottom']: + orient = 'vertical' + else: + orient = 'horizontal' + + super().__init__(orientation=orient) + self.content = None + self.toolbar = None + self.init() + self.show_firstpage() + + def on_size(self,obj,size): + if self.content is None: + return + x,y = size + self.toolbar.width = x + self.content.width = x + self.content.height = y - self.toolbar.height + + def showPage(self,obj): + self._show_page(obj.opts) + + def show_firstpage(self,t=None): + d = self.widget_ids['_home_'] + d.dispatch('on_press') + + def init(self): + self.initFlag = True + self.mywidgets = {} + self.content = BoxLayout() + self.widget_ids['content'] = self.content + for t in self.opts.tools: + parenturl = None + if hasattr(self,'parenturl'): + parenturl = self.parenturl + t.img_src = absurl(t.img_src,parenturl) + + opts = self.opts + self.toolbar = Toolbar(ancestor=self, **self.opts) + if self.opts.tool_at in ['top','left']: + self.add_widget(self.toolbar) + self.add_widget(self.content) + else: + self.add_widget(self.content) + self.add_widget(self.toolbar) + Clock.schedule_once(self.show_firstpage,0.5) + +if __name__ == '__main__': + from blocksapp import BlocksApp + app = BlocksApp() + app.run() diff --git a/kivyblocks/ttf/DroidSansFallback.ttf b/kivyblocks/ttf/DroidSansFallback.ttf new file mode 100755 index 0000000..6f92880 Binary files /dev/null and b/kivyblocks/ttf/DroidSansFallback.ttf differ diff --git a/kivyblocks/uploadfile.py b/kivyblocks/uploadfile.py new file mode 100644 index 0000000..73b0798 --- /dev/null +++ b/kivyblocks/uploadfile.py @@ -0,0 +1,10 @@ +import requests + +url = "http://localhost:8080/uploadfile.dspy" + +files = { + "afile":("uploadfile.py",open("uploadfile.py","rb"),"application/python") +} + +r = requests.post(url,files=files) +print(r.text) diff --git a/kivyblocks/utils.py b/kivyblocks/utils.py new file mode 100644 index 0000000..9851718 --- /dev/null +++ b/kivyblocks/utils.py @@ -0,0 +1,181 @@ +import os +from kivy.app import App +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 Image +from appPublic.dictObject import DictObject + +from .kivysize import KivySizes + +alert_widget= None + +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 = Image(source=fp,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 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) + x='(%d,%d)mm' % screenPhysicalSize() + if isHandHold(): + x = x + ':hh' + alert_widget.title = str(title) + x + 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) + diff --git a/kivyblocks/vplayer.py b/kivyblocks/vplayer.py new file mode 100644 index 0000000..2d0618c --- /dev/null +++ b/kivyblocks/vplayer.py @@ -0,0 +1,431 @@ +import os +import sys +from traceback import print_exc +from kivy.core.window import Window +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.video import Video +from kivy.uix.slider import Slider +from kivy.uix.popup import Popup +from kivy.uix.button import Button +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.filechooser import FileChooserListView +from kivy.uix.label import Label +from kivy.app import App +from kivy.clock import Clock +from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, \ + NumericProperty, DictProperty, OptionProperty +from .utils import * +from .baseWidget import PressableImage + + +class VPlayer(FloatLayout): + fullscreen = BooleanProperty(False) + exit = BooleanProperty(False) + stoped_play = BooleanProperty(False) + paused_play = BooleanProperty(False) + + def __init__(self,vfile=None,loop=False, + openfile_img=None, + exit_img = None, + pause_img = None, + play_img = None, + mute_img = None, + track_img = None, + next_img = None, + replay_img = None, + can_openfile=False, + can_cut=False, + can_replay=False, + can_changevolume=True + ): + super().__init__() + self.allow_screensaver = False + print(self,vfile) + self._video = Video(allow_stretch=True,pos_hint={'x': 0, 'y': 0},size_hint=(1,1)) + self.add_widget(self._video) + self.loop = loop + self.openfile_img = openfile_img + self.can_openfile = can_openfile + self.can_replay = can_replay + self.can_cut = can_cut + self.can_changevolume = can_changevolume + if self.openfile_img: + self.can_openfile = True + self.exit_img = exit_img + self.play_img = play_img + self.pause_img = pause_img + self.mute_img = mute_img + self.track_img = track_img + self.next_img = next_img + self.replay_img = replay_img + self.ffplayer = None + self.menubar = None + self._popup = None + self.manualMode = False + self.old_path = os.getcwd() + self.pb = None + if vfile: + if type(vfile) == type([]): + self.playlist = vfile + else: + self.playlist = [vfile] + self.curplay = 0 + self.play() + else: + self.playlist = [] + self.curplay = -1 + self._video.bind(eos=self.video_end) + self._video.bind(state=self.on_state) + self._video.bind(loaded=self.createProgressbar) + self._video.bind(on_touch_down=self.buildMenu) + self.register_event_type('on_playend') + + def play(self,o=None,v=None): + if self.curplay >= 0: + self._video.source = self.playlist[self.curplay] + self._video.state = 'play' + + def on_playend(self,o=None,v=None): + pass + + def addPlaylist(self,lst): + self.playlist += lst + + def video_end(self,t,v): + self.curplay += 1 + if not self.loop and self.curplay >= len(self.playlist): + self.dispatch('on_playend') + return + self.curplay = self.curplay % len(self.playlist) + self._video.source = self.playlist[self.curplay] + self._video.state = 'play' + + def totime(self,dur): + h = dur / 3600 + m = dur % 3600 / 60 + s = dur % 60 + return '%02d:%02d:%02d' % (h,m,s) + + def createProgressbar(self,obj,v): + if hasattr(self._video._video, '_ffplayer'): + self.ffplayer = self._video._video._ffplayer + + if self.pb is None: + self.pb = BoxLayout(orientation='horizontal', + size_hint = (0.99,None),height=CSize(1.4)) + btn_menu=Button(text='M',size_hint=(None,None),text_size=CSize(1,1),size=CSize(1.2,1,2)) + btn_menu.bind(on_press=self.buildMenu) + btn_volume=Button(text='V',size_hint=(None,None),text_size=CSize(1,1),size=CSize(1.2,1,2)) + btn_volume.bind(on_press=self.volumeControl) + self.curposition = Label(text='0',width=CSize(4), + size_hint_x=None) + self.curposition.align='right' + self.maxposition = Label(text=self.totime(self._video.duration), + width=CSize(4),size_hint_x=None) + self.maxposition.align = 'left' + # self.slider = ProgressBar(value=0,max=max) + self.slider = Slider(min=0, + max=self._video.duration, + value=0, + orientation='horizontal', + step=0.01) + self.slider.bind(on_touch_down=self.enterManualMode) + self.slider.bind(on_touch_up=self.endManualMode) + self.manual_mode=False + + self.add_widget(self.pb) + self.pb.add_widget(btn_menu) + self.pb.add_widget(self.curposition) + self.pb.add_widget(self.slider) + self.pb.add_widget(self.maxposition) + # self.pb.add_widget(self.btn_volume) + self.pb.pos = (0,0) + Clock.schedule_interval(self.update_slider,1) + + def volumeControl(self,obj,v): + self.volumeCtrl = BoxLayout(orientation='vertical',size_hint=(None,None),size=CSize(1.4,10)) + self.pos = self.width - self.volumeCtrl.width,CSize(1.4) + self.add_widget(self.volumeCtrl) + btn_mute = Button(text='Mute',size_hint_y=None,height=CSize(1.4)) + if self._video.volume <= 0.001: + btn_mute.text = 'Sound' + btn_menu.bind(on_press=self.mute) + self.volumeCtrl.add_widget(btn_mute) + slider = Slider(min=0, + max=1, + value=self._video.volume, + orientation='vertical', + step=0.01) + slider.bind(on_value=self.setVolume) + self.volumeCtrl.add_widegt(slider) + btn_audioswitch = Button(text='track',size_hint_y=None,height=CSize(1.4)) + btn_audioswitch.bind(on_press=self.audioswitch) + self.volumeCtrl.add_widget(btn_audioswitch) + + def enterManualMode(self,obj,touch): + if not self.slider.collide_point(*touch.pos): + return + self.manualMode = True + + def endManualMode(self,obj,touch): + if not self.manualMode: + return + if self._video.duration < 0.0001: + return + self._video.seek(self.slider.value/self._video.duration) + self.manualMode = False + + def update_slider(self,t): + self.curposition.text = self.totime(self._video.position) + if not self.manualMode: + self.slider.value = self._video.position + self.slider.max = self._video.duration + self.maxposition.text = self.totime(self._video.duration) + + def beforeDestroy(self): + try: + self.pause() + except Exception as e: + print_exc() + return True + + def on_state(self,o,v): + print('onstate()',o,v,self._video.state) + + def on_fullscreen(self, instance, value): + window = self.get_parent_window() + print('window.size=',window.size) + if not window: + Logger.warning('VideoPlayer: Cannot switch to fullscreen, ' + 'window not found.') + if value: + self.fullscreen = False + return + if not self.parent: + Logger.warning('VideoPlayer: Cannot switch to fullscreen, ' + 'no parent.') + if value: + self.fullscreen = False + return + + if value: + self._fullscreen_state = state = { + 'parent': self.parent, + 'pos': self.pos, + 'size': self.size, + 'pos_hint': self.pos_hint, + 'size_hint': self.size_hint, + 'window_children': window.children[:]} + + # remove all window children + for child in window.children[:]: + window.remove_widget(child) + + # put the video in fullscreen + if state['parent'] is not window: + state['parent'].remove_widget(self) + window.add_widget(self) + + # ensure the video widget is in 0, 0, and the size will be + # readjusted + self.pos = (0, 0) + self.size = (100, 100) + self.pos_hint = {} + self.size_hint = (1, 1) + else: + state = self._fullscreen_state + window.remove_widget(self) + for child in state['window_children']: + window.add_widget(child) + self.pos_hint = state['pos_hint'] + self.size_hint = state['size_hint'] + self.pos = state['pos'] + self.size = state['size'] + if state['parent'] is not window: + state['parent'].add_widget(self) + + def buildMenu(self,obj,touch): + if not self.collide_point(*touch.pos): + print('not inside the player') + return + + if touch.is_double_tap: + self.fullscreen = False if self.fullscreen else True + if self.menubar: + self.remove_widget(self.menubar) + print('doube_tap') + return + + if self.menubar: + print('delete menubar') + self.remove_widget(self.menubar) + self.menubar = None + return + + self.menubar = BoxLayout(orientation='horizontal', + size_hint_y=None,height=CSize(1.4)) + self.btn_pause = PressableImage(source=blockImage('pause.jpg'), + size_hint=(None,None), + size=CSize(3,3) + ) + if self._video.state == 'pause': + self.btn_pause.source = blockImage('play.jpg') + self.btn_pause.bind(on_press=self.pause) + self.menubar.add_widget(self.btn_pause) + self.btn_mute = PressableImage(source=blockImage('mute.jpg'), + size_hint=(None,None), + size=CSize(3,3) + ) + self.btn_mute.bind(on_press=self.mute) + self.menubar.add_widget(self.btn_mute) + if self.can_openfile: + btn_open = Button(text='open') + btn_open.bind(on_press=self.openfile) + self.menubar.add_widget(btn_open) + btn_cut = PressableImage(source=blockImage('next.jpg'), + size_hint=(None,None), + size=CSize(3,3) + ) + btn_cut.bind(on_press=self.endplay) + self.menubar.add_widget(btn_cut) + btn_replay = PressableImage(source=blockImage('replay.jpg'), + size_hint=(None,None), + size=CSize(3,3) + ) + btn_replay.bind(on_press=self.replay) + self.menubar.add_widget(btn_replay) + + self.btn_audioswitch = PressableImage( \ + source=blockImage('musictrack.jpg'), + size_hint=(None,None), + size=CSize(3,3) + ) + + self.btn_audioswitch.bind(on_press=self.audioswitch) + self.menubar.add_widget(self.btn_audioswitch) + self.menubar.pos = CSize(0,1.4) + self.add_widget(self.menubar) + + def endplay(self,btn): + self._video.seek(1.0,precise=True) + + def replay(self,btn): + self._video.seek(0.0,precise=True) + + def hideMenu(self): + self._popup.dismiss() + self.remove_widget(self.menubar) + self.menubar = None + + def audioswitch(self,btn): + if self.ffplayer is not None: + self.ffplayer.request_channel('audio') + + def setVolume(self,obj,v): + self._video.volume = v + + def mute(self,btn): + if self._video.volume > 0.001: + self.old_volume = self._video.volume + self._video.volume = 0.0 + btn.source = blockImage('volume.jpg') + else: + self._video.volume = self.old_volume + btn.source = blockImage('mute.jpg') + + def stop(self): + print(self) + self._video.state = 'stop' + + def on_disabled(self,o,v): + if self.disabled: + self.stop() + del self._video + + def pause(self,t=None): + if self._video.state == 'play': + self._video.state = 'pause' + self.btn_pause.source = blockImage('play.jpg') + else: + self._video.state = 'play' + self.btn_pause.source = blockImage('pause.jpg') + + def openfile(self,t): + if self._popup is None: + def vfilter(path,filename): + vexts = ['.avi', + '.mpg', + '.mpe', + '.mpeg', + '.mlv', + '.dat', + '.mp4', + '.flv', + '.mov', + '.rm', + '.mkv', + '.rmvb', + '.asf', + '.3gp' + ] + for ext in vexts: + if filename.endswith(ext): + return True + return False + c = BoxLayout(orientation='vertical') + self.file_chooser = FileChooserListView() + self.file_chooser.filters = [vfilter] + self.file_chooser.multiselect = True + self.file_chooser.path = self.old_path + self.file_chooser.bind(on_submit=self.loadFilepath) + c.add_widget(self.file_chooser) + b = BoxLayout(size_hint_y=None,height=35) + c.add_widget(b) + cancel = Button(text='Cancel') + cancel.bind(on_press=self.cancelopen) + load = Button(text='load') + load.bind(on_press=self.playfile) + b.add_widget(load) + b.add_widget(cancel) + self._popup = Popup(title='Open file',content=c,size_hint=(0.9,0.9)) + self._popup.open() + + def cancelopen(self,obj): + self.hideMenu() + + def loadFilepath(self,obj,fpaths,evt): + print('fp=',fpaths,type(fpaths),'evt=',evt) + self.hideMenu() + self.playlist = fpaths + self.curplay = 0 + self._video.source = self.playlist[self.curplay] + self._video.state = 'play' + + def playfile(self,obj): + print('obj') + self.hideMenu() + self.playlist = [] + for f in self.file_chooser.selection: + fp = os.path.join(self.file_chooser.path,f) + self.playlist.append(fp) + self.curplay = 0 + self._video.source = self.playlist[self.curplay] + self._video.state = 'play' + +if __name__ == '__main__': + class MyApp(App): + def build(self): + vf = None + if len(sys.argv) > 1: + vf = sys.argv[1:] + self.player = VPlayer(vfile=vf, + loop=True, + can_openfile=True, + can_move = True, + can_cut=True, + can_replay=True, + can_changevolume = True + ) + return self.player + + MyApp().run() diff --git a/kivyblocks/widgetExt/__init__.py b/kivyblocks/widgetExt/__init__.py new file mode 100755 index 0000000..1bb4c0d --- /dev/null +++ b/kivyblocks/widgetExt/__init__.py @@ -0,0 +1,25 @@ +from kivy.utils import platform +from .binstateimage import BinStateImage +from .jsoncodeinput import JsonCodeInput +from .inputext import FloatInput,IntegerInput,StrInput,SelectInput, BoolInput, AmountInput, Password +from .scrollwidget import ScrollWidget +from .messager import Messager +__all__ = [ +BinStateImage, +JsonCodeInput, +FloatInput, +AmountInput, +Password, +BoolInput, +IntegerInput, +StrInput, +SelectInput, +ScrollWidget, +Messager, +] + +if platform == 'android': + print('***********************************8') + from .phonebutton import PhoneButton + from .androidwebview import AWebView + __all__ = __all__ + [PhoneButton, AWebView] diff --git a/kivyblocks/widgetExt/androidwebview.py b/kivyblocks/widgetExt/androidwebview.py new file mode 100755 index 0000000..5bdb82d --- /dev/null +++ b/kivyblocks/widgetExt/androidwebview.py @@ -0,0 +1,38 @@ +from kivy.uix.widget import Widget +from kivy.clock import Clock +from jnius import autoclass +from android.runnable import run_on_ui_thread + + +WebView = autoclass('android.webkit.WebView') +WebViewClient = autoclass('android.webkit.WebViewClient') +activity = autoclass('org.kivy.android.PythonActivity').mActivity + + +class AWebView(Widget): + def __init__(self, url=None): + super(AWebView, self).__init__() + self.baseUrl = url + self.create_webview() + # Clock.schedule_once(self.create_webview, 0) + + @run_on_ui_thread + def create_webview(self, *args): + print('create_webview() begining') + self.webview = WebView(activity) + settings = self.webview.getSettings() + settings.setJavaScriptEnabled(True) + settings.setUseWideViewPort(True) # enables viewport html meta tags + settings.setLoadWithOverviewMode(True) # uses viewport + settings.setSupportZoom(True) # enables zoom + settings.setBuiltInZoomControls(True) # enables zoom controls + wvc = WebViewClient() + self.webview.setWebViewClient(wvc) + activity.setContentView(self.webview) + if self.baseUrl is not None: + self.webview.loadUrl(self.baseUrl) + print('go to the url=',self.baseUrl) + print('create_webview() finished') + + def open(self,url): + self.webview.loadUrl(url) diff --git a/kivyblocks/widgetExt/binstateimage.py b/kivyblocks/widgetExt/binstateimage.py new file mode 100755 index 0000000..980f673 --- /dev/null +++ b/kivyblocks/widgetExt/binstateimage.py @@ -0,0 +1,43 @@ +from kivy.uix.image import Image,AsyncImage +from kivy.uix.button import Button +from kivy.graphics import Color +from kivy.uix.boxlayout import BoxLayout +from kivy.properties import NumericProperty + +class BinStateImage(Button): + binstate = NumericProperty(1) + def __init__(self,source1='../images/bullet_arrow_right.png',source2='../images/bullet_arrow_down.png',**kv): + self.bin_state_images = [ + source1, + source2 + ] + kv.update({'source':source1}) + super(BinStateImage,self).__init__() + self.myImgWidget = Image(**kv) + self.size = self.myImgWidget.size + #self.background_color = Color(1,0,0,1) + self.bind(on_release=self.changeState) + + def changeState(self,instance): + #print('on_release fired') + if instance.binstate == 1: + instance.binstate = 2 + else: instance.binstate = 1 + self.myImgWidget.source = self.bin_state_images[instance.binstate-1] + self.myImgWidget.reload() + +if __name__ == '__main__': + from kivy.app import App + class MyApp(App): + def build(self): + m = BoxLayout(orientation='vertical') + img = BinStateImage() + img.bind(binstate=self.showState) + m.add_widget(img) + b = Button(text='GGGG') + m.add_widget(b) + return m + def showState(self,img,v): + pass #print('cur state=',img.binstate,v) + + MyApp().run() diff --git a/kivyblocks/widgetExt/camera.py b/kivyblocks/widgetExt/camera.py new file mode 100644 index 0000000..9c31cd1 --- /dev/null +++ b/kivyblocks/widgetExt/camera.py @@ -0,0 +1,89 @@ +import android +import android.activity +from os import remove +from jnius import autoclass, cast +from plyer.facades import Camera +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +PythonActivity = autoclass('org.renpy.android.PythonActivity') +MediaStore = autoclass('android.provider.MediaStore') +Uri = autoclass('android.net.Uri') + +class AndroidCamera(Camera): + def _take_picture(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + activity.startActivityForResult(intent, 0x123) + + + + def _take_video(self, on_complete, filename=None): + + assert(on_complete is not None) + + self.on_complete = on_complete + + self.filename = filename + + android.activity.unbind(on_activity_result=self._on_activity_result) + + android.activity.bind(on_activity_result=self._on_activity_result) + + intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + + uri = Uri.parse('file://' + filename) + + parcelable = cast('android.os.Parcelable', uri) + + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + + + + # 0 = low quality, suitable for MMS messages, + + # 1 = high quality + + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1) + + activity.startActivityForResult(intent, 0x123) + + + + def _on_activity_result(self, requestCode, resultCode, intent): + + if requestCode != 0x123: + + return + + android.activity.unbind(on_activity_result=self._on_activity_result) + + if self.on_complete(self.filename): + + self._remove(self.filename) + + + + def _remove(self, fn): + + try: + + remove(fn) + + except OSError: + + pass + + + + + +def instance(): + return AndroidCamera() diff --git a/kivyblocks/widgetExt/camerabutton.py b/kivyblocks/widgetExt/camerabutton.py new file mode 100644 index 0000000..e69de29 diff --git a/kivyblocks/widgetExt/dataloader.py b/kivyblocks/widgetExt/dataloader.py new file mode 100755 index 0000000..c2706a6 --- /dev/null +++ b/kivyblocks/widgetExt/dataloader.py @@ -0,0 +1,22 @@ +from kivy.event import EventDispatcher + +class DataLoader(EventDispatcher): + def __init__(self,**kw): + self.register_event_type('on_loaded') + self.register_event_type('on_loaderror') + super(DataLoader,self).__init__(**kw) + + def loadData(self): + pass + + def dataLoaded(self,d): + self.dispatch('on_loaded',d) + + def loadError(self,e): + self.dispatch('on_loaderror',e) + + def on_loaded(self,d): + pass #print('on_loaded,data=',d) + + def on_loaderror(self,a,e): + pass #print('error:',e) diff --git a/kivyblocks/widgetExt/filedataloader.py b/kivyblocks/widgetExt/filedataloader.py new file mode 100644 index 0000000..d869372 --- /dev/null +++ b/kivyblocks/widgetExt/filedataloader.py @@ -0,0 +1,13 @@ +import codecs +from .dataloader import DataLoader + +class FileDataLoader(DataLoader): + def loadData(self,filename): + try: + with codecs.open(filename,'r','utf8') as f: + self.dataLoaded(text) + return f.read() + except Exception as e: + self.loadError(e) + + diff --git a/kivyblocks/widgetExt/httpdataloader.py b/kivyblocks/widgetExt/httpdataloader.py new file mode 100644 index 0000000..ed6b7a2 --- /dev/null +++ b/kivyblocks/widgetExt/httpdataloader.py @@ -0,0 +1,47 @@ +import sys +sys.path.append('..') +sys.path.append('.') +import time + +class HttpDataLoader(DataLoader): + def __init__(self): + super(HttpDataLoader,self).__init__() + self.hc = HttpClient() + + async def loadData(self,url,method='GET',params={},headers={}): + try: + resp = None + if method=='GET': + resp = await self.hc.get(url,params=params,headers=headers) + else: + resp = await self.hc.post(url,data=params,headers=headers) + return resp + except Exception as e: + print('loadData(%s) Error ' % url,e) + self.loadError(e) + +if __name__ == '__main__': + import sys + from async_app import AsyncApp, wait_coro + from kivy.uix.boxlayout import BoxLayout + from kivy.uix.button import Button + from kivy.uix.textinput import TextInput + class MyApp(AsyncApp): + def build(self): + root = BoxLayout(orientation='vertical') + btn = Button(text='get Remote data',size_hint_y=None,height=44) + btn.bind(on_release=self.getData) + root.add_widget(btn) + self.txt = TextInput(multiline=True,readonly=True) + root.add_widget(self.txt) + return root + def getData(self,btn): + url = sys.argv[1] if len(sys.argv)>1 else 'https://www.baidu.com' + hdl = HttpDataLoader() + hdl.bind(on_loaded=self.showData) + wait_coro(hdl.loadData,url) + self.loop.call_later(0.001,hdl.loadData(url)) + + def showData(self,instance,x): + self.txt.text = x + MyApp().run() diff --git a/kivyblocks/widgetExt/inputext.py b/kivyblocks/widgetExt/inputext.py new file mode 100755 index 0000000..ed7088e --- /dev/null +++ b/kivyblocks/widgetExt/inputext.py @@ -0,0 +1,242 @@ +import sys +import re +from kivy.uix.image import Image +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.textinput import TextInput +from kivy.uix.button import Button +from kivy.uix.dropdown import DropDown +from kivy.uix.switch import Switch +from kivy.metrics import sp,dp +from kivy.app import App +from kivy.properties import BooleanProperty + +from ..threadcall import HttpClient +from ..utils import CSize + +class BoolInput(Switch): + change = BooleanProperty(False) + def __init__(self,**kw): + a = DictObject() + if kw.get('defaultvalue',None) is None: + a.active = False + else: + a.active = kw.get('defaultvalue') + if kw.get('value',None) is not None: + a.active = kw.get('value') + + super().__init__(**a) + self.bind(active=on_active) + self.bind(change=self.on_change) + + def on_change(self,t,v): + pass + + def on_active(self,t,v): + change = True + + def getValue(self): + return self.active + + def setValue(self,v): + self.active = v + +class StrInput(TextInput): + change = BooleanProperty(False) + def __init__(self,**kv): + if kv is None: + kv = {} + a = { + # "allow_copy":True, + "font_size":CSize(1), + "multiline":False, + "halign":"left", + "hint_text":"", + "size_hint_y":None, + "height":CSize(2) + } + if kv.get('tip'): + a['hint_text'] = kv['tip'] + a.update(kv) + a['multiline'] = False + super(StrInput,self).__init__(**a) + self.bind(focus=self.on_focus) + self.bind(text=self.on_text) + self.bind(change=self.on_change) + + def on_change(self,t,v): + if v: + pass + + def on_text(self,t,v): + self.change = True + + def on_focus(self,t,v): + self.change = False + + def getValue(self): + return self.text + + def setValue(self,v): + self.text = v + +class Password(StrInput): + def __init__(self, **kw): + kw['password'] = True + super().__init__(**kw) + +class IntegerInput(StrInput): + def __init__(self,**kw): + a = {} + a.update(kw) + a['halign'] = 'right' + super().__init__(**a) + + pat = re.compile('[^0-9]') + def insert_text(self, substring, from_undo=False): + pat = self.pat + s = re.sub(pat, '', substring) + return StrInput.insert_text(self,s, from_undo=from_undo) + +class FloatInput(IntegerInput): + pat = re.compile('[^0-9]') + def filter(self,substring): + pat = self.pat + if '.' in self.text: + s = re.sub(pat, '', substring) + else: + s = '.'.join([re.sub(pat, '', s) for s in substring.split('.', 1)]) + return s + + def insert_text(self, substring, from_undo=False): + s = self.filter(substring) + return StrInput.insert_text(self,s, from_undo=from_undo) + +class AmountInput(FloatInput): + def filter(self,substring): + s = super(AmountInput,self).filter(substring) + a = s.split('.') + b = a[0] + if len(b)>3: + k = [] + while len(b)>3: + x = b[-3:] + k.insert(0,x) + b = b[:-3] + a[0] = ','.join(k) + s = '.'.join(a) + return '.'.join(a) + + def insert_text(self, substring, from_undo=False): + s = self.filter(substring) + return StrInput.insert_text(self,s, from_undo=from_undo) + +class MyDropDown(DropDown): + def __init__(self,**kw): + super(MyDropDown,self).__init__() + self.options = kw + self.textField = kw.get('textField',None) + self.valueField = kw.get('valueField',None) + if kw.get('url') is not None: + self.url = kw.get('url') + self.setDataByUrl(self.url) + else: + self.si_data = kw.get('data') + self.setData(self.si_data) + self.bind(on_select=lambda instance, x: self.selectfunc(x)) + + def selectfunc(self,v): + f = self.options.get('on_select') + if f is not None: + return f(v) + + def getTextByValue(self,v): + for d in self.si_data: + if d[self.valueField] == v: + return d[self.textField] + return str(v) + + def getValueByText(self,v): + for d in self.si_data: + if d[self.textField] == v: + return d[self.valueField] + return '' + + def setData(self,data): + self.si_data = data + self.clear_widgets() + for d in data: + dd = (d[self.valueField],d[self.textField]) + b = Button(text=d[self.textField],font_size=CSize(1), + size_hint_y=None, + height=CSize(1.8)) + setattr(b,'kw_data',dd) + b.bind(on_release=lambda btn: self.select(btn.kw_data)) + self.add_widget(b) + #print(dd) + + def setDataByUrl(self,url,params={}): + def x(obj,resp): + if resp.status_code == 200: + d = resp.json() + self.setData(d) + + app = App.get_running_app() + app.hc.get(url,params=params,callback=x) + + + def showme(self,w): + #print('show it ',w) + self.target = w + self.open(w) + +class SelectInput(BoxLayout): + def __init__(self,**kw): + super(SelectInput,self).__init__(orientation='horizontal', size_hint_y=None,height=CSize(1.8)) + self.tinp = StrInput() + self.tinp.readonly = True + newkw = {} + newkw.update(kw) + newkw.update({'on_select':self.setData}) + self.dropdown = MyDropDown(**newkw) + if kw.get('value'): + self.si_data = kw.get('value') + self.text = self.dropdown.getTextByValue(self.si_data) + else: + self.si_data = '' + self.text = '' + self.add_widget(self.tinp) + self.tinp.bind(focus=self.showDropdown) + + def showDropdown(self,instance,yn): + # if self.collide_point(*touch.pos): + if yn: + self.tinp.focus = False + self.dropdown.showme(self) + + def setData(self,d): + self.tinp.si_data = d[0] + self.tinp.text = d[1] + + def setValue(self,v): + self.tinp.si_value = v + self.tinp.text = self.dropdown.getTextByValue(v) + + def getValue(self): + return self.tinp.si_value + +if __name__ == '__main__': + from kivy.app import App + from kivy.uix.boxlayout import BoxLayout + class MyApp(App): + def build(self): + root = BoxLayout(orientation='vertical') + x = SelectInput(width=CSize(15),value='1',data=[{'value':'1','text':'ban'},{'value':'0','text':'nu'}],textField='text',valueField='value') + root.add_widget(x) + b = Button(text='drop', size_hint=(None, None)) + root.add_widget(b) + dd = MyDropDown(width=CSize(15),value='1',data=[{'value':'1','text':'nan'},{'value':'0','text':'nu'}],textField='text',valueField='value',on_select=x.setData) + b.bind(on_release=dd.showme) + return root + MyApp().run() + + diff --git a/kivyblocks/widgetExt/jsoncodeinput.py b/kivyblocks/widgetExt/jsoncodeinput.py new file mode 100755 index 0000000..7fbac1a --- /dev/null +++ b/kivyblocks/widgetExt/jsoncodeinput.py @@ -0,0 +1,6 @@ +from pygments.lexers.data import JsonLexer +from kivy.uix.codeinput import CodeInput + +class JsonCodeInput(CodeInput): + def __init__(self,**kw): + super(JsonCodeInput,self).__init__(lexer = JsonLexer()) \ No newline at end of file diff --git a/kivyblocks/widgetExt/messager.py b/kivyblocks/widgetExt/messager.py new file mode 100644 index 0000000..3918dd5 --- /dev/null +++ b/kivyblocks/widgetExt/messager.py @@ -0,0 +1,27 @@ +from kivy.uix.popup import Popup +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.textinput import TextInput +from appPublic.Singleton import SingletonDecorator + +@SingletonDecorator +class Messager: + def __init__(self): + self.w = Popup(content=BoxLayout(orientation='vertical'), + title="Error info",size_hint=(0.8,0.8)) + self.messager = TextInput(size=self.w.content.size, + multiline=True,readonly=True) + self.w.content.add_widget(self.messager) + + def show_error(self,e): + self.w.title = "error" + self.messager.text = str(e) + self.w.open() + + def show_info(self,info): + self.w.title = "info" + self.messager.text = str(info) + self.w.open() + + def hide_error(self): + self.w.dismiss() + diff --git a/kivyblocks/widgetExt/phonebutton.py b/kivyblocks/widgetExt/phonebutton.py new file mode 100644 index 0000000..3922c8e --- /dev/null +++ b/kivyblocks/widgetExt/phonebutton.py @@ -0,0 +1,31 @@ +from kivy.uix.button import Button +from jnius import autoclass +from jnius import cast +from .messager import Messager + +class PhoneButton(Button): + def __init__(self,**kw): + self.options = kw + self.phone_number = kw.get('phone_number') + if self.phone_number is None: + raise Exception('PhoneButton,miss phone_number') + + del self.options['phone_number'] + self.options.update({'text':'call'}) + super(PhoneButton,self).__init__(**self.options) + self.bind(on_release=self.makecall) + + def makecall(self,inst): + try: + self.disabled = True + Intent = autoclass('android.content.Intent') + Uri = autoclass('android.net.Uri') + PythonActivity = autoclass('org.renpy.android.PythonActivity') + intent = Intent(Intent.ACTION_CALL) + intent.setData(Uri.parse("tel:" + self.phone_number)) + currentActivity = cast('android.app.Activity', + PythonActivity.mActivity) + currentActivity.startActivity(intent) + except Exception as e: + msger = Messager() + msger.show_error(e) diff --git a/kivyblocks/widgetExt/scrollwidget.py b/kivyblocks/widgetExt/scrollwidget.py new file mode 100755 index 0000000..a070917 --- /dev/null +++ b/kivyblocks/widgetExt/scrollwidget.py @@ -0,0 +1,46 @@ +from kivy.uix.scrollview import ScrollView +from kivy.uix.gridlayout import GridLayout +from kivy.uix.boxlayout import BoxLayout +from kivy.graphics import Color, Ellipse,Rectangle + +class ScrollWidget(ScrollView): + def __init__(self,**kw): + self._inner = BoxLayout(orientation='vertical',padding=2, + spacing=2,size_hint=(None,None)) + super(ScrollWidget,self).__init__(**kw) + self._inner.bind( + minimum_height=self._inner.setter('height')) + self._inner.bind( + minimum_width=self._inner.setter('width')) + super(ScrollWidget,self).add_widget(self._inner) + + def add_widget(self,widget,**kw): + a = self._inner.add_widget(widget,**kw) + return a + + def clear_widgets(self,**kw): + a = self._inner.clear_widgets(**kw) + + def remove_widget(self,widget,**kw): + a = self._inner.remove_widget(widget,**kw) + return a + +if __name__ == '__main__': + from kivy.app import App + from kivy.uix.label import Label + from kivy.uix.button import Button + import codecs + + class MyApp(App): + def build(self): + root = ScrollWidget(size=(400,400), + pos_hint={'center_x': .5, 'center_y': .5} + ) + with codecs.open(__file__,'r','utf-8') as f: + txt = f.read() + lines = txt.split('\n') + for l in lines: + root.add_widget(Label(text=l,color=(1,1,1,1),size_hint=(None,None),size=(1200,40))) + return root + + MyApp().run() diff --git a/kivyblocks/widgetExt/sqldataloadder.py b/kivyblocks/widgetExt/sqldataloadder.py new file mode 100755 index 0000000..4640805 --- /dev/null +++ b/kivyblocks/widgetExt/sqldataloadder.py @@ -0,0 +1,32 @@ +from .dataloader import DataLoader +from appPublic.sqlorAPI import runSQLIterator,DBPools,runSQLPaging +from twisted.internet import defer, reactor + + +class SQLDataLoader(DataLoader): + def __init__(self,dbdesc,**kw): + self.dbdesc = dbdesc + super(DataLoader,self).__init__(**kw) + + def _run(self,ns): + @runSQLIterator + def _sql(db,ns): + return self.dbdesc['sqldesc'] + + d = [ i for i in self._sql(self.dbdesc['db'],ns) ] + return d + + def loadData(self,ns): + d = defer.maybeDeferred(self._run,ns) + d.callback(self.dataLaoded) + return d + +class SQLPaggingLoader(SQLDataLoader): + def _run(self,ns): + @runSQLIterator + def _sql(db,ns): + return self.dbdesc['sqldesc'] + + d = self._sql(self.dbdesc['db'],ns) + return d + \ No newline at end of file diff --git a/kivyblocks/widgetExt/tree.py b/kivyblocks/widgetExt/tree.py new file mode 100644 index 0000000..9ee9ec9 --- /dev/null +++ b/kivyblocks/widgetExt/tree.py @@ -0,0 +1,24 @@ +""" +tree description json format: +{ + "url":a http(s) url for get data, + "checkbox":boolean, show a checkbox before text + "data":if undefined url,use data to construct the tree + "params":parameters attached to the http(s) request via url + "headers":headers need to set for the http request via url + "height":widget 's height + "width":widget's width +} + +data structure : +{ + "id":identified field, + "widgettype":widget type, + ...other data... + "__children__":[ + ] +} +""" +from .scrollwidget import ScrollWidget +from kivy.uix.treeview import TreeView + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..49a746f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +jinja2 +kivy +ffpyplayer +pillow +requests +git+https://github.com/yumoqing/kivycalendar +git+https://github.com/yumoqing/appPublic +git+https://github.com/yumoqing/sqlor +git+https://github.com/yumoqing/ahserver diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..ff2ba46 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + + +from distutils.core import setup +from setuptools import setup, find_packages + +# usage: +# python setup.py bdist_wininst generate a window executable file +# python setup.py bdist_egg generate a egg file +# Release information about eway + +version = "0.0.4" +description = "kivy blocks is a tool to build kivy ui with json format uidesc files" +author = "yumoqing" +email = "yumoqing@icloud.com" + +packages=find_packages() +package_data = { +"kivyblocks":['imgs/*.png', 'imgs/*.gif','imgs/*.jpg','ttf/*.ttf', 'ui/*.uidesc' ], +} + +setup( + name="kivyblocks", + version=version, + + # uncomment the following lines if you fill them out in release.py + description=description, + author=author, + author_email=email, + + install_requires=[ + "kivy", + "appPublic", + "sqlor" + ], + packages=packages, + package_data=package_data, + keywords = [ + ], + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + platforms= 'any' +) diff --git a/test/buildozer.spec b/test/buildozer.spec new file mode 100644 index 0000000..08696a8 --- /dev/null +++ b/test/buildozer.spec @@ -0,0 +1,305 @@ +[app] + +# (str) Title of your application +title = UI blocks + +# (str) Package name +package.name = blocks + +# (str) Package domain (needed for android/ios packaging) +package.domain = com.bsppo + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas,json + +# (list) List of inclusions using pattern matching +source.include_patterns = conf/*.json + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = 0.2 + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma separated e.g. requirements = sqlite3,kivy +requirements = pillow, ffmpeg, ffpyplayer, pygments,markupsafe,jinja2,requests,python3,kivy,git+https://github.com/yumoqing/appPublic,git+https://github.com/yumoqing/kivycalendar,git+https://github.com/yumoqing/kivyblocks + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) +orientation = all + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# change the major version of python used by the app +osx.python_version = 3 + +# Kivy version to use +osx.kivy_version = 1.9.1 + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 1 + +# (string) Presplash background color (for new android toolchain) +# Supported formats are: #RRGGBB #AARRGGBB or one of the following names: +# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, +# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, +# olive, purple, silver, teal. +#android.presplash_color = #FFFFFF + +# (list) Permissions +android.permissions = INTERNET, CAMERA, VIBRATE, CALL_PHONE, CALL_PRIVILEGED + +# (int) Target Android API, should be as high as possible. +#android.api = 27 + +# (int) Minimum API your APK will support. +#android.minapi = 21 + +# (int) Android SDK version to use +#android.sdk = 20 + +# (str) Android NDK version to use +#android.ndk = 17c + +# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. +#android.ndk_api = 21 + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (bool) If True, then automatically accept SDK license +# agreements. This is intended for automation only. If set to False, +# the default, you will be shown the license when first running +# buildozer. +# android.accept_sdk_license = False + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) Pattern to whitelist for the whole project +#android.whitelist = + +# (str) Path to a custom whitelist file +#android.whitelist_src = + +# (str) Path to a custom blacklist file +#android.blacklist_src = + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (list) Android AAR archives to add (currently works only with sdl2_gradle +# bootstrap) +#android.add_aars = + +# (list) Gradle dependencies to add (currently works only with sdl2_gradle +# bootstrap) +#android.gradle_dependencies = + +# (list) Java classes to add as activities to the manifest. +#android.add_activites = com.example.ExampleActivity + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (str) launchMode to set for the main activity +#android.manifest.launch_mode = standard + +# (list) Android additional libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_arm64_v8a = libs/android-v8/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (list) Android shared libraries which will be added to AndroidManifest.xml using tag +#android.uses_library = + +# (str) Android logcat filters to use +#android.logcat_filters = *:S python:D + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 +android.arch = armeabi-v7a + +# +# Python for android (p4a) specific +# + +# (str) python-for-android fork to use, defaults to upstream (kivy) +#p4a.fork = kivy + +# (str) python-for-android branch to use, defaults to master +#p4a.branch = master + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#p4a.source_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes = + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../kivy-ios +# Alternately, specify the URL and branch of a git checkout: +ios.kivy_ios_url = https://github.com/kivy/kivy-ios +ios.kivy_ios_branch = master + +# Another platform dependency: ios-deploy +# Uncomment to use a custom checkout +#ios.ios_deploy_dir = ../ios_deploy +# Or specify URL and branch +ios.ios_deploy_url = https://github.com/phonegap/ios-deploy +ios.ios_deploy_branch = 1.7.0 + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +build_dir = /home/ymq/.buildozer_build + +# (str) Path to build output (i.e. .apk, .ipa) storage +bin_dir = /home/ymq/pydev/github/blockserver/wwwroot/android_apks + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/test/conf/config.json b/test/conf/config.json new file mode 100644 index 0000000..efcd31e --- /dev/null +++ b/test/conf/config.json @@ -0,0 +1,21 @@ +{ + "font_sizes":{ + "smallest":1.5, + "small":2.5, + "normal":3.5, + "large":4.5, + "huge":5.5, + "hugest":6.5 + }, + "font_name":"normal", + "uihome":"http://www.bsppo.com:10080", + "udws":[ + "/udw/udws.uidesc" + ], + "root":{ + "widgettype":"urlwidget", + "options":{ + "url":"/toolpage.ui" + } + } +} diff --git a/test/main.py b/test/main.py new file mode 100644 index 0000000..b971563 --- /dev/null +++ b/test/main.py @@ -0,0 +1,19 @@ +import sys +import os +from appPublic.folderUtils import ProgramPath +from appPublic.jsonConfig import getConfig + +from kivyblocks.blocksapp import BlocksApp + +if __name__ == '__main__': + pp = ProgramPath() + workdir = pp + if len(sys.argv) > 1: + workdir = sys.argv[1] + print('ProgramPath=',pp,'workdir=',workdir) + + config = getConfig(workdir,NS={'workdir':workdir,'ProgramPath':pp}) + myapp = BlocksApp() + myapp.run() + myapp.workers.running = False + diff --git a/test/rv.py b/test/rv.py new file mode 100644 index 0000000..b7620be --- /dev/null +++ b/test/rv.py @@ -0,0 +1,76 @@ +from kivy.app import App +from kivy.uix.gridlayout import GridLayout +from kivy.uix.image import Image +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label +from kivy.uix.button import Button +from kivyblocks.utils import CSize +from kivyblocks.widgetExt.scrollwidget import ScrollWidget +from kivy.uix.scrollview import ScrollView +from kivy.clock import Clock +from kivyblocks.responsivelayout import VResponsiveLayout + +class VGridLayout(GridLayout): + def __init__(self,**kw): + kwargs = kw.copy() + if kwargs.get('box_width'): + del kwargs['box_width'] + if not kwargs.get('cols'): + kwargs['cols'] = 1 + self.box_width = CSize(kw.get('box_width',0)) + self.options = kw + super().__init__(**kwargs) + self.setColsTask = None + + def on_size(self,o,s): + if self.setColsTask is not None: + self.setColsTask.cancel() + self.setColsTask = Clock.schedule_once(self.setCols,0.2) + + def setCols(self,t): + if self.box_width == 0: + self.box_width = self.width / self.options.get('cols',1) + else: + self.cols = int(self.width / self.box_width) + + def setBoxWidth(self,w): + self.box_width = w + +class ResponsiveLayout(ScrollView): + def __init__(self, cols=2, box_width=15, **options): + self.options = options + super().__init__(**options) + self._inner = VGridLayout(cols=cols,box_width=box_width) + +class Box(BoxLayout): + def __init__(self,**kw): + super().__init__(**kw) + self.size_hint = (None, None) + self.height = CSize(kw.get('height',6)) + self.width = CSize(kw.get('width',8)) + +class MyApp(App): + def build(self): + r = BoxLayout(orientation='vertical') + s = ResponsiveLayout(box_width=15,size_hint=(1,1)) + b = Button(text='add box', font_size=CSize(2), + size_hint_y=None, + height=CSize(2.8) + ) + r.add_widget(b) + r.add_widget(s) + self.l = s + self.box_cnt = 0 + b.bind(on_press=self.add_box) + return r + + def add_box(self,o): + box = Box(width=15) + t = Label(text='box ' + str(self.box_cnt)) + box.add_widget(t) + self.l.add_widget(box) + self.box_cnt += 1 + +if __name__ == '__main__': + MyApp().run() + diff --git a/test/scroll.py b/test/scroll.py new file mode 100644 index 0000000..5180248 --- /dev/null +++ b/test/scroll.py @@ -0,0 +1,61 @@ +from kivy.app import App +from kivy.uix.scrollview import ScrollView +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label + +text=""" +The Widget class is the base class required for creating Widgets. This widget class was designed with a couple of principles in mind: + + Event Driven + + Widget interaction is built on top of events that occur. If a property changes, the widget can respond to the change in the ‘on_’ callback. If nothing changes, nothing will be done. That’s the main goal of the Property class. + + Separation Of Concerns (the widget and its graphical representation) + + Widgets don’t have a draw() method. This is done on purpose: The idea is to allow you to create your own graphical representation outside the widget class. Obviously you can still use all the available properties to do that, so that your representation properly reflects the widget’s current state. Every widget has its own Canvas that you can use to draw. This separation allows Kivy to run your application in a very efficient manner. + + Bounding Box / Collision + + Often you want to know if a certain point is within the bounds of your widget. An example would be a button widget where you only want to trigger an action when the button itself is actually touched. For this, you can use the collide_point() method, which will return True if the point you pass to it is inside the axis-aligned bounding box defined by the widget’s position and size. If a simple AABB is not sufficient, you can override the method to perform the collision checks with more complex shapes, e.g. a polygon. You can also check if a widget collides with another widget with collide_widget(). + +We also have some default values and behaviors that you should be aware of: + + A Widget is not a Layout: it will not change the position or the size of its children. If you want control over positioning or sizing, use a Layout. + The default size of a widget is (100, 100). This is only changed if the parent is a Layout. For example, if you add a Label inside a Button, the label will not inherit the button’s size or position because the button is not a Layout: it’s just another Widget. + The default size_hint is (1, 1). If the parent is a Layout, then the widget size will be the parent layout’s size. + on_touch_down(), on_touch_move(), on_touch_up() don’t do any sort of collisions. If you want to know if the touch is inside your widget, use collide_point(). + +""" +class MyApp(App): + def build(self): + x = ScrollView() + b = BoxLayout(orientation="vertical", + size_hint=(None,None) + ) + b.bind(minimum_height=b.setter('height')) + b.bind(minimum_width=b.setter('width')) + self.textout(b) + x.do_scroll_x = True + x.do_scroll_y = True + x.scroll_x = 1 + x.scroll_y = 1 + print(b.size,x.size) + x.add_widget(b) + return x + + def textout(self,b): + txt = text * 50 + maxwidth = 0 + for l in txt.split('\n'): + l = Label(text=l,text_size=(None,40),size_hint=(None,None),height=40) + l.width = l.texture_size[0] + l.valign = "middle" + l.align = "left" + print(l.width) + if maxwidth < l.width: + maxwidth = l.width + b.add_widget(l) + b.width = 4500 + + +MyApp().run() diff --git a/test/ttdg.py b/test/ttdg.py new file mode 100644 index 0000000..508265a --- /dev/null +++ b/test/ttdg.py @@ -0,0 +1,384 @@ +import os +import sys +from functools import partial +from kivyblocks.dg import DataGrid +from kivy.app import App +from kivy.clock import Clock +from appPublic.folderUtils import ProgramPath +from appPublic.jsonConfig import getConfig +from appPublic.timecost import TimeCost +from kivyblocks.blocksapp import BlocksApp + +if __name__ == '__main__': + pp = ProgramPath() + workdir = pp + if len(sys.argv) > 1: + workdir = sys.argv[1] + print('ProgramPath=',pp,'workdir=',workdir) + + config = getConfig(workdir,NS={'workdir':workdir,'ProgramPath':pp}) + + + desc = { + "paging":True, + "fields":[ + { + "freeze":True, + "width":8, + "name":"name", + "label":"Name", + "datatype":"str" + }, + { + "width":50, + "name":"subject", + "label":"Subject", + "datatype":"str" + }, + { + "width":10, + "name":"age", + "label":"Age", + "datatype":"int" + }, + { + "width":10, + "name":"gender", + "label":"Gender", + "datatype":"int" + }, + { + "width":10, + "name":"grade", + "label":"Grade", + "datatype":"str" + }, + { + "width":10, + "name":"since", + "label":"Since", + "datatype":"date" + } + ] + } + data = """name1 subject1 34 1 1 1992 +name2 subject2 34 1 1 1992 +name3 subject3 34 1 1 1992 +name4 subject4 34 1 1 1992 +name5 subject5 34 1 1 1992 +name6 subject6 34 1 1 1992 +name7 subject7 34 1 1 1992 +name8 subject8 34 1 1 1992 +name9 subject9 34 1 1 1992 +name10 subject10 34 1 1 1992 +name11 subject11 34 1 1 1992 +name12 subject12 34 1 1 1992 +name13 subject13 34 1 1 1992 +name14 subject14 34 1 1 1992 +name15 subject15 34 1 1 1992 +name16 subject16 34 1 1 1992 +name17 subject17 34 1 1 1992 +name18 subject18 34 1 1 1992 +name19 subject19 34 1 1 1992 +name20 subject20 34 1 1 1992 +name21 subject21 34 1 1 1992 +name22 subject22 34 1 1 1992 +name23 subject23 34 1 1 1992 +name24 subject24 34 1 1 1992 +name25 subject25 34 1 1 1992 +name26 subject26 34 1 1 1992 +name27 subject27 34 1 1 1992 +name28 subject28 34 1 1 1992 +name29 subject29 34 1 1 1992 +name30 subject30 34 1 1 1992 +name31 subject31 34 1 1 1992 +name32 subject32 34 1 1 1992 +name33 subject33 34 1 1 1992 +name34 subject34 34 1 1 1992 +name35 subject35 34 1 1 1992 +name36 subject36 34 1 1 1992 +name37 subject37 34 1 1 1992 +name38 subject38 34 1 1 1992 +name39 subject39 34 1 1 1992 +name40 subject40 34 1 1 1992 +name41 subject41 34 1 1 1992 +name42 subject42 34 1 1 1992 +name43 subject43 34 1 1 1992 +name44 subject44 34 1 1 1992 +name45 subject45 34 1 1 1992 +name46 subject46 34 1 1 1992 +name47 subject47 34 1 1 1992 +name48 subject48 34 1 1 1992 +name49 subject49 34 1 1 1992 +name50 subject50 34 1 1 1992 +name51 subject51 34 1 1 1992 +name52 subject52 34 1 1 1992 +name53 subject53 34 1 1 1992 +name54 subject54 34 1 1 1992 +name55 subject55 34 1 1 1992 +name56 subject56 34 1 1 1992 +name57 subject57 34 1 1 1992 +name58 subject58 34 1 1 1992 +name59 subject59 34 1 1 1992 +name60 subject60 34 1 1 1992 +name61 subject61 34 1 1 1992 +name62 subject62 34 1 1 1992 +name63 subject63 34 1 1 1992 +name64 subject64 34 1 1 1992 +name65 subject65 34 1 1 1992 +name66 subject66 34 1 1 1992 +name67 subject67 34 1 1 1992 +name68 subject68 34 1 1 1992 +name69 subject69 34 1 1 1992 +name70 subject70 34 1 1 1992 +name71 subject71 34 1 1 1992 +name72 subject72 34 1 1 1992 +name73 subject73 34 1 1 1992 +name74 subject74 34 1 1 1992 +name75 subject75 34 1 1 1992 +name76 subject76 34 1 1 1992 +name77 subject77 34 1 1 1992 +name78 subject78 34 1 1 1992 +name79 subject79 34 1 1 1992 +name80 subject80 34 1 1 1992 +name81 subject81 34 1 1 1992 +name82 subject82 34 1 1 1992 +name83 subject83 34 1 1 1992 +name84 subject84 34 1 1 1992 +name85 subject85 34 1 1 1992 +name86 subject86 34 1 1 1992 +name87 subject87 34 1 1 1992 +name88 subject88 34 1 1 1992 +name89 subject89 34 1 1 1992 +name90 subject90 34 1 1 1992 +name91 subject91 34 1 1 1992 +name92 subject92 34 1 1 1992 +name93 subject93 34 1 1 1992 +name94 subject94 34 1 1 1992 +name95 subject95 34 1 1 1992 +name96 subject96 34 1 1 1992 +name97 subject97 34 1 1 1992 +name98 subject98 34 1 1 1992 +name99 subject99 34 1 1 1992 +name100 subject100 34 1 1 1992 +name101 subject101 34 1 1 1992 +name102 subject102 34 1 1 1992 +name103 subject103 34 1 1 1992 +name104 subject104 34 1 1 1992 +name105 subject105 34 1 1 1992 +name106 subject106 34 1 1 1992 +name107 subject107 34 1 1 1992 +name108 subject108 34 1 1 1992 +name109 subject109 34 1 1 1992 +name110 subject110 34 1 1 1992 +name111 subject111 34 1 1 1992 +name112 subject112 34 1 1 1992 +name113 subject113 34 1 1 1992 +name114 subject114 34 1 1 1992 +name115 subject115 34 1 1 1992 +name116 subject116 34 1 1 1992 +name117 subject117 34 1 1 1992 +name118 subject118 34 1 1 1992 +name119 subject119 34 1 1 1992 +name120 subject120 34 1 1 1992 +name121 subject121 34 1 1 1992 +name122 subject122 34 1 1 1992 +name123 subject123 34 1 1 1992 +name124 subject124 34 1 1 1992 +name125 subject125 34 1 1 1992 +name126 subject126 34 1 1 1992 +name127 subject127 34 1 1 1992 +name128 subject128 34 1 1 1992 +name129 subject129 34 1 1 1992 +name130 subject130 34 1 1 1992 +name131 subject131 34 1 1 1992 +name132 subject132 34 1 1 1992 +name133 subject133 34 1 1 1992 +name134 subject134 34 1 1 1992 +name135 subject135 34 1 1 1992 +name136 subject136 34 1 1 1992 +name137 subject137 34 1 1 1992 +name138 subject138 34 1 1 1992 +name139 subject139 34 1 1 1992 +name140 subject140 34 1 1 1992 +name141 subject141 34 1 1 1992 +name142 subject142 34 1 1 1992 +name143 subject143 34 1 1 1992 +name144 subject144 34 1 1 1992 +name145 subject145 34 1 1 1992 +name146 subject146 34 1 1 1992 +name147 subject147 34 1 1 1992 +name148 subject148 34 1 1 1992 +name149 subject149 34 1 1 1992 +name150 subject150 34 1 1 1992 +name151 subject151 34 1 1 1992 +name152 subject152 34 1 1 1992 +name153 subject153 34 1 1 1992 +name154 subject154 34 1 1 1992 +name155 subject155 34 1 1 1992 +name156 subject156 34 1 1 1992 +name157 subject157 34 1 1 1992 +name158 subject158 34 1 1 1992 +name159 subject159 34 1 1 1992 +name160 subject160 34 1 1 1992 +name161 subject161 34 1 1 1992 +name162 subject162 34 1 1 1992 +name163 subject163 34 1 1 1992 +name164 subject164 34 1 1 1992 +name165 subject165 34 1 1 1992 +name166 subject166 34 1 1 1992 +name167 subject167 34 1 1 1992 +name168 subject168 34 1 1 1992 +name169 subject169 34 1 1 1992 +name170 subject170 34 1 1 1992 +name171 subject171 34 1 1 1992 +name172 subject172 34 1 1 1992 +name173 subject173 34 1 1 1992 +name174 subject174 34 1 1 1992 +name175 subject175 34 1 1 1992 +name176 subject176 34 1 1 1992 +name177 subject177 34 1 1 1992 +name178 subject178 34 1 1 1992 +name179 subject179 34 1 1 1992 +name180 subject180 34 1 1 1992 +name181 subject181 34 1 1 1992 +name182 subject182 34 1 1 1992 +name183 subject183 34 1 1 1992 +name184 subject184 34 1 1 1992 +name185 subject185 34 1 1 1992 +name186 subject186 34 1 1 1992 +name187 subject187 34 1 1 1992 +name188 subject188 34 1 1 1992 +name189 subject189 34 1 1 1992 +name190 subject190 34 1 1 1992 +name191 subject191 34 1 1 1992 +name192 subject192 34 1 1 1992 +name193 subject193 34 1 1 1992 +name194 subject194 34 1 1 1992 +name195 subject195 34 1 1 1992 +name196 subject196 34 1 1 1992 +name197 subject197 34 1 1 1992 +name198 subject198 34 1 1 1992 +name199 subject199 34 1 1 1992 +name200 subject200 34 1 1 1992 +name201 subject201 34 1 1 1992 +name202 subject202 34 1 1 1992 +name203 subject203 34 1 1 1992 +name204 subject204 34 1 1 1992 +name205 subject205 34 1 1 1992 +name206 subject206 34 1 1 1992 +name207 subject207 34 1 1 1992 +name208 subject208 34 1 1 1992 +name209 subject209 34 1 1 1992 +name210 subject210 34 1 1 1992 +name211 subject211 34 1 1 1992 +name212 subject212 34 1 1 1992 +name213 subject213 34 1 1 1992 +name214 subject214 34 1 1 1992 +name215 subject215 34 1 1 1992 +name216 subject216 34 1 1 1992 +name217 subject217 34 1 1 1992 +name218 subject218 34 1 1 1992 +name219 subject219 34 1 1 1992 +name220 subject220 34 1 1 1992 +name221 subject221 34 1 1 1992 +name222 subject222 34 1 1 1992 +name223 subject223 34 1 1 1992 +name224 subject224 34 1 1 1992 +name225 subject225 34 1 1 1992 +name226 subject226 34 1 1 1992 +name227 subject227 34 1 1 1992 +name228 subject228 34 1 1 1992 +name229 subject229 34 1 1 1992 +name230 subject230 34 1 1 1992 +name231 subject231 34 1 1 1992 +name232 subject232 34 1 1 1992 +name233 subject233 34 1 1 1992 +name234 subject234 34 1 1 1992 +name235 subject235 34 1 1 1992 +name236 subject236 34 1 1 1992 +name237 subject237 34 1 1 1992 +name238 subject238 34 1 1 1992 +name239 subject239 34 1 1 1992 +name240 subject240 34 1 1 1992 +name241 subject241 34 1 1 1992 +name242 subject242 34 1 1 1992 +name243 subject243 34 1 1 1992 +name244 subject244 34 1 1 1992 +name245 subject245 34 1 1 1992 +name246 subject246 34 1 1 1992 +name247 subject247 34 1 1 1992 +name248 subject248 34 1 1 1992 +name249 subject249 34 1 1 1992 +name250 subject250 34 1 1 1992 +name251 subject251 34 1 1 1992 +name252 subject252 34 1 1 1992 +name253 subject253 34 1 1 1992 +name254 subject254 34 1 1 1992 +name255 subject255 34 1 1 1992 +name256 subject256 34 1 1 1992 +name257 subject257 34 1 1 1992 +name258 subject258 34 1 1 1992 +name259 subject259 34 1 1 1992 +name260 subject260 34 1 1 1992 +name261 subject261 34 1 1 1992 +name262 subject262 34 1 1 1992 +name263 subject263 34 1 1 1992 +name264 subject264 34 1 1 1992 +name265 subject265 34 1 1 1992 +name266 subject266 34 1 1 1992 +name267 subject267 34 1 1 1992 +name268 subject268 34 1 1 1992 +name269 subject269 34 1 1 1992 +name270 subject270 34 1 1 1992 +name271 subject271 34 1 1 1992 +name272 subject272 34 1 1 1992 +name273 subject273 34 1 1 1992 +name274 subject274 34 1 1 1992 +name275 subject275 34 1 1 1992 +name276 subject276 34 1 1 1992 +name277 subject277 34 1 1 1992 +name278 subject278 34 1 1 1992 +name279 subject279 34 1 1 1992 +name280 subject280 34 1 1 1992 +name281 subject281 34 1 1 1992 +name282 subject282 34 1 1 1992 +name283 subject283 34 1 1 1992 +name284 subject284 34 1 1 1992 +name285 subject285 34 1 1 1992 +name286 subject286 34 1 1 1992 +name287 subject287 34 1 1 1992 +name288 subject288 34 1 1 1992 +name289 subject289 34 1 1 1992 +name290 subject290 34 1 1 1992 +name291 subject291 34 1 1 1992 +name292 subject292 34 1 1 1992 +name293 subject293 34 1 1 1992""" + + class MyApp(App): + def build(self): + with TimeCost('create widget') as tc: + dg = DataGrid(**desc) + Clock.schedule_once(self.loadData,1) + return dg + + def loadData(self,t=None): + d = [] + for t in data.split('\n'): + r = self.text2rec(t) + d.append(r) + with TimeCost('setData()') as tc: + self.root.setData(d) + return + + def text2rec(self,text): + d = text.split('\t') + r = {} + for i,f in enumerate(desc['fields']): + r[f['name']] = d[i] + return r + + MyApp().run() + tc = TimeCost('show') + tc.show() +