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