Overview
Comment:pylint fixes, doc additions
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: a88ee411640c2facb954c66a637d202d3dd5958541043384908f69c69c7225c8
User & Date: mario on 2022-10-27 10:43:54
Other Links: manifest | tags
Context
2022-10-28
07:06
ad PluginMeta dict wrapper for briefer property access check-in: 9427b32486 user: mario tags: trunk
2022-10-27
10:43
pylint fixes, doc additions check-in: a88ee41164 user: mario tags: trunk
07:58
resurrect functions for doc check-in: f0163c8621 user: mario tags: trunk
Changes

Modified html/gui.html from [4c36b56296] to [95289d3b21].

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
</section>
<section>
</section>
<section>
<h2 class="section-title" id="header-functions">Functions</h2>
<dl>
<dt id="pluginconf.gui.option_entry"><code class="name flex">
<span>def <span class="ident">option_entry</span></span>(<span>o, config)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.plugin_entry"><code class="name flex">
<span>def <span class="ident">plugin_entry</span></span>(<span>e, plugin_states)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.plugin_layout"><code class="name flex">
<span>def <span class="ident">plugin_layout</span></span>(<span>ls, config, plugin_states, opt_label=False)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.read_options"><code class="name flex">
<span>def <span class="ident">read_options</span></span>(<span>files)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.window"><code class="name flex">
<span>def <span class="ident">window</span></span>(<span>config={}, plugin_states={}, files=['*/*.py'], plugins={}, opt_label=False, theme='DefaultNoMoreNagging', **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>Reads *.py files and crafts a settings dialog from meta data.</p>




<h2 id="parameters">Parameters</h2>
<dl>
<dt><strong><code>config</code></strong> :&ensp;<code>dict</code></dt>
<dd>Config settings, updated after dialog completion</dd>
<dt><strong><code>plugin_states</code></strong> :&ensp;<code>dict</code></dt>
<dd>Plugin activation states, also input/output</dd>
<dt><strong><code>files</code></strong> :&ensp;<code>list</code></dt>
<dd>Glob list of *.py files to extract meta definitions from</dd>
<dt><strong><code>plugins</code></strong> :&ensp;<code>dict</code></dt>
<dd>Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected</dd>
<dt><strong><code>opt_label</code></strong> :&ensp;<code>bool</code></dt>
<dd>Show config name= as label</dd>


<dt><strong><code>**kwargs</code></strong> :&ensp;<code>dict</code></dt>
<dd>Other options are passed on to PySimpleGUI</dd>





</dl></div>
</dd>
<dt id="pluginconf.gui.wrap"><code class="name flex">
<span>def <span class="ident">wrap</span></span>(<span>s, w=50)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</section>
<section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="pluginconf.gui.cast"><code class="flex name class">
<span>class <span class="ident">cast</span></span>
</code></dt>
<dd>
<div class="desc"></div>
<h3>Static methods</h3>
<dl>
<dt id="pluginconf.gui.cast.bool"><code class="name flex">
<span>def <span class="ident">bool</span></span>(<span>v)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.cast.fromtype"><code class="name flex">
<span>def <span class="ident">fromtype</span></span>(<span>v, opt)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt id="pluginconf.gui.cast.int"><code class="name flex">
<span>def <span class="ident">int</span></span>(<span>v)</span>
</code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
</dl>
</section>
</article>
<nav id="sidebar">







|


|


|


|


|


|





|


|



>
>
>
>


|

|







>
>


>
>
>
>
>



|


|






|
|


|


|
|


|

|
|


|

|
|


|







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
</section>
<section>
</section>
<section>
<h2 class="section-title" id="header-functions">Functions</h2>
<dl>
<dt id="pluginconf.gui.option_entry"><code class="name flex">
<span>def <span class="ident">option_entry</span></span>(<span>opt, config)</span>
</code></dt>
<dd>
<div class="desc"><p>widgets for single config option</p></div>
</dd>
<dt id="pluginconf.gui.plugin_entry"><code class="name flex">
<span>def <span class="ident">plugin_entry</span></span>(<span>pmd, plugin_states)</span>
</code></dt>
<dd>
<div class="desc"><p>checkbox for plugin name</p></div>
</dd>
<dt id="pluginconf.gui.plugin_layout"><code class="name flex">
<span>def <span class="ident">plugin_layout</span></span>(<span>pmd_list, config, plugin_states, opt_label=False)</span>
</code></dt>
<dd>
<div class="desc"><p>craft list of widgets for each read plugin</p></div>
</dd>
<dt id="pluginconf.gui.read_options"><code class="name flex">
<span>def <span class="ident">read_options</span></span>(<span>files)</span>
</code></dt>
<dd>
<div class="desc"><p>read files, return dict of {id:pmd} for all plugins</p></div>
</dd>
<dt id="pluginconf.gui.window"><code class="name flex">
<span>def <span class="ident">window</span></span>(<span>config, plugin_states, files=['*/*.py'], **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>Reads *.py files and crafts a settings dialog from meta data.</p>
<p>Where <code>plugin_states{}</code> is usually an entry in <code>config{}</code> itself. Depending on plugin
and option names, it might even be a flat/shared namespace for both. Per default you'd
set <code>files=["plugins/*.py", __file__]</code> to be read. But with <code>files=[]</code> it's possible to
provide a <code>plugins=pluginconf.get_plugin_meta()</code> or prepared plugin/options dict instead.</p>
<h2 id="parameters">Parameters</h2>
<dl>
<dt><strong><code>config</code></strong> :&ensp;<code>dict 🔁</code></dt>
<dd>Config settings, updated after dialog completion</dd>
<dt><strong><code>plugin_states</code></strong> :&ensp;<code>dict 🔁</code></dt>
<dd>Plugin activation states, also input/output</dd>
<dt><strong><code>files</code></strong> :&ensp;<code>list</code></dt>
<dd>Glob list of *.py files to extract meta definitions from</dd>
<dt><strong><code>plugins</code></strong> :&ensp;<code>dict</code></dt>
<dd>Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected</dd>
<dt><strong><code>opt_label</code></strong> :&ensp;<code>bool</code></dt>
<dd>Show config name= as label</dd>
<dt><strong><code>theme</code></strong> :&ensp;<code>str</code></dt>
<dd>Set PSG window theme.</dd>
<dt><strong><code>**kwargs</code></strong> :&ensp;<code>dict</code></dt>
<dd>Other options are passed on to PySimpleGUI</dd>
</dl>
<h2 id="returns">Returns</h2>
<dl>
<dt><strong><code>True</code></strong> :&ensp;<code>if changed config{} values are to be saved (the dict will be updated in any case)</code></dt>
<dd>&nbsp;</dd>
</dl></div>
</dd>
<dt id="pluginconf.gui.wrap"><code class="name flex">
<span>def <span class="ident">wrap</span></span>(<span>text, width=50)</span>
</code></dt>
<dd>
<div class="desc"><p>textwrap for <code>description</code> and <code>help</code> option fields</p></div>
</dd>
</dl>
</section>
<section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="pluginconf.gui.Cast"><code class="flex name class">
<span>class <span class="ident">Cast</span></span>
</code></dt>
<dd>
<div class="desc"><p>map option types (from strings)</p></div>
<h3>Static methods</h3>
<dl>
<dt id="pluginconf.gui.Cast.bool"><code class="name flex">
<span>def <span class="ident">bool</span></span>(<span>val)</span>
</code></dt>
<dd>
<div class="desc"><p>map boolean literals</p></div>
</dd>
<dt id="pluginconf.gui.Cast.fromtype"><code class="name flex">
<span>def <span class="ident">fromtype</span></span>(<span>val, opt)</span>
</code></dt>
<dd>
<div class="desc"><p>cast according to option type</p></div>
</dd>
<dt id="pluginconf.gui.Cast.int"><code class="name flex">
<span>def <span class="ident">int</span></span>(<span>val)</span>
</code></dt>
<dd>
<div class="desc"><p>verify integer</p></div>
</dd>
</dl>
</dd>
</dl>
</section>
</article>
<nav id="sidebar">
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<li><code><a title="pluginconf.gui.window" href="#pluginconf.gui.window">window</a></code></li>
<li><code><a title="pluginconf.gui.wrap" href="#pluginconf.gui.wrap">wrap</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="pluginconf.gui.cast" href="#pluginconf.gui.cast">cast</a></code></h4>
<ul class="">
<li><code><a title="pluginconf.gui.cast.bool" href="#pluginconf.gui.cast.bool">bool</a></code></li>
<li><code><a title="pluginconf.gui.cast.fromtype" href="#pluginconf.gui.cast.fromtype">fromtype</a></code></li>
<li><code><a title="pluginconf.gui.cast.int" href="#pluginconf.gui.cast.int">int</a></code></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p>
</footer>
</body>
</html>







|

|
|
|












148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<li><code><a title="pluginconf.gui.window" href="#pluginconf.gui.window">window</a></code></li>
<li><code><a title="pluginconf.gui.wrap" href="#pluginconf.gui.wrap">wrap</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="pluginconf.gui.Cast" href="#pluginconf.gui.Cast">Cast</a></code></h4>
<ul class="">
<li><code><a title="pluginconf.gui.Cast.bool" href="#pluginconf.gui.Cast.bool">bool</a></code></li>
<li><code><a title="pluginconf.gui.Cast.fromtype" href="#pluginconf.gui.Cast.fromtype">fromtype</a></code></li>
<li><code><a title="pluginconf.gui.Cast.int" href="#pluginconf.gui.Cast.int">int</a></code></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p>
</footer>
</body>
</html>

Modified pluginconf/gui.py from [a9823722c2] to [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
# encoding: UTF-8
# api: python
# type: ui
# category: io
# title: Config GUI
# description: Display plugins + options in setup window
# version: 0.8
# depends: python:pysimplegui (>= 4.0)
# priority: optional
# config: -

#
# 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 PySimpleGUI as sg
import pluginconf
import glob, json, os, re, textwrap


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


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





    Parameters
    ----------
    config : dict
        Config settings, updated after dialog completion
    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


    **kwargs : dict
        Other options are passed on to PySimpleGUI




    """
    


    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([[sg.Button("Cancel"), sg.Button("Save")]], element_justification='right')]
    ]
    if not "title" in kwargs:
        kwargs["title"] = "Options"
    if not "font" in kwargs:
        kwargs["font"] = "Sans 11"
    win = sg.Window(layout=layout, resizable=1, **kwargs)

    # wait for save/exit        
    event,data = win.read()
    win.close()
    if event=="Save":
        for k,v in data.items():
            if options.get(k):
                #@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
        return True
    #print(config, plugin_states)
    
    

# craft list of widgets for each read plugin
def plugin_layout(ls, config, plugin_states, opt_label=False):
    layout = []
    for plg in ls:
        #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(option_entry(opt, config))
    return layout
    

# checkbox for plugin name
def plugin_entry(e, plugin_states):
    id = e["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)))
         ]
    ]

# widgets for single config option
def option_entry(o, config):

    #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:
        help = wrap(help, 60)
    options[name] = o
    val = config.get(name, o.get("value", ""))
    if o.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 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 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 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 type == "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)
        ]
    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)


        ]
    return []


#-- 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


    )


#-- map option types (from strings)
class cast:
    @staticmethod
    def bool(v):

        if v 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
    @staticmethod
    def fromtype(v, opt):

        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


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


|







>















>
>
>
>
>


<



|



|


|
>
>
>
>
>


|

|







>
>


>
>
>
>

|
>
>







|


|


|

|



|
|

|
|
|

|
|
|
|
|


|
|
>
|
<

|





|


|
>
|
<
|

|
|
|
|
|
|
|
|
|
|
|


<
|
>

|
|
|
|
>
>
|
<
|
<
|

|
|
|
|

|
|
|
|

|
|
|

|
|
|
|

|
<
|
|
|
|

|
|
>
|
|
>
>

|


<

<
>
|
|
>
>
|

|
|
|

|
>
|


>

|
>
>
|

|
>

|
|
|
|
|
|
|
|
|
|
|
|

>
|
<
|
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: 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



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


#-- show configuation window
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 settings, updated after dialog completion
    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([[sg.Button("Cancel"), sg.Button("Save")]], element_justification='right')]
    ]
    if "title" not in kwargs:
        kwargs["title"] = "Options"
    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()
    win.close()
    if event == "Save":
        for key, val in data.items():
            if OPTIONS.get(key):
                #@ToDo: handle array[key] names
                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)


def plugin_layout(pmd_list, config, plugin_states, opt_label=False):
    """ craft list of widgets for each read plugin """

    layout = []
    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(option_entry(opt, config))
    return layout

def plugin_entry(pmd, plugin_states):
    """ checkbox for plugin name """

    name = pmd["id"]
    return [
        [
            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)))
        ]
    ]


def option_entry(opt, config):
    """ widgets for single config option """
    #print(o)
    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", ""))


    widget = []

    if opt.get("hidden"):
        pass
    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 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 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 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 typedef == "select":

        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 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 widget



def read_options(files):

    """ 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)]
    }


class Cast:
    """ map option types (from strings) """

    @staticmethod
    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(val):
        """ verify integer """
        return int(val) if re.match(r"-?\d+", val) else 0

    @staticmethod
    def fromtype(val, opt):
        """ cast according to option type """
        if not opt.get("type"):
            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 """

    return "\n".join(textwrap.wrap(text, width)) if text else ""