Index: html/gui.html ================================================================== --- html/gui.html +++ html/gui.html @@ -30,89 +30,100 @@

Functions

-def option_entry(o, config) +def option_entry(opt, config)
-
+

widgets for single config option

-def plugin_entry(e, plugin_states) +def plugin_entry(pmd, plugin_states)
-
+

checkbox for plugin name

-def plugin_layout(ls, config, plugin_states, opt_label=False) +def plugin_layout(pmd_list, config, plugin_states, opt_label=False)
-
+

craft list of widgets for each read plugin

def read_options(files)
-
+

read files, return dict of {id:pmd} for all plugins

-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)
+
 
-def wrap(s, w=50) +def wrap(text, width=50)
-
+

textwrap for description and help option fields

Classes

-
-class cast +
+class Cast
-
+

map option types (from strings)

Static methods

-
-def bool(v) -
-
-
-
-
-def fromtype(v, opt) -
-
-
-
-
-def int(v) -
-
-
+
+def bool(val) +
+
+

map boolean literals

+
+
+def fromtype(val, opt) +
+
+

cast according to option type

+
+
+def int(val) +
+
+

verify integer

@@ -139,15 +150,15 @@
  • Classes

  • Index: pluginconf/gui.py ================================================================== --- pluginconf/gui.py +++ pluginconf/gui.py @@ -1,15 +1,16 @@ # 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. # @@ -21,184 +22,209 @@ # """ 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(ls, config, plugin_states, opt_label=False): + + +def plugin_layout(pmd_list, config, plugin_states, opt_label=False): + """ craft list of widgets for each read plugin """ 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(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 "" + 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 ""