1import os
2from contextlib import contextmanager
3from collections import namedtuple
4
5from cudatext import *
6import cudax_lib as apx
7FILE_OPTS = apx.OPT2PROP # to check if option can be in file scope
8
9import traceback
10
11# dbg
12import time
13
14_   = apx.get_translation(__file__)  # I18N
15
16
17TITLE_DEFAULT = _('CudaText Preferences')
18
19OptChange = namedtuple('OptChange', 'name scope value lexer old_value')
20
21fn_icons = {
22    'asc': 'asc.png', # ascending order
23    'desc': 'desc.png', # descending
24}
25PLING_HISTORY_JSON  = os.path.join(app_path(APP_DIR_SETTINGS), 'plugin history.json')
26FORMS_CFG_JSON = os.path.join(app_path(APP_DIR_SETTINGS), 'forms data.json')
27PLING_KEY = 'dlg_preferences'
28STATE_KEY_TREE_W = 'tree_w'
29STATE_KEY_DESCR_MEMO_H = 'descr_memo_h'
30STATE_KEY_FILTER_STR = 'filter_str'
31STATE_KEY_FILTER_HIST = 'filter_history'
32STATE_KEY_FILTER_VISIBLE = 'filter_visible' #TODO remove
33STATE_KEY_COL_CFG = 'columns'
34STATE_KEY_SORT_COL = 'sort_column'
35STATE_KEY_SEL_OPT = 'selected_option'
36
37SUBSET_KEYS = [
38    STATE_KEY_FILTER_STR,
39    STATE_KEY_FILTER_HIST,
40    STATE_KEY_SEL_OPT,
41]
42
43
44IS_DBG = False
45LOG = False
46
47
48VK_ENTER = 13
49VK_F = ord('F')
50VK_ESCAPE = 27
51LIST_SEP = chr(1)
52IS_WIN = os.name=='nt'
53
54BTN_H = app_proc(PROC_GET_GUI_HEIGHT, 'button')
55BTN_W = BTN_H*3
56PAD = 2
57
58# colores
59COL_FONT = 0
60COL_SPLITTER = 0
61
62GLOBAL_OP_CMT = 'Note: this option is global'
63TREE_ITEM_ALL = _('[ All ]')
64
65# columns
66COL_SECTION     = 'Section'
67COL_OPT_NAME    = 'Option'
68COL_MODIFIED    = '!'
69COL_VAL_DEFAULT = 'Default'
70COL_VAL_USER    = 'User'
71COL_VAL_LEX     = 'Lexer'
72COL_VAL_FILE    = 'File'
73COL_VAL_MAIN    = 'Value' # current value -- most specific f,l,u,def value
74
75UI_COLUMNS = {
76    COL_SECTION     : _('Section'),
77    COL_OPT_NAME    : _('Option'),
78    COL_VAL_DEFAULT : _('Default'),
79    COL_VAL_USER    : _('User'),
80    COL_VAL_LEX     : _('Lexer'),
81    COL_VAL_FILE    : _('File'),
82    COL_VAL_MAIN    : _('Current value'),
83}
84
85OPTS_COLUMN_MAP = {
86    COL_SECTION     : 'chp',
87    COL_OPT_NAME    : 'opt',
88    COL_MODIFIED    : '!',
89    COL_VAL_DEFAULT : 'def',
90    COL_VAL_USER    : 'uval',
91    COL_VAL_LEX     : 'lval',
92    COL_VAL_FILE    : 'fval',
93    # + Value - most specific scope value
94}
95
96# order in UI
97COLS_LIST = [
98    COL_SECTION,
99    COL_OPT_NAME,
100    COL_MODIFIED,
101    COL_VAL_DEFAULT,
102    COL_VAL_MAIN,
103    COL_VAL_USER,
104    COL_VAL_LEX,
105    COL_VAL_FILE,
106]
107
108
109opt_col_cfg = [("Option", 70), ("Value", 100)]
110
111ui_max_history_edits = 20
112
113filter_history = []
114
115def load_imagelist(ic_filename_map):
116    ind_map = {}
117    h_iml = imagelist_proc(0, IMAGELIST_CREATE)
118    _icons_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons')
119    for name,fn_icon in ic_filename_map.items():
120        _path = os.path.join(_icons_dir, fn_icon)
121        imind = imagelist_proc(h_iml, IMAGELIST_ADD, _path)
122        ind_map[name] = imind
123    return h_iml, ind_map
124
125def get_tree_path_names(h_tree, item_id, l=None):
126    ''' returns list, node names starting with deepest
127    '''
128    if l is None:
129        l = []
130    prop = tree_proc(h_tree, TREE_ITEM_GET_PROPS, id_item=item_id)
131    l.append(prop['text'])
132
133    parent_id = prop.get('parent')
134    if parent_id:
135        get_tree_path_names(h_tree, parent_id, l)
136    return l
137
138def get_tree_path(h_tree, item_id):
139    """ tree path for tree item
140    """
141    path_names = get_tree_path_names(h_tree, item_id)
142    path_names.reverse()
143    return '/'.join(path_names)
144
145@contextmanager
146def ignore_edit(h, ed_):
147    """ turns off PROP_RO + deactivates Editor -- then restores
148        ? to not send `on_change` when changing `editor_combo` text
149    """
150    is_ro = ed_.get_prop(PROP_RO)
151    if is_ro:
152        ed_.set_prop(PROP_RO, False)
153
154    h_ed = ed_.get_prop(PROP_HANDLE_SELF)
155    #NOTE: widgets are never deleted here, so `DLG_CTL_COUNT` should not be a problem
156    for n in range(dlg_proc(h, DLG_CTL_COUNT)):
157        h_ctl = dlg_proc(h, DLG_CTL_HANDLE, index=n)
158        if h_ed == h_ctl:
159            # disable temporarily
160            dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={'act': False})
161            break
162
163    try:
164        yield
165    finally:
166        if is_ro:
167            ed_.set_prop(PROP_RO, True)
168        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={'act': True})
169
170
171def map_option_value(opt, val=None, caption=None):
172    """ for map options - returns caption for provided 'val'ue, or value for provided 'caption'
173        "val" -- 0;  "caption" -- (0) don't activate"
174    """
175    frm = opt['frm']
176    if frm in ['int2s', 'str2s']:
177        jdc = opt['jdc'] # list('(a) by Alt+click', ...)
178        dct = opt['dct'] # list(['a', 'by Alt+click'], ...)
179
180        if val is not None:
181            for i,item in enumerate(dct):
182                if item[0] == val:
183                    return jdc[i]     # return str
184
185        elif caption is not None:
186            ind = jdc.index(caption)
187            val, _cap = dct[ind]
188            return val
189
190
191        else:
192            raise OptionMapValueError('require "val" or "caption"')
193
194    elif frm in ['font', 'strs', 'font-e']:
195        if val      is not None: return val
196        if caption  is not None: return caption
197
198    else:
199        raise OptionMapValueError('Unsupported option format: {}'.format((opt["opt"], opt["frm"])))
200    raise OptionMapValueError('Couldn"t find: {}, {}\n + {}'.format(val, caption, opt))
201
202
203def json_update(path, key, val):
204    """ loads json at 'path' if exists, puts 'k':'v' into it and saves
205    """
206    import json
207
208    if os.path.exists(path):
209        with open(path, 'r', encoding='utf-8') as f:
210            j = json.load(f)
211    else:
212        j = {}
213
214    j[key] = val
215    j_str = json.dumps(j, indent=2)
216
217    with open(path, 'w', encoding='utf-8') as f:
218        f.write(j_str)
219
220def format_opt_change(ch):
221    scope_str = ch.scope
222    if   ch.scope=='u': scope_str = ui_column(COL_VAL_USER)
223    elif ch.scope=='l': scope_str = ui_column(COL_VAL_LEX) +': '+str(ch.lexer)
224    elif ch.scope=='f': scope_str = ui_column(COL_VAL_FILE)+': '+os.path.basename(ed.get_filename())
225
226    if ch.value is None:    val_str = _('reset')
227    else:                   val_str = '{} -> {}'.format(ch.old_value, ch.value)
228
229    return '{} [{}] {}'.format(ch.name, scope_str, val_str)
230
231def ui_column(colname):
232    return UI_COLUMNS.get(colname, colname)
233
234
235class DialogMK2:
236
237    _h_list_iml = None # handle for option-list's imagelist
238    _lb_icon_inds = {} # listbox icons
239
240    def __init__(self, optman, title=None, subset=None, how=None):
241        """ optman -- cd_opts_dlg.py/OptionsMan
242            how --
243            * how.get('hide_fil', False)
244            * how.get('hide_lex_fil', False)
245            * how.get('only_for_ul', not ens['tofi'])         # Forbid to switch fo File ops
246
247            - how.get('stor_json', 'user.json')
248            - how.get('only_with_def', False) # Forbid to switch fo User+Lexer ops
249        """
250        global ui_max_history_edits
251
252        #TODO get value from options if not present
253        ui_max_history_edits = optman.get_scope_value('ui_max_history_edits', scope='u',
254                                                        default=ui_max_history_edits)
255        self._form_rect = {} # dict - x,y,w,h
256        self._state = {}
257
258        self.title = title or TITLE_DEFAULT
259        self.optman = optman
260        self.subset = subset # None - ok
261        # gather not-available scopes
262        self.hidden_scopes = [] # 'l' and/or 'f'
263        if (how  and  how.get('hide_fil')  or  how.get('hide_lex_fil')  or  how.get('only_for_ul')) \
264                                                                            or not ed.get_filename():
265            self.hidden_scopes.append('f')
266        if how.get('hide_lex_fil')  or  not ed.get_prop(PROP_LEXER_FILE):
267            self.hidden_scopes.append('l')
268
269
270        self._load_dlg_cfg()
271
272        _sort_val = self._state.get(STATE_KEY_SORT_COL, COL_OPT_NAME)
273        self.current_sort = _sort_val.lstrip('-')
274        self.sort_reverse = _sort_val.startswith('-')
275        self._last_applied_filter = None
276        self._cur_opt_name = None
277        self._closing = False
278
279        self.h = None
280        self._h_tree = None
281        self._h_col_menu = None
282        self._h_help = None
283        self._cur_value_ed = 'str' # type for: _set_value_editor()
284
285        self.val_eds = ValueEds(self.on_opt_val_edit)
286
287        self._opt_changes = []
288        self._list_opt_names = [] # current displayed list of option-names
289        # '' -> User -- showing default value, edited value will be added to 'user' scope
290        self._col_toggle_cmds = {} # column name -> toggle lambda -- for menu, to toggle list columns
291        self._scope_captions = { # expanded alter
292                'u'  : ui_column(COL_VAL_USER),
293                ''   : ui_column(COL_VAL_USER),
294                'def': ui_column(COL_VAL_USER),
295                }
296
297
298    @property
299    def filter_val(self):
300        if self.h:
301            return self._filter_ed.get_text_all()
302        return ''
303    @filter_val.setter
304    def filter_val(self, value): #SKIP
305        if self.h:
306            val_str = str(value)
307            if val_str != self._filter_ed.get_text_all():
308                self._filter_ed.set_text_all(val_str)
309
310    @property
311    def columns(self):
312        """ returns (column_captions, column_widths)
313        """
314        captions = []
315        widths = []
316        _col_cfg = opt_col_cfg
317
318        # hide lexer and file scopes  if disabled
319        if self.hidden_columns:
320            _col_cfg = [colcfg for colcfg in _col_cfg  if colcfg[0] not in self.hidden_columns]
321
322        _total_w = sum(w for name,w in _col_cfg if isinstance(w, int))
323        for caption,w in _col_cfg:
324            captions.append(caption)
325
326            # width: to negative percentages for listbox -- except '!' <- in px
327            if isinstance(w, int):
328                w = -round(w/_total_w*100)
329            else:
330                w = int(w[:-2]) # "100px" => 100
331            widths.append(w)
332
333        return captions,widths
334
335    @property
336    def hidden_columns(self):
337        if not hasattr(self, '_hidden_columns'):
338            self._hidden_columns = set()
339
340            if self.hidden_scopes:
341                if 'l' in self.hidden_scopes:   self._hidden_columns.add(COL_VAL_LEX)
342                if 'f' in self.hidden_scopes:   self._hidden_columns.add(COL_VAL_FILE)
343
344        return self._hidden_columns
345
346    @property
347    def scope(self):
348        """ returns current scope char: u,l,f
349        """
350        scope_str = self.scope_ed.get_text_all()
351        if scope_str == ui_column(COL_VAL_USER):             return 'u'
352        elif scope_str.startswith(ui_column(COL_VAL_LEX)):   return 'l'
353        elif scope_str.startswith(ui_column(COL_VAL_FILE)):  return 'f'
354
355
356    def _load_dlg_cfg(self):
357        import json
358
359        if os.path.exists(FORMS_CFG_JSON):
360            with open(FORMS_CFG_JSON, 'r', encoding='utf-8') as f:
361                j = json.load(f)
362                _old_title = _('CudaText options lite')
363                j_form = j.get(self.title, j.get(_old_title))
364            if j_form:
365                self._form_rect = {k:v for k,v in j_form.items()
366                                        if v  and  k in {'x', 'y', 'w', 'h'}}
367
368        if os.path.exists(PLING_HISTORY_JSON):
369            with open(PLING_HISTORY_JSON, 'r', encoding='utf-8') as f:
370                j_all = json.load(f)
371
372            j = j_all.get(PLING_KEY)
373            if j:
374                _state_keys = {
375                    STATE_KEY_TREE_W,
376                    STATE_KEY_DESCR_MEMO_H,
377                    STATE_KEY_FILTER_STR,
378                    STATE_KEY_FILTER_VISIBLE,
379                    STATE_KEY_SEL_OPT,
380                    STATE_KEY_SORT_COL,
381                }
382                self._state = {k:v for k,v in j.items()  if k in _state_keys}
383
384                # if subset - overwrite general values with subset's
385                _subsets = j.get('subsets')
386                if self.subset  and  _subsets:
387                    self._state.update(_subsets.get(self.subset, {}))
388
389
390                # filter history
391                _filt_hist = j.get(STATE_KEY_FILTER_HIST)
392                if _filt_hist:
393                    filter_history.clear()
394                    filter_history.extend(_filt_hist)
395
396                # list columns
397                _col_cfg = j.get(STATE_KEY_COL_CFG)
398                if _col_cfg:
399                    import re
400
401                    # check if only integers and str (~"100px")
402                    for i in range(len(_col_cfg)):
403                        item = _col_cfg[i]
404                        colname,w = item
405                        if not isinstance(w, int)  and  not (isinstance(w, str)
406                                                                and re.match('^\d+px$', w)):
407                            print(_('NOTE: {}: invalid column width format: {}')
408                                        .format(self.title, item))
409                            _col_cfg[i] = (colname,100)
410
411                    opt_col_cfg.clear()
412                    opt_col_cfg.extend(_col_cfg)
413                pass;       LOG and print(' --- Loaded state: '+json.dumps(j, indent=4))
414
415            # no history - load from opted plugin
416            else:
417                j_opted = j_all.get('cd_opts_dlg', {}).get('dlg')
418                if j_opted:
419                    opted_state = {
420                        STATE_KEY_DESCR_MEMO_H: j_opted.get("df.cmnt_heght"),
421                        STATE_KEY_SEL_OPT:      j_opted.get("df.cur_op"),
422                    }
423                    self._state = {k:v  for k,v in opted_state.items()  if v is not None}
424
425                    filter_history.clear()
426                    filter_history.extend(j_opted.get('df.h.cond', []))
427
428
429    def _save_dlg_cfg(self):
430        if self._closing is None:
431            return
432
433        # window position/dimensions
434        form_prop = dlg_proc(self.h, DLG_PROP_GET)
435        j_form = {'x':form_prop['x'], 'y':form_prop['y'], 'w':form_prop['w'], 'h':form_prop['h']}
436        json_update(FORMS_CFG_JSON,  key=self.title,  val=j_form)
437
438        # states
439        j = {}
440        j[STATE_KEY_TREE_W] = dlg_proc(self.h, DLG_CTL_PROP_GET, name='category_tree')['w']
441        j[STATE_KEY_DESCR_MEMO_H] = dlg_proc(self.h, DLG_CTL_PROP_GET, name='panel_value')['h']
442        j[STATE_KEY_FILTER_STR] = self.filter_val
443        j[STATE_KEY_FILTER_HIST] = filter_history
444        j[STATE_KEY_FILTER_VISIBLE] = dlg_proc(self.h, DLG_CTL_PROP_GET, name='panel_filter')['vis']
445        j[STATE_KEY_SORT_COL] = self.current_sort if not self.sort_reverse else '-'+self.current_sort
446        j[STATE_KEY_SEL_OPT] = self._cur_opt_name
447
448        # save some options separately -- 3rd party options: move from `j` to `j/subsets/<subset>`
449        if self.subset:
450            j_subset = {k:j.pop(k) for k in SUBSET_KEYS}
451            _subsets = j.setdefault('subsets', {})
452            _subsets[self.subset] = j_subset
453
454        j[STATE_KEY_COL_CFG] = opt_col_cfg
455
456        json_update(PLING_HISTORY_JSON,  PLING_KEY,  j )
457
458    def configure_columns(self):
459        global opt_col_cfg
460
461        caption = _('Columns widths. In pixels (50px) or relative (100)')
462
463        _colnames, _widths = zip(*opt_col_cfg) # start values
464        colnames, widths = list(_colnames), list(_widths) # working values
465        while True:
466            flat_columns = [str(a)   for item in zip(colnames,widths)   for a in item]
467            res = dlg_input_ex(len(colnames), caption, *flat_columns)
468            if not res:
469                break
470            else: # have result -> validate
471                for i in range(len(colnames)):
472                    item = res[i]
473                    if item.isdecimal():
474                        res[i] = int(item)
475                    elif (item.endswith('px') and item[:-2].isdecimal()):
476                        pass
477                    else: # error
478                        widths = res
479                        colnames[i] = _colnames[i] + _(' (Error!)')
480                        break
481
482                else: # all is well - stop `While`
483                    break
484
485        if res:
486            new_cfg = list(zip(_colnames, res))
487            _start_cfg = opt_col_cfg[:]
488
489            # try to apply new config -- revert if failed (jic)
490            try:
491                opt_col_cfg = new_cfg  # global
492
493                self.update_list_layout()
494                _opts = self.get_filtered_opts()
495                self.update_list(_opts)
496            except Exception as ex:
497                opt_col_cfg = _start_cfg  # revert changes
498
499                msg = _('failed to apply new columns config: {}. {}').format(new_cfg, ex)
500                print('NOTE: {}: {}'.format(self.title, msg))
501
502
503
504    def show(self):
505        if not self.h:
506            self.h, self.opt_comment_ed = self.init_form()
507
508        nitems = self._fill_tree(self.optman.tree['kids'])
509        if nitems <= 1:
510            dlg_proc(self.h, DLG_CTL_PROP_SET, name='category_tree', prop={'vis': False})
511            dlg_proc(self.h, DLG_CTL_PROP_SET, name='splitter_left', prop={'vis': False})
512
513        self.update_list_layout()
514
515        # restore filter
516        _filter_val = self._state.get(STATE_KEY_FILTER_STR, '')
517        self.set_filter(_filter_val)
518        if _filter_val:
519            self.toggle_filter(show=True)
520            self._filter_ed.set_caret(0,0, len(_filter_val),0)  # select filter text on start
521
522        # restore selected-option (+show it)
523        last_sel_opt = self._state.get(STATE_KEY_SEL_OPT)
524        if self._list_opt_names:
525            if last_sel_opt  and  last_sel_opt in self._list_opt_names:
526                _ind = self._list_opt_names.index(last_sel_opt)
527            else:   # if no saved selected opt - select first
528                _ind = 0
529            listbox_proc(self._h_list, LISTBOX_SET_SEL, index=_ind)
530            _top = max(0, _ind-3)
531            listbox_proc(self._h_list, LISTBOX_SET_TOP, index=_top)
532            #### click event
533            self._on_opt_click(id_dlg=self.h, id_ctl=-1)
534
535        # focus filter on start
536        timer_proc(TIMER_START_ONE,  callback=lambda *args,**vargs: self._filter_ed.focus(),   interval=100)
537
538        # DBG #############
539        if IS_DBG:
540            DialogMK2._dlg = self
541            cmds = [    'from cuda_prefs.dlg import DialogMK2',
542                        'globals()["dlg"] = DialogMK2._dlg',]
543            app_proc(PROC_EXEC_PYTHON, '\n'.join(cmds))
544            del DialogMK2._dlg
545
546            dlg_proc(self.h, DLG_SHOW_NONMODAL)
547            return
548        ###########
549
550        dlg_proc(self.h, DLG_SHOW_MODAL)
551        dlg_proc(self.h, DLG_FREE)
552
553        del self.optman
554        del self._list_opt_names
555        del self._col_toggle_cmds
556
557    def init_form(self):
558        global COL_FONT
559        global COL_SPLITTER
560
561        # load icons only once
562        def get_list_imagelist(): #SKIP
563            if not DialogMK2._h_list_iml:
564                DialogMK2._h_list_iml,  DialogMK2._lb_icon_inds = load_imagelist(fn_icons)
565
566            return DialogMK2._h_list_iml
567
568        h = dlg_proc(0, DLG_CREATE)
569
570        colors = app_proc(PROC_THEME_UI_DICT_GET, '')
571        COL_FONT = colors['EdTextFont']['color']
572        COL_SPLITTER = colors['SplitMain']['color']
573        color_form_bg = colors['TabBg']['color']
574
575        ###### FORM #######################
576        dlg_proc(h, DLG_PROP_SET, prop={
577                'cap': self.title,
578                'w': 848, 'h': 576,
579                'w_min': 550, 'h_min': 250,
580                'border': DBORDER_SIZE,
581                'color': color_form_bg,
582                #'on_mouse_exit': self.dlgcolor_mouse_exit,
583                'keypreview': True,
584                'on_key_down': self._on_key,
585                'on_close': lambda *args, **vargs: self._save_dlg_cfg(),
586                'topmost': True,
587                })
588
589        ###### MAIN PANEL
590        n = dlg_proc(h, DLG_CTL_ADD, 'panel')
591        dlg_proc(h, DLG_CTL_PROP_SET, index=n,   prop={
592                'name': 'panel_main',
593                'align': ALIGN_CLIENT,
594                'sp_l': PAD*2, 'sp_t': PAD*2, 'sp_r': PAD*2, 'sp_b': PAD*2 + BTN_H + PAD*2,
595                })
596
597
598        ### tree ##########################
599        n = dlg_proc(h, DLG_CTL_ADD, 'treeview')
600        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
601                'name': 'category_tree',
602                'p': 'panel_main',
603                'align': ALIGN_LEFT,
604                'w': 100,
605                #'sp_r': PAD,
606                'on_change': self._on_tree_click,
607                })
608        self._h_tree = dlg_proc(h, DLG_CTL_HANDLE, index=n)
609        tree_proc(self._h_tree, TREE_THEME)
610
611
612        ### RIGHT PANEL #########################
613        n = dlg_proc(h, DLG_CTL_ADD, 'panel')
614        dlg_proc(h, DLG_CTL_PROP_SET, index=n,   prop={
615                'name': 'panel_right',
616                'p': 'panel_main',
617                'align': ALIGN_CLIENT,
618                #'sp_l': PAD,
619                })
620        # listbox ##########
621        n = dlg_proc(h, DLG_CTL_ADD, 'listbox_ex')
622        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
623                'p': 'panel_right',
624                'align': ALIGN_CLIENT,
625                'sp_t': PAD,
626                'border': True,
627                'act': True, # to call on_change
628                'on_click': self._on_opt_click,
629                'on_change': self._on_opt_click,
630                'on_click_header': self._on_header_click,
631                'on_menu': self.listbox_menu,
632                })
633        self._h_list = dlg_proc(h, DLG_CTL_HANDLE, index=n)
634
635        ### FILTER panel ############################
636        n = dlg_proc(h, DLG_CTL_ADD, 'panel')
637        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
638                'name': 'panel_filter',
639                'p': 'panel_right',
640                'align': ALIGN_TOP,  'h': BTN_H, 'max_h': BTN_H,
641                #'vis': self._state.get(STATE_KEY_FILTER_VISIBLE, False),
642                })
643        # filter label
644        n = dlg_proc(h, DLG_CTL_ADD, 'label')
645        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
646                'name': 'filter_label',
647                'p': 'panel_filter',
648                'a_l': ('', '['), 'a_t': ('', '-'),
649                'sp_l': PAD*2,
650                'cap': _('Filter: '),
651                'font_color': COL_FONT,
652                })
653        # filter combo ##########
654        n = dlg_proc(h, DLG_CTL_ADD, 'editor_combo')
655        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
656                'name': 'filter',
657                'p': 'panel_filter',
658                'h': BTN_H, 'max_h': BTN_H,
659                #'align': ALIGN_CLIENT,
660                'sp_r': 20,
661                'a_l': ('filter_label', ']'),   'a_r': ('', ']'),  'a_t': ('filter_label', '-'),
662                'on_change': self._on_filter,
663                'on_key_down': self._on_filter, # for later -- live filter
664                })
665        h_ed = dlg_proc(h, DLG_CTL_HANDLE, index=n)
666        self._filter_ed = Editor(h_ed)
667
668
669        ### BOTTOM PANEL ###############################
670        n = dlg_proc(h, DLG_CTL_ADD, 'panel')
671        dlg_proc(h, DLG_CTL_PROP_SET, index=n,   prop={
672                'name': 'panel_value',
673                'p': 'panel_right',
674                'align': ALIGN_BOTTOM,
675                'h': 120,
676                'h_min': 100, # avoid resizing to zero
677                'y': 4000,  # https://github.com/Alexey-T/CudaText/issues/3679#issuecomment-904845613
678                'sp_t': PAD,
679                })
680        # scope combo ##########
681        n = dlg_proc(h, DLG_CTL_ADD, 'editor_combo')
682        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
683                'name': 'scope',
684                'p': 'panel_value',
685                'h': BTN_H, 'max_h': BTN_H,       'w': 100, 'max_w': 100,
686                'a_l': None,   'a_r': ('', ']'),  'a_t': ('', '['),
687                'act': True,
688                'on_change': self._on_scope_change,
689                })
690        h_scope_ed = dlg_proc(h, DLG_CTL_HANDLE, index=n)
691        # scope label ###
692        n = dlg_proc(h, DLG_CTL_ADD, 'label')
693        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
694                'name': 'scope_label',
695                'p': 'panel_value',
696                'h': BTN_H, 'max_h': BTN_H,
697                'a_l': None,   'a_r': ('scope', '['),  'a_t': ('scope', '-'),
698                'sp_t': 3,
699                'cap': _('Scope: '),
700                'font_color': COL_FONT,
701                })
702        # btn reset ###########
703        n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
704        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
705                'name': ValueEds.VALUE_ED_RESET,
706                'p': 'panel_value',
707                'h': BTN_H, 'max_h': BTN_H,
708                'w': BTN_W, 'max_w': BTN_W,
709                'a_l': None,   'a_r': ('scope_label', '['),  'a_t': ('', '['),
710                'sp_l': PAD, 'sp_r': 32,
711                'cap': _('Reset'),
712                'on_change': self._on_reset,
713                })
714        # option description #########
715        n = dlg_proc(h, DLG_CTL_ADD, 'editor')
716        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
717                'name': 'descr_memo',
718                'p': 'panel_value',
719                'sp_t': BTN_H + PAD,
720                'align': ALIGN_CLIENT,
721                'h': 100,
722                })
723        h_ed = dlg_proc(h, DLG_CTL_HANDLE, index=n)
724        edt = Editor(h_ed)
725
726        n = dlg_proc(h, DLG_CTL_ADD, 'label')
727        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
728                'name': 'mod_label',
729                'p': 'panel_value',
730                'a_t': ('scope', '-'),
731                'sp_r': PAD,
732                'cap': _('[mod]'),
733                'font_color': COL_FONT,
734                })
735
736
737        ### SPLITTERS ###
738        # list--opt_description
739        n = dlg_proc(h, DLG_CTL_ADD, 'splitter')
740        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
741                'p': 'panel_right',
742                'align': ALIGN_BOTTOM,
743                'x': 0, 'y': 4000, 'h': 4,
744                'color': COL_SPLITTER,
745                })
746        # tree--list
747        n = dlg_proc(h, DLG_CTL_ADD, 'splitter')
748        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
749                'name': 'splitter_left',
750                'p': 'panel_main',
751                'align': ALIGN_LEFT,
752                'x': 100, 'y': 0, 'w': 4,
753                'color': COL_SPLITTER,
754                })
755
756
757        ### Bottom Btns ###################
758        # OK #######
759        n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
760        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
761                'name': 'btn_ok',
762                'h': BTN_H, 'max_h': BTN_H,
763                'w': BTN_W, 'max_w': BTN_W,
764                'a_l': None, 'a_t': None, 'a_r': ('', ']'),  'a_b': ('', ']'),
765                'sp_r': PAD*2, 'sp_b': PAD*2,
766                'cap': _('OK'),
767                'on_change': lambda *args, **vargs: (self.apply_changes(closing=True), self.close()),
768                })
769        # Apply #######
770        n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
771        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
772                'name': 'btn_apply',
773                'h': BTN_H, 'max_h': BTN_H,
774                'w': BTN_W, 'max_w': BTN_W,
775                'a_l': None, 'a_t': None, 'a_r': ('btn_ok', '['),  'a_b': ('', ']'),
776                'sp_r': PAD*2, 'sp_b': PAD*2,
777                'cap': _('Apply'),
778                'on_change': lambda *args, **vargs: self.apply_changes(),
779                })
780        # Cancel #######
781        n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
782        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
783                'name': 'btn_cancel',
784                'h': BTN_H, 'max_h': BTN_H,
785                'w': BTN_W, 'max_w': BTN_W,
786                'a_l': None, 'a_t': None, 'a_r': ('btn_apply', '['),  'a_b': ('', ']'),
787                'sp_r': PAD*2, 'sp_b': PAD*2,
788                'cap': _('Cancel'),
789                'on_change': lambda *args, **vargs: self.close(),
790                })
791        # help #######
792        n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
793        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
794                'h': BTN_H, 'max_h': BTN_H,
795                'w': BTN_W, 'max_w': BTN_W,
796                'a_l': ('', '['),  'a_t': None, 'a_r': None, 'a_b': ('', ']'),
797                'sp_l': PAD*2, 'sp_b': PAD*2,
798                'cap': _('Help'),
799                'on_change': self.dlg_help,
800                })
801
802        # reverse buttons for Windows: [Cancel, Apply, OK] => [OK, Apply, Cancel]
803        if IS_WIN:
804            dlg_proc(h, DLG_CTL_PROP_SET, name='btn_cancel',    prop={'a_r': ('',           ']')})
805            dlg_proc(h, DLG_CTL_PROP_SET, name='btn_apply',     prop={'a_r': ('btn_cancel', '[')})
806            dlg_proc(h, DLG_CTL_PROP_SET, name='btn_ok',        prop={'a_r': ('btn_apply',  '[')})
807
808
809        ### listbox
810        listbox_proc(self._h_list, LISTBOX_SET_COLUMN_SEP, text=LIST_SEP)
811        # + icons
812        h_iml = get_list_imagelist()
813        listbox_proc(self._h_list, LISTBOX_SET_HEADER_IMAGELIST, text=h_iml)
814
815
816        edt.set_prop(PROP_RO, True)
817        edt.set_prop(PROP_RULER, False)
818        edt.set_prop(PROP_MARGIN, 2000)
819        edt.set_prop(PROP_GUTTER_ALL, False)
820        edt.set_prop(PROP_MINIMAP, False)
821        edt.set_prop(PROP_MICROMAP, False)
822        edt.set_prop(PROP_LAST_LINE_ON_TOP, False)
823        edt.set_prop(PROP_HILITE_CUR_LINE, False)
824        edt.set_prop(PROP_WRAP, WRAP_ON_WINDOW)
825
826        # scopes combo
827        scopes = [ui_column(COL_VAL_USER)]
828        lex = ed.get_prop(PROP_LEXER_FILE)
829        if lex  and  'l' not in self.hidden_scopes:
830            scopes.append(ui_column(COL_VAL_LEX)+': '+lex)
831            self._scope_captions['l'] = scopes[-1]
832        if ed.get_filename()  and  'f' not in self.hidden_scopes:
833            filename = os.path.split(ed.get_filename())[1]
834            scopes.append(ui_column(COL_VAL_FILE)+': '+filename)
835            self._scope_captions['f'] = scopes[-1]
836        self.scope_ed = Editor(h_scope_ed)
837        self.scope_ed.set_prop(PROP_RO, True)
838        self.scope_ed.set_prop(PROP_COMBO_ITEMS, '\n'.join(scopes))
839
840        dlg_proc(h, DLG_SCALE)
841
842        # unscale saved-state dimensions
843        if self._form_rect:
844            dlg_proc(h, DLG_PROP_SET, prop=self._form_rect)
845        if self._state.get(STATE_KEY_TREE_W):
846            dlg_proc(h, DLG_CTL_PROP_SET, name='category_tree', prop={
847                    'w': self._state.get(STATE_KEY_TREE_W),
848                    })
849        if self._state.get(STATE_KEY_DESCR_MEMO_H):
850            dlg_proc(h, DLG_CTL_PROP_SET, name='panel_value', prop={
851                    'h': self._state.get(STATE_KEY_DESCR_MEMO_H),
852                    })
853
854        return h, edt
855
856    def update_list(self, opts):
857        column_names, _col_widths = self.columns
858        columns_items = []
859        _value_cols = {COL_VAL_DEFAULT,  COL_VAL_USER,  COL_VAL_LEX,  COL_VAL_FILE}
860        for col_title in column_names:
861            if col_title == COL_VAL_MAIN:
862                col_values = [str(self.optman.get_opt_active_value(op, is_ui=True)) for op in opts]
863
864            elif col_title in _value_cols:
865                _col_key = OPTS_COLUMN_MAP.get(col_title)
866                _scope = 'def'  if _col_key == 'def' else  _col_key[0] # 'uval' -> 'u',  'def' -> 'def'
867                col_values = [str(self.optman.get_opt_scope_value(op, _scope, is_ui=True)) for op in opts]
868
869            else:
870                _col_key = OPTS_COLUMN_MAP.get(col_title)
871                col_values = [str(op.get(_col_key, '')) for op in opts] # can't use generator - _col_key
872
873            columns_items.append(col_values)
874
875        self._list_opt_names = [op['opt'] for op in opts]
876
877        listbox_proc(self._h_list, LISTBOX_DELETE_ALL)
878
879        _addedn = 0
880        _max_seps = 0
881        for row in zip(*columns_items):
882            row_txt = LIST_SEP.join(row)
883            _max_seps = row_txt.count(LIST_SEP)
884
885            listbox_proc(self._h_list, LISTBOX_ADD, index=-1, text=row_txt)
886            _addedn += 1
887        _rows = listbox_proc(self._h_list, LISTBOX_GET_COUNT)
888
889        # select  current option
890        if self._list_opt_names:
891            ind = 0
892            if self._cur_opt_name  and  self._cur_opt_name in self._list_opt_names:
893                ind = self._list_opt_names.index(self._cur_opt_name)
894            #listbox_proc(self._h_list, LISTBOX_SET_TOP, max(0, ind-3)) # selecting is not helpful
895            listbox_proc(self._h_list, LISTBOX_SET_SEL, ind)
896
897        self._on_opt_click(id_dlg=self.h, id_ctl=-1)
898
899
900    def update_list_layout(self):
901        # columns
902        column_captions, column_widths = self.columns
903        column_widths[-1] = 0   # last col to 'fill' - to avoid h-scrollbar
904        _ui_columns = map(lambda cap: ui_column(cap), column_captions) # generator
905        column_captions_str = LIST_SEP.join(_ui_columns)
906        listbox_proc(self._h_list, LISTBOX_SET_COLUMNS, text=column_widths) # width<0 means value in %
907        listbox_proc(self._h_list, LISTBOX_SET_HEADER, text=column_captions_str)
908
909        # sort-icons
910        header_icon_cfg = []
911        if self.current_sort and self.current_sort in column_captions:
912            sort_col_ind = column_captions.index(self.current_sort)
913            _order_name = 'asc'  if not self.sort_reverse else  'desc'
914            _icon_ind = DialogMK2._lb_icon_inds[_order_name]
915            header_icon_cfg = [-1]*sort_col_ind + [_icon_ind] # ~[-1, -1, -1, ind]
916        listbox_proc(self._h_list, LISTBOX_SET_HEADER_IMAGEINDEXES, text=header_icon_cfg)
917
918    def get_filtered_opts(self):
919        sort_field = OPTS_COLUMN_MAP.get(self.current_sort, self.current_sort)
920        return self.optman.get_list(self.filter_val, sort_field, reverse=self.sort_reverse)
921
922    def set_filter(self, filter_str, tree_click=False):
923        if not filter_str:
924            self._filter_ed.set_text_all('')
925        if self._last_applied_filter == filter_str:
926            return
927
928        if tree_click and not filter_str:
929            self.toggle_filter(show=False)
930
931        self._last_applied_filter = filter_str
932        self.filter_val = filter_str
933
934        opts = self.get_filtered_opts()
935        pass;       LOG and print(' __ set_filter: opts len: {}'.format(len(opts)))
936        self.update_list(opts)
937
938        # history
939        if filter_str:
940            try:
941                ind = filter_history.index(filter_str)
942                del filter_history[ind]
943            except ValueError:
944                pass
945
946            filter_history.append(filter_str)
947            del filter_history[:-ui_max_history_edits]
948
949        # update combo items
950        self._filter_ed.set_prop(PROP_COMBO_ITEMS, '\n'.join(reversed(filter_history)))
951
952
953    def set_sort(self, sort_name):
954        pass;       LOG and print(' setting sort: {}'.format(sort_name))
955
956        if self.current_sort == sort_name: # switch order
957            self.sort_reverse = not self.sort_reverse
958        else:
959            self.current_sort = sort_name
960            self.sort_reverse = False  # back to ascending
961        self.update_list_layout()
962
963        # if not present in map -- special value - send as is
964        opts = self.get_filtered_opts()
965        pass;       LOG and print(' __ set_sort: opts len: {}'.format(len(opts)))
966        self.update_list(opts)
967
968
969    # ignore no change
970    def add_opt_change(self, name, scope, val=None):
971        """ val=None -- remove option binding for scope
972        """
973        _old_val = self.optman.get_scope_value(name, scope)
974
975        # check if already have a opt_change for this option+scope -> ovewrite (delete old)
976        for i,change in enumerate(self._opt_changes):
977            if change.name == name and change.scope == scope:
978                del self._opt_changes[i]
979                break
980
981        if val is not None:  ### setting value
982            if val == _old_val: # no change - ignore
983                return
984        else:  ### removing value
985            if _old_val is None: # no change - ignore
986                return
987
988        # if resetting value -- ask confirmation
989        scam = app_proc(PROC_GET_KEYSTATE, '')
990        if scam != 'c'  and  self.scope != 'f'  and  val is None:
991            _scope_cap = self._scope_captions[self.scope]
992            _jval = self.optman.get_opt_scope_value(self._cur_opt, scope=self.scope, is_ui=True)
993            _msg = _('Remove option [{}]\n   {} = {!r}\n?').format(_scope_cap,  self._cur_opt_name,  _jval)
994            res = msg_box(_msg, MB_OKCANCEL + MB_ICONQUESTION)
995            if res != ID_OK:
996                return
997
998        lex = ed.get_prop(PROP_LEXER_FILE)  if scope == 'l' else None
999        opt_change = OptChange(name,  scope,  val,  lexer=lex,  old_value=_old_val)
1000        pass;       LOG and print('NOTE: new option change: '+str(opt_change))
1001        msg_status(_('Option: ') + format_opt_change(opt_change))
1002        self._opt_changes.append(opt_change)
1003
1004
1005    def _fill_tree(self, d, parent=0):
1006        if parent == 0:
1007            item_id = tree_proc(self._h_tree, TREE_ITEM_ADD, text=TREE_ITEM_ALL)
1008        n = 0
1009        for name,d_ in d.items():
1010            # add item
1011            item_id = tree_proc(self._h_tree, TREE_ITEM_ADD, id_item=parent, index=-1, text=name)
1012            d_['item_id'] = item_id
1013            n += 1
1014
1015            items = d_.get('kids')
1016            if items:
1017                n += self._fill_tree(items, parent=item_id)
1018        return n
1019
1020
1021    def on_opt_val_edit(self, id_dlg, id_ctl, data='', info=''):
1022        """ "change" callback for: option-edit field, 'edit-value' btn
1023        """
1024        ed_name = self.val_eds.get_name(id_ctl)
1025        prop_type = self._cur_opt['frm']
1026        pass;       LOG and print(' + ed name: {} [{}]'.format(ed_name, prop_type))
1027
1028        if ed_name == ValueEds.WGT_NAME__EDIT:       # str, int, float, -hotk  + ###
1029            if prop_type == '#rgb'  or  prop_type == '#rgb-e':
1030                self._update_rgb_edit()
1031
1032            self.toggle_mod_indicator(by_timer=True)
1033
1034            key_code, key_state = data
1035            if key_code != VK_ENTER:
1036                return
1037
1038            val = self.val_eds.get_edited_value(self._cur_opt)
1039            if val is None:
1040                return
1041
1042        elif ed_name == ValueEds.WGT_NAME__COMBO:   # font, int2s, str2s, strs ###
1043            val = self.val_eds.val_combo.get_text_all()
1044            # only accept values from combo-items
1045            if val not in self.val_eds.val_combo.get_prop(PROP_COMBO_ITEMS):
1046                pass;       LOG and print('NOTE: val not in combo: {}'.format(val))
1047                return
1048
1049            val = map_option_value(self._cur_opt, caption=val)
1050
1051        elif ed_name == ValueEds.WGT_NAME__CHECK:                       # bool ###
1052            val = self.val_eds.cb_value
1053
1054        elif ed_name == ValueEds.WGT_NAME__BTN_EDIT: # edit btn: hotk, color, json, file ###
1055            val = self._dlg_value(prop_type)
1056            if val is not None:
1057                with ignore_edit(self.h, self.val_eds.val_edit):
1058                    self.val_eds.val_edit.set_text_all(str(val))
1059
1060                if prop_type in {'#rgb', '#rgb-e'}:
1061                    self._update_rgb_edit()
1062            else: # canceled dialog
1063                return
1064
1065        self.toggle_mod_indicator(show=True)
1066        self.add_opt_change(self._cur_opt_name, self.scope, val)
1067
1068
1069    def _on_opt_click(self, id_dlg, id_ctl, data='', info=''):
1070        #print('LIST CIKCK: {}'.format((id_dlg, id_ctl, data, info)))
1071
1072        _sel_ind = listbox_proc(self._h_list, LISTBOX_GET_SEL)
1073        if _sel_ind == -1  or  not self._list_opt_names:  #  nothing selected disable bottom panel
1074            self._clear_opt_edits()
1075            dlg_proc(self.h, DLG_CTL_PROP_SET, name='panel_value', prop={'en':False})
1076            return
1077
1078        # enable bottom panel before manipulations
1079        dlg_proc(self.h, DLG_CTL_PROP_SET, name='panel_value', prop={'en':True})
1080
1081        self._cur_opt_name = self._list_opt_names[_sel_ind]
1082        self._cur_opt = self.optman.get_opt(self._cur_opt_name)
1083        with ignore_edit(id_dlg, self.opt_comment_ed):
1084            self.opt_comment_ed.set_text_all(self._cur_opt.get('cmt', ''))
1085
1086        # if have a change for this option -- show it
1087        is_opt_modified = False
1088        removed_scopes = set(self.hidden_scopes)
1089        for opt_change in reversed(self._opt_changes):
1090            if opt_change.name == self._cur_opt_name:
1091                is_opt_modified = True
1092                if opt_change.value is not None:  # setting value
1093                    # (scope, val) - [f],[l],[u], [def]
1094                    _opt = self.optman.get_opt(opt_change.name)
1095                    ui_val = self.optman.value2uival(_opt, opt_change.value)
1096                    active_scoped_val = (opt_change.scope,  ui_val)
1097                    pass;       LOG and print('NOTE: using change value: '+str(opt_change))
1098                    break
1099                else: # unsetting option
1100                    removed_scopes.add(opt_change.scope)
1101        else: # no matching changes
1102            #active_scoped_val = self.optman.get_opt_active_value(self._cur_opt, is_ui=False, with_scope=True)
1103            # skip values that were reset,
1104            scopes = (scope  for scope in ['f', 'l', 'u', 'def']    if scope not in removed_scopes)
1105            scoped_vals = ((sc, self.optman.get_opt_scope_value(self._cur_opt, sc, is_ui=False))    for sc in scopes)
1106            active_scope = next(sc for sc,val in  scoped_vals  if val is not None) # result - is not None
1107            active_scope_val = self.optman.get_opt_scope_value(self._cur_opt, active_scope, is_ui=True) # for UI
1108            active_scoped_val = (active_scope, active_scope_val)
1109            pass;       LOG and print(' *** using option value: {}; removed:{}'.format(active_scoped_val, removed_scopes))
1110
1111        self.toggle_mod_indicator(show=is_opt_modified)
1112
1113        new_scope, _new_val = active_scoped_val
1114
1115        # set scope
1116        new_scope_name = self._scope_captions[new_scope]
1117        with ignore_edit(self.h, self.scope_ed):
1118            self.scope_ed.set_text_all(new_scope_name)
1119        self.val_eds.set_type(self.h,  self._cur_opt, scoped_val=active_scoped_val)
1120
1121        # rgb stuff
1122        prop_type = self._cur_opt['frm']
1123        if prop_type in {'#rgb', '#rgb-e'}:
1124            self._update_rgb_edit()
1125
1126
1127    def _on_reset(self, id_dlg, id_ctl, data='', info=''):
1128        """ remove option for current scope
1129        """
1130        self.add_opt_change(self._cur_opt_name, self.scope, val=None)
1131        # update value for current scope
1132        self._on_scope_change(-1,-1)
1133
1134    def _on_scope_change(self, id_dlg, id_ctl, data='', info=''):
1135        if not self._cur_opt:
1136            return
1137
1138        # check if have 'value' for 'scope' in ._opt_changes
1139        for opt_change in reversed(self._opt_changes):
1140            if opt_change.name == self._cur_opt_name  and  opt_change.scope == self.scope:
1141                cur_scope_val = opt_change.value  or  ''
1142                self.toggle_mod_indicator(show=True)
1143                break
1144        else:
1145            cur_scope_val = self.optman.get_opt_scope_value(self._cur_opt, scope=self.scope, is_ui=True)
1146            self.toggle_mod_indicator(show=False)
1147
1148        pass;       LOG and print(' -- scoped val:{}:[{}]'.format(self.scope, cur_scope_val))
1149
1150        self.val_eds.set_type(self.h,  self._cur_opt, scoped_val=(self.scope, cur_scope_val))
1151
1152    def _on_filter(self, id_dlg, id_ctl, data='', info=''):
1153        if isinstance(data, tuple):     # on_key_down
1154            key_code, key_state = data
1155            if key_code == VK_ENTER  and  not key_state:
1156                # reset tree selection if selected item not in new filter
1157                selected_node = tree_proc(self._h_tree, TREE_ITEM_GET_SELECTED)
1158                if selected_node:
1159                     path = get_tree_path(self._h_tree, item_id=selected_node)
1160                     if '@'+path not in self.filter_val.split():
1161                        tree_proc(self._h_tree, TREE_ITEM_SELECT, id_item=0)
1162
1163                _t0 = time.time()
1164                self.set_filter(self.filter_val)
1165                _t1 = time.time()
1166                pass;       LOG and print('* set-filter time:{:.3f}s'.format(_t1-_t0))
1167
1168        #else:   # on_change  (typing, pasting)
1169            #print('        . CHANGE')
1170
1171    def _on_tree_click(self, id_dlg, id_ctl, data='', info=''):
1172        if data == 0:   # deselected items
1173            return
1174
1175        path = get_tree_path(self._h_tree, item_id=data)
1176        if path == TREE_ITEM_ALL:  # show all
1177            self.set_filter('', tree_click=True)
1178        else:
1179            new_filter = '@'+path
1180
1181            _keys = app_proc(PROC_GET_KEYSTATE, '')
1182            is_adding = set(_keys) == set('cL')
1183            if is_adding  and  self.filter_val:
1184                if new_filter in self.filter_val.split(): # already in filter - ignore
1185                    return
1186                new_filter = self.filter_val +' '+ new_filter
1187
1188            self.set_filter(new_filter)
1189
1190    def _on_header_click(self, id_dlg, id_ctl, data='', info=''):
1191        pass;       LOG and print('--- Header click-: {}'.format((id_dlg, id_ctl, data, info)))
1192        column_captions, _col_ws = self.columns
1193        col_ind = data
1194        self.set_sort(column_captions[col_ind])
1195
1196    def _on_key(self, id_dlg, id_ctl, data='', info=''):
1197        key_code = id_ctl
1198        state = data
1199        #print(' on -key:{}'.format((key_code, state)))
1200
1201        if key_code == VK_F  and  state == 'c': # Ctrl+F -- show+focus filter
1202            self.toggle_filter(show=True)
1203            self._filter_ed.focus()
1204            return False # consumed
1205
1206        elif key_code == VK_ESCAPE  and  not state:  # <escape> in filter - clear
1207            if self._filter_ed.get_prop(PROP_FOCUSED)  and  self.filter_val:
1208                self.set_filter('')
1209                #self.toggle_filter(show=False)
1210                return False # consumed
1211
1212    def listbox_menu(self, id_dlg, id_ctl, data='', info=''):
1213        if data['y'] < listbox_proc(self._h_list, LISTBOX_GET_ITEM_H): # is header click
1214            # create menu on first run
1215            if not self._h_col_menu:
1216                self._h_col_menu = menu_proc(0, MENU_CREATE)
1217
1218                for colname in COLS_LIST:
1219                    if colname in self.hidden_columns:
1220                        continue
1221
1222                    la = lambda col=colname: self.on_toggle_col(col)
1223                    ui_col_name = ui_column(colname)
1224                    item_id = menu_proc(self._h_col_menu, MENU_ADD,
1225                                command=la, caption=ui_col_name, tag=colname)
1226
1227                    _enabled = colname != COL_OPT_NAME # 'option name' column - always shown
1228                    menu_proc(item_id, MENU_SET_ENABLED, command=_enabled)
1229
1230                menu_proc(self._h_col_menu, MENU_ADD, caption='-')
1231
1232                la = lambda: self.configure_columns()
1233                menu_proc(self._h_col_menu, MENU_ADD, command=la, caption=_('Configure...'))
1234            #end if
1235
1236
1237            # update check state
1238            current_columns, _col_ws = self.columns
1239            for prop in menu_proc(self._h_col_menu, MENU_ENUM):
1240                _checked = prop['tag'] in current_columns
1241                menu_proc(prop['id'], MENU_SET_CHECKED, command=_checked)
1242
1243            menu_proc(self._h_col_menu, MENU_SHOW)
1244
1245    def on_toggle_col(self, info):
1246        pass;       LOG and print('NOTE: toggling column: '+str(info))
1247
1248        col_cfg = opt_col_cfg[:]
1249
1250        colname = info
1251        cur_col_names = [name for name,_w in col_cfg]
1252        if colname in cur_col_names:  # disableg column
1253            del opt_col_cfg[cur_col_names.index(colname)]
1254        else:  # add new column
1255            new_col_w = 100
1256            if   colname == '!':          new_col_w = '19px'
1257            elif colname == COL_SECTION:  new_col_w = '120px'
1258
1259            opt_col_cfg.append((colname, new_col_w))
1260            opt_col_cfg.sort(key=lambda item: COLS_LIST.index(item[0]))
1261        pass;       LOG and print(' -- new columns: '+str(opt_col_cfg))
1262
1263        self.update_list_layout()
1264        _opts = self.get_filtered_opts()
1265        self.update_list(_opts)
1266
1267
1268    def _clear_opt_edits(self):
1269        """ disables: 'scope combo', 'option comment'
1270        """
1271        with ignore_edit(self.h, self.opt_comment_ed):
1272            self.opt_comment_ed.set_text_all('')
1273        with ignore_edit(self.h, self.scope_ed):
1274            self.scope_ed.set_text_all('')
1275
1276        self.val_eds.clear_edits(self.h)
1277
1278    def _dlg_value(self, prop_type):
1279        """ editing option value with a dialog
1280            returns: new value
1281        """
1282
1283        if prop_type == 'hotk':    # HOTKEY
1284            val = dlg_hotkey(title=self._cur_opt_name)
1285
1286        elif prop_type in {'#rgb', '#rgb-e'}:  # RGB;  val == None or correct html color
1287            # empty ('-e') -- only for edit field
1288            cur_scol = self.val_eds.val_edit.get_text_all()
1289            try:
1290                int_col = apx.html_color_to_int(cur_col)
1291            except:
1292                int_col = 0xffffff
1293
1294            val = dlg_color(int_col)
1295
1296            if val is not None:
1297                try:
1298                    val = apx.int_to_html_color(val)
1299                except:
1300                    val = None
1301
1302        elif prop_type == 'file':
1303            caption = _('Choose file: {}').format(self._cur_opt_name)
1304            val = dlg_file(is_open=False, init_filename='', init_dir='', filters='', caption=caption)
1305
1306        elif prop_type == 'json':
1307            from .dlg_json import JsonEd
1308
1309            j_ed = JsonEd(self._cur_opt, self.scope)
1310            val = j_ed.edit_json()
1311        #end if
1312
1313        return val
1314
1315
1316    def toggle_filter(self, show=False):
1317        #dlg_proc(self.h, DLG_CTL_PROP_SET, name='panel_filter', prop={'vis': show})
1318
1319        if show == False:  # if hiding filter - reset tree selection to 'All'
1320            for item_id,name in tree_proc(self._h_tree, TREE_ITEM_ENUM):
1321                if name == TREE_ITEM_ALL:
1322                    tree_proc(self._h_tree, TREE_ITEM_SELECT, id_item=item_id)
1323
1324    def toggle_mod_indicator(self, tag='', info='', show=True, by_timer=False):
1325        if by_timer:
1326            timer_proc(TIMER_START_ONE, self.toggle_mod_indicator, 30, tag='ed_check_state')
1327        else:
1328            if tag == 'ed_check_state':
1329                ed_line_state = self.val_eds.val_edit.get_prop(PROP_LINE_STATE, 0)
1330                if ed_line_state == LINESTATE_NORMAL:
1331                    return
1332
1333            dlg_proc(self.h, DLG_CTL_PROP_SET, name='mod_label', prop={'vis':show})
1334
1335    def apply_changes(self, closing=False):
1336        """ batch apply qued option changes
1337        """
1338        pass;       LOG and print('APPLY_CHANGES')
1339
1340        # check if current value in edit is changed, create option change if it is
1341        try:
1342            edit_val = self.val_eds.get_edited_value(self._cur_opt)
1343        except ValueError:      # exception happens when trying to cast empty str to float|int
1344            edit_val = None
1345        if edit_val is not None  and  edit_val != '':
1346            self.add_opt_change(self._cur_opt_name, self.scope, edit_val)
1347
1348        if not self._opt_changes  and  not closing:
1349            msg_status(_("No option changes has been made"))
1350            return
1351
1352        for i,change in enumerate(self._opt_changes):
1353            is_last = i == len(self._opt_changes) - 1
1354            if change.value is not None: # set value
1355                self.optman.set_opt(name=change.name,  scope=change.scope,  val=change.value,
1356                        lexer=change.lexer,  apply_=is_last)
1357            else: # removing value
1358                self.optman.reset_opt(name=change.name,  scope=change.scope,
1359                        lexer=change.lexer,  apply_=is_last)
1360
1361        self._opt_changes.clear()
1362        self.optman.on_opts_change()
1363        _opts = self.get_filtered_opts()
1364        self.update_list(_opts)
1365
1366    def _update_rgb_edit(self, tag='', info=''):
1367        """ empty tag - ques in timer
1368        """
1369        if not tag:
1370            timer_proc(TIMER_START_ONE, self._update_rgb_edit, 100, tag='on_timer')
1371        else:
1372            if self._closing is not None:
1373                ValueEds.update_ed_color(self.val_eds.val_edit)
1374
1375
1376    def dlg_help(self, *args, **vargs):
1377        if self._h_help == None:
1378            w, h = 600, 450
1379            self._h_help = dlg_proc(0, DLG_CREATE)
1380
1381            colors = app_proc(PROC_THEME_UI_DICT_GET, '')
1382            col_ed_bg = colors['EdTextBg']['color']
1383            col_ed_font = colors['EdTextFont']['color']
1384            color_form_bg = colors['ButtonBorderPassive']['color']
1385
1386            dlg_proc(self._h_help, DLG_PROP_SET,
1387                        prop={'cap': _('Help'),
1388                            'w': w,
1389                            'h': h,
1390                            'resize': True,
1391                            'color': color_form_bg,
1392                            }
1393                        )
1394
1395            n = dlg_proc(self._h_help, DLG_CTL_ADD, 'memo')
1396            dlg_proc(self._h_help, DLG_CTL_PROP_SET, index=n,
1397                        prop={
1398                            'name': 'help_memo',
1399                            'align': ALIGN_CLIENT,
1400                            'val': HELP_TEXT,
1401                            'sp_a':6,
1402                            'color': col_ed_bg,
1403                            'font_color': col_ed_font,
1404                            }
1405                        )
1406
1407        dlg_proc(self._h_help, DLG_SHOW_MODAL)
1408
1409    def close(self):
1410        self._save_dlg_cfg()
1411
1412        self._closing = True
1413
1414        dlg_proc(self.h, DLG_HIDE)
1415
1416
1417class ValueEds:
1418    """ * Responsible for: widgets for editing different value formats
1419        * Formats: bool, float, font, font-e, hotk, int, int2s, str, str2s, strs,
1420    """
1421    VALUE_ED_PANEL = 'panel_value'
1422    VALUE_ED_RESET = 'btn_val_reset'
1423
1424    WGT_NAME__EDIT       = 'cur_val__edit'
1425    WGT_NAME__COMBO      = 'cur_val__combo'
1426    WGT_NAME__CHECK      = 'cur_val__check'
1427
1428    WGT_NAME__BTN_EDIT   = 'cur_val__edit_btn'
1429
1430    type_map = {
1431        'str':      WGT_NAME__EDIT,
1432        'bool':     WGT_NAME__CHECK,
1433        'float':    WGT_NAME__EDIT,
1434        'font':     WGT_NAME__COMBO,
1435        'font-e':   WGT_NAME__COMBO,
1436        'hotk':     WGT_NAME__EDIT,
1437        'int':      WGT_NAME__EDIT,
1438        'int2s':    WGT_NAME__COMBO,
1439        'str2s':    WGT_NAME__COMBO,
1440        'strs':     WGT_NAME__COMBO,
1441
1442        '#rgb':     WGT_NAME__EDIT,
1443        '#rgb-e':   WGT_NAME__EDIT,
1444        'file':     WGT_NAME__EDIT,
1445        'json':     WGT_NAME__EDIT,
1446    }
1447
1448    EXTRA_BTN_TYPES = {'hotk', '#rgb', '#rgb-e', 'file', 'json'}
1449
1450    FN_CHECKBOX_ICONS = {
1451        False: 'cb_unckecked.png',
1452        None:  'cb_none.png',
1453        True:  'cb_checked.png',
1454    }
1455
1456    _h_cb_iml = None
1457    _cb_icons = {} # False, None, True -> imagelist index
1458
1459    def __init__(self, val_change_callback):
1460        self._val_change_callback = val_change_callback
1461        self._ctl_names = {} # id_ctl -> name
1462        self._current_type = None
1463        self.val_edit = None
1464        self.val_combo = None
1465
1466        self._ignore_input = False
1467
1468    @property
1469    def cb_value(self):
1470        """ returns True, False, None
1471        """
1472        imind = button_proc(self._h_cbx, BTN_GET_IMAGEINDEX)
1473
1474        for val,ind in ValueEds._cb_icons.items():
1475            if ind == imind:
1476                return val
1477
1478    def set_type(self, h, opt, scoped_val):
1479        M = ValueEds
1480
1481        scope, value = scoped_val
1482
1483        newtype = opt.get('frm')
1484
1485        pass;       LOG and print('* SET type-value-ed: type:{}, val:{}'.format(
1486                                                                        newtype, (scope, value, type(value))))
1487
1488        self._hide_val_ed(h)
1489
1490        # unsupported option format
1491        if newtype not in M.type_map:
1492            print(_('OptionsError: unsupported option type: ')+str(newtype))
1493            return
1494
1495
1496        # disable option editing?  (some options cannot be a file opt)
1497        if scope == 'f'  and  not opt['opt'] in FILE_OPTS \
1498                or  scope == 'l'  and  GLOBAL_OP_CMT in opt['cmt']:
1499
1500            pass;       LOG and print('NOTE: option NA: disabling')
1501            n = self._wgt_ind(h, M.type_map['str'], show=True) # ~resets wgt props
1502            self._current_type = 'str'
1503
1504            hint = _('Not available for a file')  if scope=='f' else  _('Not available for lexers')
1505            dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
1506                    'en': False,
1507                    'texthint': hint,
1508                    })
1509            with ignore_edit(h, self.val_edit):
1510                self.val_edit.set_text_all('')
1511            return
1512
1513
1514        type_wgt_name = M.type_map[newtype]
1515        # show option-edit widget  and gets widget index
1516        n = self._wgt_ind(h, type_wgt_name, show=True) # ~resets wgt props
1517
1518        if newtype == 'str':
1519            self.val_edit.set_text_all(value or '')
1520
1521        elif newtype == 'bool':
1522            #NOTE: UI bool values are: True, False, and '' for empty scope value
1523            if   value is True:  imind = ValueEds._cb_icons[value]
1524            elif value is False: imind = ValueEds._cb_icons[value]
1525            else:                imind = ValueEds._cb_icons[None]
1526
1527            button_proc(self._h_cbx, BTN_SET_IMAGEINDEX, imind)
1528
1529        elif newtype == 'float':
1530            self.val_edit.set_text_all(str(value) or '')
1531
1532        elif newtype == 'font'  or  newtype == 'font-e': # font-e already has empty value in list
1533            self.val_combo.set_prop(PROP_COMBO_ITEMS, '\n'.join(opt['lst']))
1534            with ignore_edit(h, self.val_combo):
1535                self.val_combo.set_text_all(value)
1536
1537        elif newtype == 'hotk':
1538            self.val_edit.set_text_all(value)
1539            self.val_edit.set_prop(PROP_RO, True)
1540
1541            self.layout_ed_btn(h, n, '...')
1542
1543        elif newtype == 'int':
1544            self.val_edit.set_text_all(str(value))
1545            self.val_edit.set_prop(PROP_NUMBERS_ONLY, True)
1546
1547        elif newtype == 'int2s':
1548            #ed_val = map_option_value(opt, caption=value)
1549            with ignore_edit(h, self.val_combo):
1550                self.val_combo.set_text_all(value)
1551            self.val_combo.set_prop(PROP_COMBO_ITEMS, '\n'.join(opt['jdc']))
1552
1553        elif newtype == 'str2s':
1554            #ed_val = map_option_value(opt, caption=value)
1555            with ignore_edit(h, self.val_combo):
1556                self.val_combo.set_text_all(value)
1557            self.val_combo.set_prop(PROP_COMBO_ITEMS, '\n'.join(opt['jdc']))
1558
1559        elif newtype == 'strs':
1560            with ignore_edit(h, self.val_combo):
1561                self.val_combo.set_text_all(value)
1562            self.val_combo.set_prop(PROP_COMBO_ITEMS, '\n'.join(opt['lst']))
1563
1564        elif newtype == '#rgb-e'  or  newtype == '#rgb':
1565            self.val_edit.set_text_all(str(value))
1566
1567            self.val_edit.set_prop(PROP_GUTTER_ALL, True)
1568            self.val_edit.set_prop(PROP_GUTTER_STATES, False)
1569            self.val_edit.set_prop(PROP_GUTTER_NUM, False)
1570
1571            self.layout_ed_btn(h, n, '...')
1572
1573        elif newtype == 'file':
1574            self.val_edit.set_text_all(str(value))
1575
1576            self.layout_ed_btn(h, n, _('Choose file...'))
1577
1578        elif newtype == 'json':
1579            self.val_edit.set_text_all(str(value))
1580            self.val_edit.set_prop(PROP_RO, True)
1581
1582            self.layout_ed_btn(h, n, _('Edit...'))
1583        #end if
1584
1585        # set line state: normal (not edited)
1586        if type_wgt_name == M.WGT_NAME__EDIT:
1587            self.val_edit.set_prop(PROP_LINE_STATE, (0, LINESTATE_NORMAL))
1588
1589        self._current_type = newtype
1590
1591    def clear_edits(self, h):
1592        M = ValueEds
1593
1594        self._hide_val_ed(h)
1595        _n = self._wgt_ind(h, M.WGT_NAME__EDIT, show=True) # ~resets wgt props
1596        self.val_edit.set_text_all('')
1597
1598    def get_name(self, id_ctl):
1599        return self._ctl_names.get(id_ctl)
1600
1601    def get_edited_value(self, opt):
1602        """ if 'WGT_NAME__EDIT' is current editor - return parsed value
1603            else None
1604
1605        """
1606        M = ValueEds
1607
1608        type_wgt_name = M.type_map[self._current_type]
1609
1610        if type_wgt_name == M.WGT_NAME__EDIT:
1611            val = self.val_edit.get_text_all()
1612
1613            prop_type = opt['frm']
1614            if prop_type == 'int':
1615                val = int(val)
1616            elif prop_type == 'float':
1617                val = float(val)
1618            elif prop_type in {'#rgb', '#rgb-e'}:
1619                if val == '':
1620                    if prop_type != '#rgb-e':
1621                        msg_status(_('Option "{}" does not accept empty value').format(opt['opt']))
1622                        return
1623                else:
1624                    try:
1625                        apx.html_color_to_int(val)
1626                    except:
1627                        msg_status(_('Incorrect color token: ') + val)
1628                        return
1629            return val
1630
1631
1632    def _wgt_ind(self, h, name, show=False):
1633        """ creates widget if didn't exist
1634            returns: widget's form index
1635        """
1636        M = ValueEds
1637
1638        default_props = {
1639            'name': name,
1640            'p': M.VALUE_ED_PANEL,
1641            'h': BTN_H, 'max_h': BTN_H,
1642            'a_l': ('mod_label', ']'),
1643            'a_t': (M.VALUE_ED_RESET, '['),
1644            'a_r': (M.VALUE_ED_RESET, '['),
1645            'a_b': (M.VALUE_ED_RESET, ']'),
1646            #'sp_l': PAD,
1647            'act': True, 'en': True,
1648        }
1649
1650        #TODO validate name
1651        if name == M.WGT_NAME__EDIT:
1652            n = dlg_proc(h, DLG_CTL_FIND, prop=name)
1653            if n == -1:     # add if not already
1654                n = dlg_proc(h, DLG_CTL_ADD, 'editor_edit')
1655                h_ed = dlg_proc(h, DLG_CTL_HANDLE, index=n)
1656                self._ctl_names[n] = name
1657                self.val_edit = Editor(h_ed)
1658
1659            # resetting to defaults
1660            _props = {**default_props,
1661                    'on_key_down': self._val_change_callback,
1662                    'texthint': '',}
1663            dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop=_props)
1664
1665            ValueEds.reset_edt(self.val_edit)
1666
1667        elif name == M.WGT_NAME__COMBO:
1668            n = dlg_proc(h, DLG_CTL_FIND, prop=name)
1669            if n == -1:     # add if not already
1670                n = dlg_proc(h, DLG_CTL_ADD, 'editor_combo')
1671
1672                _props = {**default_props,   'on_change': self._val_change_callback,}
1673                dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop=_props)
1674
1675                h_ed = dlg_proc(h, DLG_CTL_HANDLE, index=n)
1676                self._ctl_names[n] = name
1677                self.val_combo = Editor(h_ed)
1678                self.val_combo.set_prop(PROP_RO, True)
1679
1680        elif name == M.WGT_NAME__CHECK:
1681            n = dlg_proc(h, DLG_CTL_FIND, prop=name)
1682            if n == -1:     # add if not already
1683                n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
1684                dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
1685                        **default_props,
1686                        'cap': _('Enable'),
1687                        'act': True,
1688                        'on_change': self._on_cb_click_proxy,
1689                        'font_color': COL_FONT,
1690                        })
1691                self._h_cbx = dlg_proc(h, DLG_CTL_HANDLE, index=n)
1692                self._ctl_names[n] = name
1693
1694                button_proc(self._h_cbx, BTN_SET_FLAT, True)
1695                button_proc(self._h_cbx, BTN_SET_KIND, BTNKIND_TEXT_ICON_HORZ)
1696                # icons  (checkbox)
1697                h_iml = ValueEds._get_checkbox_imagelist()
1698                button_proc(self._h_cbx, BTN_SET_IMAGELIST, h_iml)
1699            #end if
1700
1701        # Extra
1702        elif name == M.WGT_NAME__BTN_EDIT:
1703            n = dlg_proc(h, DLG_CTL_FIND, prop=name)
1704            if n == -1:     # add if not already
1705                n = dlg_proc(h, DLG_CTL_ADD, 'button_ex')
1706                dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
1707                        **default_props,
1708                        'cap': '...',
1709                        'on_change': self._val_change_callback,
1710                        })
1711                self._ctl_names[n] = name
1712        #end if
1713
1714        if show:   # set visible
1715            dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={'vis': True})
1716
1717        return n
1718
1719    def _hide_val_ed(self, h):
1720        M = ValueEds
1721
1722        if not self._current_type:
1723            return
1724
1725        to_hide = [M.type_map[self._current_type]]
1726
1727        if self._current_type in M.EXTRA_BTN_TYPES:
1728            to_hide.append(M.WGT_NAME__BTN_EDIT)
1729
1730        for name in to_hide:
1731            dlg_proc(h, DLG_CTL_PROP_SET, name=name, prop={'vis':False})
1732
1733    def _on_cb_click_proxy(self, id_dlg, id_ctl, data='', info=''):
1734        """ changes button_ex checkbox icon to proper value, and sends control click to default callback
1735        """
1736        cb_val = self.cb_value
1737        # cycle: [None => ] True => False => True...
1738        if   cb_val is True:  nextind = ValueEds._cb_icons[False]
1739        #elif cb_val is False: nextind = ValueEds._cb_icons[True]
1740        else:                 nextind = ValueEds._cb_icons[True]
1741
1742        button_proc(self._h_cbx, BTN_SET_IMAGEINDEX, nextind)
1743
1744        self._val_change_callback(id_dlg, id_ctl, data, info)
1745
1746    def layout_ed_btn(self, h, n, caption):
1747        M = ValueEds
1748
1749        w = int(BTN_W*0.5) if caption == '...' else BTN_W
1750
1751        btn_n = self._wgt_ind(h, M.WGT_NAME__BTN_EDIT, show=True)
1752        dlg_proc(h, DLG_CTL_PROP_SET, index=btn_n, prop={
1753                'a_l': None,       'a_r': (M.VALUE_ED_RESET, '['),
1754                'w': w, 'max_w': w, 'sp_l': 2,
1755                'cap': caption,
1756        })
1757        # ... edit
1758        dlg_proc(h, DLG_CTL_PROP_SET, index=n, prop={
1759                #'a_l': ('', '['),
1760                'a_r': (M.WGT_NAME__BTN_EDIT, '[')
1761        })
1762
1763    def update_ed_color(edt):
1764        try:
1765            int_col = apx.html_color_to_int(edt.get_text_all())
1766        except:     # invalid color -> reset to theme color
1767            colors = app_proc(PROC_THEME_UI_DICT_GET, '')
1768            int_col = colors['EdGutterBg']['color']
1769        edt.set_prop(PROP_COLOR, (COLOR_ID_GutterBg, int_col))
1770
1771
1772    def reset_edt(edt):
1773        edt.set_prop(PROP_NUMBERS_ONLY, False)
1774        edt.set_prop(PROP_RO, False)
1775        edt.set_prop(PROP_GUTTER_ALL, False)
1776
1777    @classmethod
1778    def _get_checkbox_imagelist(cls):
1779        # load icons only once
1780        if not ValueEds._h_cb_iml:
1781            ValueEds._h_cb_iml,  ValueEds._cb_icons = load_imagelist(ValueEds.FN_CHECKBOX_ICONS)
1782
1783        return ValueEds._h_cb_iml
1784
1785class OptionMapValueError(Exception):
1786    pass
1787
1788HELP_TEXT = _("""About "Filter"
1789 Suitable options will contain all specified words.
1790 Tips and tricks:
1791 • Add "#" to search the words also in comments.
1792 • Add "@sec" to show options from section with "sec" in name.
1793   Several sections are allowed.
1794   Click item in sections tree with Ctrl to add it.
1795 • To show only overridden options:
1796   - Add "!"   to show only User+Lexer+File.
1797   - Add "!!"  to show only Lexer+File
1798   - Add "!!!" to show only File.
1799 • Use "<" or ">" for word boundary.
1800     Example:
1801       size> <tab
1802     selects "tab_size" but not "ui_tab_size" or "tab_size_x".
1803
1804 • Values in table column "!"
1805     !   option is set in "user.json",
1806     !!  option is set in "lexer NNN.json",
1807     !!! option is set for current file,
1808     L   default value is from "settings_default/lexer NNN.json",
1809     +   not CudaText standard option.
1810""")
1811