1''' Lib for Plugin 2Authors: 3 Andrey Kvichansky (kvichans on github.com) 4Version: 5 '1.0.3 2018-02-01' 6Content 7 log Logger with timing 8 get_translation i18n 9 dlg_wrapper Wrapper for dlg_custom: pack/unpack values, h-align controls 10ToDo: (see end of file) 11''' 12 13import sys, os, gettext, logging, inspect, re, subprocess 14from time import perf_counter 15import cudatext as app 16import cudax_lib as apx 17 18pass; # Logging 19pass; from pprint import pformat 20 21GAP = 5 22c13,c10,c9 = chr(13),chr(10),chr(9) 23REDUCTS = {'lb' :'label' 24 , 'ln-lb' :'linklabel' 25 , 'ed' :'edit' 26 , 'sp-ed' :'spinedit' 27 , 'me' :'memo' 28 , 'bt' :'button' 29 , 'rd' :'radio' 30 , 'ch' :'check' 31 , 'ch-bt' :'checkbutton' 32 , 'ch-gp' :'checkgroup' 33 , 'rd-gp' :'radiogroup' 34 , 'cb' :'combo' 35 , 'cb-ro' :'combo_ro' 36 , 'lbx' :'listbox' 37 , 'ch-lbx' :'checklistbox' 38 , 'lvw' :'listview' 39 , 'ch-lvw' :'checklistview' 40 , 'tabs' :'tabs' 41 } 42 43def f(s, *args, **kwargs):return s.format(*args, **kwargs) 44 45def log(msg='', *args, **kwargs): 46 if args or kwargs: 47 msg = msg.format(*args, **kwargs) 48 if Tr.tr is None: 49 Tr.tr=Tr() 50 return Tr.tr.log(msg) 51 52class Tr : 53 tr=None 54 """ Трассировщик. 55 Основной (единственный) метод: log(строка) - выводит указанную строку в лог. 56 Управляется через команды в строках для вывода. 57 Команды: 58 >> Увеличить сдвиг при выводе будущих строк (пока жив возвращенный объект) 59 (:) Начать замер нового вложенного периода, закончить когда умрет возвращенный объект 60 (== Начать замер нового вложенного периода 61 ==> Вывести длительность последнего периода 62 ==) Вывести длительность последнего периода и закончить его замер 63 =}} Отменить все замеры 64 Вызов log с командой >> (увеличить сдвиг) возвращает объект, 65 который при уничтожении уменьшит сдвиг 66 """ 67 sec_digs = 2 # Точность отображения секунд, кол-во дробных знаков 68 se_fmt = '' 69 mise_fmt = '' 70 homise_fmt = '' 71 def __init__(self, log_to_file=None) : 72 # Поля объекта 73 self.gap = '' # Отступ 74 self.tm = perf_counter() # Отметка времени о запуске 75 self.stms = [] # Отметки времени о начале замера спец.периода 76 77 if log_to_file: 78 logging.basicConfig( filename=log_to_file 79 ,filemode='w' 80 ,level=logging.DEBUG 81 ,format='%(message)s' 82 ,datefmt='%H:%M:%S' 83 ,style='%') 84 else: # to stdout 85 logging.basicConfig( stream=sys.stdout 86 ,level=logging.DEBUG 87 ,format='%(message)s' 88 ,datefmt='%H:%M:%S' 89 ,style='%') 90 # Tr() 91 def __del__(self): 92 logging.shutdown() 93 94 class TrLiver : 95 cnt = 0 96 """ Автоматически сокращает gap при уничножении 97 Показывает время своей жизни""" 98 def __init__(self, tr, ops) : 99 # Поля объекта 100 self.tr = tr 101 self.ops= ops 102 self.tm = 0 103 self.nm = Tr.TrLiver.cnt 104 if '(:)' in self.ops : 105 # Начать замер нового интервала 106 self.tm = perf_counter() 107 def log(self, msg='') : 108 if '(:)' in self.ops : 109 msg = '{}(:)=[{}]{}'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ), msg ) 110 logging.debug( self.tr.format_msg(msg, ops='') ) 111 def __del__(self) : 112 #pass; logging.debug('in del') 113 if '(:)' in self.ops : 114 msg = '{}(:)=[{}]'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ) ) 115 logging.debug( self.tr.format_msg(msg, ops='') ) 116 if '>>' in self.ops : 117 self.tr.gap = self.tr.gap[:-1] 118 119 def log(self, msg='') : 120 if '(:)' in msg : 121 Tr.TrLiver.cnt += 1 122 msg = msg.replace( '(:)', '{}(:)'.format(Tr.TrLiver.cnt) ) 123 logging.debug( self.format_msg(msg) ) 124 if '>>' in msg : 125 self.gap = self.gap + c9 126 # Создаем объект, который при разрушении сократит gap 127 if '>>' in msg or '(:)' in msg: 128 return Tr.TrLiver(self,('>>' if '>>' in msg else '')+('(:)' if '(:)' in msg else '')) 129 # return Tr.TrLiver(self,iif('>>' in msg,'>>','')+iif('(:)' in msg,'(:)','')) 130 else : 131 return self 132 # Tr.log 133 134# def format_msg(self, msg, dpth=2, ops='+fun:ln +wait==') : 135 def format_msg(self, msg, dpth=3, ops='+fun:ln +wait==') : 136 if '(==' in msg : 137 # Начать замер нового интервала 138 self.stms = self.stms + [perf_counter()] 139 msg = msg.replace( '(==', '(==[' + Tr.format_tm(0) + ']' ) 140 141 if '+fun:ln' in ops : 142 frCaller= inspect.stack()[dpth] # 0-format_msg, 1-Tr.log|Tr.TrLiver, 2-log, 3-need func 143 try: 144 cls = frCaller[0].f_locals['self'].__class__.__name__ + '.' 145 except: 146 cls = '' 147 fun = (cls + frCaller[3]).replace('.__init__','()') 148 ln = frCaller[2] 149 msg = '[{}]{}{}:{} '.format( Tr.format_tm( perf_counter() - self.tm ), self.gap, fun, ln ) + msg 150 else : 151 msg = '[{}]{}'.format( Tr.format_tm( perf_counter() - self.tm ), self.gap ) + msg 152 153 if '+wait==' in ops : 154 if ( '==)' in msg or '==>' in msg ) and len(self.stms)>0 : 155 # Закончить/продолжить замер последнего интервала и вывести его длительность 156 sign = '==)' if '==)' in msg else '==>' 157 # sign = icase( '==)' in msg, '==)', '==>' ) 158 stm = '[{}]'.format( Tr.format_tm( perf_counter() - self.stms[-1] ) ) 159 msg = msg.replace( sign, sign+stm ) 160 if '==)' in msg : 161 del self.stms[-1] 162 163 if '=}}' in msg : 164 # Отменить все замеры 165 self.stms = [] 166 167 return msg.replace('¬',c9).replace('¶',c10) 168 # Tr.format 169 170 @staticmethod 171 def format_tm(secs) : 172 """ Конвертация количества секунд в 12h34'56.78" """ 173 if 0==len(Tr.se_fmt) : 174 Tr.se_fmt = '{:'+str(3+Tr.sec_digs)+'.'+str(Tr.sec_digs)+'f}"' 175 Tr.mise_fmt = "{:2d}'"+Tr.se_fmt 176 Tr.homise_fmt = "{:2d}h"+Tr.mise_fmt 177 h = int( secs / 3600 ) 178 secs = secs % 3600 179 m = int( secs / 60 ) 180 s = secs % 60 181 return Tr.se_fmt.format(s) \ 182 if 0==h+m else \ 183 Tr.mise_fmt.format(m,s) \ 184 if 0==h else \ 185 Tr.homise_fmt.format(h,m,s) 186 # return icase( 0==h+m, Tr.se_fmt.format(s) 187 # , 0==h, Tr.mise_fmt.format(m,s) 188 # , Tr.homise_fmt.format(h,m,s) ) 189 # Tr.format_tm 190 # Tr 191 192def get_translation(plug_file): 193 ''' Part of i18n. 194 Full i18n-cycle: 195 1. All GUI-string in code are used in form 196 _('') 197 2. These string are extracted from code to 198 lang/messages.pot 199 with run 200 python.exe <python-root>\Tools\i18n\pygettext.py -p lang <plugin>.py 201 3. Poedit (or same program) create 202 <module>\lang\ru_RU\LC_MESSAGES\<module>.po 203 from (cmd "Update from POT") 204 lang/messages.pot 205 It allows to translate all "strings" 206 It creates (cmd "Save") 207 <module>\lang\ru_RU\LC_MESSAGES\<module>.mo 208 4. <module>.mo can be placed also in dir 209 CudaText\data\langpy\ru_RU\LC_MESSAGES\<module>.mo 210 The dir is used first. 211 5. get_translation uses the file to realize 212 _('') 213 ''' 214 lng = app.app_proc(app.PROC_GET_LANG, '') 215 plug_dir= os.path.dirname(plug_file) 216 plug_mod= os.path.basename(plug_dir) 217 lng_dirs= [ 218 app.app_path(app.APP_DIR_DATA) +os.sep+'langpy', 219 plug_dir +os.sep+'lang', 220 ] 221 _ = lambda x: x 222 pass; #return _ 223 for lng_dir in lng_dirs: 224 lng_mo = lng_dir+'/{}/LC_MESSAGES/{}.mo'.format(lng, plug_mod) 225 if os.path.isfile(lng_mo): 226 t = gettext.translation(plug_mod, lng_dir, languages=[lng]) 227 _ = t.gettext 228 t.install() 229 break 230 return _ 231 #def get_translation 232 233def get_desktop_environment(): 234 #From http://stackoverflow.com/questions/2035657/what-is-my-current-desktop-environment 235 # and http://ubuntuforums.org/showthread.php?t=652320 236 # and http://ubuntuforums.org/showthread.php?t=652320 237 # and http://ubuntuforums.org/showthread.php?t=1139057 238 if sys.platform in ["win32", "cygwin"]: 239 return "win" 240 elif sys.platform == "darwin": 241 return "mac" 242 else: #Most likely either a POSIX system or something not much common 243 desktop_session = os.environ.get("DESKTOP_SESSION") 244 if desktop_session is not None: #easier to match if we doesn't have to deal with character cases 245 desktop_session = desktop_session.lower() 246 if desktop_session in ["gnome","unity", "cinnamon", "mate", "xfce4", "lxde", "fluxbox", 247 "blackbox", "openbox", "icewm", "jwm", "afterstep","trinity", "kde"]: 248 return desktop_session 249 ## Special cases ## 250 # Canonical sets $DESKTOP_SESSION to Lubuntu rather than LXDE if using LXDE. 251 # There is no guarantee that they will not do the same with the other desktop environments. 252 elif "xfce" in desktop_session or desktop_session.startswith("xubuntu"): 253 return "xfce4" 254 elif desktop_session.startswith("ubuntu"): 255 return "unity" 256 elif desktop_session.startswith("lubuntu"): 257 return "lxde" 258 elif desktop_session.startswith("kubuntu"): 259 return "kde" 260 elif desktop_session.startswith("razor"): # e.g. razorkwin 261 return "razor-qt" 262 elif desktop_session.startswith("wmaker"): # e.g. wmaker-common 263 return "windowmaker" 264 if os.environ.get('KDE_FULL_SESSION') == 'true': 265 return "kde" 266 elif os.environ.get('GNOME_DESKTOP_SESSION_ID'): 267 if not "deprecated" in os.environ.get('GNOME_DESKTOP_SESSION_ID'): 268 return "gnome2" 269 #From http://ubuntuforums.org/showthread.php?t=652320 270 elif is_running("xfce-mcs-manage"): 271 return "xfce4" 272 elif is_running("ksmserver"): 273 return "kde" 274 return "unknown" 275def is_running(process): 276 #From http://www.bloggerpolis.com/2011/05/how-to-check-if-a-process-is-running-using-python/ 277 # and http://richarddingwall.name/2009/06/18/windows-equivalents-of-ps-and-kill-commands/ 278 try: #Linux/Unix 279 s = subprocess.Popen(["ps", "axw"],stdout=subprocess.PIPE) 280 except: #Windows 281 s = subprocess.Popen(["tasklist", "/v"],stdout=subprocess.PIPE) 282 for x in s.stdout: 283 if re.search(process, str(x)): 284 return True 285 return False 286 287ENV2FITS= {'win': 288 {'check' :-2 289 ,'edit' :-3 290 ,'button' :-4 291 ,'combo_ro' :-4 292 ,'combo' :-3 293 ,'checkbutton':-4 294 ,'linklabel' : 0 295 ,'spinedit' :-3 296 } 297 ,'unity': 298 {'check' :-3 299 ,'edit' :-5 300 ,'button' :-4 301 ,'combo_ro' :-5 302 ,'combo' :-6 303 ,'checkbutton':-3 304 ,'linklabel' : 0 305 ,'spinedit' :-5 306 } 307 ,'mac': 308 {'check' :-1 309 ,'edit' :-3 310 ,'button' :-3 311 ,'combo_ro' :-2 312 ,'combo' :-3 313 ,'checkbutton':-2 314 ,'linklabel' : 0 315 ,'spinedit' : 0 ##?? 316 } 317 } 318fit_top_by_env__cash = {} 319def fit_top_by_env__clear(): 320 global fit_top_by_env__cash 321 fit_top_by_env__cash = {} 322def fit_top_by_env(what_tp, base_tp='label'): 323 """ Get "fitting" to add to top of first control to vertical align inside text with text into second control. 324 The fittings rely to platform: win, unix(kde,gnome,...), mac 325 """ 326 if what_tp==base_tp: 327 return 0 328 if (what_tp, base_tp) in fit_top_by_env__cash: 329 pass; #log('cashed what_tp, base_tp={}',(what_tp, base_tp)) 330 return fit_top_by_env__cash[(what_tp, base_tp)] 331 env = get_desktop_environment() 332 pass; #env = 'mac' 333 fit4lb = ENV2FITS.get(env, ENV2FITS.get('win')) 334 fit = 0 335 if base_tp=='label': 336 fit = apx.get_opt('dlg_wrapper_fit_va_for_'+what_tp, fit4lb.get(what_tp, 0)) 337 else: 338 fit = fit_top_by_env(what_tp) - fit_top_by_env(base_tp) 339 pass; #log('what_tp, base_tp, fit={}',(what_tp, base_tp, fit)) 340 return fit_top_by_env__cash.setdefault((what_tp, base_tp), fit) 341 #def fit_top_by_env 342 343def dlg_wrapper(title, w, h, cnts, in_vals={}, focus_cid=None): 344 """ Wrapper for dlg_custom. 345 Params 346 title, w, h Title, Width, Height 347 cnts List of static control properties 348 [{cid:'*', tp:'*', t:1,l:1,w:1,r:1,b;1,h:1, cap:'*', hint:'*', en:'0', props:'*', items:[*], valign_to:'cid'}] 349 cid (opt)(str) C(ontrol)id. Need only for buttons and controls with value (and for tid) 350 tp (str) Control types from wiki or short names 351 t (opt)(int) Top 352 tid (opt)(str) Ref to other control cid for horz-align text in both controls 353 l (int) Left 354 r,b,w,h (opt)(int) Position. w>>>r=l+w, h>>>b=t+h, b can be omitted 355 cap (str) Caption for labels and buttons 356 hint (opt)(str) Tooltip 357 en (opt)('0'|'1'|True|False) Enabled-state 358 props (opt)(str) See wiki 359 act (opt)('0'|'1'|True|False) Will close dlg when changed 360 items (str|list) String as in wiki. List structure by types: 361 [v1,v2,] For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox 362 (head, body) For listview, checklistview 363 head [(cap,width),(cap,width),] 364 body [[r0c0,r0c1,],[r1c0,r1c1,],[r2c0,r2c1,],] 365 in_vals Dict of start values for some controls 366 {'cid':val} 367 focus (opt) Control cid for start focus 368 Return 369 btn_cid Clicked/changed control cid 370 {'cid':val} Dict of new values for the same (as in_vals) controls 371 Format of values is same too. 372 [cid] List of controls with changed values 373 Short names for types 374 lb label 375 ln-lb linklabel 376 ed edit 377 sp-ed spinedit 378 me memo 379 bt button 380 rd radio 381 ch check 382 ch-bt checkbutton 383 ch-gp checkgroup 384 rd-gp radiogroup 385 cb combo 386 cb-ro combo_ro 387 lbx listbox 388 ch-lbx checklistbox 389 lvw listview 390 ch-lvw checklistview 391 Example. 392 def ask_number(ask, def_val): 393 cnts=[dict( tp='lb',tid='v',l=3 ,w=70,cap=ask) 394 ,dict(cid='v',tp='ed',t=3 ,l=73,w=70) 395 ,dict(cid='!',tp='bt',t=45 ,l=3 ,w=70,cap=_('OK'),props='1') 396 ,dict(cid='-',tp='bt',t=45 ,l=73,w=70,cap=_('Cancel'))] 397 vals={'v':def_val} 398 while True: 399 btn,vals=dlg_wrapper('Example',146,75,cnts,vals,'v') 400 if btn is None or btn=='-': return def_val 401 if not re.match(r'\d+$', vals['v']): continue 402 return vals['v'] 403 """ 404 pass; #log('in_vals={}',pformat(in_vals, width=120)) 405 cid2i = {cnt['cid']:i for i,cnt in enumerate(cnts) if 'cid' in cnt} 406 if True: 407 # Checks 408 no_tids = {cnt['tid'] for cnt in cnts if 'tid' in cnt and cnt['tid'] not in cid2i} 409 if no_tids: 410 raise Exception(f('No cid(s) for tid(s): {}', no_tids)) 411 no_vids = {cid for cid in in_vals if cid not in cid2i} 412 if no_vids: 413 raise Exception(f('No cid(s) for vals: {}', no_vids)) 414 ctrls_l = [] 415 for cnt in cnts: 416 tp = cnt['tp'] 417 tp = REDUCTS.get(tp, tp) 418 if tp=='--': 419 # Horz-line 420 t = cnt.get('t') 421 l = cnt.get('l', 0) # def: from DlgLeft 422 r = cnt.get('r', l+cnt.get('w', w)) # def: to DlgRight 423 lst = ['type=label'] 424 lst+= ['cap='+'—'*1000] 425 lst+= ['en=0'] 426 lst+= ['pos={l},{t},{r},0'.format(l=l,t=t,r=r)] 427 ctrls_l+= [chr(1).join(lst)] 428 continue#for cnt 429 430 lst = ['type='+tp] 431 # Simple props 432 for k in ['cap', 'hint', 'props']: 433 if k in cnt: 434 lst += [k+'='+str(cnt[k])] 435 # Props with preparation 436 # Position: 437 # t[op] or tid, l[eft] required 438 # w[idth] >>> r[ight ]=l+w 439 # h[eight] >>> b[ottom]=t+h 440 # b dont need for buttons, edit, labels 441 l = cnt['l'] 442 t = cnt.get('t', 0) 443 if 'tid' in cnt: 444 # cid for horz-align text 445 bs_cnt = cnts[cid2i[cnt['tid']]] 446 bs_tp = bs_cnt['tp'] 447 t = bs_cnt['t'] + fit_top_by_env(tp, REDUCTS.get(bs_tp, bs_tp)) 448# t = bs_cnt['t'] + top_plus_for_os(tp, REDUCTS.get(bs_tp, bs_tp)) 449 r = cnt.get('r', l+cnt.get('w', 0)) 450 b = cnt.get('b', t+cnt.get('h', 0)) 451 lst += ['pos={l},{t},{r},{b}'.format(l=l,t=t,r=r,b=b)] 452 if 'en' in cnt: 453 val = cnt['en'] 454 lst += ['en='+('1' if val in [True, '1'] else '0')] 455 456 if 'items' in cnt: 457 items = cnt['items'] 458 if isinstance(items, str): 459 pass 460 elif tp in ['listview', 'checklistview']: 461 # For listview, checklistview: "\t"-separated items. 462 # first item is column headers: title1+"="+size1 + "\r" + title2+"="+size2 + "\r" +... 463 # other items are data: cell1+"\r"+cell2+"\r"+... 464 # ([(hd,wd)], [[cells],[cells],]) 465 items = '\t'.join(['\r'.join(['='.join((hd,sz)) for hd,sz in items[0]])] 466 +['\r'.join(row) for row in items[1]] 467 ) 468 else: 469 # For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox: "\t"-separated lines 470 items = '\t'.join(items) 471 lst+= ['items='+items] 472 473 # Prepare val 474 if cnt.get('cid') in in_vals: 475 in_val = in_vals[cnt['cid']] 476 if False:pass 477 elif tp in ['check', 'radio', 'checkbutton'] and isinstance(in_val, bool): 478 # For check, radio, checkbutton: value "0"/"1" 479 in_val = '1' if in_val else '0' 480 elif tp=='memo': 481 # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2)) 482 if isinstance(in_val, list): 483 in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val]) 484 else: 485 in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t') 486 elif tp=='checkgroup' and isinstance(in_val, list): 487 # For checkgroup: ","-separated checks (values "0"/"1") 488 in_val = ','.join(in_val) 489 elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple): 490 # For checklistbox, checklistview: index+";"+checks 491 in_val = ';'.join( (str(in_val[0]), ','.join( in_val[1]) ) ) 492 lst+= ['val='+str(in_val)] 493 494 if 'act' in cnt: # must be last in lst 495 val = cnt['act'] 496 lst += ['act='+('1' if val in [True, '1'] else '0')] 497 pass; #log('lst={}',lst) 498 ctrls_l+= [chr(1).join(lst)] 499 #for cnt 500 pass; #log('ok ctrls_l={}',pformat(ctrls_l, width=120)) 501 502 ans = app.dlg_custom(title, w, h, '\n'.join(ctrls_l), cid2i.get(focus_cid, -1)) 503 if ans is None: return None, None, None # btn_cid, {cid:v}, [cid] 504 505 btn_i, \ 506 vals_ls = ans[0], ans[1].splitlines() 507 aid = cnts[btn_i]['cid'] 508 # Parse output values 509 an_vals = {cid:vals_ls[cid2i[cid]] for cid in in_vals} 510 for cid in an_vals: 511 cnt = cnts[cid2i[cid]] 512 tp = cnt['tp'] 513 tp = REDUCTS.get(tp, tp) 514 in_val = in_vals[cid] 515 an_val = an_vals[cid] 516 if False:pass 517 elif tp=='memo': 518 # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2)) 519 if isinstance(in_val, list): 520 an_val = [v.replace(chr(2), '\t') for v in an_val.split('\t')] 521 #in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val]) 522 else: 523 an_val = an_val.replace('\t','\n').replace(chr(2), '\t') 524 #in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t') 525 elif tp=='checkgroup' and isinstance(in_val, list): 526 # For checkgroup: ","-separated checks (values "0"/"1") 527 an_val = an_val.split(',') 528 #in_val = ','.join(in_val) 529 elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple): 530 an_val = an_val.split(';') 531 an_val = (an_val[0], an_val[1].split(',')) 532 #in_val = ';'.join(in_val[0], ','.join(in_val[1])) 533 elif isinstance(in_val, bool): 534 an_val = an_val=='1' 535 elif tp=='listview': 536 an_val = -1 if an_val=='' else int(an_val) 537 else: 538 an_val = type(in_val)(an_val) 539 an_vals[cid] = an_val 540 #for cid 541 return aid, an_vals, [cid for cid in in_vals if in_vals[cid]!=an_vals[cid]] 542 #def dlg_wrapper 543 544def get_hotkeys_desc(cmd_id, ext_id=None, keys_js=None, def_ans=''): 545 """ Read one or two hotkeys for command 546 cmd_id [+ext_id] 547 from 548 settings\keys.json 549 Return 550 def_ans If no hotkeys for the command 551 'Ctrl+Q' 552 'Ctrl+Q * Ctrl+W' If one hotkey for the command 553 'Ctrl+Q/Ctrl+T' 554 'Ctrl+Q * Ctrl+W/Ctrl+T' If two hotkeys for the command 555 """ 556 if keys_js is None: 557 keys_json = app.app_path(app.APP_DIR_SETTINGS)+os.sep+'keys.json' 558 keys_js = apx._json_loads(open(keys_json).read()) if os.path.exists(keys_json) else {} 559 560 cmd_id = f('{},{}', cmd_id, ext_id) if ext_id else cmd_id 561 if cmd_id not in keys_js: 562 return def_ans 563 cmd_keys= keys_js[cmd_id] 564 desc = '/'.join([' * '.join(cmd_keys.get('s1', [])) 565 ,' * '.join(cmd_keys.get('s2', [])) 566 ]).strip('/') 567 return desc 568 #def get_hotkeys_desc 569 570if __name__ == '__main__' : # Tests 571 def test_ask_number(ask, def_val): 572 cnts=[dict( tp='lb',tid='v',l=3 ,w=70,cap=ask) 573 ,dict(cid='v',tp='ed',t=3 ,l=73,w=70) 574 ,dict(cid='!',tp='bt',t=45 ,l=3 ,w=70,cap=_('OK'),props='1') 575 ,dict(cid='-',tp='bt',t=45 ,l=73,w=70,cap=_('Cancel'))] 576 vals={'v':def_val} 577 while True: 578 btn,vals=dlg_wrapper('Example',146,75,cnts,vals,'v') 579 if btn is None or btn=='-': return def_val 580 if not re.match(r'\d+$', vals['v']): continue 581 return vals['v'] 582 ask_number('ask_____________', '____smth') 583