[1198] | 1 | import tkinter as tk
|
---|
| 2 | import tkinter.ttk as ttk
|
---|
| 3 | from typing import Dict, List, Optional, Tuple
|
---|
| 4 | from datetime import datetime
|
---|
| 5 | from time import mktime
|
---|
| 6 | from gui.widgets.ScrolledText import ScrolledText
|
---|
| 7 | import re
|
---|
| 8 |
|
---|
| 9 | trans = {
|
---|
| 10 | "~": r"\~",
|
---|
| 11 | "\\": r"\\",
|
---|
| 12 | "\"": r"\"",
|
---|
| 13 | "\n": r"\n",
|
---|
| 14 | "\t": r"\t"}
|
---|
| 15 |
|
---|
| 16 | def encodeString(data: str) -> str:
|
---|
| 17 | return data.translate(data.maketrans(trans))
|
---|
| 18 |
|
---|
| 19 | datetimeformat = '%Y-%m-%d %H:%M:%S'
|
---|
| 20 |
|
---|
| 21 | class Property:
|
---|
| 22 | def __init__(self, Id: str = "", Name: str = "", Type: str = "", p: Dict = None) -> None:
|
---|
| 23 | self.p = {}
|
---|
| 24 | self.p["id"] = Id
|
---|
| 25 | self.p["name"] = Name
|
---|
| 26 | self.p["type"] = Type
|
---|
| 27 | self.p["flags"] = 0
|
---|
| 28 | self.p["help"] = ""
|
---|
| 29 | self.p["value"] = None
|
---|
| 30 | if p:
|
---|
| 31 | self.p = {**self.p, **p}
|
---|
| 32 |
|
---|
| 33 | class PropertyCallback:
|
---|
| 34 | def __init__(self, widget: tk.Widget, Id: str, xtype: str) -> None:
|
---|
| 35 | self.widget: tk.Widget = widget
|
---|
| 36 | self.id = Id
|
---|
| 37 | self._changed = False
|
---|
| 38 | self.type = xtype[0]
|
---|
| 39 | self.subtype = None
|
---|
| 40 | if len(xtype) > 1:
|
---|
| 41 | self.subtype = xtype[1]
|
---|
| 42 |
|
---|
| 43 | if isinstance(self.widget, tk.Checkbutton):
|
---|
| 44 | self.var = self.widget.variable
|
---|
| 45 | elif isinstance(self.widget, tk.Text):
|
---|
| 46 | self.widget.edit_modified(False)
|
---|
| 47 | elif isinstance(self.widget, tk.Button):
|
---|
| 48 | pass
|
---|
| 49 |
|
---|
| 50 | self.initValue = self.value()
|
---|
| 51 |
|
---|
| 52 | def changed(self) -> bool:
|
---|
| 53 | if isinstance(self.widget, tk.Text):
|
---|
| 54 | self._changed = self.widget.edit_modified()
|
---|
| 55 | else:
|
---|
| 56 | if self.initValue != self.value():
|
---|
| 57 | self._changed = True
|
---|
| 58 | return self._changed
|
---|
| 59 |
|
---|
| 60 | def restart(self) -> None:
|
---|
| 61 | self.initValue = self.value()
|
---|
| 62 | self._changed = False
|
---|
| 63 |
|
---|
| 64 | def value(self) -> str:
|
---|
| 65 | if isinstance(self.widget, tk.Checkbutton):
|
---|
| 66 | return self._checkButton()
|
---|
| 67 | elif isinstance(self.widget, ttk.Combobox):
|
---|
| 68 | return self._combobox()
|
---|
| 69 | elif isinstance(self.widget, ttk.Spinbox):
|
---|
| 70 | return self._spinbox()
|
---|
| 71 | elif isinstance(self.widget, tk.Text):
|
---|
| 72 | return encodeString(self._text())
|
---|
| 73 | elif isinstance(self.widget, tk.Entry):
|
---|
| 74 | return self._entry()
|
---|
| 75 | return None
|
---|
| 76 |
|
---|
| 77 | def rawValue(self, translate: bool = True) -> str:
|
---|
| 78 | if self.type == 'd':
|
---|
| 79 | if self.subtype == 'b':
|
---|
| 80 | return int(self.value(), 2)
|
---|
| 81 | elif self.subtype == 'c':
|
---|
| 82 | return int(self.value(), 16)
|
---|
| 83 | else:
|
---|
| 84 | return self.value()
|
---|
| 85 | elif self.type == 'f':
|
---|
| 86 | if self.subtype == 't':
|
---|
| 87 | el = datetime.strptime(self.value(), datetimeformat)
|
---|
| 88 | t = el.timetuple()
|
---|
| 89 | return mktime(t)
|
---|
| 90 | elif self.subtype == 'i':
|
---|
| 91 | return float(self.value())
|
---|
| 92 | else:
|
---|
| 93 | return self.value()
|
---|
| 94 | elif self.type == 's':
|
---|
| 95 | if not translate and isinstance(self.widget, tk.Text):
|
---|
| 96 | return self._text()
|
---|
| 97 | else:
|
---|
| 98 | return self.value()
|
---|
| 99 | elif self.type == 'p':
|
---|
| 100 | return self.value()
|
---|
| 101 |
|
---|
| 102 | def updateValue(self, value: Property) -> None:
|
---|
| 103 | _, cb = propertyToTkinter(value, self.widget.master)
|
---|
| 104 |
|
---|
| 105 | if isinstance(self.widget, tk.Checkbutton):
|
---|
| 106 | if value.p["value"] == '1' or value.p["value"] == 1:
|
---|
| 107 | self.widget.select()
|
---|
| 108 | else:
|
---|
| 109 | self.widget.deselect()
|
---|
| 110 | elif isinstance(self.widget, ttk.Combobox):
|
---|
| 111 | if hasattr(self.widget, "mapvalues"):
|
---|
| 112 | self.widget.set(list(self.widget.mapvalues.keys())[list(self.widget.mapvalues.values()).index(cb.value())])
|
---|
| 113 | else:
|
---|
| 114 | self.widget.set(cb.value())
|
---|
| 115 | elif isinstance(self.widget, ttk.Spinbox):
|
---|
| 116 | self.widget.set(cb.value())
|
---|
| 117 | elif isinstance(self.widget, tk.Text):
|
---|
| 118 | self.widget.config(state=tk.NORMAL)
|
---|
| 119 | if len(self.value()) > 0:
|
---|
| 120 | self.widget.delete("1.0", tk.END)
|
---|
| 121 | self.widget.insert("1.0", cb._text()) #translates and escapes \\ as expected
|
---|
| 122 | self.widget.edit_modified(False)
|
---|
| 123 | self.widget.config(state=tk.DISABLED)
|
---|
| 124 | elif isinstance(self.widget, tk.Entry):
|
---|
| 125 | self.widget.delete(0, tk.END)
|
---|
| 126 | self.widget.insert(0, cb.value())
|
---|
| 127 |
|
---|
| 128 | def _checkButton(self) -> str:
|
---|
| 129 | return "1" if self.var.get() else "0"
|
---|
| 130 |
|
---|
| 131 | def _combobox(self) -> str:
|
---|
| 132 | if hasattr(self.widget, "mapvalues"):
|
---|
| 133 | return self.widget.mapvalues.get(self.widget.get())
|
---|
| 134 | else:
|
---|
| 135 | return self.widget.get()
|
---|
| 136 |
|
---|
| 137 | def _spinbox(self) -> str:
|
---|
| 138 | return str(self.widget.get())
|
---|
| 139 |
|
---|
| 140 | def _text(self) -> str:
|
---|
| 141 | return self.widget.get("1.0", "end").strip()
|
---|
| 142 |
|
---|
| 143 | def _entry(self) -> str:
|
---|
| 144 | return self.widget.get().strip()
|
---|
| 145 |
|
---|
| 146 | def propertyToTkinter(prop: Property, master: tk.Widget) -> Optional[Tuple[tk.Widget, PropertyCallback]]:
|
---|
| 147 | global trans
|
---|
| 148 | widget = None
|
---|
| 149 | callback: PropertyCallback = None
|
---|
| 150 | t = prop.p["type"].split()
|
---|
| 151 | propType = t[0]
|
---|
| 152 |
|
---|
| 153 | readonly = True if int(prop.p["flags"]) & 1 or int(prop.p["flags"]) & 16 else False
|
---|
| 154 | visible = False if int(prop.p["flags"]) & 32 else True
|
---|
| 155 |
|
---|
| 156 | if not visible:
|
---|
| 157 | return None, None
|
---|
| 158 |
|
---|
| 159 | class MinMaxDefault:
|
---|
| 160 | def __init__(self, minValue, maxValue, default = None) -> None:
|
---|
| 161 | self.minValue = minValue
|
---|
| 162 | self.maxValue = maxValue
|
---|
| 163 | self.default = default
|
---|
| 164 |
|
---|
| 165 | def minMaxDefault(prop_type: List) -> MinMaxDefault:
|
---|
| 166 | if len(prop_type) >= 4:
|
---|
| 167 | return MinMaxDefault(prop_type[1], prop_type[2], prop_type[3])
|
---|
| 168 | elif len(prop_type) >= 3:
|
---|
| 169 | return MinMaxDefault(prop_type[1], prop_type[2])
|
---|
| 170 | else:
|
---|
| 171 | return MinMaxDefault(None, None)
|
---|
| 172 |
|
---|
| 173 | minmax = minMaxDefault(t)
|
---|
| 174 |
|
---|
| 175 | if propType[0] == 'd':
|
---|
| 176 | value = int(prop.p["value"])
|
---|
| 177 | if len(propType) > 1:
|
---|
| 178 | if propType[1] == 'b':
|
---|
| 179 | value = bin(value)
|
---|
| 180 | elif propType[1] == 'c':
|
---|
| 181 | value = hex(value)
|
---|
| 182 |
|
---|
| 183 | if minmax.minValue == '0' and minmax.maxValue == '1' and prop.p["type"].find('~') == -1:
|
---|
| 184 | var = tk.BooleanVar()
|
---|
| 185 | widget = tk.Checkbutton(master=master, text='', onvalue=1, offvalue=0, var=var)
|
---|
| 186 | if value == 1:
|
---|
| 187 | widget.select()
|
---|
| 188 | if readonly:
|
---|
| 189 | widget['state'] = 'disabled'
|
---|
| 190 | widget.variable = var
|
---|
| 191 | elif prop.p["type"].find('~') > 0:
|
---|
| 192 | options = prop.p["type"].split('~')[1:]
|
---|
| 193 | widget = ttk.Combobox(master=master, values=options, state="readonly")
|
---|
| 194 | widget.mapvalues = {k:v for (v, k) in enumerate(options, int(t[1]))}
|
---|
| 195 | widget.set(options[value])
|
---|
| 196 | else:
|
---|
| 197 | minVal = 0 if minmax.minValue is None else minmax.minValue
|
---|
| 198 | maxVal = 100 if minmax.maxValue is None else minmax.maxValue
|
---|
| 199 | widget = ttk.Spinbox(master=master, from_=minVal, to=maxVal)
|
---|
| 200 | widget.set(value)
|
---|
| 201 | if readonly:
|
---|
| 202 | widget["state"] = "disabled"
|
---|
| 203 | elif propType[0] == 'f':
|
---|
| 204 | value = None
|
---|
| 205 | var = None
|
---|
| 206 | if len(propType) > 1:
|
---|
| 207 | if propType[1] == 't':
|
---|
| 208 | timestamp = float(prop.p["value"])
|
---|
| 209 | value = datetime.fromtimestamp(timestamp).strftime(datetimeformat)
|
---|
| 210 | elif propType[1] == 'i':
|
---|
| 211 | var = tk.StringVar()
|
---|
| 212 | value = float(prop.p["value"])
|
---|
| 213 | else:
|
---|
| 214 | var = tk.StringVar()
|
---|
| 215 | value = float(prop.p["value"])
|
---|
| 216 |
|
---|
| 217 | if var:
|
---|
| 218 | def callback(*_):
|
---|
| 219 | val = var.get()
|
---|
| 220 | try:
|
---|
| 221 | v = float(val)
|
---|
| 222 | if minmax.minValue and v < float(minmax.minValue):
|
---|
| 223 | v = float(minmax.minValue)
|
---|
| 224 | elif minmax.maxValue and v > float(minmax.maxValue):
|
---|
| 225 | v = float(minmax.maxValue)
|
---|
| 226 | var.set(str(v))
|
---|
| 227 | except ValueError:
|
---|
| 228 | pass
|
---|
| 229 |
|
---|
| 230 | def on_validate(string):
|
---|
| 231 | regex = re.compile(r"(\+|\-)?[0-9.]*$")
|
---|
| 232 | result = regex.match(string)
|
---|
| 233 | return (string == ""
|
---|
| 234 | or (string.count('+') <= 1
|
---|
| 235 | and string.count('-') <= 1
|
---|
| 236 | and string.count(',') <= 1
|
---|
| 237 | and result is not None
|
---|
| 238 | and result.group(0) != ""))
|
---|
| 239 |
|
---|
| 240 | var.trace_add("write", callback)
|
---|
| 241 | widget = tk.Entry(master=master, validate="key", textvariable=var)
|
---|
| 242 | vcmd = (widget.register(on_validate), '%P')
|
---|
| 243 | widget.config(validatecommand=vcmd)
|
---|
| 244 | else:
|
---|
| 245 | widget = tk.Entry(master=master)
|
---|
| 246 | widget.insert(0, value)
|
---|
| 247 | if readonly:
|
---|
| 248 | widget["state"] = "disabled"
|
---|
| 249 | elif propType[0] == 's':
|
---|
| 250 | if t[-1].find('~') >= 0:
|
---|
| 251 | options = t[-1].split('~')[1:]
|
---|
| 252 | widget = ttk.Combobox(master=master)
|
---|
| 253 | widget["values"] = options
|
---|
| 254 | widget.set(prop.p["value"])
|
---|
| 255 | else:
|
---|
| 256 | multiline = True if len(t) > 1 and t[1] != '0' else False
|
---|
| 257 | if multiline:
|
---|
| 258 | widget = ScrolledText(master=master, height=1 + multiline * 3)
|
---|
| 259 | widget.insert(tk.INSERT, prop.p["value"])
|
---|
| 260 | else:
|
---|
| 261 | widget = tk.Entry(master=master)
|
---|
| 262 | widget.insert(0, prop.p["value"])
|
---|
| 263 | if readonly:
|
---|
| 264 | widget["state"] = "disabled"
|
---|
| 265 | elif propType[0] == 'p':
|
---|
| 266 | widget = tk.Button(master=master, text=prop.p["name"], command=prop.p["value"])
|
---|
| 267 | callback = PropertyCallback(widget, prop.p["id"], propType)
|
---|
| 268 | widget.anchor('w')
|
---|
| 269 |
|
---|
| 270 | if widget and propType[0] != 'p':
|
---|
| 271 | callback = PropertyCallback(widget, prop.p["id"], propType)
|
---|
| 272 | widget.anchor('w')
|
---|
| 273 |
|
---|
| 274 | return widget, callback |
---|