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