1''' Lib for Plugin
2Authors:
3    Andrey Kvichansky    (kvichans on github.com)
4Version:
5    '2.1.17 2021-04-16'
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
15
16try:
17    import  cudatext            as app
18    from    cudatext        import ed
19    import  cudax_lib           as apx
20except:
21    import  sw                  as app
22    from    sw              import ed
23    from . import cudax_lib     as apx
24
25pass;                           # Logging
26pass;                           from pprint import pformat
27pass;                           import tempfile
28
29odict       = collections.OrderedDict
30T,F,N       = True, False, None
31GAP         = 5
32c13,c10,c9  = chr(13),chr(10),chr(9)
33REDUCTIONS  = {'lb'     :'label'
34            ,  'ln-lb'  :'linklabel'
35            ,  'llb'    :'linklabel'
36            ,  'ed'     :'edit'             # ro_mono_brd
37            ,  'ed_pw'  :'edit_pwd'
38            ,  'sp-ed'  :'spinedit'         # min_max_inc
39            ,  'sed'    :'spinedit'         # min_max_inc
40            ,  'me'     :'memo'             # ro_mono_brd
41            ,  'bt'     :'button'           # def_bt
42            ,  'rd'     :'radio'
43            ,  'ch'     :'check'
44            ,  'ch-bt'  :'checkbutton'
45            ,  'ch-b'   :'checkbutton'
46            ,  'chb'    :'checkbutton'
47            ,  'ch-gp'  :'checkgroup'
48            ,  'rd-gp'  :'radiogroup'
49            ,  'cb'     :'combo'
50            ,  'cb-ro'  :'combo_ro'
51            ,  'cb-r'   :'combo_ro'
52            ,  'cbr'    :'combo_ro'
53            ,  'lbx'    :'listbox'
54            ,  'ch-lbx' :'checklistbox'
55            ,  'clx'    :'checklistbox'
56            ,  'lvw'    :'listview'
57            ,  'ch-lvw' :'checklistview'
58            ,  'tabs'   :'tabs'
59            ,  'clr'    :'colorpanel'
60            ,  'im'     :'image'
61            ,  'f-lb'   :'filter_listbox'
62            ,  'f-lvw'  :'filter_listview'
63            ,  'fr'     :'bevel'
64            ,  'pn'     :'panel'
65            ,  'gr'     :'group'
66            ,  'sp'     :'splitter'
67
68            ,  'tvw'    :'treeview'
69            ,  'edr'    :'editor'
70            ,  'sb'     :'statusbar'
71            ,  'bte'    :'button_ex'
72
73#           ,  'fid'    :'focused'
74            ,  'cols'   :'columns'
75            }
76
77def f(s, *args, **kwargs):return s.format(*args, **kwargs)
78
79def log(msg='', *args, **kwargs):
80    if args or kwargs:
81        msg = msg.format(*args, **kwargs)
82    if Tr.tr is None:
83        Tr.tr=Tr()
84    return Tr.tr.log(msg)
85
86class Tr :
87    tr=None
88    """ Трассировщик.
89        Основной (единственный) метод: log(строка) - выводит указанную строку в лог.
90        Управляется через команды в строках для вывода.
91        Команды:
92            >>  Увеличить сдвиг при выводе будущих строк (пока жив возвращенный объект)
93            (:) Начать замер нового вложенного периода, закончить когда умрет возвращенный объект
94            (== Начать замер нового вложенного периода
95            ==> Вывести длительность последнего периода
96            ==) Вывести длительность последнего периода и закончить его замер
97            =}} Отменить все замеры
98        Вызов log с командой >> (увеличить сдвиг) возвращает объект,
99            который при уничтожении уменьшит сдвиг
100        """
101    sec_digs        = 2                     # Точность отображения секунд, кол-во дробных знаков
102    se_fmt          = ''
103    mise_fmt        = ''
104    homise_fmt      = ''
105    def __init__(self, log_to_file=None) :
106        # Поля объекта
107        self.gap    = ''                # Отступ
108        self.tm     = perf_counter()    # Отметка времени о запуске
109        self.stms   = []                # Отметки времени о начале замера спец.периода
110
111        if log_to_file:
112            logging.basicConfig( filename=log_to_file
113                                ,filemode='w'
114                                ,level=logging.DEBUG
115                                ,format='%(message)s'
116                                ,datefmt='%H:%M:%S'
117                                ,style='%')
118        else: # to stdout
119            logging.basicConfig( stream=sys.stdout
120                                ,level=logging.DEBUG
121                                ,format='%(message)s'
122                                ,datefmt='%H:%M:%S'
123                                ,style='%')
124        # Tr()
125    def __del__(self):
126        logging.shutdown()
127
128    class TrLiver :
129        cnt = 0
130        """ Автоматически сокращает gap при уничножении
131            Показывает время своей жизни"""
132        def __init__(self, tr, ops) :
133            # Поля объекта
134            self.tr = tr
135            self.ops= ops
136            self.tm = 0
137            self.nm = Tr.TrLiver.cnt
138            if '(:)' in self.ops :
139                # Начать замер нового интервала
140                self.tm = perf_counter()
141        def log(self, msg='') :
142            if '(:)' in self.ops :
143                msg = '{}(:)=[{}]{}'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ), msg )
144                logging.debug( self.tr.format_msg(msg, ops='') )
145        def __del__(self) :
146            #pass;                  logging.debug('in del')
147            if '(:)' in self.ops :
148                msg = '{}(:)=[{}]'.format( self.nm, Tr.format_tm( perf_counter() - self.tm ) )
149                logging.debug( self.tr.format_msg(msg, ops='') )
150            if '>>' in self.ops :
151                self.tr.gap = self.tr.gap[:-1]
152
153    def log(self, msg='') :
154        if '(:)' in msg :
155            Tr.TrLiver.cnt += 1
156            msg     = msg.replace( '(:)', '{}(:)'.format(Tr.TrLiver.cnt) )
157        logging.debug( self.format_msg(msg) )
158        if '>>' in msg :
159            self.gap = self.gap + c9
160            # Создаем объект, который при разрушении сократит gap
161        if '>>' in msg or '(:)' in msg:
162            return Tr.TrLiver(self,('>>' if '>>' in msg else '')+('(:)' if '(:)' in msg else ''))
163            # return Tr.TrLiver(self,iif('>>' in msg,'>>','')+iif('(:)' in msg,'(:)',''))
164        else :
165            return self
166        # Tr.log
167
168#   def format_msg(self, msg, dpth=2, ops='+fun:ln +wait==') :
169    def format_msg(self, msg, dpth=3, ops='+fun:ln +wait==') :
170        if '(==' in msg :
171            # Начать замер нового интервала
172            self.stms   = self.stms + [perf_counter()]
173            msg = msg.replace( '(==', '(==[' + Tr.format_tm(0) + ']' )
174
175        if '###' in msg :
176            # Показать стек
177            st_inf  = '\n###'
178            for fr in inspect.stack()[1+dpth:]:
179                try:
180                    cls = fr[0].f_locals['self'].__class__.__name__ + '.'
181                except:
182                    cls = ''
183                fun     = (cls + fr[3]).replace('.__init__','()')
184                ln      = fr[2]
185                st_inf  += '    {}:{}'.format(fun, ln)
186            msg    += st_inf
187
188        if '+fun:ln' in ops :
189            frCaller= inspect.stack()[dpth] # 0-format_msg, 1-Tr.log|Tr.TrLiver, 2-log, 3-need func
190            try:
191                cls = frCaller[0].f_locals['self'].__class__.__name__ + '.'
192            except:
193                cls = ''
194            fun     = (cls + frCaller[3]).replace('.__init__','()')
195            ln      = frCaller[2]
196            msg     = '[{}]{}{}:{} '.format( Tr.format_tm( perf_counter() - self.tm ), self.gap, fun, ln ) + msg
197        else :
198            msg     = '[{}]{}'.format( Tr.format_tm( perf_counter() - self.tm ), self.gap ) + msg
199
200        if '+wait==' in ops :
201            if ( '==)' in msg or '==>' in msg ) and len(self.stms)>0 :
202                # Закончить/продолжить замер последнего интервала и вывести его длительность
203                sign    = '==)' if '==)' in msg else '==>'
204                # sign    = icase( '==)' in msg, '==)', '==>' )
205                stm = '[{}]'.format( Tr.format_tm( perf_counter() - self.stms[-1] ) )
206                msg = msg.replace( sign, sign+stm )
207                if '==)' in msg :
208                    del self.stms[-1]
209
210            if '=}}' in msg :
211                # Отменить все замеры
212                self.stms   = []
213
214        return msg.replace('¬',c9).replace('¶',c10)
215        # Tr.format
216
217    @staticmethod
218    def format_tm(secs) :
219        """ Конвертация количества секунд в 12h34'56.78" """
220        if 0==len(Tr.se_fmt) :
221            Tr.se_fmt       = '{:'+str(3+Tr.sec_digs)+'.'+str(Tr.sec_digs)+'f}"'
222            Tr.mise_fmt     = "{:2d}'"+Tr.se_fmt
223            Tr.homise_fmt   = "{:2d}h"+Tr.mise_fmt
224        h = int( secs / 3600 )
225        secs = secs % 3600
226        m = int( secs / 60 )
227        s = secs % 60
228        return Tr.se_fmt.format(s) \
229                if 0==h+m else \
230               Tr.mise_fmt.format(m,s) \
231                if 0==h else \
232               Tr.homise_fmt.format(h,m,s)
233        # return icase( 0==h+m,   Tr.se_fmt.format(s)
234        #             , 0==h,     Tr.mise_fmt.format(m,s)
235        #             ,           Tr.homise_fmt.format(h,m,s) )
236        # Tr.format_tm
237    # Tr
238
239def get_desktop_environment():
240    #From http://stackoverflow.com/questions/2035657/what-is-my-current-desktop-environment
241    # and http://ubuntuforums.org/showthread.php?t=652320
242    # and http://ubuntuforums.org/showthread.php?t=652320
243    # and http://ubuntuforums.org/showthread.php?t=1139057
244    if sys.platform in ["win32", "cygwin"]:
245        return "win"
246    elif sys.platform == "darwin":
247        return "mac"
248    else: #Most likely either a POSIX system or something less common
249        desktop_session = os.environ.get("DESKTOP_SESSION")
250        if desktop_session is not None: #easier to match if we don' have to deal with character cases
251            desktop_session = desktop_session.lower()
252            if desktop_session in ["gnome","unity", "cinnamon", "mate", "xfce4", "lxde", "fluxbox",
253                                   "blackbox", "openbox", "icewm", "jwm", "afterstep","trinity", "kde"]:
254                return desktop_session
255            ## Special cases ##
256            # Canonical sets $DESKTOP_SESSION to Lubuntu rather than LXDE if using LXDE.
257            # There is no guarantee that they will not do the same with the other desktop environments.
258            elif "xfce" in desktop_session or desktop_session.startswith("xubuntu"):
259                return "xfce4"
260            elif desktop_session.startswith("ubuntu"):
261                return "unity"
262            elif desktop_session.startswith("lubuntu"):
263                return "lxde"
264            elif desktop_session.startswith("kubuntu"):
265                return "kde"
266            elif desktop_session.startswith("razor"): # e.g. razorkwin
267                return "razor-qt"
268            elif desktop_session.startswith("wmaker"): # e.g. wmaker-common
269                return "windowmaker"
270        if os.environ.get('KDE_FULL_SESSION') == 'true':
271            return "kde"
272        elif os.environ.get('GNOME_DESKTOP_SESSION_ID'):
273            if not "deprecated" in os.environ.get('GNOME_DESKTOP_SESSION_ID'):
274                return "gnome2"
275        #From http://ubuntuforums.org/showthread.php?t=652320
276        elif is_running("xfce-mcs-manage"):
277            return "xfce4"
278        elif is_running("ksmserver"):
279            return "kde"
280    return "unknown"
281def is_running(process):
282    #From http://www.bloggerpolis.com/2011/05/how-to-check-if-a-process-is-running-using-python/
283    # and http://richarddingwall.name/2009/06/18/windows-equivalents-of-ps-and-kill-commands/
284    try: #Linux/Unix
285        s = subprocess.Popen(["ps", "axw"],stdout=subprocess.PIPE)
286    except: #Windows
287        s = subprocess.Popen(["tasklist", "/v"],stdout=subprocess.PIPE)
288    for x in s.stdout:
289        if re.search(process, str(x)):
290            return True
291    return False
292
293ENV2FITS= {'win':
294            {'check'      :-2
295            ,'radio'      :-2
296            ,'edit'       :-3
297            ,'button'     :-4
298            ,'combo_ro'   :-4
299            ,'combo'      :-3
300            ,'checkbutton':-5
301            ,'linklabel'  : 0
302            ,'spinedit'   :-3
303            }
304          ,'unity':
305            {'check'      :-3
306            ,'radio'      :-3
307            ,'edit'       :-5
308            ,'button'     :-4
309            ,'combo_ro'   :-5
310            ,'combo'      :-6
311            ,'checkbutton':-4
312            ,'linklabel'  : 0
313            ,'spinedit'   :-6
314            }
315          ,'mac':
316            {'check'      :-1
317            ,'radio'      :-1
318            ,'edit'       :-3
319            ,'button'     :-3
320            ,'combo_ro'   :-2
321            ,'combo'      :-3
322            ,'checkbutton':-2
323            ,'linklabel'  : 0
324            ,'spinedit'   : 0   ##??
325            }
326          }
327fit_top_by_env__cash    = {}
328def fit_top_by_env__clear():
329    global fit_top_by_env__cash
330    fit_top_by_env__cash    = {}
331def fit_top_by_env(what_tp, base_tp='label'):
332    """ Get "fitting" to add to top of first control to vertical align inside text with text into second control.
333        The fittings rely to platform: win, unix(kde,gnome,...), mac
334    """
335    if what_tp==base_tp:
336        return 0
337    if (what_tp, base_tp) in fit_top_by_env__cash:
338        pass;                  #log('cashed what_tp, base_tp={}',(what_tp, base_tp))
339        return fit_top_by_env__cash[(what_tp, base_tp)]
340    env     = get_desktop_environment()
341    pass;                      #env = 'mac'
342    fit4lb  = ENV2FITS.get(env, ENV2FITS.get('win'))
343    fit     = 0
344    if base_tp=='label':
345        fit = apx.get_opt('dlg_wrapper_fit_va_for_'+what_tp, fit4lb.get(what_tp, 0))
346        pass;                  #fit_o=fit
347        fit = _os_scale(app.DLG_PROP_GET, {'y':fit})['y']
348        pass;                  #log('what_tp,fit_o,fit,h={}',(what_tp,fit_o,fit,get_gui_height(what_tp)))
349    else:
350        fit = fit_top_by_env(what_tp) - fit_top_by_env(base_tp)
351    pass;                      #log('what_tp, base_tp, fit={}',(what_tp, base_tp, fit))
352    return fit_top_by_env__cash.setdefault((what_tp, base_tp), fit)
353   #def fit_top_by_env
354
355def rgb_to_int(r,g,b):
356    return r | (g<<8) | (b<<16)
357def dlg_wrapper(title, w, h, cnts, in_vals={}, focus_cid=None):
358    """ Wrapper for dlg_custom.
359        Params
360            title, w, h     Title, Width, Height
361            cnts            List of static control properties
362                                [{cid:'*', tp:'*', t:1,l:1,w:1,r:1,b;1,h:1,tid:'cid', cap:'*', hint:'*', en:'0', props:'*', items:[*], act='0'}]
363                                cid         (opt)(str) C(ontrol)id. Need only for buttons and controls with value (and for tid)
364                                tp               (str) Control types from wiki or short names
365                                t           (opt)(int) Top
366                                tid         (opt)(str) Ref to other control cid for horz-align text in both controls
367                                l                (int) Left
368                                r,b,w,h     (opt)(int) Position. w>>>r=l+w, h>>>b=t+h, b can be omitted
369                                cap              (str) Caption for labels and buttons
370                                hint        (opt)(str) Tooltip
371                                en          (opt)('0'|'1'|True|False) Enabled-state
372                                props       (opt)(str) See wiki
373                                act         (opt)('0'|'1'|True|False) Will close dlg when changed
374                                items            (str|list) String as in wiki. List structure by types:
375                                                            [v1,v2,]     For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox
376                                                            (head, body) For listview, checklistview
377                                                                head    [(cap,width),(cap,width),]
378                                                                body    [[r0c0,r0c1,],[r1c0,r1c1,],[r2c0,r2c1,],]
379            in_vals         Dict of start values for some controls
380                                {'cid':val}
381            focus_cid       (opt) Control cid for start focus
382        Return
383            btn_cid         Clicked/changed control cid
384            {'cid':val}     Dict of new values for the same (as in_vals) controls
385                                Format of values is same too.
386            focus_cid       Focused control cid
387            [cid]           List of controls with changed values
388        Short names for types
389            lb      label
390            ln-lb   linklabel
391            ed      edit
392            sp-ed   spinedit
393            me      memo
394            bt      button
395            rd      radio
396            ch      check
397            ch-bt   checkbutton
398            ch-gp   checkgroup
399            rd-gp   radiogroup
400            cb      combo
401            cb-ro   combo_ro
402            lbx     listbox
403            ch-lbx  checklistbox
404            lvw     listview
405            ch-lvw  checklistview
406        Example.
407            def ask_number(ask, def_val):
408                cnts=[dict(        tp='lb',tid='v',l=3 ,w=70,cap=ask)
409                     ,dict(cid='v',tp='ed',t=3    ,l=73,w=70)
410                     ,dict(cid='!',tp='bt',t=45   ,l=3 ,w=70,cap='OK',props='1')
411                     ,dict(cid='-',tp='bt',t=45   ,l=73,w=70,cap='Cancel')]
412                vals={'v':def_val}
413                while True:
414                    aid,vals,fid,chds=dlg_wrapper('Example',146,75,cnts,vals,'v')
415                    if aid is None or btn=='-': return def_val
416                    if not re.match(r'\d+$', vals['v']): continue
417                    return vals['v']
418    """
419    pass;                      #log('in_vals={}',pformat(in_vals, width=120))
420    cnts        = [cnt for cnt in cnts if cnt.get('vis', True) in (True, '1')]
421    cid2i       = {cnt['cid']:i for i,cnt in enumerate(cnts) if 'cid' in cnt}
422    if True:
423        # Checks
424        no_tids = {cnt['tid']   for   cnt in    cnts    if 'tid' in cnt and  cnt['tid'] not in cid2i}
425        if no_tids:
426            raise Exception(f('No cid(s) for tid(s): {}', no_tids))
427        no_vids = {cid          for   cid in    in_vals if                          cid not in cid2i}
428        if no_vids:
429            raise Exception(f('No cid(s) for vals: {}', no_vids))
430
431    simpp   = ['cap','hint'
432              ,'props'
433              ,'color'
434              ,'font_name', 'font_size', 'font_color', 'font'
435              ,'act'
436              ,'en','vis'
437             #,'tag'
438              ]
439    ctrls_l = []
440    for cnt in cnts:
441        tp      = cnt['tp']
442        tp      = REDUCTIONS.get(tp, tp)
443        if tp=='--':
444            # Horz-line
445            t   = cnt.get('t')
446            l   = cnt.get('l', 0)                   # def: from DlgLeft
447            r   = cnt.get('r', l+cnt.get('w', w))   # def: to   DlgRight
448            lst = ['type=label']
449            lst+= ['cap='+'—'*1000]
450            lst+= ['font_color='+str(rgb_to_int(185,185,185))]
451            lst+= ['pos={l},{t},{r},0'.format(l=l,t=t,r=r)]
452            ctrls_l+= [chr(1).join(lst)]
453            continue#for cnt
454
455        lst     = ['type='+tp]
456
457        # Preprocessor
458        if 'props' in cnt:
459            pass
460        elif tp=='label' and cnt['cap'][0]=='>':
461            #   cap='>smth' --> cap='smth', props='1' (r-align)
462            cnt['cap']  = cnt['cap'][1:]
463            cnt['props']= '1'
464        elif tp=='label' and cnt.get('ralign'):
465            cnt['props']=    cnt.get('ralign')
466        elif tp=='button' and cnt.get('def_bt') in ('1', True):
467            cnt['props']= '1'
468        elif tp=='spinedit' and cnt.get('min_max_inc'):
469            cnt['props']=       cnt.get('min_max_inc')
470        elif tp=='linklabel' and cnt.get('url'):
471            cnt['props']=        cnt.get('url')
472        elif tp=='listview' and cnt.get('grid'):
473            cnt['props']=       cnt.get('grid')
474        elif tp=='tabs' and cnt.get('at_botttom'):
475            cnt['props']=   cnt.get('at_botttom')
476        elif tp=='colorpanel' and cnt.get('brdW_fillC_fontC_brdC'):
477            cnt['props']=         cnt.get('brdW_fillC_fontC_brdC')
478        elif tp in ('edit', 'memo') and cnt.get('ro_mono_brd'):
479            cnt['props']=               cnt.get('ro_mono_brd')
480
481#       # Simple props
482#       for k in ['cap', 'hint', 'props', 'font_name', 'font_size', 'font_color', 'font', 'name']:
483#               lst += [k+'='+str(cnt[k])]
484
485        if 'props' in cnt:
486            props = cnt.pop('props').split(',')
487            for p_i, p_s in enumerate(props):
488                lst += ['ex'+str(p_i)+'='+p_s]
489
490        # Position:
491        #   t[op] or tid, l[eft] required
492        #   w[idth]  >>> r[ight ]=l+w
493        #   h[eight] >>> b[ottom]=t+h
494        #   b dont need for buttons, edit, labels
495        l       = cnt['l']
496        t       = cnt.get('t', 0)
497        if 'tid' in cnt:
498            # cid for horz-align text
499            bs_cnt  = cnts[cid2i[cnt['tid']]]
500            bs_tp   = bs_cnt['tp']
501            t       = bs_cnt['t'] + fit_top_by_env(tp, REDUCTIONS.get(bs_tp, bs_tp))
502        r       = cnt.get('r', l+cnt.get('w', 0))
503        b       = cnt.get('b', t+cnt.get('h', 0))
504        lst    += ['pos={l},{t},{r},{b}'.format(l=l,t=t,r=r,b=b)]
505#       if 'en' in cnt:
506#           val     = cnt['en']
507#           lst    += ['en='+('1' if val in [True, '1'] else '0')]
508
509        if 'items' in cnt:
510            items   = cnt['items']
511            if isinstance(items, str):
512                pass
513            elif tp in ['listview', 'checklistview']:
514                # For listview, checklistview: "\t"-separated items.
515                #   first item is column headers: title1+"="+size1 + "\r" + title2+"="+size2 + "\r" +...
516                #   other items are data: cell1+"\r"+cell2+"\r"+...
517                # ([(hd,wd)], [[cells],[cells],])
518                items   = '\t'.join(['\r'.join(['='.join((hd,sz)) for hd,sz in items[0]])]
519                                   +['\r'.join(row) for row in items[1]]
520                                   )
521            else:
522                # For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox: "\t"-separated lines
523                items   = '\t'.join(items)
524            lst+= ['items='+items]
525
526        # Prepare val
527        if cnt.get('cid') in in_vals:
528            in_val = in_vals[cnt['cid']]
529            if False:pass
530            elif tp in ['check', 'radio', 'checkbutton'] and isinstance(in_val, bool):
531                # For check, radio, checkbutton: value "0"/"1"
532                in_val  = '1' if in_val else '0'
533            elif tp=='memo':
534                # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2))
535                if isinstance(in_val, list):
536                    in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val])
537                else:
538                    in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t')
539            elif tp=='checkgroup' and isinstance(in_val, list):
540                # For checkgroup: ","-separated checks (values "0"/"1")
541                in_val = ','.join(in_val)
542            elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple):
543                # For checklistbox, checklistview: index+";"+checks
544                in_val = ';'.join( (str(in_val[0]), ','.join( in_val[1]) ) )
545            lst+= ['val='+str(in_val)]
546
547#       if 'act' in cnt:    # must be last in lst
548#           val     = cnt['act']
549#           lst    += ['act='+('1' if val in [True, '1'] else '0')]
550
551        # Simple props
552        for k in simpp:
553            if k in cnt:
554                v   = cnt[k]
555                v   = ('1' if v else '0') if isinstance(v, bool) else str(v)
556                lst += [k+'='+v]
557        pass;                  #log('lst={}',lst)
558        ctrls_l+= [chr(1).join(lst)]
559       #for cnt
560    pass;                      #log('ok ctrls_l={}',pformat(ctrls_l, width=120))
561
562    pass;                      #ctrls_fn=tempfile.gettempdir()+os.sep+'dlg_custom_ctrls.txt'
563    pass;                      #open(ctrls_fn, 'w', encoding='UTF-8').write('\n'.join(ctrls_l).replace('\r',''))
564    pass;                      #log(f(r'app.dlg_custom("{t}",{w},{h},open(r"{fn}",encoding="UTF-8").read(), {f})',t=title, w=w, h=h, fn=ctrls_fn, f=cid2i.get(focus_cid, -1)))
565    ans     = app.dlg_custom(title, w, h, '\n'.join(ctrls_l), cid2i.get(focus_cid, -1))
566    if ans is None: return None, None, None, None   # btn_cid, {cid:v}, focus_cid, [cid]
567    pass;                      #log('ans={})',ans)
568
569    btn_i,  \
570    vals_ls = ans[0], ans[1].splitlines()
571    pass;                      #log('btn_i,vals_ls={})',(btn_i,vals_ls))
572
573    focus_cid   = ''
574    if vals_ls[-1].startswith('focused='):
575        # From API 1.0.156 dlg_custom also returns index of active control
576        focus_n_s   = vals_ls.pop()
577        focus_i     = int(focus_n_s.split('=')[1])
578        focus_cid   = cnts[focus_i].get('cid', '')
579        pass;                  #log('btn_i,vals_ls,focus_cid={})',(btn_i,vals_ls,focus_cid))
580
581    act_cid     = cnts[btn_i]['cid']
582    # Parse output values
583    an_vals = {cid:vals_ls[cid2i[cid]] for cid in in_vals}
584    for cid in an_vals:
585        cnt     = cnts[cid2i[cid]]
586        tp      = cnt['tp']
587        tp      = REDUCTIONS.get(tp, tp)
588        in_val  = in_vals[cid]
589        an_val  = an_vals[cid]
590        pass;                  #log('tp,in_val,an_val={})',(tp,in_val,an_val))
591        if False:pass
592        elif tp=='memo':
593            # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2))
594            if isinstance(in_val, list):
595                an_val = [v.replace(chr(2), '\t') for v in an_val.split('\t')]
596               #in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val])
597            else:
598                an_val = an_val.replace('\t','\n').replace(chr(2), '\t')
599               #in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t')
600        elif tp=='checkgroup' and isinstance(in_val, list):
601            # For checkgroup: ","-separated checks (values "0"/"1")
602            an_val = an_val.split(',')
603           #in_val = ','.join(in_val)
604        elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple):
605            an_val = an_val.split(';')
606            an_val = (an_val[0], an_val[1].split(','))
607           #in_val = ';'.join(in_val[0], ','.join(in_val[1]))
608        elif isinstance(in_val, bool):
609            an_val = an_val=='1'
610        elif tp=='listview':
611            an_val = -1 if an_val=='' else int(an_val)
612        else:
613            an_val = type(in_val)(an_val)
614            pass;              #log('type(in_val),an_val={})',(type(in_val),an_val))
615        an_vals[cid]    = an_val
616       #for cid
617    chds    = [cid for cid in in_vals if in_vals[cid]!=an_vals[cid]]
618    if focus_cid:
619        # If out focus points to button then will point to a unique changed control
620        focus_tp= cnts[cid2i[focus_cid]]['tp']
621        focus_tp= REDUCTIONS.get(focus_tp, focus_tp)
622        if focus_tp in ('button'):
623            focus_cid   = '' if len(chds)!=1 else chds[0]
624    return  act_cid \
625        ,   an_vals \
626        ,   focus_cid \
627        ,   chds
628   #def dlg_wrapper
629
630DLG_CTL_ADD_SET = 26
631DLG_PROC_I2S={
6320:'DLG_CREATE'
633,1:'DLG_FREE'
634,5:'DLG_SHOW_MODAL'
635,6:'DLG_SHOW_NONMODAL'
636,7:'DLG_HIDE'
637,8:'DLG_FOCUS'
638,9:'DLG_SCALE'
639,10:'DLG_PROP_GET'
640,11:'DLG_PROP_SET'
641,12:'DLG_DOCK'
642,13:'DLG_UNDOCK'
643,20:'DLG_CTL_COUNT'
644,21:'DLG_CTL_ADD'
645,26:'DLG_CTL_ADD_SET'
646,22:'DLG_CTL_PROP_GET'
647,23:'DLG_CTL_PROP_SET'
648,24:'DLG_CTL_DELETE'
649,25:'DLG_CTL_DELETE_ALL'
650,30:'DLG_CTL_FOCUS'
651,31:'DLG_CTL_FIND'
652,32:'DLG_CTL_HANDLE'
653}
654_SCALED_KEYS = ('x', 'y', 'w', 'h'
655            ,  'w_min', 'w_max', 'h_min', 'h_max'
656            ,  'sp_l', 'sp_r', 'sp_t', 'sp_b', 'sp_a'
657            )
658def _os_scale(id_action, prop=None, index=-1, index2=-1, name=''):
659    pass;                      #return prop
660    pass;                      #log('prop={}',({k:prop[k] for k in prop if k in ('x','y')}))
661    pass;                      #logb=name in ('tolx', 'tofi') and id_action==app.DLG_CTL_PROP_GET
662    pass;                      #log('name,prop={}',(name,{k:prop[k] for k in prop if k in ('h','_ready_h')})) if logb else 0
663    ppi     = app.app_proc(app.PROC_GET_SYSTEM_PPI, '')
664    if ppi==96:
665        return prop
666    scale   = ppi/96
667    pass;                      #log('id_dialog, id_action,scale={}',(id_dialog, DLG_PROC_I2S[id_action],scale))
668    if False:pass
669    elif id_action in (app.DLG_PROP_SET     , app.DLG_PROP_GET
670                      ,app.DLG_CTL_PROP_SET , app.DLG_CTL_PROP_GET
671                      ,'scale', 'unscale'):
672
673        def scale_up(prop_dct):
674            for k in _SCALED_KEYS:
675                if k in prop_dct and '_ready_'+k not in prop_dct:
676                    prop_dct[k]   =             round(prop_dct[k] * scale)      # Scale!
677
678        def scale_dn(prop_dct):
679            for k in _SCALED_KEYS:
680                if k in prop_dct and '_ready_'+k not in prop_dct:
681                    prop_dct[k]   =             round(prop_dct[k] / scale)      # UnScale!
682
683#       pass;                   print('a={}, ?? pr={}'.format(DLG_PROC_I2S[id_action], {k:prop[k] for k in prop if k in _SCALED_KEYS or k=='name'}))
684        if False:pass
685        elif id_action==app.DLG_PROP_SET:                   scale_up(prop)
686        elif id_action==app.DLG_CTL_PROP_SET and -1!=index: scale_up(prop)
687        elif id_action==app.DLG_CTL_PROP_SET and ''!=name:  scale_up(prop)
688        elif id_action==app.DLG_PROP_GET:                   scale_dn(prop)
689        elif id_action==app.DLG_CTL_PROP_GET and -1!=index: scale_dn(prop)
690        elif id_action==app.DLG_CTL_PROP_GET and ''!=name:  scale_dn(prop)
691
692        elif id_action==  'scale':                          scale_up(prop)
693        elif id_action=='unscale':                          scale_dn(prop)
694        pass;                  #print('a={}, ok pr={}'.format(DLG_PROC_I2S[id_action], {k:prop[k] for k in prop if k in _SCALED_KEYS or k=='name'}))
695    pass;                      #log('name,prop={}',(name,{k:prop[k] for k in prop if k in ('h','_ready_h')})) if logb else 0
696    return prop
697   #def _os_scale
698
699gui_height_cache= { 'button'            :0
700                  , 'label'             :0
701                  , 'linklabel'         :0
702                  , 'combo'             :0
703                  , 'combo_ro'          :0
704                  , 'edit'              :0
705                  , 'spinedit'          :0
706                  , 'check'             :0
707                  , 'radio'             :0
708                  , 'checkbutton'       :0
709                  , 'filter_listbox'    :0
710                  , 'filter_listview'   :0
711#                 , 'scrollbar'         :0
712                  }
713def get_gui_height(ctrl_type):
714    """ Return real OS-specific height of some control
715             'button'
716             'label' 'linklabel'
717             'combo' 'combo_ro'
718             'edit' 'spinedit'
719             'check' 'radio' 'checkbutton'
720             'filter_listbox' 'filter_listview'
721             'scrollbar'
722    """
723    global gui_height_cache
724    if 0 == gui_height_cache['button']:
725        for tpc in gui_height_cache:
726            gui_height_cache[tpc]   = app.app_proc(app.PROC_GET_GUI_HEIGHT, tpc)
727        pass;                  #log('gui_height_cache={}',(gui_height_cache))
728        idd=app.dlg_proc(         0,    app.DLG_CREATE)
729        for tpc in gui_height_cache:
730            idc=app.dlg_proc(   idd,    app.DLG_CTL_ADD, tpc)
731            pass;              #log('tpc,idc={}',(tpc,idc))
732            prc = {'name':tpc, 'x':0, 'y':0, 'w':1, 'cap':tpc
733                , 'h':gui_height_cache[tpc]}
734            if tpc in ('combo' 'combo_ro'):
735                prc['items']='item0'
736            app.dlg_proc(       idd,    app.DLG_CTL_PROP_SET, index=idc, prop=prc)
737        app.dlg_proc(           idd,    app.DLG_PROP_SET, prop={'x':-1000, 'y':-1000, 'w':100, 'h':100})
738        app.dlg_proc(           idd,    app.DLG_SHOW_NONMODAL)
739
740        ppi     = app.app_proc(app.PROC_GET_SYSTEM_PPI, '')
741        if ppi!=96:
742            # Try to scale height of controls
743            scale   = ppi/96
744            for tpc in gui_height_cache:
745                prc     = app.dlg_proc( idd,    app.DLG_CTL_PROP_GET, name=tpc)
746                sc_h    = round(prc['h'] * scale)
747                app.dlg_proc( idd,    app.DLG_CTL_PROP_SET, name=tpc, prop=dict(h=sc_h))
748
749        for tpc in gui_height_cache:
750            prc = app.dlg_proc( idd,    app.DLG_CTL_PROP_GET, name=tpc)
751            pass;              #log('prc={}',(prc))
752            gui_height_cache[tpc]   = prc['h']
753        app.dlg_proc(           idd,    app.DLG_FREE)
754        pass;                  #log('gui_height_cache={}',(gui_height_cache))
755
756    return gui_height_cache.get(ctrl_type, app.app_proc(app.PROC_GET_GUI_HEIGHT, ctrl_type))
757   #def get_gui_height
758
759def dlg_proc_wpr(id_dialog, id_action, prop='', index=-1, index2=-1, name=''):
760    """ Wrapper on app.dlg_proc
761        1. To set/get dlg-props in scaled OS
762        2. New command DLG_CTL_ADD_SET to set props of created ctrl
763        3. Correct prop for ('label', 'button', 'checkbutton'): if no 'h' then set 'h' as OS default
764    """
765    if id_action==app.DLG_SCALE:
766        return
767#   if id_dialog==0:
768#       print('idd=dlg_proc(0, {})'.format(DLG_PROC_I2S[id_action]))
769#   else:
770#       print('dlg_proc(idd, {}, index={}, name="{}", prop={}, index2={})'.format(   DLG_PROC_I2S[id_action],
771#           index, name,
772#           '""' if type(prop)==str else prop,
773##           '""' if type(prop)==str else {k:prop[k] for k in prop if k not in ['on_change']},
774#           index2))
775    #print('#dlg_proc id_action='+str(id_action)+' prop='+repr(prop))
776
777    scale_on_set    = id_action in (app.DLG_PROP_SET, app.DLG_CTL_PROP_SET)
778    scale_on_get    = id_action in (app.DLG_PROP_GET, app.DLG_CTL_PROP_GET)
779    res             = ''
780    if id_action==DLG_CTL_ADD_SET:  # Join ADD and SET for a control
781        res = ctl_ind = \
782        app.dlg_proc(id_dialog, app.DLG_CTL_ADD, name, -1, -1, '')       # type in name
783#       if name in ('label', 'button', 'checkbutton') and 'h' not in prop:
784#           prop['h'] = app.dlg_proc(id_dialog, app.DLG_CTL_PROP_GET, index=ctl_ind)['h']
785        _os_scale(              app.DLG_CTL_PROP_SET, prop, ctl_ind, -1, '')
786        app.dlg_proc(id_dialog, app.DLG_CTL_PROP_SET, prop, ctl_ind, -1, '')
787    else:
788        _os_scale(                    id_action, prop, index, index2, name) if scale_on_set else 0
789        res = app.dlg_proc(id_dialog, id_action, prop, index, index2, name)
790        pass;                  #log('res={}',({k:res[k] for k in res if k in ('x','y')})) if id_action==app.DLG_PROP_GET else 0
791
792    _os_scale(id_action, res, index, index2, name)               if scale_on_get else 0
793    return res
794   #def dlg_proc_wpr
795
796LMBD_HIDE   = lambda cid,ag:None
797class BaseDlgAgent:
798    """
799    Simple helper to use dlg_proc(). See wiki.freepascal.org/CudaText_API#dlg_proc
800
801    Main features:
802    - All controls are created once then some of them can be changed via callbacks (attribute 'call').
803    - The helper stores config version of form's and control's attributes.
804      So the helper can return two versions of attributes: configured and actual (live).
805    - Helper marshals complex data ('items' for type=list/combo/..., 'val' for type=memo)
806    - Helper can to save/restore form position and sizes to/from file "settings/forms data.json".
807      Key for saving is form's 'cap' (default) or a value in call BaseDlgAgent(..., option={'form data key':'smth'})
808      If value of 'form data key' is empty then helper doesnt save/restore.
809
810    Format of constructor
811        BaseDlgAgent(ctrls, form=None, focused=None)
812            ctrls             [(name,{...})]        To create controls with the names
813                                                    All attributes from
814                                                        wiki.freepascal.org/CudaText_API#Control_properties
815                                                        excluded: name, callback
816            form              {...}                 To configure form
817                                                    All attributes from
818                                                        wiki.freepascal.org/CudaText_API#Form_properties
819            focused           name_to_focus         To set focus
820
821    Format of callback for a control
822        def callname(name_of_event_control, agent):
823            # Reactions
824            return None                         #   To hide form
825            return {}                           #   To keep controls and form state
826            return {'ctrls':  [(name,{...})]    #   To change controls with the names
827                   ,'form':   {...}             #   To change form
828                   ,'focused':name_to_focus     #   To set focus
829                   }                            #   Any key ('ctrls','form','focused') can be omitted.
830    Callback cannot add new controls or change type values.
831
832    Useful methods of agent
833    - agent.cattr(name, '??', live=T)               To get a control actual/configured attribute
834    - agent.cattrs(name, ['??'], live=T)            To get dict of a control listed actual/configured attributes
835    - agent.fattr('??', live=T)                     To get actual/configured form attribute
836    - agent.fattrs(live=T, ['??']=None)             To get actual/configured all/listed form attributes
837
838    Tricks
839    Automatically set some values for attributes
840        - 'act':True    If 'call'   set in a control (not 'button')
841        - 'w_min':w     If 'resize' set in the form
842        - 'h_min':h     If 'resize' set in the form
843
844    Example. Dialog with two buttons.
845    BaseDlgAgent(
846        ctrls=[('b1', dict(type='button',             cap='Click', x=0, y= 0, w=100,
847                call=lambda name,ag:{'ctrls':[('b1',{'cap':'OK',             'w':70})]}
848               ))
849              ,('b2', dict(type='button', cap='Close', x=0, y=30, w=100,
850                call=lambda name,ag:None)
851               )]
852    ,   form=dict(cap='Two buttons', x=0, y=0, w=100, h=60)
853    ,   focused='b1'
854    ).show()
855    """
856
857    def activate(self):
858        """ Set focus to the form """
859        app.dlg_proc(self.id_dlg, app.DLG_FOCUS)
860       #def activate
861
862    def hide(self):
863        app.dlg_proc(self.id_dlg, app.DLG_HIDE)
864       #def hide
865
866    def show(self, callbk_on_exit=None):
867        """ Show the form """
868        ed_caller   = ed
869
870#       app.dlg_proc(self.id_dlg, app.DLG_SCALE)        #??
871#       pass;                   pr_   = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, name='edch')
872#       pass;                   log('exit,pr_={}',('edch', {k:v for k,v in pr_.items() if k in ('h','y')}))
873        app.dlg_proc(self.id_dlg, app.DLG_SHOW_MODAL)
874#       pass;                   pr_   = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, name='edch')
875#       pass;                   log('exit,pr_={}',('edch', {k:v for k,v in pr_.items() if k in ('h','y')}))
876        pass;                  #log('ok DLG_SHOW_MODAL',())
877
878        BaseDlgAgent._form_acts('save', id_dlg=self.id_dlg, key4store=self.opts.get('form data key'))
879        if callbk_on_exit:  callbk_on_exit(self)
880        dlg_proc_wpr(self.id_dlg, app.DLG_FREE)
881
882        ed_to_fcs   = ed_caller \
883                        if 'on_exit_focus_to_ed' not in self.opts else \
884                      self.opts['on_exit_focus_to_ed']
885        pass;                  #log('self.opts={}',(self.opts))
886        pass;                  #log('ed_to_fcs.get_filename()={}',(ed_to_fcs.get_filename()))
887        if ed_to_fcs:
888            ed_to_fcs.focus()
889#           pass;               log('self.opts={}',(self.opts))
890#           self.opts['on_exit_focus_to_ed'].focus()
891#       else:
892#           ed_caller.focus()
893       #def show
894
895    def fattr(self, attr, live=True, defv=None):
896        """ Return one form property """
897        attr= 'focused' if attr=='fid' else attr
898        pr  = dlg_proc_wpr(self.id_dlg
899                        , app.DLG_PROP_GET)     if live else    self.form
900        pass;                  #log('pr={}',(pr))
901        rsp = pr.get(attr, defv)
902        if live and attr=='focused':
903            prf = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, index=rsp)
904            rsp = prf['name'] if prf else None
905        return rsp
906       #def fattr
907
908    def fattrs(self, live=True, attrs=None):
909        """ Return form properties """
910        pr  = dlg_proc_wpr(self.id_dlg
911                        , app.DLG_PROP_GET)     if live else    self.form
912        return pr   if not attrs else   {attr:pr.get(attr) for attr in attrs}
913       #def fattrs
914
915    def cattr(self, name, attr, live=True, defv=None):
916        """ Return one the control property """
917        live= False if attr in ('type',) else live          # Unchangable
918        pr  = dlg_proc_wpr(self.id_dlg
919                        , app.DLG_CTL_PROP_GET
920                        , name=name)            if live else    self.ctrls[name]
921        attr    = REDUCTIONS.get(attr, attr)
922        if attr not in pr:  return defv
923        rsp = pr[attr]
924        if not live:        return rsp
925        if attr=='val':     return self._take_val(name, rsp, defv)
926        return                     self._take_it_cl(name, attr, rsp, defv)
927#       return self._take_val(name, rsp, defv)   if attr=='val' and live else    rsp
928       #def cattr
929
930    def cattrs(self, name, attrs=None, live=True):
931        """ Return the control properties """
932        pr  = dlg_proc_wpr(self.id_dlg
933                        , app.DLG_CTL_PROP_GET
934                        , name=name)            if live else    self.ctrls[name]
935        attrs   = attrs if attrs else list(pr.keys())
936        pass;                  #log('pr={}',(pr))
937        rsp     = {attr:pr.get(attr) for attr in attrs if attr not in ('val','on_change','callback')}
938        if 'val' in attrs:
939            rsp['val'] = self._take_val(name, pr.get('val')) if live else pr.get('val')
940        return rsp
941       #def cattrs
942
943    def chandle(self, name):
944        return app.dlg_proc(self.id_dlg, app.DLG_CTL_HANDLE, name=name)
945
946    def bind_do(self, names=None, gui2data=True):
947        names   = names if names else self.binds.keys()
948        assert self.bindof
949        for name in names:
950            if name not in self.binds: continue
951            attr    = 'val'
952            if gui2data:
953                self.bindof.__setattr__(self.binds[name], self.cattr(name, attr))
954            else:
955                val = self.bindof.__getattr(self.binds[name])
956                self.update({'ctrls':[(name, {attr:val})]})
957       #def bind_do
958
959    def __init__(self, ctrls, form=None, focused=None, options=None):
960        # Fields
961        self.opts   = options if options else {}
962
963        self.id_dlg = dlg_proc_wpr(0, app.DLG_CREATE)
964        self.ctrls  = None                      # Conf-attrs of all controls by name (may be with 'val')
965        self.form   = None                      # Conf-attrs of form
966#       self.callof = self.opts.get('callof')   # Object for callbacks
967        self.bindof = self.opts.get('bindof')   # Object for bind control's values to object's fields
968        self.binds  = {}                        # {name:'other obj field name'}
969
970        self._setup_base(ctrls, form, focused)
971
972        rtf     = self.opts.get('gen_repro_to_file', False)
973        rtf_fn  = rtf if isinstance(rtf, str) else 'repro_dlg_proc.py'
974        if rtf:   self._gen_repro_code(tempfile.gettempdir()+os.sep+rtf_fn)
975       #def __init__
976
977    def _setup_base(self, ctrls, form, focused=None):
978        """ Arrange and fill all: controls attrs, form attrs, focus.
979            Params
980                ctrls   [(id, {})]
981                form    {}
982                focused id
983        """
984        #NOTE: DlgAg init
985        self.ctrls  = odict(ctrls)
986        self.form   = form.copy()     if form   else {}
987
988#       if 'checks'=='checks':
989#           if focused and focused not in self.ctrls:
990#               raise Exception(f('Unknown focused: {}', focused))
991
992        # Create controls
993        for name, cfg_ctrl in ctrls:
994            assert 'type' in cfg_ctrl
995            # Create control
996            cfg_ctrl.pop('callback', None)
997            cfg_ctrl.pop('on_change', None)
998            ind_c   = dlg_proc_wpr(self.id_dlg
999                        , DLG_CTL_ADD_SET
1000                        , name=cfg_ctrl['type']
1001                        , prop=self._prepare_c_pr(name, cfg_ctrl))
1002            pass;              #cfg_ctrl['_idc']    = ind_c         # While API bug: name isnot work if control is in panel
1003            pass;              #log('ind_c,cfg_ctrl[type]={}',(ind_c,cfg_ctrl['type']))
1004           #for cnt
1005
1006        if self.form:
1007            fpr     = self.form
1008            if fpr.get('resize', False):
1009                fpr['w_min']    = fpr.get('w_min', fpr['w'])
1010                fpr['h_min']    = fpr.get('h_min', fpr['h'])
1011            fpr     = BaseDlgAgent._form_acts('move', form=fpr      # Move and (maybe) resize
1012                                             , key4store=self.opts.get('form data key'))
1013            fpr['topmost']      = app.app_api_version()<'1.0.270' or app.app_proc(app.PROC_WINDOW_TOPMOST_GET, '')
1014#           fpr['topmost']      = True
1015            dlg_proc_wpr(       self.id_dlg
1016                            , app.DLG_PROP_SET
1017                            , prop=fpr)
1018
1019        if focused in self.ctrls:
1020            self.form['focused']   = focused
1021            app.dlg_proc(   self.id_dlg
1022                        , app.DLG_CTL_FOCUS
1023                        , name=focused)
1024       #def _setup_base
1025
1026    def _take_val(self, name, liv_val, defv=None):
1027        tp      = self.ctrls[name]['type']
1028        old_val = self.ctrls[name].get('val', defv)
1029        new_val = liv_val
1030        if False:pass
1031        elif tp=='memo':
1032            # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2))
1033            if isinstance(old_val, list):
1034                new_val = [v.replace(chr(2), '\t') for v in liv_val.split('\t')]
1035               #liv_val = '\t'.join([v.replace('\t', chr(2)) for v in old_val])
1036            else:
1037                new_val = liv_val.replace('\t','\n').replace(chr(2), '\t')
1038               #liv_val = old_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t')
1039        elif tp=='checkgroup' and isinstance(old_val, list):
1040            # For checkgroup: ","-separated checks (values "0"/"1")
1041            new_val = liv_val.split(',')
1042           #in_val = ','.join(in_val)
1043        elif tp in ['checklistbox', 'checklistview'] and isinstance(old_val, tuple):
1044            new_val = liv_val.split(';')
1045            new_val = (new_val[0], new_val[1].split(','))
1046           #liv_val = ';'.join(old_val[0], ','.join(old_val[1]))
1047        elif isinstance(old_val, bool):
1048            new_val = liv_val=='1'
1049        elif tp=='listview':
1050            new_val = -1 if liv_val=='' else int(liv_val)
1051        elif old_val is not None:
1052            new_val = type(old_val)(liv_val)
1053        return new_val
1054       #def _take_val
1055
1056    def _take_it_cl(self, name, attr, liv_val, defv=None):
1057        tp      = self.ctrls[name]['type']
1058        old_val = self.ctrls[name].get(attr, defv)
1059        pass;                  #log('name, attr, isinstance(old_val, str)={}',(name, attr, isinstance(old_val, str)))
1060        if isinstance(old_val, str):
1061            # No need parsing - config was by string
1062            return liv_val
1063        new_val = liv_val
1064
1065        if attr=='items':
1066            if tp in ['listview', 'checklistview']:
1067                # For listview, checklistview: "\t"-separated items.
1068                #   first item is column headers: title1+"="+size1 + "\r" + title2+"="+size2 + "\r" +...
1069                #   other items are data: cell1+"\r"+cell2+"\r"+...
1070                # ([(hd,wd)], [[cells],[cells],])
1071                header_rows = new_val.split('\t')
1072                new_val =[[h.split('=')  for h in header_rows[0].split('\r')]
1073                         ,[r.split('\r') for r in header_rows[1:]]
1074                         ]
1075            else:
1076                # For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox: "\t"-separated lines
1077                new_val     = new_val.split('\t')
1078
1079        if attr=='columns':
1080            # For listview, checklistview:
1081            #   "\t"-separated of
1082            #       "\r"-separated
1083            #           Name, Width, Min Width, Max Width, Alignment (str), Autosize('0'/'1'), Visible('0'/'1')
1084            # [{nm:str, wd:num, mi:num, ma:num, al:str, au:bool, vi:bool}]
1085            pass;              #log('new_val={}',repr(new_val))
1086            new_val= [ci.split('\r')      for ci in new_val.split('\t')]
1087#           new_val= [ci.split('\r')[:-1] for ci in new_val.split('\t')[:-1]]   # API bug
1088            pass;              #log('new_val={}',repr(new_val))
1089            int_sc = lambda s: _os_scale('unscale', {'w':int(s)})['w']
1090            new_val= [dict(nm=       ci[0]
1091#                         ,wd=int( ci[1])
1092#                         ,mi=int( ci[2])
1093#                         ,ma=int( ci[3])
1094                          ,wd=int_sc(ci[1])
1095                          ,mi=int_sc(ci[2])
1096                          ,ma=int_sc(ci[3])
1097                          ,au='1'==  ci[4]
1098                          ,vi='1'==  ci[5]
1099                          ) for ci in new_val]
1100            pass;              #log('new_val={}',(new_val))
1101
1102        return new_val
1103       #def _take_it_cl
1104
1105    def _prepare_it_vl(self, c_pr, cfg_ctrl, opts={}):
1106        tp      = cfg_ctrl['type']
1107
1108        if 'val' in cfg_ctrl        and opts.get('prepare val', True):
1109            in_val  = cfg_ctrl['val']
1110            if False:pass
1111            elif tp=='memo':
1112                # For memo: "\t"-separated lines (in lines "\t" must be replaced to chr(2))
1113                if isinstance(in_val, list):
1114                    in_val = '\t'.join([v.replace('\t', chr(2)) for v in in_val])
1115                else:
1116                    in_val = in_val.replace('\t', chr(2)).replace('\r\n','\n').replace('\r','\n').replace('\n','\t')
1117            elif tp=='checkgroup' and isinstance(in_val, list):
1118                # For checkgroup: ","-separated checks (values "0"/"1")
1119                in_val = ','.join(in_val)
1120            elif tp in ['checklistbox', 'checklistview'] and isinstance(in_val, tuple):
1121                # For checklistbox, checklistview: index+";"+checks
1122                in_val = ';'.join( (str(in_val[0]), ','.join( in_val[1]) ) )
1123            c_pr['val']     = in_val
1124
1125        if 'items' in cfg_ctrl        and opts.get('prepare items', True):
1126            items   = cfg_ctrl['items']
1127            if isinstance(items, str):
1128                pass
1129            elif tp in ['listview', 'checklistview']:
1130                # For listview, checklistview: "\t"-separated items.
1131                #   first item is column headers: title1+"="+size1 + "\r" + title2+"="+size2 + "\r" +...
1132                #   other items are data: cell1+"\r"+cell2+"\r"+...
1133                # ([(hd,wd)], [[cells],[cells],])
1134                items   = '\t'.join(['\r'.join(['='.join((hd,sz)) for hd,sz in items[0]])]
1135                                   +['\r'.join(row) for row in items[1]]
1136                                   )
1137                pass;          #log('items={}',repr(items))
1138            else:
1139                # For combo, combo_ro, listbox, checkgroup, radiogroup, checklistbox: "\t"-separated lines
1140                items   = '\t'.join(items)
1141            c_pr['items']   = items
1142
1143        if 'cols' in cfg_ctrl        and opts.get('prepare cols', True):
1144            cols   = cfg_ctrl['cols']
1145            cfg_ctrl['columns'] = cols
1146            if isinstance(cols, str):
1147                pass
1148            else:
1149                # For listview, checklistview:
1150                #   "\t"-separated of
1151                #       "\r"-separated
1152                #           Name, Width, Min Width, Max Width, Alignment (str), Autosize('0'/'1'), Visible('0'/'1')
1153                pass;          #log('cols={}',(cols))
1154                str_sc = lambda n: str(_os_scale('scale', {'w':n})['w'])
1155                cols   = '\t'.join(['\r'.join([       cd[    'nm']
1156#                                             ,str(   cd[    'wd']   )
1157#                                             ,str(   cd.get('mi' ,0))
1158#                                             ,str(   cd.get('ma' ,0))
1159                                              ,str_sc(cd[    'wd']   )
1160                                              ,str_sc(cd.get('mi' ,0))
1161                                              ,str_sc(cd.get('ma' ,0))
1162                                              ,       cd.get('al','')
1163                                              ,'1' if cd.get('au',False) else '0'
1164                                              ,'1' if cd.get('vi',True) else '0'
1165                                              ])
1166                                    for cd in cols]
1167                                  )
1168                pass;          #log('cols={}',repr(cols))
1169            c_pr['columns'] = cols
1170            pass;              #log('isinstance(cfg_ctrl[columns], str)={}',(isinstance(cfg_ctrl['columns'], str)))
1171
1172        return c_pr
1173       #def _prepare_it_vl
1174
1175    def _prepare_c_pr(self, name, cfg_ctrl, opts={}):
1176        pass;                  #log('name, cfg_ctrl={}',(name, cfg_ctrl))
1177        c_pr    = {k:v for (k,v) in cfg_ctrl.items() if k not in ['call', 'bind', 'items', 'val']}
1178        c_pr['name'] = name
1179        tp      = cfg_ctrl['type']
1180
1181        c_pr    = self._prepare_it_vl(c_pr, cfg_ctrl, opts)
1182
1183        if cfg_ctrl.get('bind'):
1184            self.binds[name]    = cfg_ctrl['bind']
1185
1186        if callable(cfg_ctrl.get('call'))        and opts.get('prepare call', True):
1187            if tp!='button':
1188                c_pr['act'] = True
1189            user_callbk = cfg_ctrl['call']
1190
1191            def bda_c_callbk(idd, idc, data):
1192                pass;              #log('idc,name={}',(idc,name))
1193                upds = user_callbk(name, self)
1194                if upds is None:                                        # To hide/close
1195                    app.dlg_proc(self.id_dlg, app.DLG_HIDE)
1196                    return
1197                elif not upds:                                          # No changes
1198                    return
1199                self.update( ctrls  =odict(upds.get('ctrls',  []))
1200                            ,form   =upds.get('form',   {})
1201                            ,focused=upds.get('focused',None))
1202               #def bda_c_callbk
1203            on_what         = 'on_select' \
1204                                if tp in ('listview', 'treeview') else \
1205                              'on_click' \
1206                                if tp in ('linklabel') else \
1207                              'on_change'
1208            c_pr[on_what]   = bda_c_callbk
1209
1210        return c_pr
1211       #def _prepare_c_pr
1212
1213    def update(self, ctrls={}, form={}, focused=None):
1214        """ Change some attrs of form/controls """
1215        pass;                  #log('',())
1216        pass;                  #log('ctrls={}',(ctrls))
1217        pass;                  #log('form={}',(form))
1218        pass;                  #log('focused={}',(focused))
1219        if form:
1220            self.form.update(form)
1221            pass;              #log('form={}',(self.fattrs(live=F)))
1222            pass;              #log('form={}',(self.fattrs()))
1223            pass;              #log('form={}',(form))
1224            dlg_proc_wpr(   self.id_dlg
1225                        , app.DLG_PROP_SET
1226                        , prop=form)
1227
1228        for name, new_ctrl in ctrls.items():
1229            pass;              #log('name, new_ctrl={}',(name, new_ctrl))
1230
1231            cfg_ctrl= self.ctrls[name]
1232            cfg_ctrl.update(new_ctrl)
1233            new_ctrl['type']    = cfg_ctrl['type']
1234            c_prop  =self._prepare_c_pr(name, new_ctrl, {'ctrls':ctrls})
1235            pass;              #log('c_prop={}',(c_prop)) if new_ctrl['type']=='listview' else None
1236            dlg_proc_wpr(   self.id_dlg
1237                        , app.DLG_CTL_PROP_SET
1238                        , name=name
1239                        , prop=c_prop
1240                        )
1241
1242        if focused in self.ctrls:
1243            self.form['focused']    = focused
1244            app.dlg_proc(   self.id_dlg
1245                        , app.DLG_CTL_FOCUS
1246                        , name=focused)
1247       #def _update
1248
1249    def _gen_repro_code(self, rerpo_fn):
1250        # Repro-code
1251        l       = '\n'
1252        cattrs  = [  ('type', 'name', 'tag', 'act')
1253                    ,('x', 'y', 'w', 'h', 'w_min', 'h_min', 'w_max', 'h_max', 'cap', 'hint', 'p')
1254                    ,('en', 'vis', 'focused', 'tab_stop', 'tab_order'
1255                     ,'props', 'ex0', 'ex1', 'ex2', 'ex3', 'ex4', 'ex5', 'ex6', 'ex7', 'ex8', 'ex9'
1256                     ,'sp_l', 'sp_r', 'sp_t', 'sp_b', 'sp_a', 'a_l', 'a_r', 'a_t', 'a_b', 'align')
1257                    ,('val', 'items', 'columns')
1258                    ,('tp', 't', 'b', 'l', 'r', 'tid', 'a')
1259                    ]
1260        fattrs  = [  ('x', 'y', 'w', 'h', 'cap', 'tag')
1261                    ,('resize', 'w_min', 'w_max', 'h_min', 'h_max')
1262                    ,('vis', 'keypreview')
1263                    ]
1264        def out_attrs(pr, attrs, out=''):
1265            pr          = pr.copy()
1266            out         += '{'
1267            afix        = ''
1268            for ats in attrs:
1269                apr     =   {k:pr.pop(k) for k in ats if k in pr}
1270                if apr:
1271                    out += afix + ', '.join(repr(k) + ':' + repr(apr[k]) for k in ats if k in apr)
1272                    afix= '\n,'
1273            apr =           {k:pr.pop(k) for k in pr.copy() if k[0:3]!='on_'}
1274            if apr:
1275                out     += afix + repr(apr).strip('{}')
1276            for k in pr:
1277#               pr  = {k:(lambda idd,idc,data:print(repr(k))) for k in pr}
1278                out     += afix + f('"{}":(lambda idd,idc,data:print("{}"))', k, k)
1279            out         += '}'
1280            return out
1281        srp     =    ''
1282        srp    +=    'idd=dlg_proc(0, DLG_CREATE)'
1283        pass;                  #log('app.dlg_proc(self.id_dlg, app.DLG_CTL_COUNT)={}',(app.dlg_proc(self.id_dlg, app.DLG_CTL_COUNT)))
1284        for idC in range(app.dlg_proc(self.id_dlg, app.DLG_CTL_COUNT)):
1285            prC = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, index=idC)
1286            if ''==prC.get('hint', ''):                 prC.pop('hint', None)
1287            if ''==prC.get('tag', ''):                  prC.pop('tag', None)
1288            if ''==prC.get('cap', ''):                  prC.pop('cap', None)
1289            if ''==prC.get('items', None):              prC.pop('items')
1290            if prC.get('tab_stop', None):               prC.pop('tab_stop')
1291            if prC['type'] in ('label',):               prC.pop('tab_stop', None)
1292            if prC['type'] in ('bevel',):               (prC.pop('tab_stop', None)
1293                                                        ,prC.pop('tab_order', None))
1294            if prC['type'] not in ('listview'
1295                                  ,'checklistview'):    prC.pop('columns', None)
1296            if prC['type'] in ('label'
1297                              ,'bevel'
1298                              ,'button'):               prC.pop('val', None)
1299            if prC['type'] in ('button'):               prC.pop('act', None)
1300            if not prC.get('act', False):               prC.pop('act', None)
1301            if not prC.get('focused', False):           prC.pop('focused', None)
1302            if prC.get('vis', True):                    prC.pop('vis', None)
1303            if prC.get('en', True):                     prC.pop('en', None)
1304            name = prC['name']
1305            c_pr = self.ctrls[name].copy()
1306            c_pr = self._prepare_it_vl(c_pr, c_pr)
1307            prC.update({k:v for k,v in c_pr.items() if k not in ('callback','call')})
1308            srp+=l+f('idc=dlg_proc(idd, DLG_CTL_ADD,"{}")', prC['type'])
1309            prC.pop('type', None)
1310            srp+=l+f('dlg_proc(idd, DLG_CTL_PROP_SET, index=idc, prop={})', out_attrs(prC, cattrs))
1311        prD     = dlg_proc_wpr(self.id_dlg, app.DLG_PROP_GET)
1312        prD.update(self.form)
1313        srp    +=l+f('dlg_proc(idd, DLG_PROP_SET, prop={})', out_attrs(prD, fattrs))
1314        srp    +=l+f('dlg_proc(idd, DLG_CTL_FOCUS, name="{}")', prD['focused'])
1315        srp    +=l+  'dlg_proc(idd, DLG_SHOW_MODAL)'
1316        srp    +=l+  'dlg_proc(idd, DLG_FREE)'
1317        open(rerpo_fn, 'w', encoding='UTF-8').write(srp)
1318        pass;                   log(r'exec(open(r"{}", encoding="UTF-8").read())', rerpo_fn)
1319       #def _gen_repro_code
1320
1321    @staticmethod
1322    def _form_acts(act, form=None, id_dlg=None, key4store=None):
1323        """ Save/Restore pos of form """
1324        pass;                  #log('act, form, id_dlg={}',(act, form, id_dlg))
1325        CFG_JSON= app.app_path(app.APP_DIR_SETTINGS)+os.sep+'forms data.json'
1326        stores  = json.loads(open(CFG_JSON).read(), object_pairs_hook=odict) \
1327                    if os.path.exists(CFG_JSON) and os.path.getsize(CFG_JSON) != 0 else \
1328                  odict()
1329
1330        def get_form_key(prs):
1331            fm_cap  = prs['cap']
1332            fm_cap  = fm_cap[:fm_cap.rindex(' (')]      if ' (' in fm_cap else fm_cap
1333            fm_cap  = fm_cap[:fm_cap.rindex(' [')]      if ' [' in fm_cap else fm_cap
1334            return fm_cap #if ' (' not in fm_cap else fm_cap[:fm_cap.rindex(' (')]
1335
1336        if False:pass
1337
1338        if act=='move' and form:
1339            fm_key  = key4store if key4store else get_form_key(form)
1340            pass;              #log('fm_key={}',(fm_key))
1341            if fm_key not in stores:    return form
1342            prev    = stores[fm_key]
1343            if not form.get('resize', False):
1344                prev.pop('w', None)
1345                prev.pop('h', None)
1346            form.update(prev)
1347            pass;              #log('!upd form={}',(form))
1348            return form
1349
1350        if act=='save' and id_dlg:
1351            dlg_pr  = dlg_proc_wpr(id_dlg, app.DLG_PROP_GET)
1352            fm_key  = key4store if key4store else get_form_key(dlg_pr)
1353            pass;              #log('{}={}', fm_key,{k:v for k,v in dlg_pr.items() if k in ('x','y','w','h')})
1354            stores[fm_key]  = {k:v for k,v in dlg_pr.items() if k in ('x','y','w','h')}
1355            open(CFG_JSON, 'w').write(json.dumps(stores, indent=4))
1356       #def _form_acts
1357
1358   #class BaseDlgAgent
1359
1360ALI_CL  = app.ALIGN_CLIENT
1361ALI_LF  = app.ALIGN_LEFT
1362ALI_RT  = app.ALIGN_RIGHT
1363ALI_TP  = app.ALIGN_TOP
1364ALI_BT  = app.ALIGN_BOTTOM
1365class DlgAgent(BaseDlgAgent):
1366    """
1367    Helper to use dlg_proc(). See wiki.freepascal.org/CudaText_API#dlg_proc
1368
1369    Main base features :
1370    - All controls are created once then some of them can be changed via callbacks (attribute 'call').
1371    - The helper stores config version of form's and control's attributes.
1372      So the helper can return two versions of attributes: configured and actual (live).
1373    - Helper marshals complex data ('items' for type=list/combo/..., 'val' for type=memo)
1374    - Helper can to save/restore form position and sizes to/from file "settings/forms data.json".
1375
1376    Main extra features:
1377    - Helper handles attributes names of controls
1378        Helper adds short synonyms:
1379            cid is name
1380            tp  is type
1381            fid is focused
1382        Helper adds new attributes to simplify config:
1383            l,r,t,b,tid,a,aid
1384        are translated to live
1385            x,y,w,h,a_*,sp*
1386    - Helper allows to aligns texts from linear controls (by tid attribute)
1387
1388    Terms
1389        conf-attr - configured attribute (key and value passed to agent from plugin)
1390        live-attr - actual attribute     (key and value taked from dlg_proc)
1391
1392    Rules
1393    1. All controls have conf-attr 'cid'|'name'. It must be unique.
1394    2. All controls have conf-attr 'tp'|'type'.
1395        Value of 'tp'|'type' can be any API values or shortened variants from REDUCTIONS.
1396    3. Control position can be set
1397        - directly by x,y,w,h
1398        - computed by enough subset of l,r,w,t,b,h (x=l, y=t, l+w=r, t+h=b)
1399        - computed by tid (refer to cid|name of control, from which to get t and add a bit to align texts)
1400    4. Controls values (attribute 'val') can be passed to agent as separate collection:
1401        - parameter 'vals' to call constructor
1402        - key 'vals' in dict to return from callback
1403    5. Need focused control cid can be passed to agent
1404        - parameter 'fid' to call constructor
1405        - key 'fid' in dict to return from callback
1406        - key 'fid' into parameter 'form'
1407       Live focused control cid can be asked as agent.fattr('fid')
1408    6. Anchors 'aid' and 'a' are used to plan control's position on form resize.
1409        Note: agent's anchors cannot set initial control position (except to center control).
1410        'aid' points to a tagret: form (empty value, default) or cid.
1411        'a' has string value which can include character:
1412            - |     to center           the control by horz/vert with the target
1413            t T     to anchor top    of the control to top/bottom of the target
1414            b B     to anchor bottom of the control to top/bottom of the target
1415            l L     to anchor left   of the control to left/right of the target
1416            r R     to anchor right  of the control to left/right of the target
1417        Padding with target (live-attrs 'sp_*') are auto-calculated by initial positions of both elements.
1418        Examples
1419            a='|lR'
1420    7. Format of callback for a control
1421        def callname(name_of_event_control, agent):
1422            'smth'
1423            return None                         #   To hide form
1424            return {}                           #   To keep controls and form state
1425            return {'ctrls':[(nm,{...})]        #   To change controls with pointed names
1426                   ,'vals':{nm:...}             #   To change controls 'val' with pointed names
1427                   ,'form':{...}                #   To change form attributes
1428                   ,'focused':name_to_focus     #   To change focus
1429                   ,'fid':cid_to_focus          #   To change focus
1430                   }                            #   Any key ('ctrls','vals','form','fid','focused') can be omitted.
1431        Callback cannot add/del controls or change cid,type,a,aid
1432        Callback have to conside the form as it has initial size - agent will recalculate to actual state.
1433        Useful methods of agent
1434        - agent.cattr(cid, '??', live=T)        To get a control actual/configured attribute
1435        - agent.cattrs(cid, ['??'], live=T)     To get dict of a control listed actual/configured attributes
1436        - agent.cval(cid, live=T)               To get actual/configured 'val' attribute (short of agent.cattr(cid, 'val'))
1437        - agent.cvals([cid], live=T)            To get dict of actual/configured 'val' the listed attributes
1438        - agent.fattr('??', live=T)             To get actual/configured form attribute
1439        - agent.fattrs(live=T, ['??']=None)     To get actual/configured all/listed form attributes
1440    8. Match of different conf-attr and live-attr keys:
1441        cid     name
1442        tp      type
1443        l       x
1444        r       x+w
1445        t       y
1446        b       y+h
1447        tid     y
1448        aid     a_*[0]
1449        a       a_*[1], sp*
1450        fid     focused
1451        call    callback
1452    9. List of same conf-attr and live-attr keys
1453        w h
1454        cap hint
1455        props
1456        color
1457        font_name font_size font_color font
1458        val
1459        act
1460        en vis
1461        tag
1462        tab_stop tab_order
1463    10. Tricks
1464        - 'tid' vertical aligns not a pair of controls themself but text in its.
1465          For this it uses platform specific data. The data can be configured by call dlg_valign_consts()
1466        - If cap of a label starts with '>' then the character is cut and 'prop' set to '1' to right alignment
1467        - Attributes
1468            def_bt spinedit url grid at_botttom brdW_fillC_fontC_brdC ro_mono_brd
1469          are used as temporary and readable version of 'prop'.
1470          They can be use to configure as others but key to ask is 'prop'.
1471        - Attribute
1472            sto     tab_stop
1473          is used as temporary version of 'tab_stop'.
1474
1475    Example. Horz-resized modal dialog with two buttons. First button is stretched, second is pinned to right.
1476    DlgAgent(
1477        ctrls=[('b1', dict(type='button',             cap='Click', x=0, y= 0, w=100, a='lR'
1478                call=lambda name,ag:{'ctrls':[('b1',{'cap':'OK',             'w':70})]}
1479               ))
1480              ,('b2', dict(type='button', cap='Close', x=0, y=30, w=100,             a='LR'
1481                call=lambda name,ag:None)
1482               )]
1483    ,   form=dict(cap='Two buttons', x=0, y=0, w=100, h=60)
1484    ,   focused='b1'
1485    ).show()
1486    """
1487
1488    def __init__(self, ctrls, vals=None, form=None, fid=None, focused=None, options=None):
1489        options = options or {}
1490        super().__init__([], options={k:v for k,v in options.items() if k not in ['gen_repro_to_file']})
1491        # Inherited Fields
1492#       self.opts
1493#       self.id_dlg
1494#       self.ctrls
1495#       self.form
1496        self._setup(ctrls, vals, form, focused or fid)
1497
1498        rtf     = options.get('gen_repro_to_file', False)
1499        rtf_fn  = rtf if isinstance(rtf, str) else 'repro_dlg_proc.py'
1500        if rtf:   self._gen_repro_code(tempfile.gettempdir()+os.sep+rtf_fn)
1501       #def __init__
1502
1503    def cval(self, cid, live=True, defv=None):
1504        """ Return the control val property """
1505        return self.cattr(cid, 'val', live=live, defv=defv)
1506    def cvals(self, cids, live=True):
1507        """ Return the controls val property """
1508        return {cid:self.cattr(cid, 'val', live=live) for cid in cids}
1509
1510    def _setup(self, ctrls, vals=None, form=None, fid=None):
1511        """ Arrange and fill all: controls static/dynamic attrs, form attrs, focus.
1512            Params
1513                ctrls   [{}]
1514                vals    {cid:v}
1515                form    {}
1516                fid     str
1517        """
1518        #NOTE: DlgAg init
1519        self.ctrls  = odict(ctrls)      if isinstance(ctrls, list) else ctrls
1520        self.form   = form.copy()       if form     else {}
1521
1522        if 'checks'=='checks':
1523            no_tids = {cnt['tid'] for cnt in ctrls if 'tid' in cnt and cnt['tid'] not in self.ctrls}
1524            if no_tids:
1525                raise Exception(f('No cid for tid: {}', no_tids))
1526            no_vids = {cid for cid in vals if cid not in self.ctrls} if vals else None
1527            if no_vids:
1528                raise Exception(f('No cid for val: {}', no_vids))
1529#           if fid and fid not in self.ctrls:
1530#               raise Exception(f('No fid: {}', fid))
1531
1532        if vals:
1533            for cid, val in vals.items():
1534                self.ctrls[cid]['val']  = val
1535
1536        # Create controls
1537        for cid,cfg_ctrl in self.ctrls.items():
1538            cfg_ctrl.pop('callback', None)
1539            cfg_ctrl.pop('on_change', None)
1540#           cid     = cfg_ctrl.get('cid', cfg_ctrl.get('name'))
1541#           cfg_ctrl['cid']     = cid
1542#           cfg_ctrl['name']    = cid
1543            assert 'type' in cfg_ctrl or 'tp'  in cfg_ctrl
1544            tp      = cfg_ctrl.get('tp',  cfg_ctrl.get('type'))
1545            cfg_ctrl['tp']      = tp
1546            cfg_ctrl['type']    = REDUCTIONS.get(tp, tp)
1547            ind_c   = dlg_proc_wpr(self.id_dlg
1548                        , DLG_CTL_ADD_SET
1549                        , name=cfg_ctrl['type']
1550                        , prop=self._prepare_c_pr(cid, cfg_ctrl))
1551            pass;              #cfg_ctrl['_idc']    = ind_c         # While API bug: name isnot work if control is in panel
1552           #for cnt
1553
1554        # Resize callback
1555        if 'on_resize' in self.form:
1556            user_callbk = self.form['on_resize']
1557            def da_rs_callbk(id_dlg, id_ctl=-1, data=''):
1558                upds    = user_callbk(self)
1559                if upds:
1560                    self._update_on_call(upds)
1561            self.form['on_resize'] = da_rs_callbk
1562
1563        # Resize on start
1564        fpr     = self.form
1565        w0      = fpr['w']
1566        h0      = fpr['h']
1567        if fpr.get('resize', False):
1568            self._prepare_anchors()                                 # a,aid -> a_*,sp_*
1569            fpr['w_min']    = fpr.get('w_min', fpr['w'])
1570            fpr['h_min']    = fpr.get('h_min', fpr['h'])
1571        pass;                  #log('fpr is self.form={}',(fpr is self.form))
1572        fpr     = BaseDlgAgent._form_acts('move', form=fpr)         # Move and (maybe) resize
1573        pass;                  #log('fpr is self.form={}',(fpr is self.form))
1574        if 'on_resize' in self.form and \
1575           (fpr['w'] != w0 or \
1576            fpr['h'] != h0):
1577            pass;              #log('fpr[w],fpr[h],w0,h0={}',(fpr['w'], fpr['h'], w0,h0))
1578            self.form['on_resize'](self)
1579
1580        fpr['topmost']      = app.app_api_version()<'1.0.270' or app.app_proc(app.PROC_WINDOW_TOPMOST_GET, '')
1581#       fpr['topmost']      = True
1582        dlg_proc_wpr(           self.id_dlg
1583                            , app.DLG_PROP_SET
1584                            , prop=fpr)                         # Upd live-attrs
1585
1586        fid     = fid   if fid in self.ctrls else     self.form.get('fid')
1587        if fid in self.ctrls:
1588            self.form['fid']    = fid                           # Upd conf-attrs
1589            self.form['focused']= fid
1590            app.dlg_proc(   self.id_dlg
1591                        , app.DLG_CTL_FOCUS
1592                        , name=fid)                             # Upd live-attrs
1593
1594        pass;                   self._gen_repro_code(tempfile.gettempdir()+os.sep+'repro_dlg_proc.py')     if F else None
1595       #def _setup
1596
1597    EXTRA_C_ATTRS   = ['tp','l','t','r','b','tid','a','aid']
1598    def _prepare_c_pr(self, cid, cfg_ctrl, opts={}):
1599        pass;                  #log('cid, cfg_ctrl={}',(cid, cfg_ctrl))
1600#       cid     = cfg_ctrl['cid']
1601        tp      = cfg_ctrl['type']  # reduced
1602        DlgAgent._preprocessor(cfg_ctrl, tp)                                # sto -> tab_stop, ... -> props
1603        c_pr    = super()._prepare_c_pr(cid
1604                    , {k:v for k,v in cfg_ctrl.items() if k not in DlgAgent.EXTRA_C_ATTRS}
1605                    , {'prepare call':False})                               # items -> items, val -> val
1606        c_pr.update(self._prep_pos_attrs(cfg_ctrl, cid, opts.get('ctrls')))                    # l,r,t,b,tid -> x,y,w,h
1607        pass;                  #log('c_pr={}',(c_pr))
1608
1609        def get_proxy_cb(u_callbk, event):
1610            def da_c_callbk(idd, idc, data):
1611                pass;          #log('ev,idc,cid,data={}',(event,idc,cid,data))
1612                if tp in ('listview',) and type(data) in (tuple, list):
1613                    if not data[1]: return  # Skip event "selection loss"
1614                    # Crutch for Linux! Wait fix in core
1615                    event_val   = app.dlg_proc(idd, app.DLG_CTL_PROP_GET, index=idc)['val']
1616                    if event_val!=data[0]:
1617                        app.dlg_proc(idd, app.DLG_CTL_PROP_SET, index=idc, prop={'val':data[0]})
1618                pass;          #log('?? u_callbk',())
1619                upds    = u_callbk(cid, self, data)
1620                pass;          #log('ok u_callbk upds={}',(upds))
1621                if upds is None:                                        # To hide/close
1622                    app.dlg_proc(self.id_dlg, app.DLG_HIDE)
1623                    return
1624                elif not upds:                                          # No changes
1625                    return
1626                pass;          #log('upds={}',(upds))
1627                pass;          #log('?? _update_on_call',())
1628                self._update_on_call(upds)
1629                pass;          #log('ok _update_on_call',())
1630               #def da_c_callbk
1631            return da_c_callbk
1632           #def get_proxy_cb
1633
1634        for on_key in [k for k in cfg_ctrl if (k=='call' or k.startswith('on_')) and callable(cfg_ctrl.get(k))]:
1635            user_callbk     = cfg_ctrl[on_key]
1636            on_what         =  on_key \
1637                                if on_key.startswith('on_') else \
1638                              'on_select' \
1639                                if tp in ('listview', 'treeview') else \
1640                              'on_click' \
1641                                if tp in ('linklabel') else \
1642                              'on_change'
1643            pass;              #log('cid,tp,on_what={}',(cid,tp,on_what))
1644            if tp!='button':
1645                c_pr['act'] = True
1646            c_pr[on_what]   = get_proxy_cb(user_callbk, on_what)
1647#           pass;               log('?? on_what={}',on_what)
1648#           pass;               log('  c_pr[on_what]()={}',(c_pr[on_what](0,0,'')))
1649#           pass;               log('ok on_what={}',on_what)
1650           #for on_key
1651
1652        if callable(cfg_ctrl.get('menu')):
1653            c_pr['on_menu'] = lambda idd, idc, data: cfg_ctrl['menu'](cid, self)
1654
1655        pass;                  #log('c_pr={}',(c_pr)) if c_pr['type']=='checkbutton' else 0
1656        return c_pr
1657       #def _prepare_c_pr
1658
1659    def show_menu(self, cid, mn_content, where='+h'):
1660        """ cid             Control to show menu near it
1661            mn_content      [{cap:'', tag:'', en:T, ch:F, rd:F, cmd:(lambda ag, tag:''), sub:[]}]
1662            where           Menu position
1663                                '+h' - under the control
1664                                '+w' - righter the control
1665        """
1666        pr      = self.cattrs(cid, ('x','y','w','h'))
1667        x, y    = pr['x']+(pr['w'] if '+w' in where else 0) \
1668                , pr['y']+(pr['h'] if '+h' in where else 0)
1669        pass;                  #log('x, y={}',(x, y))
1670        prXY    = _os_scale('scale', {'x':x, 'y':y})
1671        x, y    = prXY['x'], prXY['y']
1672        pass;                  #log('x, y={}',(x, y))
1673        x, y    = app.dlg_proc(self.id_dlg, app.DLG_COORD_LOCAL_TO_SCREEN, index=x, index2=y)
1674        pass;                  #log('x, y={}',(x, y))
1675
1676        def da_mn_callbk(it):
1677            pass;              #log('it[tag]={}',(it['tag']))
1678            u_callbk= it['cmd']
1679            upds    = u_callbk(self, it.get('tag', ''))
1680            if upds is None:                                        # To hide/close
1681                app.dlg_proc(self.id_dlg, app.DLG_HIDE)
1682                return
1683            if not upds:    return  # No changes
1684            self._update_on_call(upds)
1685           #def da_mn_callbk
1686
1687        def fill_mn(mid_prn, its):
1688            for it in its:
1689                if it['cap']=='-':
1690                    app.menu_proc(  mid_prn, app.MENU_ADD, caption='-');
1691                    continue
1692                mid =(app.menu_proc(mid_prn, app.MENU_ADD, caption=it['cap'], command= lambda _it=it:da_mn_callbk(_it))     # _it=it solves lambda closure problem
1693                        if 'cmd' in it else
1694                      app.menu_proc(mid_prn, app.MENU_ADD, caption=it['cap'])
1695                     )
1696                if 'key' in it and it['key']:
1697                    app.menu_proc(      mid, app.MENU_SET_HOTKEY            , command=     it['key'])
1698                if 'en' in it:
1699                    app.menu_proc(      mid, app.MENU_SET_ENABLED           , command=bool(it['en']))
1700                if 'ch' in it:
1701                    app.menu_proc(      mid, app.MENU_SET_CHECKED           , command=bool(it['ch']))
1702                if 'rd' in it:
1703                    app.menu_proc(      mid, app.MENU_SET_RADIOITEM         , command=bool(it['rd']))
1704                if 'sub' in it:
1705                    fill_mn(mid, it['sub'])
1706           #def fill_mn
1707
1708        mid_top = app.menu_proc(    0,       app.MENU_CREATE)
1709        fill_mn(mid_top, mn_content)
1710        app.menu_proc(              mid_top, app.MENU_SHOW                  , command=f('{},{}', x, y))
1711       #def show_menu
1712
1713    def _update_on_call(self, upds):
1714        if isinstance(upds, tuple) or isinstance(upds, list) :          # Allow to use list of upd data
1715            upds    = deep_upd(upds)
1716            pass;      #log('upds={}',(upds))
1717        ctrls_u = odict(upds.get('ctrls',  []))
1718        pass;          #log('ctrls_u={}',(ctrls_u))
1719        vals    = upds.get('vals',   {})
1720        form    = upds.get('form',   {})
1721        fid     = upds.get('fid'  , upds.get('focused', form.get('fid', form.get('focused'))))
1722        if False:pass
1723        elif vals and not ctrls_u:
1724            ctrls_u     = { cid_    :  {'val':val} for cid_, val in vals.items()}
1725        elif vals and     ctrls_u:
1726            for cid_, val in vals.items():
1727                if cid_ not in ctrls_u:
1728                    ctrls_u[cid_]   =  {'val':val}
1729                else:
1730                    ctrls_u[cid_]['val']    = val
1731        for cid_, c in ctrls_u.items():
1732            pass;      #log('cid_, c={}',(cid_, c))
1733            c.pop('callback', None)
1734            c.pop('on_change', None)
1735            c.pop('call', None)
1736            c['type']   = self.ctrls[cid_]['type']
1737        super(DlgAgent,self).update( ctrls  =ctrls_u
1738                                    ,form   =form
1739                                    ,focused=fid)
1740        if fid in self.ctrls:
1741            self.form['fid']    = fid
1742       #def _update_on_call
1743
1744    def _prepare_anchors(self):
1745        """ Translate attrs 'a' 'aid' to 'a_*','sp_*'
1746            Values for 'a' are str-mask with signs
1747                'l' 'L'    fixed distanse ctrl-left     to trg-left  or trg-right
1748                't' 'T'    fixed distanse ctrl-top      to trg-top   or trg-bottom
1749                'r' 'R'    fixed distanse ctrl-right    to trg-left  or trg-right
1750                'b' 'B'    fixed distanse ctrl-bottom   to trg-top   or trg-bottom
1751        """
1752        fm_w    = self.form['w']
1753        fm_h    = self.form['h']
1754        for cid,cnt in self.ctrls.items():
1755            anc     = cnt.get('a'  , '')
1756            if not anc: continue
1757            aid     = cnt.get('aid', cnt.get('p', ''))    # '' anchor to form
1758            trg_w,  \
1759            trg_h   = fm_w, fm_h
1760            if aid in self.ctrls:
1761                prTrg   = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, name=aid)
1762                trg_w,  \
1763                trg_h   = prTrg['w'], prTrg['h']
1764            prOld   = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, name=cid)
1765            pass;              #prOld   = prOld if prOld else \
1766                     #dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, index=self.ctrls[cid]['_idc'])    # While API bug: name isnot work if control is in panel
1767            pass;               logb=cid in ('tolx', 'tofi')
1768            pass;              #nat_prOld=app.dlg_proc(self.id_dlg, app.DLG_CTL_PROP_GET, name=cid)
1769            pass;              #log('cid,nat-prOld={}',(cid,{k:v for k,v in nat_prOld.items() if k in ('x','y','w','h','_ready_h')})) if logb else 0
1770            pass;              #log('cid,    prOld={}',(cid,{k:v for k,v in     prOld.items() if k in ('x','y','w','h','_ready_h')})) if logb else 0
1771            pass;              #log('cid,anc,trg_w,trg_h,prOld={}',(cid,anc,trg_w,trg_h, {k:v for k,v in prOld.items() if k in ('x','y','w','h')})) \
1772                               #    if logb else 0
1773            prAnc   = {}
1774            if '-' in anc:
1775                # Center by horz
1776                prAnc.update(dict( a_l=(aid, '-')
1777                                  ,a_r=(aid, '-')))
1778            if 'L' in anc and 'R' in anc:
1779                # Both left/right to form right
1780                pass;          #log('+L +R') if logb else 0
1781                prAnc.update(dict( a_l=None                                             # (aid, ']'), sp_l=trg_w-prOld['x']
1782                                  ,a_r=(aid, ']'), sp_r=trg_w-prOld['x']-prOld['w']))
1783            if 'L' in anc and 'R' not in anc:
1784                # Left to form right
1785                pass;          #log('+L -R') if logb else 0
1786                prAnc.update(dict( a_l=(aid, '['), sp_l=trg_w-prOld['x']
1787                                  ,a_r=None))
1788            if 'l' in anc and 'R' in anc:
1789                # Left to form left. Right to form right.
1790                pass;          #log('+l +R') if logb else 0
1791                prAnc.update(dict( a_l=(aid, '['), sp_l=      prOld['x']
1792                                  ,a_r=(aid, ']'), sp_r=trg_w-prOld['x']-prOld['w']))
1793            if '|' in anc:
1794                # Center by vert
1795                prAnc.update(dict( a_t=(aid, '-')
1796                                  ,a_b=(aid, '-')))
1797            if 'T' in anc and 'B' in anc:
1798                # Both top/bottom to form bottom
1799                pass;          #log('+T +B') if logb else 0
1800                prAnc.update(dict( a_t=None      #, sp_t=trg_h-prOld['y']                # a_t=(aid, ']') - API bug
1801                                  ,a_b=(aid, ']'), sp_b=trg_h-prOld['y']-prOld['h']))
1802            elif 'T' in anc and 'B' not in anc:
1803                # Top to form bottom
1804                pass;          #log('+T -B') if logb else 0
1805                prAnc.update(dict( a_t=(aid, ']'), sp_t=trg_h-prOld['y']                # a_t=(aid, ']') - API bug
1806                                  ,a_b=None))
1807            if 't' in anc and 'B' in anc:
1808                # Top to form top. Bottom to form bottom.
1809                pass;          #log('+t +B') if logb else 0
1810                prAnc.update(dict( a_t=(aid, '['), sp_t=      prOld['y']
1811                                  ,a_b=(aid, ']'), sp_b=trg_h-prOld['y']-prOld['h']))
1812            if prAnc:
1813                pass;          #log('aid,prAnc={}',(cid, prAnc)) if logb else 0
1814                cnt.update(prAnc)
1815                dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_SET, name=cid, prop=prAnc)
1816#               pass;           pr_   = dlg_proc_wpr(self.id_dlg, app.DLG_CTL_PROP_GET, name=cid)
1817#               pass;           log('cid,pr_={}',(cid, {k:v for k,v in pr_.items() if k in ('h','y', 'sp_t', 'sp_b', 'a_t', 'a_b')}))
1818       #def _prepare_anchors
1819
1820    def _prep_pos_attrs(self, cnt, cid, ctrls4t=None):
1821        # Position:
1822        #   t[op] or tid, l[eft] required
1823        #   w[idth]  >>> r[ight ]=l+w
1824        #   h[eight] >>> b[ottom]=t+h
1825        #   b dont need for buttons, edit, labels
1826#       if not [k for k in cnt.keys() if k in ('l','t','r','b','tid')]:
1827#           return {k:v for (k,v) in cnt.items() if k in ('x','y','w','h')}
1828
1829        pass;                  #log('cid, cnt={}',(cid, cnt))
1830        prP     =  {}
1831
1832        if 'h' not in cnt \
1833        and cnt['type'] in (  'button', 'checkbutton'
1834                            , 'label'
1835                            , 'combo', 'combo_ro'
1836                            , 'edit', 'spinedit'
1837                            , 'check', 'radio'
1838                            , 'filter_listbox', 'filter_listview'
1839                            ):
1840            # OS specific control height
1841            cnt['h']    = get_gui_height(cnt['type'])
1842            prP['_ready_h'] = True
1843#           cnt['h']    = app.app_proc(app.PROC_GET_GUI_HEIGHT, cnt['type'])
1844
1845        if 'l' in cnt:
1846            prP['x']    = cnt['l']
1847        if 'r' in cnt and 'x' in prP:
1848            prP['w']    = cnt['r'] - prP['x']
1849        if 'w' in cnt:
1850            prP['w']    = cnt['w']
1851
1852        if 't' in cnt:
1853            prP['y']    = cnt['t']
1854#       t       = cnt.get('t', 0)   if 't' in cnt else  self.cattr(cid, 't', live=False)
1855        elif 'tid' in cnt:
1856            ctrls4t = ctrls4t if ctrls4t else self.ctrls
1857            assert cnt['tid'] in ctrls4t
1858            # cid for horz-align text
1859            bs_cnt4t= ctrls4t[   cnt['tid']]
1860            bs_cnt  = self.ctrls[cnt['tid']]
1861            bs_tp   = bs_cnt['tp']
1862            bs_tp   = REDUCTIONS.get(bs_tp, bs_tp)
1863            tp      = self.ctrls[cid]['tp']
1864            tp      = REDUCTIONS.get(tp, tp)
1865            pass;              #log('tp, bs_tp, fit, bs_cnt={}',(tp, bs_tp, fit_top_by_env(tp, bs_tp), bs_cnt))
1866            t       = bs_cnt4t['t'] + fit_top_by_env(tp, bs_tp)
1867            prP['y']    = t
1868        if 'b' in cnt and 'y' in prP:
1869            prP['h']    = cnt['b'] - prP['y']
1870        if 'h' in cnt:
1871            prP['h']    = cnt['h']
1872
1873#       b       = cnt.get('b', t+cnt.get('h', 0))
1874
1875#       l       = cnt['l']          if 'l' in cnt else  self.cattr(cid, 'l', live=False)
1876#       r       = cnt.get('r', l+cnt.get('w', 0))
1877#       prP     =  dict(x=l, y=t, w=r-l)
1878#       prP.update(dict(h=cnt.get('h')))    if 0!=cnt.get('h', 0) else 0
1879        pass;                  #log('cid, prP={}',(cid, prP))
1880        return prP
1881       #def _prep_pos_attrs
1882
1883    @staticmethod
1884    def _preprocessor(cnt, tp):
1885        if 'ali' in cnt:
1886            cnt['align'] = cnt.pop('ali')
1887        if 'sp_lr' in cnt:
1888            cnt['sp_l'] = cnt['sp_r']               = cnt.pop('sp_lr')
1889        if 'sp_lrt' in cnt:
1890            cnt['sp_l'] = cnt['sp_r'] = cnt['sp_t'] = cnt.pop('sp_lrt')
1891        if 'sp_lrb' in cnt:
1892            cnt['sp_l'] = cnt['sp_r'] = cnt['sp_b'] = cnt.pop('sp_lrb')
1893
1894        if 'sto' in cnt:
1895            cnt['tab_stop'] = cnt.pop('sto')
1896        if 'tor' in cnt:
1897            cnt['tab_order'] = cnt.pop('tor')
1898
1899        if 'props' in cnt:
1900            pass
1901        elif tp=='label' and 'cap' in cnt and cnt['cap'][0]=='>':
1902            #   cap='>smth' --> cap='smth', props='1' (r-align)
1903            cnt['cap']  = cnt['cap'][1:]
1904            cnt['props']= '1'
1905        elif tp=='label' and    cnt.get('ralign'):
1906            cnt['props']=       cnt.pop('ralign')
1907        elif tp=='button' and cnt.get('def_bt') in ('1', True):
1908            cnt['props']= '1'
1909        elif tp=='spinedit' and cnt.get('min_max_inc'):
1910            cnt['props']=       cnt.pop('min_max_inc')
1911        elif tp=='linklabel' and    cnt.get('url'):
1912            cnt['props']=           cnt.pop('url')
1913        elif tp=='listview' and cnt.get('grid'):
1914            cnt['props']=       cnt.pop('grid')
1915        elif tp=='tabs' and     cnt.get('at_botttom'):
1916            cnt['props']=       cnt.pop('at_botttom')
1917        elif tp=='colorpanel' and   cnt.get('brdW_fillC_fontC_brdC'):
1918            cnt['props']=           cnt.pop('brdW_fillC_fontC_brdC')
1919        elif tp in ('edit', 'memo') and cnt.get('ro_mono_brd'):
1920            cnt['props']=               cnt.pop('ro_mono_brd')
1921
1922        if 'props' in cnt and app.app_api_version()>='1.0.224':
1923            # Convert props to ex0..ex9
1924            #   See 'Prop "ex"' at wiki.freepascal.org/CudaText_API
1925            lsPr = cnt.pop('props').split(',')
1926            if False:pass
1927            elif tp=='button':
1928                cnt['ex0']  = '1'==lsPr[0]  #bool: default for Enter key
1929            elif tp in ('edit', 'memo'):
1930                cnt['ex0']  = '1'==lsPr[0]  #bool: read-only
1931                cnt['ex1']  = '1'==lsPr[1]  #bool: font is monospaced
1932                cnt['ex2']  = '1'==lsPr[2]  #bool: show border
1933            elif tp=='spinedit':
1934                cnt['ex0']  =  int(lsPr[0]) #int:  min value
1935                cnt['ex1']  =  int(lsPr[1]) #int:  max value
1936                cnt['ex2']  =  int(lsPr[2]) #int:  increment
1937            elif tp=='label':
1938                cnt['ex0']  = '1'==lsPr[0]  #bool: right aligned
1939            elif tp=='linklabel':
1940                cnt['ex0']  = lsPr[0]       #str: URL. Should not have ','. Clicking on http:/mailto: URLs should work, result of clicking on other kinds depends on OS.
1941            elif tp=='listview':
1942                cnt['ex0']  = '1'==lsPr[0]  #bool: show grid lines
1943            elif tp=='tabs':
1944                cnt['ex0']  = '1'==lsPr[0]  #bool: show tabs at bottom
1945            elif tp=='colorpanel':
1946                cnt['ex0']  =  int(lsPr[0]) #int:  border width (from 0)
1947                cnt['ex1']  =  int(lsPr[1]) #int:  color of fill
1948                cnt['ex2']  =  int(lsPr[2]) #int:  color of font
1949                cnt['ex3']  =  int(lsPr[3]) #int:  color of border
1950            elif tp=='filter_listview':
1951                cnt['ex0']  = '1'==lsPr[0]  #bool: filter works for all columns
1952            elif tp=='image':
1953                cnt['ex0']  = '1'==lsPr[0]  #bool: center picture
1954                cnt['ex1']  = '1'==lsPr[1]  #bool: stretch picture
1955                cnt['ex2']  = '1'==lsPr[2]  #bool: allow stretch in
1956                cnt['ex3']  = '1'==lsPr[3]  #bool: allow stretch out
1957                cnt['ex4']  = '1'==lsPr[4]  #bool: keep origin x, when big picture clipped
1958                cnt['ex5']  = '1'==lsPr[5]  #bool: keep origin y, when big picture clipped
1959            elif tp=='trackbar':
1960                cnt['ex0']  =  int(lsPr[0]) #int:  orientation (0: horz, 1: vert)
1961                cnt['ex1']  =  int(lsPr[1]) #int:  min value
1962                cnt['ex2']  =  int(lsPr[2]) #int:  max value
1963                cnt['ex3']  =  int(lsPr[3]) #int:  line size
1964                cnt['ex4']  =  int(lsPr[4]) #int:  page size
1965                cnt['ex5']  = '1'==lsPr[5]  #bool: reversed
1966                cnt['ex6']  =  int(lsPr[6]) #int:  tick marks position (0: bottom-right, 1: top-left, 2: both)
1967                cnt['ex7']  =  int(lsPr[7]) #int:  tick style (0: none, 1: auto, 2: manual)
1968            elif tp=='progressbar':
1969                cnt['ex0']  =  int(lsPr[0]) #int:  orientation (0: horz, 1: vert, 2: right-to-left, 3: top-down)
1970                cnt['ex1']  =  int(lsPr[1]) #int:  min value
1971                cnt['ex2']  =  int(lsPr[2]) #int:  max value
1972                cnt['ex3']  = '1'==lsPr[3]  #bool: smooth bar
1973                cnt['ex4']  =  int(lsPr[4]) #int:  step
1974                cnt['ex5']  =  int(lsPr[5]) #int:  style (0: normal, 1: marquee)
1975                cnt['ex6']  = '1'==lsPr[6]  #bool: show text (only for some OSes)
1976            elif tp=='progressbar_ex':
1977                cnt['ex0']  =  int(lsPr[0]) #int:  style (0: text only, 1: horz bar, 2: vert bar, 3: pie, 4: needle, 5: half-pie)
1978                cnt['ex1']  =  int(lsPr[1]) #int:  min value
1979                cnt['ex2']  =  int(lsPr[2]) #int:  max value
1980                cnt['ex3']  = '1'==lsPr[3]  #bool: show text
1981                cnt['ex4']  =  int(lsPr[4]) #int:  color of background
1982                cnt['ex5']  =  int(lsPr[5]) #int:  color of foreground
1983                cnt['ex6']  =  int(lsPr[6]) #int:  color of border
1984            elif tp=='bevel':
1985                cnt['ex0']  =  int(lsPr[0]) #int:  shape (0: sunken panel, 1: 4 separate lines - use it as border for group of controls, 2: top line, 3: bottom line, 4: left line, 5: right line, 6: no lines, empty space)
1986            elif tp=='splitter':
1987                cnt['ex0']  = '1'==lsPr[0]  #bool: beveled style
1988                cnt['ex1']  = '1'==lsPr[1]  #bool: instant repainting
1989                cnt['ex2']  = '1'==lsPr[2]  #bool: auto snap to edge
1990                cnt['ex3']  =  int(lsPr[3]) #int:  min size
1991       #def _preprocessor
1992
1993#class DlgAgent
1994
1995
1996######################################
1997#NOTE: dlg_valign_consts
1998######################################
1999def dlg_valign_consts():
2000    pass;                      #log('ok')
2001    rsp     = False
2002    UP,DN   = '↑↑','↓↓'
2003    DLG_W,  \
2004    DLG_H   = 335, 310
2005    ctrls   = ['check'
2006              ,'edit'
2007              ,'button'
2008              ,'combo_ro'
2009              ,'combo'
2010              ,'checkbutton'
2011              ,'linklabel'
2012              ,'spinedit'
2013              ,'radio'
2014              ]
2015    ctrls_sp= [('_sp'+str(1+ic),nc) for ic, nc in enumerate(ctrls)]
2016    fits    = {sp:fit_top_by_env(nc) for sp, nc in ctrls_sp}
2017    hints   = {sp:nc+': '+str(fits[sp]) for sp, nc in ctrls_sp}
2018
2019    def save():
2020        nonlocal rsp
2021        scam        = app.app_proc(app.PROC_GET_KEYSTATE, '') if app.app_api_version()>='1.0.143' else ''
2022        if not scam:#aid_m=='save':
2023            for sp, nc in ctrls_sp:
2024                fit = fits[sp]
2025                if fit==fit_top_by_env(nc): continue#for ic, nc
2026                apx.set_opt('dlg_wrapper_fit_va_for_'+nc, fit)
2027               #for ic, nc
2028            fit_top_by_env__clear()
2029            rsp = True
2030            return None#break#while
2031
2032        if scam=='c':#aid_m=='c/save': # Report
2033            rpt = 'env:'+get_desktop_environment()
2034            rpt+= c13 + c13.join(hints.values())
2035            aid_r, *_t = dlg_wrapper(_('Report'), 230,310,
2036                 [dict(cid='rprt',tp='me'    ,t=5   ,l=5 ,h=200 ,w=220)
2037                 ,dict(           tp='lb'    ,t=215 ,l=5        ,w=220  ,cap=_('Send the report to the address'))
2038                 ,dict(cid='mail',tp='ed'    ,t=235 ,l=5        ,w=220)
2039                 ,dict(           tp='lb'    ,t=265 ,l=5        ,w=150  ,cap=_('or post it on'))
2040                 ,dict(cid='gith',tp='ln-lb' ,t=265 ,l=155      ,w=70   ,cap='GitHub',props='https://github.com/kvichans/cuda_fit_v_alignments/issues')
2041                 ,dict(cid='-'   ,tp='bt'    ,t=280 ,l=205-80   ,w=80   ,cap=_('Close'))
2042                 ], dict(rprt=rpt
2043                        ,mail='kvichans@mail.ru')
2044                 ,  focus_cid='rprt')
2045        return {}
2046       #def save
2047
2048    def up_dn(ag, cid, sht):
2049        pass;                  #log('cid,sht={}',(cid,sht))
2050        sign    = cid[-1]
2051        sp      = '_sp'+sign
2052        fits[sp]= fits[sp] + sht
2053        nonlocal hints
2054        hints   = {sp:nc+': '+str(fits[sp]) for sp, nc in ctrls_sp}
2055        return {'ctrls':[(cid ,dict(y=ag.cattr(cid, 'y')+sht ,hint=hints[sp] ))]}
2056       #def up_dn
2057
2058    cs      = ctrls
2059    cnts    = \
2060            [('lb1' ,dict(tp='lb'    ,t= 10              ,l=  5  ,w=100  ,cap=cs[0]+' ==============='                          ))
2061            ,('ch1' ,dict(tp='ch'    ,t= 10+fits['_sp1'] ,l=115  ,w=100  ,cap='=================',hint=hints['_sp1']     ,val=F))
2062            ,('up1' ,dict(tp='bt'    ,t= 10-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'ch1',-1) ))
2063            ,('dn1' ,dict(tp='bt'    ,t= 10-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'ch1', 1) ))
2064
2065            ,('lb2' ,dict(tp='lb'    ,t= 40              ,l=  5  ,w=100  ,cap=cs[1]+' ==============='                          ))
2066            ,('ed2' ,dict(tp='ed'    ,t= 40+fits['_sp2'] ,l=115  ,w=100                          ,hint=hints['_sp2']     ,val='================='))
2067            ,('up2' ,dict(tp='bt'    ,t= 40-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'ed2',-1) ))
2068            ,('dn2' ,dict(tp='bt'    ,t= 40-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'ed2', 1) ))
2069
2070            ,('lb3' ,dict(tp='lb'    ,t= 70              ,l=  5  ,w=100  ,cap=cs[2]+' ==============='                          ))
2071            ,('bt3' ,dict(tp='bt'    ,t= 70+fits['_sp3'] ,l=115  ,w=100  ,cap='=================',hint=hints['_sp3']     ))
2072            ,('up3' ,dict(tp='bt'    ,t= 70-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'bt3',-1) ))
2073            ,('dn3' ,dict(tp='bt'    ,t= 70-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'bt3', 1) ))
2074
2075            ,('lb4' ,dict(tp='lb'    ,t=100              ,l=  5  ,w=100  ,cap=cs[3]+' ==============='                          ))
2076            ,('cbo4',dict(tp='cb-ro' ,t=100+fits['_sp4'] ,l=115  ,w=100  ,items=['============='],hint=hints['_sp4']     ,val=0))
2077            ,('up4' ,dict(tp='bt'    ,t=100-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'cbo4',-1)))
2078            ,('dn4' ,dict(tp='bt'    ,t=100-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'cbo4', 1)))
2079
2080            ,('lb5' ,dict(tp='lb'    ,t=130              ,l=  5  ,w=100  ,cap=cs[4]+' ==============='                          ))
2081            ,('cb5' ,dict(tp='cb'    ,t=130+fits['_sp5'] ,l=115  ,w=100  ,items=['============='],hint=hints['_sp5']     ,val='============='))
2082            ,('up5' ,dict(tp='bt'    ,t=130-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'cb5',-1) ))
2083            ,('dn5' ,dict(tp='bt'    ,t=130-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'cb5', 1) ))
2084
2085            ,('lb6' ,dict(tp='lb'    ,t=160              ,l=  5  ,w=100  ,cap=cs[5]+' ==============='                          ))
2086            ,('chb6',dict(tp='ch-bt' ,t=160+fits['_sp6'] ,l=115  ,w=100  ,cap='==========='      ,hint=hints['_sp6']     ,val=0))
2087            ,('up6' ,dict(tp='bt'    ,t=160-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'chb6',-1)))
2088            ,('dn6' ,dict(tp='bt'    ,t=160-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'chb6', 1)))
2089
2090            ,('lb7', dict(tp='lb'    ,t=190              ,l=  5  ,w=100  ,cap=cs[6]+' ==============='                          ))
2091            ,('lnb7',dict(tp='ln-lb' ,t=190+fits['_sp7'] ,l=115  ,w=100  ,cap='=================',props=hints['_sp7']    ))
2092            ,('up7' ,dict(tp='bt'    ,t=190-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'lnb7',-1)))
2093            ,('dn7' ,dict(tp='bt'    ,t=190-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'lnb7', 1)))
2094
2095            ,('lb8' ,dict(tp='lb'    ,t=220              ,l=  5  ,w=100  ,cap=cs[7]+' 4444444444444444'                         ))
2096            ,('sp8' ,dict(tp='sp-ed' ,t=220+fits['_sp8'] ,l=115  ,w=100  ,props='0,444444444,1'  ,hint=hints['_sp8']     ,val=444444444))
2097            ,('up8' ,dict(tp='bt'    ,t=220-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'sp8',-1) ))
2098            ,('dn8' ,dict(tp='bt'    ,t=220-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'sp8', 1) ))
2099
2100            ,('lb9' ,dict(tp='lb'    ,t=250              ,l=  5  ,w=100  ,cap=cs[8]+' ==============='                          ))
2101            ,('rd9' ,dict(tp='rd'    ,t=250+fits['_sp9'] ,l=115  ,w=100  ,cap='=================',hint=hints['_sp9']     ,val=F))
2102            ,('up9' ,dict(tp='bt'    ,t=250-3            ,l=230  ,w=50   ,cap=UP ,call=lambda cid,ag,d: up_dn(ag,'rd9',-1) ))
2103            ,('dn9' ,dict(tp='bt'    ,t=250-3            ,l=280  ,w=50   ,cap=DN ,call=lambda cid,ag,d: up_dn(ag,'rd9', 1) ))
2104
2105            ,('save',dict(tp='bt'    ,t=DLG_H-30         ,l=115  ,w=100  ,cap=_('&Save')     ,call=lambda cid,ag,d: save()
2106                                                                                ,hint=_('Apply the fittings to controls of all dialogs.'
2107                                                                                        '\rCtrl+Click  - Show data to mail report.')))
2108            ,('-'   ,dict(tp='bt'    ,t=DLG_H-30         ,l=230  ,w=100  ,cap=_('Cancel')    ,call=(lambda cid,ag,d: None) ))
2109            ]
2110    agent   = DlgAgent( form=dict(cap=_('Adjust vertical alignments'), w=DLG_W, h=DLG_H)
2111                       ,ctrls=cnts ,fid = '-'
2112                               #,options={'gen_repro_to_file':'repro_dlg_valign_consts.py'}
2113            ).show()    #NOTE: dlg_valign
2114    return rsp
2115   #def dlg_valign_consts
2116
2117def get_hotkeys_desc(cmd_id, ext_id=None, keys_js=None, def_ans=''):
2118    """ Read one or two hotkeys for command
2119            cmd_id [+ext_id]
2120        from
2121            settings\keys.json
2122        Return
2123            def_ans                     If no  hotkeys for the command
2124            'Ctrl+Q'
2125            'Ctrl+Q * Ctrl+W'           If one hotkey  for the command
2126            'Ctrl+Q/Ctrl+T'
2127            'Ctrl+Q * Ctrl+W/Ctrl+T'    If two hotkeys for the command
2128    """
2129    if keys_js is None:
2130        keys_json   = app.app_path(app.APP_DIR_SETTINGS)+os.sep+'keys.json'
2131        keys_js     = apx._json_loads(open(keys_json).read()) if os.path.exists(keys_json) else {}
2132
2133    cmd_id  = f('{},{}', cmd_id, ext_id) if ext_id else cmd_id
2134    if cmd_id not in keys_js:
2135        return def_ans
2136    cmd_keys= keys_js[cmd_id]
2137    desc    = '/'.join([' * '.join(cmd_keys.get('s1', []))
2138                       ,' * '.join(cmd_keys.get('s2', []))
2139                       ]).strip('/')
2140    return desc
2141   #def get_hotkeys_desc
2142
2143######################################
2144#NOTE: plugins history
2145######################################
2146PLING_HISTORY_JSON  = app.app_path(app.APP_DIR_SETTINGS)+os.sep+'plugin history.json'
2147def get_hist(key_or_path, default=None, module_name='_auto_detect', to_file=PLING_HISTORY_JSON):
2148    """ Read from "plugin history.json" one value by string key or path (list of keys).
2149        Parameters
2150            key_or_path     Key(s) to navigate in json tree
2151                            Type: str or [str]
2152            default         Value to return  if no suitable node in json tree
2153            module_name     Start node to navigate.
2154                            If it is '_auto_detect' then name of caller module is used.
2155                            If it is None then it is skipped.
2156            to_file         Name of file to read. APP_DIR_SETTING will be joined if no full path.
2157
2158        Return              Found value or default
2159
2160        Examples (caller module is 'plg')
2161        1. If no "plugin history.json"
2162                get_hist('k')                   returns None
2163                get_hist(['p', 'k'], 0)         returns 0
2164        2. If "plugin history.json" contains
2165                {"k":1, "plg":{"k":2, "p":{"m":3}, "t":[0,1]}, "q":{"n":4}}
2166                get_hist('k', 0, None)          returns 1
2167                get_hist('k', 0)                returns 0
2168                get_hist('k', 0, 'plg')         returns 2
2169                get_hist('k', 0, 'oth')         returns 0
2170                get_hist(['p','m'], 0)          returns 3
2171                get_hist(['p','t'], [])         returns [0,1]
2172                get_hist('q', 0, None)          returns {'n':4}
2173                get_hist(['q','n'], 0, None)    returns 4
2174    """
2175    to_file = to_file   if os.sep in to_file else   app.app_path(app.APP_DIR_SETTINGS)+os.sep+to_file
2176    if not os.path.exists(to_file):
2177        pass;                  #log('not exists',())
2178        return default
2179    data    = None
2180    try:
2181        data    = json.loads(open(to_file).read())
2182    except:
2183        pass;                   log('not load: {}',sys.exc_info())
2184        return default
2185    if module_name=='_auto_detect':
2186        # commented is for Py3.5+, fails on Py3.4
2187        # caller_globals  = inspect.stack()[1].frame.f_globals
2188        caller_globals  = inspect.stack()[1][0].f_globals
2189        module_name = inspect.getmodulename(caller_globals['__file__']) \
2190                        if '__file__' in caller_globals else None
2191    keys    = [key_or_path] if type(key_or_path)==str   else key_or_path
2192    keys    = keys          if module_name is None      else [module_name]+keys
2193    parents,\
2194    key     = keys[:-1], keys[-1]
2195    for parent in parents:
2196        data= data.get(parent)
2197        if type(data)!=dict:
2198            pass;               log('not dict parent={}',(parent))
2199            return default
2200    return data.get(key, default)
2201   #def get_hist
2202
2203def set_hist(key_or_path, value, module_name='_auto_detect', kill=False, to_file=PLING_HISTORY_JSON):
2204    """ Write to "plugin history.json" one value by key or path (list of keys).
2205        If any of node doesnot exist it will be added.
2206        Or remove (if kill) one key+value pair (if suitable key exists).
2207        Parameters
2208            key_or_path     Key(s) to navigate in json tree
2209                            Type: str or [str]
2210            value           Value to set if suitable item in json tree exists
2211            module_name     Start node to navigate.
2212                            If it is '_auto_detect' then name of caller module is used.
2213                            If it is None then it is skipped.
2214            kill            Need to remove node in tree.
2215                            if kill==True parm value is ignored
2216            to_file         Name of file to write. APP_DIR_SETTING will be joined if no full path.
2217
2218        Return              value (param)   if !kill and modification is successful
2219                            value (killed)  if  kill and modification is successful
2220                            None            if  kill and no path in tree (no changes)
2221                            KeyError        if !kill and path has problem
2222        Return  value
2223
2224        Examples (caller module is 'plg')
2225        1. If no "plugin history.json"  it will become
2226            set_hist('k',0,None)        {"k":0}
2227            set_hist('k',1)             {"plg":{"k":1}}
2228            set_hist('k',1,'plg')       {"plg":{"k":1}}
2229            set_hist('k',1,'oth')       {"oth":{"k":1}}
2230            set_hist('k',[1,2])         {"plg":{"k":[1,2]}}
2231            set_hist(['p','k'], 1)      {"plg":{"p":{"k":1}}}
2232
2233        2. If "plugin history.json" contains    {"plg":{"k":1, "p":{"m":2}}}
2234                                                it will contain
2235            set_hist('k',0,None)                {"plg":{"k":1, "p":{"m":2}},"k":0}
2236            set_hist('k',0)                     {"plg":{"k":0, "p":{"m":2}}}
2237            set_hist('k',0,'plg')               {"plg":{"k":0, "p":{"m":2}}}
2238            set_hist('n',3)                     {"plg":{"k":1, "p":{"m":2}, "n":3}}
2239            set_hist(['p','m'], 4)              {"plg":{"k":1, "p":{"m":4}}}
2240            set_hist('p',{'m':4})               {"plg":{"k":1, "p":{"m":4}}}
2241            set_hist(['p','m','k'], 1)          KeyError (old m is not branch node)
2242
2243        3. If "plugin history.json" contains    {"plg":{"k":1, "p":{"m":2}}}
2244                                                it will contain
2245            set_hist('k',       kill=True)      {"plg":{       "p":{"m":2}}}
2246            set_hist('p',       kill=True)      {"plg":{"k":1}}
2247            set_hist(['p','m'], kill=True)      {"plg":{"k":1, "p":{}}}
2248            set_hist('n',       kill=True)      {"plg":{"k":1, "p":{"m":2}}}    (nothing to kill)
2249    """
2250    to_file = to_file   if os.sep in to_file else   app.app_path(app.APP_DIR_SETTINGS)+os.sep+to_file
2251    body    = json.loads(open(to_file).read(), object_pairs_hook=odict) \
2252                if os.path.exists(to_file) and os.path.getsize(to_file) != 0 else \
2253              odict()
2254
2255    if module_name=='_auto_detect':
2256        # commented is for Py3.5+, fails of Py3.4
2257        # caller_globals  = inspect.stack()[1].frame.f_globals
2258        caller_globals  = inspect.stack()[1][0].f_globals
2259        module_name = inspect.getmodulename(caller_globals['__file__']) \
2260                        if '__file__' in caller_globals else None
2261    keys    = [key_or_path] if type(key_or_path)==str   else key_or_path
2262    keys    = keys          if module_name is None      else [module_name]+keys
2263    parents,\
2264    key     = keys[:-1], keys[-1]
2265    data    = body
2266    for parent in parents:
2267        if kill and parent not in data:
2268            return None
2269        data= data.setdefault(parent, odict())
2270        if type(data)!=odict:
2271            raise KeyError()
2272    if kill:
2273        if key not in data:
2274            return None
2275        value       = data.pop(key)
2276    else:
2277        data[key]   =  value
2278    open(to_file, 'w').write(json.dumps(body, indent=2))
2279    return value
2280   #def set_hist
2281######################################
2282######################################
2283
2284def get_translation(plug_file):
2285    ''' Part of i18n.
2286        Full i18n-cycle:
2287        1. All GUI-string in code are used in form
2288            _('')
2289        2. These string are extracted from code to
2290            lang/messages.pot
2291           with run
2292            python.exe <python-root>\Tools\i18n\pygettext.py -p lang <plugin>.py
2293        3. Poedit (or same program) create
2294            <module>\lang\ru_RU\LC_MESSAGES\<module>.po
2295           from (cmd "Update from POT")
2296            lang/messages.pot
2297           It allows to translate all "strings"
2298           It creates (cmd "Save")
2299            <module>\lang\ru_RU\LC_MESSAGES\<module>.mo
2300        4. <module>.mo can be placed also in dir
2301            CudaText\data\langpy\ru_RU\LC_MESSAGES\<module>.mo
2302           The dir is used first.
2303        5. get_translation uses the file to realize
2304            _('')
2305    '''
2306    lng     = app.app_proc(app.PROC_GET_LANG, '')
2307    plug_dir= os.path.dirname(plug_file)
2308    plug_mod= os.path.basename(plug_dir)
2309    lng_dirs= [
2310                app.app_path(app.APP_DIR_DATA)  +os.sep+'langpy',
2311                plug_dir                        +os.sep+'lang',
2312              ]
2313    _       =  lambda x: x
2314    pass;                      #return _
2315    for lng_dir in lng_dirs:
2316        lng_mo  = lng_dir+'/{}/LC_MESSAGES/{}.mo'.format(lng, plug_mod)
2317        if os.path.isfile(lng_mo):
2318            t   = gettext.translation(plug_mod, lng_dir, languages=[lng])
2319            _   = t.gettext
2320            t.install()
2321            break
2322    return _
2323   #def get_translation
2324
2325_   = get_translation(__file__) # I18N
2326
2327
2328######################################
2329#NOTE: misc
2330######################################
2331def upd_dict(d1, d2):
2332    rsp = d1.copy()
2333    rsp.update(d2)
2334    return rsp
2335   #def upd_dict
2336
2337def deep_upd(dcts):
2338    pass;                      #log('dcts={}',(dcts))
2339    if not dcts:
2340        return dcts
2341    if isinstance(dcts, dict):
2342        return dcts
2343
2344    dct1, *dcts = dcts
2345    pass;                      #log('dct1, dcts={}',(dct1, dcts))
2346    rsp   = dct1.copy()
2347    for dct in dcts:
2348        for k,v in dct.items():
2349            if False:pass
2350            elif k not in rsp:
2351                rsp[k]  = v
2352            elif isinstance(rsp[k], dict) and isinstance(v, dict):
2353                rsp[k].update(v)
2354            else:
2355                rsp[k]  = v
2356    pass;                      #log('rsp={}',(rsp))
2357    return rsp
2358   #def deep_upd
2359
2360def isint(what):    return isinstance(what, int)
2361
2362def ed_of_file_open(op_file):
2363    if not app.file_open(op_file):
2364        return None
2365    for h in app.ed_handles():
2366        op_ed   = app.Editor(h)
2367        if op_ed.get_filename() and os.path.samefile(op_file, op_ed.get_filename()):
2368            return op_ed
2369    return None
2370   #def ed_of_file_open
2371
2372if __name__ == '__main__' :     # Tests
2373    class C:
2374        def m1(self, p):
2375            print('m1',p)
2376        def m2(self, p):
2377            print('m2',p)
2378
2379    c = C()
2380#   print('c.m1',dir(c.m1))
2381#   print('c',dir(c))
2382#   print('c.m1.__self__',dir(c.m1.__self__))
2383
2384    c.m1('0')
2385    rm = c.m1
2386    rm('0')
2387    rm(c, '0')
2388
2389#   DlgAgent(
2390#       ctrls=[('b1', dict(type='button',             cap='Click', x=0, y= 0, w=100, a='lR',
2391#               call=lambda name,ag:{'ctrls':[('b1',{'cap':'OK',             'w':70})]}         ##!! new w <> a
2392#              ))
2393#             ,('b2', dict(type='button', cap='Close', x=0, y=30, w=100,             a='LR',
2394#               call=lambda name,ag:None)
2395#              )]
2396#   ,   form=dict(cap='Two buttons', x=0, y=0, w=100, h=60, h_max=60, resize=True)
2397#   ,   focused='b1'
2398#   ).show()
2399
2400#   BaseDlgAgent(
2401#       ctrls=[('b1', dict(type='button', cap='Click', x=0, y= 0, w=100,
2402#                          call=lambda name,ag:{'ctrls':[('b1',{'cap':'OK', 'w':70})]}))
2403#             ,('b2', dict(type='button', cap='Close', x=0, y=30, w=100,
2404#                          call=lambda name,ag:None))]
2405#   ,   form=dict(cap='Two buttons', x=0, y=0, w=100, h=60)
2406#   ,   focused='b1'
2407#   ).show()
2408
2409#   pass
2410#   def test_ask_number(ask, def_val):
2411#       cnts=[dict(        tp='lb',tid='v',l=3 ,w=70,cap=ask)
2412#            ,dict(cid='v',tp='ed',t=3    ,l=73,w=70)
2413#            ,dict(cid='!',tp='bt',t=45   ,l=3 ,w=70,cap='OK',props='1')
2414#            ,dict(cid='-',tp='bt',t=45   ,l=73,w=70,cap='Cancel')]
2415#       vals={'v':def_val}
2416#       while True:
2417#           btn,vals,fid,chds=dlg_wrapper('Example',146,75,cnts,vals,'v')
2418#           if btn is None or btn=='-': return def_val
2419#           if not re.match(r'\d+$', vals['v']): continue
2420#           return vals['v']
2421#   ask_number('ask_____________', '____smth')
2422
2423'''
2424ToDo
2425[ ][kv-kv][14may18] Remove keys dlg_wrapper_fit_va_for_* if it's same as def
2426'''
2427