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