1''' Lib for Plugin 2Authors: 3 Andrey Kvichansky (kvichans on github.com) 4Version: 5 '1.1.3 2021-03-08' 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, time, collections, json, 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 21odict = collections.OrderedDict 22GAP = 5 23c13,c10,c9 = chr(13),chr(10),chr(9) 24REDUCTS = {'lb' :'label' 25 , 'ln-lb' :'linklabel' 26 , 'ed' :'edit' 27 , 'sp-ed' :'spinedit' 28 , 'me' :'memo' 29 , 'bt' :'button' 30 , 'rd' :'radio' 31 , 'ch' :'check' 32 , 'ch-bt' :'checkbutton' 33 , 'ch-gp' :'checkgroup' 34 , 'rd-gp' :'radiogroup' 35 , 'cb' :'combo' 36 , 'cb-ro' :'combo_ro' 37 , 'lbx' :'listbox' 38 , 'ch-lbx' :'checklistbox' 39 , 'lvw' :'listview' 40 , 'ch-lvw' :'checklistview' 41 , 'tabs' :'tabs' 42 , 'clr' :'colorpanel' 43 } 44 45def f(s, *args, **kwargs):return s.format(*args, **kwargs) 46def to01(bool_val):return '1' if bool_val else '0' 47 48def log(msg='', *args, **kwargs): 49 if args or kwargs: 50 msg = msg.format(*args, **kwargs) 51 if Tr.tr is None: 52 Tr.tr=Tr() 53 return Tr.tr.log(msg) 54 55class Tr : 56 tr=None 57 """ Трассировщик. 58 Основной (единственный) метод: log(строка) - выводит указанную строку в лог. 59 Управляется через команды в строках для вывода. 60 Команды: 61 >> Увеличить сдвиг при выводе будущих строк (пока жив возвращенный объект) 62 (:) Начать замер нового вложенного периода, закончить когда умрет возвращенный объект 63 (== Начать замер нового вложенного периода 64 ==> Вывести длительность последнего периода 65 ==) Вывести длительность последнего периода и закончить его замер 66 =}} Отменить все замеры 67 Вызов log с командой >> (увеличить сдвиг) возвращает объект, 68 который при уничтожении уменьшит сдвиг 69 """ 70 sec_digs = 2 # Точность отображения секунд, кол-во дробных знаков 71 se_fmt = '' 72 mise_fmt = '' 73 homise_fmt = '' 74 def __init__(self, log_to_file=None) : 75 # Поля объекта 76 self.gap = '' # Отступ 77 self.tm = perf_counter() # Отметка времени о запуске 78 self.stms = [] # Отметки времени о начале замера спец.периода 79 80 if log_to_file: 81 logging.basicConfig( filename=log_to_file 82 ,filemode='w' 83 ,level=logging.DEBUG 84 ,format='%(message)s' 85 ,datefmt='%H:%M:%S' 86 ,style='%') 87 else: # to stdout 88 logging.basicConfig( stream=sys.stdout 89 ,level=logging.DEBUG 90 ,format='%(message)s' 91 ,datefmt='%H:%M:%S' 92 ,style='%') 93 # Tr() 94 def __del__(self): 95 logging.shutdown() 96 97 class TrLiver : 98 cnt = 0 99 """ Автоматически сокращает gap при уничножении 100 Показывает время своей жизни""" 101 def __init__(self, tr, ops) : 102 # Поля объекта 103 self.tr = tr 104 self.ops= ops 105 self.tm = 0 106 self.nm = Tr.TrLiver.cnt 107 if '(:)' in self.ops : 108 # Начать замер нового интервала 109 self.tm = perf_counter() 110 def log(self, msg='') : 111 if '(:)' in self.ops : 112 msg = '{}(:)=[{}]{}'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ), msg ) 113 logging.debug( self.tr.format_msg(msg, ops='') ) 114 def __del__(self) : 115 #pass; logging.debug('in del') 116 if '(:)' in self.ops : 117 msg = '{}(:)=[{}]'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ) ) 118 logging.debug( self.tr.format_msg(msg, ops='') ) 119 if '>>' in self.ops : 120 self.tr.gap = self.tr.gap[:-1] 121 122 def log(self, msg='') : 123 if '(:)' in msg : 124 Tr.TrLiver.cnt += 1 125 msg = msg.replace( '(:)', '{}(:)'.format(Tr.TrLiver.cnt) ) 126 logging.debug( self.format_msg(msg) ) 127 if '>>' in msg : 128 self.gap = self.gap + c9 129 # Создаем объект, который при разрушении сократит gap 130 if '>>' in msg or '(:)' in msg: 131 return Tr.TrLiver(self,('>>' if '>>' in msg else '')+('(:)' if '(:)' in msg else '')) 132 # return Tr.TrLiver(self,iif('>>' in msg,'>>','')+iif('(:)' in msg,'(:)','')) 133 else : 134 return self 135 # Tr.log 136 137# def format_msg(self, msg, dpth=2, ops='+fun:ln +wait==') : 138 def format_msg(self, msg, dpth=3, ops='+fun:ln +wait==') : 139 if '(==' in msg : 140 # Начать замер нового интервала 141 self.stms = self.stms + [perf_counter()] 142 msg = msg.replace( '(==', '(==[' + Tr.format_tm(0) + ']' ) 143 144 if '+fun:ln' in ops : 145 frCaller= inspect.stack()[dpth] # 0-format_msg, 1-Tr.log|Tr.TrLiver, 2-log, 3-need func 146 try: 147 cls = frCaller[0].f_locals['self'].__class__.__name__ + '.' 148 except: 149 cls = '' 150 fun = (cls + frCaller[3]).replace('.__init__','()') 151 ln = frCaller[2] 152 msg = '[{}]{}{}:{} '.format( Tr.format_tm( perf_counter() - self.tm ), self.gap, fun, ln ) + msg 153 else : 154 msg = '[{}]{}'.format( Tr.format_tm( perf_counter() - self.tm ), self.gap ) + msg 155 156 if '+wait==' in ops : 157 if ( '==)' in msg or '==>' in msg ) and len(self.stms)>0 : 158 # Закончить/продолжить замер последнего интервала и вывести его длительность 159 sign = '==)' if '==)' in msg else '==>' 160 # sign = icase( '==)' in msg, '==)', '==>' ) 161 stm = '[{}]'.format( Tr.format_tm( perf_counter() - self.stms[-1] ) ) 162 msg = msg.replace( sign, sign+stm ) 163 if '==)' in msg : 164 del self.stms[-1] 165 166 if '=}}' in msg : 167 # Отменить все замеры 168 self.stms = [] 169 170 return msg.replace('¬',c9).replace('¶',c10) 171 # Tr.format 172 173 @staticmethod 174 def format_tm(secs) : 175 """ Конвертация количества секунд в 12h34'56.78" """ 176 if 0==len(Tr.se_fmt) : 177 Tr.se_fmt = '{:'+str(3+Tr.sec_digs)+'.'+str(Tr.sec_digs)+'f}"' 178 Tr.mise_fmt = "{:2d}'"+Tr.se_fmt 179 Tr.homise_fmt = "{:2d}h"+Tr.mise_fmt 180 h = secs // 3600 181 secs = secs % 3600 182 m = secs // 60 183 s = secs % 60 184 return Tr.se_fmt.format(s) \ 185 if 0==h+m else \ 186 Tr.mise_fmt.format(m,s) \ 187 if 0==h else \ 188 Tr.homise_fmt.format(h,m,s) 189 # return icase( 0==h+m, Tr.se_fmt.format(s) 190 # , 0==h, Tr.mise_fmt.format(m,s) 191 # , Tr.homise_fmt.format(h,m,s) ) 192 # Tr.format_tm 193 # Tr 194 195def get_translation(plug_file): 196 ''' Part of i18n. 197 Full i18n-cycle: 198 1. All GUI-string in code are used in form 199 _('') 200 2. These string are extracted from code to 201 lang/messages.pot 202 with run 203 python.exe <python-root>\Tools\i18n\pygettext.py -p lang <plugin>.py 204 3. Poedit (or same program) create 205 <module>\lang\ru_RU\LC_MESSAGES\<module>.po 206 from (cmd "Update from POT") 207 lang/messages.pot 208 It allows to translate all "strings" 209 It creates (cmd "Save") 210 <module>\lang\ru_RU\LC_MESSAGES\<module>.mo 211 4. <module>.mo can be placed also in dir 212 CudaText\data\langpy\ru_RU\LC_MESSAGES\<module>.mo 213 The dir is used first. 214 5. get_translation uses the file to realize 215 _('') 216 ''' 217 lng = app.app_proc(app.PROC_GET_LANG, '') 218 plug_dir= os.path.dirname(plug_file) 219 plug_mod= os.path.basename(plug_dir) 220 lng_dirs= [ 221 app.app_path(app.APP_DIR_DATA) +os.sep+'langpy', 222 plug_dir +os.sep+'lang', 223 ] 224 _ = lambda x: x 225 pass; #return _ 226 for lng_dir in lng_dirs: 227 lng_mo = lng_dir+'/{}/LC_MESSAGES/{}.mo'.format(lng, plug_mod) 228 if os.path.isfile(lng_mo): 229 t = gettext.translation(plug_mod, lng_dir, languages=[lng]) 230 _ = t.gettext 231 t.install() 232 break 233 return _ 234 #def get_translation 235 236_ = get_translation(__file__) # I18N 237 238def get_desktop_environment(): 239 #From http://stackoverflow.com/questions/2035657/what-is-my-current-desktop-environment 240 # and http://ubuntuforums.org/showthread.php?t=652320 241 # and http://ubuntuforums.org/showthread.php?t=652320 242 # and http://ubuntuforums.org/showthread.php?t=1139057 243 if sys.platform in ["win32", "cygwin"]: 244 return "win" 245 elif sys.platform == "darwin": 246 return "mac" 247 else: #Most likely either a POSIX system or something not much common 248 desktop_session = os.environ.get("DESKTOP_SESSION") 249 if desktop_session is not None: #easier to match if we doesn't have to deal with character cases 250 desktop_session = desktop_session.lower() 251 if desktop_session in ["gnome","unity", "cinnamon", "mate", "xfce4", "lxde", "fluxbox", 252 "blackbox", "openbox", "icewm", "jwm", "afterstep","trinity", "kde"]: 253 return desktop_session 254 ## Special cases ## 255 # Canonical sets $DESKTOP_SESSION to Lubuntu rather than LXDE if using LXDE. 256 # There is no guarantee that they will not do the same with the other desktop environments. 257 elif "xfce" in desktop_session or desktop_session.startswith("xubuntu"): 258 return "xfce4" 259 elif desktop_session.startswith("ubuntu"): 260 return "unity" 261 elif desktop_session.startswith("lubuntu"): 262 return "lxde" 263 elif desktop_session.startswith("kubuntu"): 264 return "kde" 265 elif desktop_session.startswith("razor"): # e.g. razorkwin 266 return "razor-qt" 267 elif desktop_session.startswith("wmaker"): # e.g. wmaker-common 268 return "windowmaker" 269 if os.environ.get('KDE_FULL_SESSION') == 'true': 270 return "kde" 271 elif os.environ.get('GNOME_DESKTOP_SESSION_ID'): 272 if not "deprecated" in os.environ.get('GNOME_DESKTOP_SESSION_ID'): 273 return "gnome2" 274 #From http://ubuntuforums.org/showthread.php?t=652320 275 elif is_running("xfce-mcs-manage"): 276 return "xfce4" 277 elif is_running("ksmserver"): 278 return "kde" 279 return "unknown" 280def is_running(process): 281 #From http://www.bloggerpolis.com/2011/05/how-to-check-if-a-process-is-running-using-python/ 282 # and http://richarddingwall.name/2009/06/18/windows-equivalents-of-ps-and-kill-commands/ 283 try: #Linux/Unix 284 s = subprocess.Popen(["ps", "axw"],stdout=subprocess.PIPE) 285 except: #Windows 286 s = subprocess.Popen(["tasklist", "/v"],stdout=subprocess.PIPE) 287 for x in s.stdout: 288 if re.search(process, str(x)): 289 return True 290 return False 291 292ENV2FITS= {'win': 293 {'check' :-2 294 ,'edit' :-3 295 ,'button' :-4 296 ,'combo_ro' :-4 297 ,'combo' :-3 298 ,'checkbutton':-4 299 ,'linklabel' : 0 300 ,'spinedit' :-3 301 } 302 ,'unity': 303 {'check' :-3 304 ,'edit' :-5 305 ,'button' :-4 306 ,'combo_ro' :-5 307 ,'combo' :-6 308 ,'checkbutton':-3 309 ,'linklabel' : 0 310 ,'spinedit' :-5 311 } 312 ,'mac': 313 {'check' :-1 314 ,'edit' :-3 315 ,'button' :-3 316 ,'combo_ro' :-2 317 ,'combo' :-3 318 ,'checkbutton':-2 319 ,'linklabel' : 0 320 ,'spinedit' : 0 ##?? 321 } 322 } 323fit_top_by_env__cash = {} 324def fit_top_by_env__clear(): 325 global fit_top_by_env__cash 326 fit_top_by_env__cash = {} 327def fit_top_by_env(what_tp, base_tp='label'): 328 """ Get "fitting" to add to top of first control to vertical align inside text with text into second control. 329 The fittings rely to platform: win, unix(kde,gnome,...), mac 330 """ 331 if what_tp==base_tp: 332 return 0 333 if (what_tp, base_tp) in fit_top_by_env__cash: 334 pass; #log('cashed what_tp, base_tp={}',(what_tp, base_tp)) 335 return fit_top_by_env__cash[(what_tp, base_tp)] 336 env = get_desktop_environment() 337 pass; #env = 'mac' 338 fit4lb = ENV2FITS.get(env, ENV2FITS.get('win')) 339 fit = 0 340 if base_tp=='label': 341 fit = apx.get_opt('dlg_wrapper_fit_va_for_'+what_tp, fit4lb.get(what_tp, 0)) 342 else: 343 fit = fit_top_by_env(what_tp) - fit_top_by_env(base_tp) 344 pass; #log('what_tp, base_tp, fit={}',(what_tp, base_tp, fit)) 345 return fit_top_by_env__cash.setdefault((what_tp, base_tp), fit) 346 #def fit_top_by_env 347 348def dlg_wrapper(title, w, h, cnts, in_vals={}, focus_cid=None): 349 """ Wrapper for dlg_custom. 350 Params 351 title, w, h Title, Width, Height 352 cnts List of static control properties 353 [{cid:'*', tp:'*', t:1,l:1,w:1,r:1,b;1,h:1,tid:'cid', cap:'*', hint:'*', en:'0', props:'*', items:[*], act='0'}] 354 cid (opt)(str) C(ontrol)id. Need only for buttons and controls with value (and for tid) 355 tp (str) Control types from wiki or short names 356 t (opt)(int) Top 357 tid (opt)(str) Ref to other control cid for horz-align text in both controls 358 l (int) Left 359 r,b,w,h (opt)(int) Position. w>>>r=l+w, h>>>b=t+h, b can be omitted 360 cap (str) Caption for labels and buttons 361 hint (opt)(str) Tooltip 362 en (opt)('0'|'1'|True|False) Enabled-state 363 props (opt)(str) See wiki 364 act (opt)('0'|'1'|True|False) Will close dlg when changed 365 items (str|list) String as in wiki. List structure by types: 366 [v1,v2,] For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox 367 (head, body) For listview, checklistview 368 head [(cap,width),(cap,width),] 369 body [[r0c0,r0c1,],[r1c0,r1c1,],[r2c0,r2c1,],] 370 in_vals Dict of start values for some controls 371 {'cid':val} 372 focus_cid (opt) Control cid for start focus 373 Return 374 btn_cid Clicked/changed control cid 375 {'cid':val} Dict of new values for the same (as in_vals) controls 376 Format of values is same too. 377 focus_cid Focused control cid 378 [cid] List of controls with changed values 379 Short names for types 380 lb label 381 ln-lb linklabel 382 ed edit 383 sp-ed spinedit 384 me memo 385 bt button 386 rd radio 387 ch check 388 ch-bt checkbutton 389 ch-gp checkgroup 390 rd-gp radiogroup 391 cb combo 392 cb-ro combo_ro 393 lbx listbox 394 ch-lbx checklistbox 395 lvw listview 396 ch-lvw checklistview 397 Example. 398 def ask_number(ask, def_val): 399 cnts=[dict( tp='lb',tid='v',l=3 ,w=70,cap=ask) 400 ,dict(cid='v',tp='ed',t=3 ,l=73,w=70) 401 ,dict(cid='!',tp='bt',t=45 ,l=3 ,w=70,cap='OK',props='1') 402 ,dict(cid='-',tp='bt',t=45 ,l=73,w=70,cap='Cancel')] 403 vals={'v':def_val} 404 while True: 405 aid,vals,fid,chds=dlg_wrapper('Example',146,75,cnts,vals,'v') 406 if aid is None or btn=='-': return def_val 407 if not re.match(r'\d+$', vals['v']): continue 408 return vals['v'] 409 """ 410 pass; #log('in_vals={}',pformat(in_vals, width=120)) 411 cid2i = {cnt['cid']:i for i,cnt in enumerate(cnts) if 'cid' in cnt} 412 if True: 413 # Checks 414 no_tids = {cnt['tid'] for cnt in cnts if 'tid' in cnt and cnt['tid'] not in cid2i} 415 if no_tids: 416 raise Exception(f('No cid(s) for tid(s): {}', no_tids)) 417 no_vids = {cid for cid in in_vals if cid not in cid2i} 418 if no_vids: 419 raise Exception(f('No cid(s) for vals: {}', no_vids)) 420 ctrls_l = [] 421 for cnt in cnts: 422 tp = cnt['tp'] 423 tp = REDUCTS.get(tp, tp) 424 if tp=='--': 425 # Horz-line 426 t = cnt.get('t') 427 l = cnt.get('l', 0) # def: from DlgLeft 428 r = cnt.get('r', l+cnt.get('w', w)) # def: to DlgRight 429 lst = ['type=label'] 430 lst+= ['cap='+'—'*1000] 431 lst+= ['en=0'] 432 lst+= ['pos={l},{t},{r},0'.format(l=l,t=t,r=r)] 433 ctrls_l+= [chr(1).join(lst)] 434 continue#for cnt 435 436 lst = ['type='+tp] 437 # Simple props 438 for k in ['cap', 'hint']: 439 if k in cnt: 440 lst += [k+'='+str(cnt[k])] 441 # Alexey: support 'ex0'..'ex9' 442 if 'props' in cnt: 443 k = cnt['props'].split(',') 444 for (k_i, k_s) in enumerate(k): 445 lst += ['ex'+str(k_i)+'='+k_s] 446 # Props with preparation 447 # Position: 448 # t[op] or tid, l[eft] required 449 # w[idth] >>> r[ight ]=l+w 450 # h[eight] >>> b[ottom]=t+h 451 # b dont need for buttons, edit, labels 452 l = cnt['l'] 453 t = cnt.get('t', 0) 454 if 'tid' in cnt: 455 # cid for horz-align text 456 bs_cnt = cnts[cid2i[cnt['tid']]] 457 bs_tp = bs_cnt['tp'] 458 t = bs_cnt['t'] + fit_top_by_env(tp, REDUCTS.get(bs_tp, bs_tp)) 459# t = bs_cnt['t'] + top_plus_for_os(tp, REDUCTS.get(bs_tp, bs_tp)) 460 r = cnt.get('r', l+cnt.get('w', 0)) 461 b = cnt.get('b', t+cnt.get('h', 0)) 462 lst += ['pos={l},{t},{r},{b}'.format(l=l,t=t,r=r,b=b)] 463 if 'en' in cnt: 464 val = cnt['en'] 465 lst += ['en='+('1' if val in [True, '1'] else '0')] 466 467 if 'items' in cnt: 468 items = cnt['items'] 469 if isinstance(items, str): 470 pass 471 elif tp in ['listview', 'checklistview']: 472 # For listview, checklistview: "\t"-separated items. 473 # first item is column headers: title1+"="+size1 + "\r" + title2+"="+size2 + "\r" +... 474 # other items are data: cell1+"\r"+cell2+"\r"+... 475 # ([(hd,wd)], [[cells],[cells],]) 476 items = '\t'.join(['\r'.join(['='.join((hd,sz)) for hd,sz in items[0]])] 477 +['\r'.join(row) for row in items[1]] 478 ) 479 else: 480 # For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox: "\t"-separated lines 481 items = '\t'.join(items) 482 lst+= ['items='+items] 483 484 # Prepare val 485 if cnt.get('cid') in in_vals: 486 in_val = in_vals[cnt['cid']] 487 if False:pass 488 elif tp in ['check', 'radio', 'checkbutton'] and isinstance(in_val, bool): 489 # For check, radio, checkbutton: value "0"/"1" 490 in_val = '1' if in_val else '0' 491 elif tp=='memo': 492 # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2)) 493 if isinstance(in_val, list): 494 in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val]) 495 else: 496 in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t') 497 elif tp=='checkgroup' and isinstance(in_val, list): 498 # For checkgroup: ","-separated checks (values "0"/"1") 499 in_val = ','.join(in_val) 500 elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple): 501 # For checklistbox, checklistview: index+";"+checks 502 in_val = ';'.join( (str(in_val[0]), ','.join( in_val[1]) ) ) 503 lst+= ['val='+str(in_val)] 504 505 if 'act' in cnt: # must be last in lst 506 val = cnt['act'] 507 lst += ['act='+('1' if val in [True, '1'] else '0')] 508 pass; #log('lst={}',lst) 509 ctrls_l+= [chr(1).join(lst)] 510 #for cnt 511 pass; #log('ok ctrls_l={}',pformat(ctrls_l, width=120)) 512 513 ans = app.dlg_custom(title, w, h, '\n'.join(ctrls_l), cid2i.get(focus_cid, -1)) 514 if ans is None: return None, None, None, None # btn_cid, {cid:v}, focus_cid, [cid] 515 516 btn_i, \ 517 vals_ls = ans[0], ans[1].splitlines() 518 519 focus_cid = '' 520 if vals_ls[-1].startswith('focused='): 521 # From API 1.0.156 dlg_custom also returns index of active control 522 focus_n_s = vals_ls.pop() 523 focus_i = int(focus_n_s.split('=')[1]) 524 focus_cid = cnts[focus_i].get('cid', '') 525 526 act_cid = cnts[btn_i]['cid'] 527 # Parse output values 528 an_vals = {cid:vals_ls[cid2i[cid]] for cid in in_vals} 529 for cid in an_vals: 530 cnt = cnts[cid2i[cid]] 531 tp = cnt['tp'] 532 tp = REDUCTS.get(tp, tp) 533 in_val = in_vals[cid] 534 an_val = an_vals[cid] 535 if False:pass 536 elif tp=='memo': 537 # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2)) 538 if isinstance(in_val, list): 539 an_val = [v.replace(chr(2), '\t') for v in an_val.split('\t')] 540 #in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val]) 541 else: 542 an_val = an_val.replace('\t','\n').replace(chr(2), '\t') 543 #in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t') 544 elif tp=='checkgroup' and isinstance(in_val, list): 545 # For checkgroup: ","-separated checks (values "0"/"1") 546 an_val = an_val.split(',') 547 #in_val = ','.join(in_val) 548 elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple): 549 an_val = an_val.split(';') 550 an_val = (an_val[0], an_val[1].strip(',').split(',')) 551 #in_val = ';'.join(in_val[0], ','.join(in_val[1])) 552 elif isinstance(in_val, bool): 553 an_val = an_val=='1' 554 elif tp=='listview': 555 an_val = -1 if an_val=='' else int(an_val) 556 else: 557 an_val = type(in_val)(an_val) 558 an_vals[cid] = an_val 559 #for cid 560 chds = [cid for cid in in_vals if in_vals[cid]!=an_vals[cid]] 561 if focus_cid: 562 # If out focus points to button then will point to a unique changed control 563 focus_tp= cnts[cid2i[focus_cid]]['tp'] 564 focus_tp= REDUCTS.get(focus_tp, focus_tp) 565 if focus_tp in ('button'): 566 focus_cid = '' if len(chds)!=1 else chds[0] 567 return act_cid \ 568 , an_vals \ 569 , focus_cid \ 570 , chds 571 #def dlg_wrapper 572 573def dlg_valign_consts(): 574 pass; #log('ok') 575 UP = '/\\' 576 UP = '↑↑' 577# UP = 'ΛΛΛ' 578 DN = '\\/' 579 DN = '↓↓' 580# DN = 'VVV' 581 DLG_W, \ 582 DLG_H = 335, 280 583 fits = dict( 584 _sp1=fit_top_by_env('check') 585 ,_sp2=fit_top_by_env('edit') 586 ,_sp3=fit_top_by_env('button') 587 ,_sp4=fit_top_by_env('combo_ro') 588 ,_sp5=fit_top_by_env('combo') 589 ,_sp6=fit_top_by_env('checkbutton') 590 ,_sp7=fit_top_by_env('linklabel') 591 ,_sp8=fit_top_by_env('spinedit') 592 ) 593 vals = dict( 594 ch1 =False 595 ,ed2 ='=======?' 596 ,cbo4=0 597 ,cb5 ='=======?' 598 ,chb6=0 599 ,sp8 =4444444 600 ) 601 focused = '-' 602 while True: 603 aid, vals, fid, chds = dlg_wrapper(_('Adjust vertical alignments') ,DLG_W, DLG_H, 604 [dict(cid='lb1' ,tp='lb' ,t= 10 ,l= 5 ,w=100 ,cap='===============' ) 605 ,dict(cid='ch1' ,tp='ch' ,t= 10+fits['_sp1'] ,l=115 ,w=100 ,cap='=======?' ,hint=fits['_sp1'] ) 606 ,dict(cid='up1' ,tp='bt' ,t= 10-3 ,l=230 ,w=50 ,cap=UP ) 607 ,dict(cid='dn1' ,tp='bt' ,t= 10-3 ,l=280 ,w=50 ,cap=DN ) 608 609 ,dict(cid='lb2' ,tp='lb' ,t= 40 ,l= 5 ,w=100 ,cap='===============' ) 610 ,dict(cid='ed2' ,tp='ed' ,t= 40+fits['_sp2'] ,l=115 ,w=100 ,hint=fits['_sp2'] ) 611 ,dict(cid='up2' ,tp='bt' ,t= 40-3 ,l=230 ,w=50 ,cap=UP ) 612 ,dict(cid='dn2' ,tp='bt' ,t= 40-3 ,l=280 ,w=50 ,cap=DN ) 613 614 ,dict(cid='lb3' ,tp='lb' ,t= 70 ,l= 5 ,w=100 ,cap='===============' ) 615 ,dict(cid='bt3' ,tp='bt' ,t= 70+fits['_sp3'] ,l=115 ,w=100 ,cap='=======?' ,hint=fits['_sp3'] ) 616 ,dict(cid='up3' ,tp='bt' ,t= 70-3 ,l=230 ,w=50 ,cap=UP ) 617 ,dict(cid='dn3' ,tp='bt' ,t= 70-3 ,l=280 ,w=50 ,cap=DN ) 618 619 ,dict(cid='lb4' ,tp='lb' ,t=100 ,l= 5 ,w=100 ,cap='===============' ) 620 ,dict(cid='cbo4' ,tp='cb-ro' ,t=100+fits['_sp4'] ,l=115 ,w=100 ,items=['=======?'] ,hint=fits['_sp4'] ) 621 ,dict(cid='up4' ,tp='bt' ,t=100-3 ,l=230 ,w=50 ,cap=UP ) 622 ,dict(cid='dn4' ,tp='bt' ,t=100-3 ,l=280 ,w=50 ,cap=DN ) 623 624 ,dict(cid='lb5' ,tp='lb' ,t=130 ,l= 5 ,w=100 ,cap='===============' ) 625 ,dict(cid='cb5' ,tp='cb' ,t=130+fits['_sp5'] ,l=115 ,w=100 ,items=['=======?'] ,hint=fits['_sp5'] ) 626 ,dict(cid='up5' ,tp='bt' ,t=130-3 ,l=230 ,w=50 ,cap=UP ) 627 ,dict(cid='dn5' ,tp='bt' ,t=130-3 ,l=280 ,w=50 ,cap=DN ) 628 629 ,dict(cid='lb6' ,tp='lb' ,t=160 ,l= 5 ,w=100 ,cap='===============' ) 630 ,dict(cid='chb6' ,tp='ch-bt' ,t=160+fits['_sp6'] ,l=115 ,w=100 ,cap='=======?' ,hint=fits['_sp6'] ) 631 ,dict(cid='up6' ,tp='bt' ,t=160-3 ,l=230 ,w=50 ,cap=UP ) 632 ,dict(cid='dn6' ,tp='bt' ,t=160-3 ,l=280 ,w=50 ,cap=DN ) 633 634 ,dict(cid='lb7' ,tp='lb' ,t=190 ,l= 5 ,w=100 ,cap='===============' ) 635 ,dict(cid='chb7' ,tp='ln-lb' ,t=190+fits['_sp7'] ,l=115 ,w=100 ,cap='=======?',props='-' ,hint=fits['_sp7'] ) 636 ,dict(cid='up7' ,tp='bt' ,t=190-3 ,l=230 ,w=50 ,cap=UP ) 637 ,dict(cid='dn7' ,tp='bt' ,t=190-3 ,l=280 ,w=50 ,cap=DN ) 638 639 ,dict(cid='lb8' ,tp='lb' ,t=220 ,l= 5 ,w=100 ,cap='4444444444444444' ) 640 ,dict(cid='sp8' ,tp='sp-ed' ,t=220+fits['_sp8'] ,l=115 ,w=100 ,props='0,4444444,1' ,hint=fits['_sp8'] ) 641 ,dict(cid='up8' ,tp='bt' ,t=220-3 ,l=230 ,w=50 ,cap=UP ) 642 ,dict(cid='dn8' ,tp='bt' ,t=220-3 ,l=280 ,w=50 ,cap=DN ) 643 644 ,dict(cid='save' ,tp='bt' ,t=DLG_H-30 ,l=115 ,w=100 ,cap=_('&Save') 645 ,hint=_('Apply the fittings to controls of all dialogs.' 646 '\rCtrl+Click - Show data to mail report.')) 647 ,dict(cid='-' ,tp='bt' ,t=DLG_H-30 ,l=230 ,w=100 ,cap=_('Cancel') ) 648 ], vals, focus_cid=focused) 649 if aid is None or aid=='-': return#while True 650 scam = app.app_proc(app.PROC_GET_KEYSTATE, '') if app.app_api_version()>='1.0.143' else '' 651 aid_m = scam + '/' + aid if scam and scam!='a' else aid # smth == a/smth 652 focused = chds[0] if 1==len(chds) else focused 653 if aid[:2]=='up' or aid[:2]=='dn': 654 pos = aid[2] 655 fits['_sp'+pos] = fits['_sp'+pos] + (-1 if aid[:2]=='up' else 1) 656 657 if aid_m=='save': 658 ctrls = ['check' 659 ,'edit' 660 ,'button' 661 ,'combo_ro' 662 ,'combo' 663 ,'checkbutton' 664 ,'linklabel' 665 ,'spinedit' 666 ] 667 for ic, nc in enumerate(ctrls): 668 fit = fits['_sp'+str(1+ic)] 669 if fit==fit_top_by_env(nc): continue#for ic, nc 670 apx.set_opt('dlg_wrapper_fit_va_for_'+nc, fit) 671 #for ic, nc 672 fit_top_by_env__clear() 673 break#while 674 675 if aid_m=='c/save': # Report 676 rpt = 'env:'+get_desktop_environment() 677 rpt+= c13+'check:' +str(fits['_sp1']) 678 rpt+= c13+'edit:' +str(fits['_sp2']) 679 rpt+= c13+'button:' +str(fits['_sp3']) 680 rpt+= c13+'combo_ro:' +str(fits['_sp4']) 681 rpt+= c13+'combo:' +str(fits['_sp5']) 682 rpt+= c13+'checkbutton:'+str(fits['_sp6']) 683 rpt+= c13+'linklabel:' +str(fits['_sp7']) 684 rpt+= c13+'spinedit:' +str(fits['_sp8']) 685 aid_r, *_t = dlg_wrapper(_('Report'), 230,310, 686 [dict(cid='rprt',tp='me' ,t=5 ,l=5 ,h=200 ,w=220) 687 ,dict( tp='lb' ,t=215 ,l=5 ,w=220 ,cap=_('Send the report to the address')) 688 ,dict(cid='mail',tp='ed' ,t=235 ,l=5 ,w=220) 689 ,dict( tp='lb' ,t=265 ,l=5 ,w=150 ,cap=_('or post it on')) 690 ,dict(cid='gith',tp='ln-lb' ,t=265 ,l=155 ,w=70 ,cap='GitHub',props='https://github.com/kvichans/cuda_fit_v_alignments/issues') 691 ,dict(cid='-' ,tp='bt' ,t=280 ,l=205-80 ,w=80 ,cap=_('Close')) 692 ], dict(rprt=rpt 693 ,mail='kvichans@mail.ru'), focus_cid='rprt') 694# if aid_r is None or btn_hlp=='-': break#while 695 #while 696 #def dlg_valign_consts 697 698def get_hotkeys_desc(cmd_id, ext_id=None, keys_js=None, def_ans=''): 699 """ Read one or two hotkeys for command 700 cmd_id [+ext_id] 701 from 702 settings\keys.json 703 Return 704 def_ans If no hotkeys for the command 705 'Ctrl+Q' 706 'Ctrl+Q * Ctrl+W' If one hotkey for the command 707 'Ctrl+Q/Ctrl+T' 708 'Ctrl+Q * Ctrl+W/Ctrl+T' If two hotkeys for the command 709 """ 710 if keys_js is None: 711 keys_json = app.app_path(app.APP_DIR_SETTINGS)+os.sep+'keys.json' 712 keys_js = apx._json_loads(open(keys_json).read()) if os.path.exists(keys_json) else {} 713 714 cmd_id = f('{},{}', cmd_id, ext_id) if ext_id else cmd_id 715 if cmd_id not in keys_js: 716 return def_ans 717 cmd_keys= keys_js[cmd_id] 718 desc = '/'.join([' * '.join(cmd_keys.get('s1', [])) 719 ,' * '.join(cmd_keys.get('s2', [])) 720 ]).strip('/') 721 return desc 722 #def get_hotkeys_desc 723 724###################################### 725#NOTE: plugins history 726###################################### 727PLING_HISTORY_JSON = app.app_path(app.APP_DIR_SETTINGS)+os.sep+'plugin history.json' 728def get_hist(key_or_path, default=None, module_name='_auto_detect', to_file=PLING_HISTORY_JSON): 729 """ Read from "plugin history.json" one value by string key or path (list of keys). 730 Parameters 731 key_or_path Key(s) to navigate in json tree 732 Type: str or [str] 733 default Value to return if no suitable node in json tree 734 module_name Start node to navigate. 735 If it is '_auto_detect' then name of caller module is used. 736 If it is None then it is skipped. 737 to_file Name of file to read. APP_DIR_SETTING will be joined if no full path. 738 739 Return Found value or default 740 741 Examples (caller module is 'plg') 742 1. If no "plugin history.json" 743 get_hist('k') returns None 744 get_hist(['p', 'k'], 0) returns 0 745 2. If "plugin history.json" contains 746 {"k":1, "plg":{"k":2, "p":{"m":3}, "t":[0,1]}, "q":{"n":4}} 747 get_hist('k', 0, None) returns 1 748 get_hist('k', 0) returns 0 749 get_hist('k', 0, 'plg') returns 2 750 get_hist('k', 0, 'oth') returns 0 751 get_hist(['p','m'], 0) returns 3 752 get_hist(['p','t'], []) returns [0,1] 753 get_hist('q', 0, None) returns {'n':4} 754 get_hist(['q','n'], 0, None) returns 4 755 """ 756 to_file = to_file if os.sep in to_file else app.app_path(app.APP_DIR_SETTINGS)+os.sep+to_file 757 if not os.path.exists(to_file): 758 pass; #log('not exists',()) 759 return default 760 data = None 761 try: 762 data = json.loads(open(to_file).read()) 763 except: 764 pass; log('not load: {}',sys.exc_info()) 765 return default 766 if module_name=='_auto_detect': 767 caller_globals = inspect.stack()[1].frame.f_globals 768 module_name = inspect.getmodulename(caller_globals['__file__']) \ 769 if '__file__' in caller_globals else None 770 pass; #log('module_name={}',(module_name)) 771 keys = [key_or_path] if type(key_or_path)==str else key_or_path 772 keys = keys if module_name is None else [module_name]+keys 773 parents,\ 774 key = keys[:-1], keys[-1] 775 for parent in parents: 776 data= data.get(parent) 777 if type(data)!=dict: 778 pass; #log('not dict parent={}',(parent)) 779 return default 780 return data.get(key, default) 781 #def get_hist 782 783def set_hist(key_or_path, value, module_name='_auto_detect', kill=False, to_file=PLING_HISTORY_JSON): 784 """ Write to "plugin history.json" one value by key or path (list of keys). 785 If any of node doesnot exist it will be added. 786 Or remove (if kill) one key+value pair (if suitable key exists). 787 Parameters 788 key_or_path Key(s) to navigate in json tree 789 Type: str or [str] 790 value Value to set if suitable item in json tree exists 791 module_name Start node to navigate. 792 If it is '_auto_detect' then name of caller module is used. 793 If it is None then it is skipped. 794 kill Need to remove node in tree. 795 if kill==True parm value is ignored 796 to_file Name of file to write. APP_DIR_SETTING will be joined if no full path. 797 798 Return value (param) if !kill and modification is successful 799 value (killed) if kill and modification is successful 800 None if kill and no path in tree (no changes) 801 KeyError if !kill and path has problem 802 Return value 803 804 Examples (caller module is 'plg') 805 1. If no "plugin history.json" it will become 806 set_hist('k',0,None) {"k":0} 807 set_hist('k',1) {"plg":{"k":1}} 808 set_hist('k',1,'plg') {"plg":{"k":1}} 809 set_hist('k',1,'oth') {"oth":{"k":1}} 810 set_hist('k',[1,2]) {"plg":{"k":[1,2]}} 811 set_hist(['p','k'], 1) {"plg":{"p":{"k":1}}} 812 813 2. If "plugin history.json" contains {"plg":{"k":1, "p":{"m":2}}} 814 it will contain 815 set_hist('k',0,None) {"plg":{"k":1, "p":{"m":2}},"k":0} 816 set_hist('k',0) {"plg":{"k":0, "p":{"m":2}}} 817 set_hist('k',0,'plg') {"plg":{"k":0, "p":{"m":2}}} 818 set_hist('n',3) {"plg":{"k":1, "p":{"m":2}, "n":3}} 819 set_hist(['p','m'], 4) {"plg":{"k":1, "p":{"m":4}}} 820 set_hist('p',{'m':4}) {"plg":{"k":1, "p":{"m":4}}} 821 set_hist(['p','m','k'], 1) KeyError (old m is not branch node) 822 823 3. If "plugin history.json" contains {"plg":{"k":1, "p":{"m":2}}} 824 it will contain 825 set_hist('k', kill=True) {"plg":{ "p":{"m":2}}} 826 set_hist('p', kill=True) {"plg":{"k":1}} 827 set_hist(['p','m'], kill=True) {"plg":{"k":1, "p":{}}} 828 set_hist('n', kill=True) {"plg":{"k":1, "p":{"m":2}}} (nothing to kill) 829 """ 830 to_file = to_file if os.sep in to_file else app.app_path(app.APP_DIR_SETTINGS)+os.sep+to_file 831 body = json.loads(open(to_file).read(), object_pairs_hook=odict) \ 832 if os.path.exists(to_file) and os.path.getsize(to_file) != 0 else \ 833 odict() 834 835 if module_name=='_auto_detect': 836 caller_globals = inspect.stack()[1].frame.f_globals 837 module_name = inspect.getmodulename(caller_globals['__file__']) \ 838 if '__file__' in caller_globals else None 839 keys = [key_or_path] if type(key_or_path)==str else key_or_path 840 keys = keys if module_name is None else [module_name]+keys 841 parents,\ 842 key = keys[:-1], keys[-1] 843 data = body 844 for parent in parents: 845 if kill and parent not in data: 846 return None 847 data= data.setdefault(parent, odict()) 848 if type(data)!=odict: 849 raise KeyError() 850 if kill: 851 if key not in data: 852 return None 853 value = data.pop(key) 854 else: 855 data[key] = value 856 open(to_file, 'w').write(json.dumps(body, indent=2)) 857 return value 858 #def set_hist 859###################################### 860###################################### 861if __name__ == '__main__' : # Tests 862 pass 863 def test_ask_number(ask, def_val): 864 cnts=[dict( tp='lb',tid='v',l=3 ,w=70,cap=ask) 865 ,dict(cid='v',tp='ed',t=3 ,l=73,w=70) 866 ,dict(cid='!',tp='bt',t=45 ,l=3 ,w=70,cap='OK',props='1') 867 ,dict(cid='-',tp='bt',t=45 ,l=73,w=70,cap='Cancel')] 868 vals={'v':def_val} 869 while True: 870 btn,vals,fid,chds=dlg_wrapper('Example',146,75,cnts,vals,'v') 871 if btn is None or btn=='-': return def_val 872 if not re.match(r'\d+$', vals['v']): continue 873 return vals['v'] 874 ask_number('ask_____________', '____smth') 875