Differences From Artifact [a9823722c2]:

To Artifact [96aaed6261]:


1
2
3

4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25





26
27
28
29
30
31
32

33
34
35
36

37
38
39






40
41
42

43
44

45
46
47
48
49
50
51


52
53




54
55



56
57
58
59
60
61
62
63

64
65
66

67
68
69

70
71

72
73
74
75
76


77
78
79
80



81
82
83
84
85
86





87
88
89
90
91




92
93
94

95
96
97
98
99
100

101
102
103
104



105
106

107
108
109
110
111
112
113
114
115
116
117
118











119
120
121
122


123
124
125
126
127
128







129
130

131
132

133
134
135
136
137




138
139
140
141
142




143
144
145
146



147
148
149
150
151




152
153

154
155
156
157
158




159
160
161
162
163







164
165

166
167
168
169
170
171
172
173






174
175
176
177



178
179
180



181
182

183
184
185




186
187


188
189
190
191
192
193
194
195
196
197
198
199
200












201

202

203
204

1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

34
35
36

37
38
39
40

41
42
43

44
45
46
47
48
49
50
51

52
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
79
80

81
82
83

84
85
86

87
88

89
90
91
92


93
94
95



96
97
98
99





100
101
102
103
104
105
106



107
108
109
110

111

112
113
114
115
116
117

118
119
120


121
122
123


124
125











126
127
128
129
130
131
132
133
134
135
136
137
138


139
140
141





142
143
144
145
146
147
148


149


150
151




152
153
154
155
156




157
158
159
160
161



162
163
164
165




166
167
168
169
170

171





172
173
174
175
176




177
178
179
180
181
182
183
184

185
186
187

188




189
190
191
192
193
194
195



196
197
198
199


200
201
202
203
204
205
206


207
208
209
210
211

212
213
214












215
216
217
218
219
220
221
222
223
224
225
226
227
228

229


230


-
+







+















+
+
+
+
+


-



-
+



-
+


-
+
+
+
+
+
+


-
+

-
+







+
+


+
+
+
+

-
+
+
+







-
+


-
+


-
+

-
+



-
-
+
+

-
-
-
+
+
+

-
-
-
-
-
+
+
+
+
+


-
-
-
+
+
+
+
-

-
+





-
+


-
-
+
+
+
-
-
+

-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+


-
-
+
+

-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
+

-
-
-
-
+
+
+
+

-
-
-
-
+
+
+
+

-
-
-
+
+
+

-
-
-
-
+
+
+
+

-
+
-
-
-
-
-
+
+
+
+

-
-
-
-
+
+
+
+
+
+
+

-
+


-

-
-
-
-
+
+
+
+
+
+

-
-
-
+
+
+

-
-
+
+
+


+

-
-
+
+
+
+

-
+
+

-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+

+
-
+
-
-
+
# encoding: UTF-8
# api: python
# type: ui
##type: gui
# category: io
# title: Config GUI
# description: Display plugins + options in setup window
# version: 0.8
# depends: python:pysimplegui (>= 4.0)
# priority: optional
# config: -
# pylint: disable=line-too-long
#
# Creates a PySimpleGUI options list. Scans a given list of *.py files
# for meta data, then populates a config{} dict and (optionally) a state
# map for plugins themselves.
#
#    jsoncfg = {}
#    pluginconf.gui.window(jsoncfg, {}, ["plugins/*.py"])
#
# Very crude, and not as many widgets as the Gtk/St2 implementation.
# Supports type: str, bool, select, int, dict, text config: options.
#

""" PySimpleGUI window to populate config dict via plugin options """


#import os
import re
#import json
import glob
import textwrap
import PySimpleGUI as sg
import pluginconf
import glob, json, os, re, textwrap


# temporarily store collected plugin config: dicts
options = {}
OPTIONS = {}


#-- show configuation window
def window(config={}, plugin_states={}, files=["*/*.py"], plugins={}, opt_label=False, theme="DefaultNoMoreNagging", **kwargs):
def window(config, plugin_states, files=["*/*.py"], **kwargs):
    """
    Reads *.py files and crafts a settings dialog from meta data.
    

    Where `plugin_states{}` is usually an entry in `config{}` itself. Depending on plugin
    and option names, it might even be a flat/shared namespace for both. Per default you'd
    set `files=["plugins/*.py", __file__]` to be read. But with `files=[]` it's possible to
    provide a `plugins=pluginconf.get_plugin_meta()` or prepared plugin/options dict instead.

    Parameters
    ----------
    config : dict
    config : dict 🔁
        Config settings, updated after dialog completion
    plugin_states : dict
    plugin_states : dict 🔁
        Plugin activation states, also input/output
    files : list
        Glob list of *.py files to extract meta definitions from
    plugins : dict
        Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected
    opt_label : bool
        Show config name= as label
    theme : str
        Set PSG window theme.
    **kwargs : dict
        Other options are passed on to PySimpleGUI

    Returns
    -------
    True : if changed config{} values are to be saved (the dict will be updated in any case)
    """
    
    plugins = kwargs.get("plugins", {})
    opt_label = kwargs.get("opt_label", False)
    theme = kwargs.get("theme", "DefaultNoMoreNagging")
    if theme:
        sg.theme(theme)
    if files:
        plugins = read_options(files)
    layout = plugin_layout(plugins.values(), config, plugin_states, opt_label=opt_label)
    layout.append([sg.T(" ")])
    #print(repr(layout))
    

    # pack window
    layout = [
        [sg.Column(layout, expand_x=1, expand_y=0, size=(575,680), scrollable="vertically", element_justification='left')],
        [sg.Column(layout, expand_x=1, expand_y=0, size=(575, 680), scrollable="vertically", element_justification='left')],
        [sg.Column([[sg.Button("Cancel"), sg.Button("Save")]], element_justification='right')]
    ]
    if not "title" in kwargs:
    if "title" not in kwargs:
        kwargs["title"] = "Options"
    if not "font" in kwargs:
    if "font" not in kwargs:
        kwargs["font"] = "Sans 11"
    win = sg.Window(layout=layout, resizable=1, **kwargs)

    # wait for save/exit        
    event,data = win.read()
    # wait for save/exit
    event, data = win.read()
    win.close()
    if event=="Save":
        for k,v in data.items():
            if options.get(k):
    if event == "Save":
        for key, val in data.items():
            if OPTIONS.get(key):
                #@ToDo: handle array[key] names
                config[k] = cast.fromtype(data[k], options[k])
            elif type(k) is str and k.startswith('p:'):
                k = k.replace('p:', '')
                if plugins.get(k):
                    plugin_states[k] = v
                config[key] = Cast.fromtype(data[key], OPTIONS[key])
            elif isinstance(key, str) and key.startswith('p:'):
                key = key.replace('p:', '')
                if plugins.get(key):
                    plugin_states[key] = val
        return True
    #print(config, plugin_states)
    
    
# craft list of widgets for each read plugin


def plugin_layout(pmd_list, config, plugin_states, opt_label=False):
    """ craft list of widgets for each read plugin """
def plugin_layout(ls, config, plugin_states, opt_label=False):
    layout = []
    for plg in ls:
    for plg in pmd_list:
        #print(plg.get("id"))
        layout = layout + plugin_entry(plg, plugin_states)
        for opt in plg["config"]:
            if opt.get("name"):
                if opt_label:
                    layout.append([sg.T(opt["name"], font=("Sans",11,"bold"), pad=((50,0),(7,0)))])
                    layout.append([sg.T(opt["name"], font=("Sans", 11, "bold"), pad=((50, 0), (7, 0)))])
                layout.append(option_entry(opt, config))
    return layout
    
# checkbox for plugin name

def plugin_entry(pmd, plugin_states):
    """ checkbox for plugin name """
def plugin_entry(e, plugin_states):
    id = e["id"]
    name = pmd["id"]
    return [
         [
             sg.Checkbox(
                  e.get("title", id), key='p:'+id, default=plugin_states.get(id, 0), tooltip=e.get("doc"), metadata="plugin",
                  font="bold", pad=(0,(8,0))
             ),
             sg.Text("({}/{})".format(e.get("type"), e.get("category")), text_color="#005", pad=(0,(8,0))),
             sg.Text(e.get("version"), text_color="#a72", pad=(0,(8,0)))
         ],
         [
             sg.Text(e.get("description", ""), tooltip=e.get("doc"), font=("sans", 10), pad=(26,(0,10)))
         ]
        [
            sg.Checkbox(
                pmd.get("title", name), key='p:'+name, default=plugin_states.get(name, 0),
                tooltip=pmd.get("doc"), metadata="plugin", font="bold", pad=(0, (8, 0))
            ),
            sg.Text("({}/{})".format(pmd.get("type"), pmd.get("category")), text_color="#005", pad=(0, (8, 0))),
            sg.Text(pmd.get("version"), text_color="#a72", pad=(0, (8, 0)))
        ],
        [
            sg.Text(pmd.get("description", ""), tooltip=pmd.get("doc"), font=("sans", 10), pad=(26, (0, 10)))
        ]
    ]

# widgets for single config option
def option_entry(o, config):
def option_entry(opt, config):
    """ widgets for single config option """
    #print(o)
    name = o.get("name", "")
    desc = wrap(o.get("description", name), 60)
    type = o.get("type", "str")
    help = o.get("help", None)
    if help:
    name = opt.get("name", "")
    desc = wrap(opt.get("description", name), 60)
    typedef = opt.get("type", "str")
    tooltip = wrap(opt.get("help", name), 60)
    OPTIONS[name] = opt
    val = config.get(name, opt.get("value", ""))

        help = wrap(help, 60)
    options[name] = o
    widget = []
    val = config.get(name, o.get("value", ""))
    if o.get("hidden"):
    if opt.get("hidden"):
        pass
    elif type == "str":
        return [
            sg.InputText(key=name, default_text=str(val), size=(20,1), pad=((50,0),3)),
            sg.Text(wrap(desc, 50), pad=(5,2), tooltip=help or name, justification='left', auto_size_text=1)
    elif typedef == "str":
        widget = [
            sg.InputText(key=name, default_text=str(val), size=(20, 1), pad=((50, 0), 3)),
            sg.Text(wrap(desc, 50), pad=(5, 2), tooltip=tooltip, justification='left', auto_size_text=1)
        ]
    elif type == "text":
        return [
            sg.Multiline(key=name, default_text=str(val), size=(45,4), pad=((40,0),3)),
            sg.Text(wrap(desc, 20), pad=(5,2), tooltip=help or name, justification='left', auto_size_text=1)
    elif typedef == "text":
        widget = [
            sg.Multiline(key=name, default_text=str(val), size=(45, 4), pad=((40, 0), 3)),
            sg.Text(wrap(desc, 20), pad=(5, 2), tooltip=tooltip, justification='left', auto_size_text=1)
        ]
    elif type == "bool":
        return [
            sg.Checkbox(wrap(desc, 70), key=name, default=cast.bool(val), tooltip=help or name, pad=((40,0),2), auto_size_text=1)
    elif typedef == "bool":
        widget = [
            sg.Checkbox(wrap(desc, 70), key=name, default=Cast.bool(val), tooltip=tooltip, pad=((40, 0), 2), auto_size_text=1)
        ]
    elif type == "int":
        return [
            sg.InputText(key=name, default_text=str(val), size=(6,1), pad=((50,0),3)),
            sg.Text(wrap(desc, 60), pad=(5,2), tooltip=help or name, auto_size_text=1)
    elif typedef == "int":
        widget = [
            sg.InputText(key=name, default_text=str(val), size=(6, 1), pad=((50, 0), 3)),
            sg.Text(wrap(desc, 60), pad=(5, 2), tooltip=tooltip, auto_size_text=1)
        ]
    elif type == "select":
    elif typedef == "select":
        #o["select"] = parse_select(o.get("select", ""))
        values = [v for v in o["select"].values()]
        return [
            sg.Combo(key=name, default_value=o["select"].get(val, val), values=values, size=(15,1), pad=((50,0),0), font="Sans 11"),
            sg.Text(wrap(desc, 47), pad=(5,2), tooltip=help or name, auto_size_text=1)
        values = opt["select"].values()
        widget = [
            sg.Combo(key=name, default_value=opt["select"].get(val, val), values=values, size=(15, 1), pad=((50, 0), 0), font="Sans 11"),
            sg.Text(wrap(desc, 47), pad=(5, 2), tooltip=tooltip, auto_size_text=1)
        ]
    elif type == "dict":  # or "table" rather ?
        return [
            sg.Table(values=config.get(name, ["", ""]), headings=o.get("columns", "Key,Value").split(","),
            num_rows=5, col_widths=30, def_col_width=30, auto_size_columns=False, max_col_width=150, key=name, tooltip=help or desc)
    elif typedef == "dict":  # or "table" rather ?
        widget = [
            sg.Table(
                values=config.get(name, ["", ""]), headings=opt.get("columns", "Key,Value").split(","),
                num_rows=5, col_widths=30, def_col_width=30, auto_size_columns=False, max_col_width=150,
                key=name, tooltip=wrap(opt.get("help", desc))
            )
        ]
    return []
    return widget


#-- read files, return dict of {id:pmd} for all plugins
def read_options(files):
    ls = [pluginconf.plugin_meta(fn=fn) for pattern in files for fn in glob.glob(pattern)]
    return dict(
        (meta["id"], meta) for meta in ls
    )
    """ read files, return dict of {id:pmd} for all plugins """
    return {
        meta["id"]: meta for meta in
        [pluginconf.plugin_meta(fn=fn) for pattern in files for fn in glob.glob(pattern)]
    }



#-- map option types (from strings)
class cast:
class Cast:
    """ map option types (from strings) """

    @staticmethod
    def bool(v):
        if v in ("1", 1, True, "true", "TRUE", "yes", "YES", "on", "ON"):
    def bool(val):
        """ map boolean literals """
        if val in ("1", 1, True, "true", "TRUE", "yes", "YES", "on", "ON"):
            return True
        return False

    @staticmethod
    def int(v):        
        return int(v) if re.match("-?\d+", v) else 0
    def int(val):
        """ verify integer """
        return int(val) if re.match(r"-?\d+", val) else 0

    @staticmethod
    def fromtype(v, opt):
    def fromtype(val, opt):
        """ cast according to option type """
        if not opt.get("type"):
            return str(v)
        elif opt["type"] == "int":
            return cast.int(v)
        elif opt["type"] == "bool":
            return cast.bool(v)
        elif opt["type"] == "select":
            inverse = dict((v,k) for k,v in opt["select"].items())
            return inverse.get(v, v)
        elif opt["type"] == "text":
            return str(v).rstrip()
        else:
            return v
            return str(val)
        if opt["type"] == "int":
            return Cast.int(val)
        if opt["type"] == "bool":
            return Cast.bool(val)
        if opt["type"] == "select":
            inverse = dict((val, key) for key, val in opt["select"].items())
            return inverse.get(val, val)
        if opt["type"] == "text":
            return str(val).rstrip()
        # else:
        return val

def wrap(text, width=50):
#-- textwrap for `description` and `help` option fields
    """ textwrap for `description` and `help` option fields """
def wrap(s, w=50):
    return "\n".join(textwrap.wrap(s, w)) if s else ""
    return "\n".join(textwrap.wrap(text, width)) if text else ""