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