import tkinter as tk import tkinter.ttk as ttk from typing import Dict, List, Optional, Tuple from datetime import datetime from time import mktime from gui.widgets.ScrolledText import ScrolledText import re trans = { "~": r"\~", "\\": r"\\", "\"": r"\"", "\n": r"\n", "\t": r"\t"} def encodeString(data: str) -> str: return data.translate(data.maketrans(trans)) datetimeformat = '%Y-%m-%d %H:%M:%S' class Property: def __init__(self, Id: str = "", Name: str = "", Type: str = "", p: Dict = None) -> None: self.p = {} self.p["id"] = Id self.p["name"] = Name self.p["type"] = Type self.p["flags"] = 0 self.p["help"] = "" self.p["value"] = None if p: self.p = {**self.p, **p} class PropertyCallback: def __init__(self, widget: tk.Widget, Id: str, xtype: str) -> None: self.widget: tk.Widget = widget self.id = Id self._changed = False self.type = xtype[0] self.subtype = None if len(xtype) > 1: self.subtype = xtype[1] if isinstance(self.widget, tk.Checkbutton): self.var = self.widget.variable elif isinstance(self.widget, tk.Text): self.widget.edit_modified(False) elif isinstance(self.widget, tk.Button): pass self.initValue = self.value() def changed(self) -> bool: if isinstance(self.widget, tk.Text): self._changed = self.widget.edit_modified() else: if self.initValue != self.value(): self._changed = True return self._changed def restart(self) -> None: self.initValue = self.value() self._changed = False def value(self) -> str: if isinstance(self.widget, tk.Checkbutton): return self._checkButton() elif isinstance(self.widget, ttk.Combobox): return self._combobox() elif isinstance(self.widget, ttk.Spinbox): return self._spinbox() elif isinstance(self.widget, tk.Text): return encodeString(self._text()) elif isinstance(self.widget, tk.Entry): return self._entry() return None def rawValue(self, translate: bool = True) -> str: if self.type == 'd': if self.subtype == 'b': return int(self.value(), 2) elif self.subtype == 'c': return int(self.value(), 16) else: return self.value() elif self.type == 'f': if self.subtype == 't': el = datetime.strptime(self.value(), datetimeformat) t = el.timetuple() return mktime(t) elif self.subtype == 'i': return float(self.value()) else: return self.value() elif self.type == 's': if not translate and isinstance(self.widget, tk.Text): return self._text() else: return self.value() elif self.type == 'p': return self.value() def updateValue(self, value: Property) -> None: _, cb = propertyToTkinter(value, self.widget.master) if isinstance(self.widget, tk.Checkbutton): if value.p["value"] == '1' or value.p["value"] == 1: self.widget.select() else: self.widget.deselect() elif isinstance(self.widget, ttk.Combobox): if hasattr(self.widget, "mapvalues"): self.widget.set(list(self.widget.mapvalues.keys())[list(self.widget.mapvalues.values()).index(cb.value())]) else: self.widget.set(cb.value()) elif isinstance(self.widget, ttk.Spinbox): self.widget.set(cb.value()) elif isinstance(self.widget, tk.Text): self.widget.config(state=tk.NORMAL) if len(self.value()) > 0: self.widget.delete("1.0", tk.END) self.widget.insert("1.0", cb._text()) #translates and escapes \\ as expected self.widget.edit_modified(False) self.widget.config(state=tk.DISABLED) elif isinstance(self.widget, tk.Entry): self.widget.delete(0, tk.END) self.widget.insert(0, cb.value()) def _checkButton(self) -> str: return "1" if self.var.get() else "0" def _combobox(self) -> str: if hasattr(self.widget, "mapvalues"): return self.widget.mapvalues.get(self.widget.get()) else: return self.widget.get() def _spinbox(self) -> str: return str(self.widget.get()) def _text(self) -> str: return self.widget.get("1.0", "end").strip() def _entry(self) -> str: return self.widget.get().strip() def propertyToTkinter(prop: Property, master: tk.Widget) -> Optional[Tuple[tk.Widget, PropertyCallback]]: global trans widget = None callback: PropertyCallback = None t = prop.p["type"].split() propType = t[0] readonly = True if int(prop.p["flags"]) & 1 or int(prop.p["flags"]) & 16 else False visible = False if int(prop.p["flags"]) & 32 else True if not visible: return None, None class MinMaxDefault: def __init__(self, minValue, maxValue, default = None) -> None: self.minValue = minValue self.maxValue = maxValue self.default = default def minMaxDefault(prop_type: List) -> MinMaxDefault: if len(prop_type) >= 4: return MinMaxDefault(prop_type[1], prop_type[2], prop_type[3]) elif len(prop_type) >= 3: return MinMaxDefault(prop_type[1], prop_type[2]) else: return MinMaxDefault(None, None) minmax = minMaxDefault(t) if propType[0] == 'd': value = int(prop.p["value"]) if len(propType) > 1: if propType[1] == 'b': value = bin(value) elif propType[1] == 'c': value = hex(value) if minmax.minValue == '0' and minmax.maxValue == '1' and prop.p["type"].find('~') == -1: var = tk.BooleanVar() widget = tk.Checkbutton(master=master, text='', onvalue=1, offvalue=0, var=var) if value == 1: widget.select() if readonly: widget['state'] = 'disabled' widget.variable = var elif prop.p["type"].find('~') > 0: options = prop.p["type"].split('~')[1:] widget = ttk.Combobox(master=master, values=options, state="readonly") widget.mapvalues = {k:v for (v, k) in enumerate(options, int(t[1]))} widget.set(options[value]) else: minVal = 0 if minmax.minValue is None else minmax.minValue maxVal = 100 if minmax.maxValue is None else minmax.maxValue widget = ttk.Spinbox(master=master, from_=minVal, to=maxVal) widget.set(value) if readonly: widget["state"] = "disabled" elif propType[0] == 'f': value = None var = None if len(propType) > 1: if propType[1] == 't': timestamp = float(prop.p["value"]) value = datetime.fromtimestamp(timestamp).strftime(datetimeformat) elif propType[1] == 'i': var = tk.StringVar() value = float(prop.p["value"]) else: var = tk.StringVar() value = float(prop.p["value"]) if var: def callback(*_): val = var.get() try: v = float(val) if minmax.minValue and v < float(minmax.minValue): v = float(minmax.minValue) elif minmax.maxValue and v > float(minmax.maxValue): v = float(minmax.maxValue) var.set(str(v)) except ValueError: pass def on_validate(string): regex = re.compile(r"(\+|\-)?[0-9.]*$") result = regex.match(string) return (string == "" or (string.count('+') <= 1 and string.count('-') <= 1 and string.count(',') <= 1 and result is not None and result.group(0) != "")) var.trace_add("write", callback) widget = tk.Entry(master=master, validate="key", textvariable=var) vcmd = (widget.register(on_validate), '%P') widget.config(validatecommand=vcmd) else: widget = tk.Entry(master=master) widget.insert(0, value) if readonly: widget["state"] = "disabled" elif propType[0] == 's': if t[-1].find('~') >= 0: options = t[-1].split('~')[1:] widget = ttk.Combobox(master=master) widget["values"] = options widget.set(prop.p["value"]) else: multiline = True if len(t) > 1 and t[1] != '0' else False if multiline: widget = ScrolledText(master=master, height=1 + multiline * 3) widget.insert(tk.INSERT, prop.p["value"]) else: widget = tk.Entry(master=master) widget.insert(0, prop.p["value"]) if readonly: widget["state"] = "disabled" elif propType[0] == 'p': widget = tk.Button(master=master, text=prop.p["name"], command=prop.p["value"]) callback = PropertyCallback(widget, prop.p["id"], propType) widget.anchor('w') if widget and propType[0] != 'p': callback = PropertyCallback(widget, prop.p["id"], propType) widget.anchor('w') return widget, callback