-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpersonalized_widgets.py
More file actions
575 lines (462 loc) · 19.6 KB
/
Copy pathpersonalized_widgets.py
File metadata and controls
575 lines (462 loc) · 19.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# This file is part of Linecraft - Frequency response display and statistics tool
# Copyright (C) 2023 - Kerem Basaran
# https://github.com/kbasaran
__email__ = "kbasaran@gmail.com"
# Linecraft is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3
# of the License, or (at your option) any later version.
# Linecraft is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public
# License along with Linecraft. If not, see <https://www.gnu.org/licenses/>
import traceback
from PySide6 import QtWidgets as qtw
from PySide6 import QtCore as qtc
from PySide6 import QtGui as qtg
import sounddevice as sd
import numpy as np
from generictools import signal_tools
import pickle
from config.app_config import singleton_settings
app_settings = singleton_settings()
import logging
if __name__ == "__main__":
logger = logging.getLogger(__name__)
else:
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger()
class FloatSpinBox(qtw.QDoubleSpinBox):
def __init__(self, name, tooltip,
decimals=2,
min_max=(None, None),
coeff_for_SI=1,
):
self._name = name
self.coeff_for_SI = coeff_for_SI
super().__init__()
if tooltip:
self.setToolTip(tooltip)
self.setStepType(qtw.QAbstractSpinBox.StepType.AdaptiveDecimalStepType)
self.setDecimals(decimals)
if min_max[0] is not None:
self.setMinimum(min_max[0])
else:
self.setMinimum(1 / 10**self.decimals())
if min_max[1] is not None:
self.setMaximum(min_max[1])
else:
self.setMaximum((1000_000 - 1) / 10**self.decimals())
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
class IntSpinBox(qtw.QSpinBox):
def __init__(self, name, tooltip,
min_max=(None, None),
coeff_for_SI=1,
):
self._name = name
self.coeff_for_SI = coeff_for_SI
super().__init__()
if tooltip:
self.setToolTip(tooltip)
if min_max[0] is not None:
self.setMinimum(min_max[0])
else:
self.setMinimum(0)
if min_max[1] is not None:
self.setMaximum(min_max[1])
else:
self.setMaximum(99_999)
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
class CheckBox(qtw.QCheckBox):
def __init__(self, name, tooltip,
):
self._name = name
super().__init__()
if tooltip:
self.setToolTip(tooltip)
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
class LineTextBox(qtw.QLineEdit):
def __init__(self, name, tooltip):
self._name = name
super().__init__()
if tooltip:
self.setToolTip(tooltip)
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
class SunkenLine(qtw.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(qtw.QFrame.HLine)
self.setFrameShadow(qtw.QFrame.Sunken)
self.setContentsMargins(0, 10, 0, 10)
class Title(qtw.QLabel):
def __init__(self, text:str):
super().__init__()
self.setText(text)
self.setStyleSheet("font-weight: bold")
self.setAlignment(qtg.Qt.AlignmentFlag.AlignCenter)
class PushButtonGroup(qtw.QWidget):
def __init__(self, names: dict, tooltips: dict, vertical=False):
"""Both names and tooltips have the same keys: short_name's
Values for names: text
"""
self._buttons = dict()
super().__init__()
layout = qtw.QVBoxLayout(self) if vertical else qtw.QHBoxLayout(self)
for key, val in names.items():
name = key + "_pushbutton"
button = qtw.QPushButton(val)
if key in tooltips:
button.setToolTip(tooltips[key])
layout.addWidget(button)
self._buttons[name] = button
def add_elements_to_dict(self, user_data_widgets: dict):
for name, button in self._buttons.items():
user_data_widgets[name] = button
def buttons(self) -> dict:
return self._buttons
class PushButton(qtw.QPushButton):
def __init__(self, name, label, tooltip):
self._name = name
super().__init__()
self.setText(label)
if tooltip:
self.setToolTip(tooltip)
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
class ChoiceButtonGroup(qtw.QWidget):
def __init__(self, group_name, names: dict, tooltips: dict, vertical=False):
"""keys for names: integers
values for names: text
"""
self._name = group_name
super().__init__()
self.button_group = qtw.QButtonGroup()
layout = qtw.QVBoxLayout(self) if vertical else qtw.QHBoxLayout(self)
for key, button_name in names.items():
button = qtw.QRadioButton(button_name)
if key in tooltips:
button.setToolTip(tooltips[key])
self.button_group.addButton(button, key)
layout.addWidget(button)
self.button_group.buttons()[0].setChecked(True)
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self.button_group
def buttons(self) -> list:
return self.button_group.buttons()
class ComboBox(qtw.QComboBox):
def __init__(self, name,
tooltip,
items: list,
):
self._name = name
super().__init__()
if tooltip:
self.setToolTip(tooltip)
for item in items:
self.addItem(*item) # tuple (text, userData), therefore *
def add_elements_to_dict(self, user_data_widgets: dict):
user_data_widgets[self._name] = self
def get_value_personalized(self):
personalized_value = dict()
personalized_value["current_index"] = self.currentIndex()
personalized_value["current_data"] = self.currentData()
personalized_value["current_text"] = self.currentText()
personalized_value["items"] = list()
for i_item in range(self.count()):
item_text = self.itemText(i_item)
item_data = self.itemData(i_item)
personalized_value["items"].append((item_text, item_data))
return personalized_value
def update_value_personalized(self, key: str, value: dict | str):
if isinstance(value, str):
in_current_text = value
in_current_data = None
else:
in_current_text = value["current_text"]
in_current_data = value.get("current_data", None)
existing_item_index = self.findText(in_current_text)
logger.debug("Items in widget: " + str([(self.itemText(i), self.itemData(i)) for i in range(self.count())])
+ "\nNew value: " + str(value)
+ "\nFound in index: " + str(existing_item_index)
)
if existing_item_index == -1: # the combobox does not yet have this stored option
self.clear()
# add all options from received argument
items = value.get("items", [])
if items:
for item in items:
self.addItem(*item)
self.setCurrentText(in_current_text)
else: # otherwise add only the item of last selection
self.addItem(in_current_text, in_current_data)
self.setCurrentIndex(0)
else: # the combobox already has this name as an item. set to it.
self.setCurrentIndex(existing_item_index)
class SubForm(qtw.QWidget):
def __init__(self):
super().__init__()
self._layout = qtw.QFormLayout(self)
class UserForm(qtw.QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._layout = qtw.QFormLayout(self) # the argument makes already here the "setLayout" for the widget
self.interactable_widgets = dict() # this is a dict of objects that user give input in, such as a
# textbox or a checkmark. key is the name of the parameter. value is the widget itself.
# buttons are also in here although they do not store a value.
def add_row(self, obj, description=None, into_form=None):
if into_form:
layout = into_form.layout()
else:
layout = self.layout()
if description:
layout.addRow(description, obj)
else:
layout.addRow(obj)
if hasattr(obj, "add_elements_to_dict"):
obj.add_elements_to_dict(self.interactable_widgets)
def update_form_values(self, values_new: dict):
# Update the widget values from a dictionary
# list of widgets that are not mentioned in argument values_new
no_dict_key_for_widget = set(
[key for key, obj in self.interactable_widgets.items()
if (not isinstance(obj, qtw.QAbstractButton) or isinstance(obj, qtw.QCheckBox))
]
) # works???????????????????????
no_widget_for_dict_key = set()
for key, value_new in values_new.items():
logger.debug(f"Updating '{key}' with '{value_new}'/{type(value_new)}")
obj = self.interactable_widgets[key]
if hasattr(obj, "update_value_personalized"):
obj.update_value_personalized(key, value_new)
elif isinstance(obj, qtw.QLineEdit):
assert isinstance(value_new, str)
obj.setText(value_new)
elif isinstance(obj, qtw.QButtonGroup):
obj.button(value_new).setChecked(True)
elif isinstance(obj, qtw.QAbstractButton):
if isinstance(obj, qtw.QCheckBox):
obj.setChecked(value_new)
else: # e.g. another type of button like QPushButton
raise ValueError(f"No value update can be done for '{type(obj)}'. Received value: {value_new}")
elif type(value_new) in [int, float]:
obj.setValue(value_new / obj.coeff_for_SI)
else:
obj.setValue(value_new)
# finally
no_dict_key_for_widget.discard(key)
if no_widget_for_dict_key | no_dict_key_for_widget:
raise ValueError(f"No data found to update the widget(s): '{no_dict_key_for_widget}'"
)
def get_value(self, name: str):
obj = self.interactable_widgets.get(name, None)
if obj is None:
raise ValueError(f"Object with name '{name}' not found.")
elif hasattr(obj, "get_value_personalized"):
return obj.get_value_personalized()
elif isinstance(obj, qtw.QLineEdit):
obj_value = obj.text()
elif isinstance(obj, qtw.QButtonGroup):
obj_value = obj.checkedId()
elif isinstance(obj, qtw.QAbstractButton):
if isinstance(obj, qtw.QCheckBox):
obj_value = obj.isChecked()
else: # e.g. QPushButton
obj_value = None
else:
if obj.coeff_for_SI:
obj_value = obj.value() * obj.coeff_for_SI
else:
obj_value = obj.value()
return obj_value
def get_form_values(self) -> dict:
"""Collects all values from the widgets in the form that have user input values.
Puts them in a dictionary and returns.
"""
values = {}
for key in self.interactable_widgets.keys():
obj_value = self.get_value(key)
if obj_value is not None: # buttons for example will give a None value
values[key] = obj_value
return values
class SoundEngine(qtc.QObject):
def __init__(self):
super().__init__()
self.verify_stream()
def verify_stream(self):
self.FS = sd.query_devices(device=sd.default.device, kind='output',
)["default_samplerate"]
# needs to be improved and tested for device changes!
if not hasattr(self, "stream"):
self.stream = sd.OutputStream(samplerate=self.FS, channels=2)
if not self.stream.active:
self.stream.start()
@qtc.Slot(float, float, float)
def beep(self, A, T, freq):
self.verify_stream()
t = np.arange(T * self.FS) / self.FS
y = A * np.sin(t * 2 * np.pi * freq)
fade_window = signal_tools.make_fade_window_n(1, 0, len(y), fade_start_end_idx=(len(y) - int(self.FS / 10), len(y)))
y = y * fade_window
pad = np.zeros(int(self.FS / 10))
y = np.concatenate([y, pad])
y = np.tile(y, self.stream.channels)
y = y.reshape((len(y) // self.stream.channels,
self.stream.channels), order='F').astype(self.stream.dtype)
y = np.ascontiguousarray(y, self.stream.dtype)
self.stream.write(y)
@qtc.Slot()
def good_beep(self):
self.beep(app_settings.get_value("A_beep") / 2, 0.1, 587.3)
@qtc.Slot()
def bad_beep(self):
self.beep(app_settings.get_value("A_beep"), 0.1, 293.7)
@qtc.Slot()
def release_all(self):
self.stream.stop(ignore_errors=True)
class ResultTextBox(qtw.QDialog):
def __init__(self, title, result_text, monospace=True, parent=None, markdown=False):
super().__init__(parent=parent)
# self.setWindowModality(qtc.Qt.WindowModality.NonModal)
layout = qtw.QVBoxLayout(self)
self.setWindowTitle(title)
self.setMinimumSize(700, 480)
text_box = qtw.QTextEdit()
text_box.setReadOnly(True)
if markdown is False:
text_box.setText(result_text)
if monospace:
family = "Monospace" if "Monospace" in qtg.QFontDatabase.families() else "Consolas"
font = text_box.font()
font.setFamily(family)
text_box.setFont(font)
else:
text_box.setMarkdown(result_text)
if monospace is True:
logging.warning("Ignoring monospace argument when using Markdown.")
layout.addWidget(text_box)
# ---- Buttons
button_group = PushButtonGroup({"ok": "OK",
},
{},
)
button_group.buttons()["ok_pushbutton"].setDefault(True)
layout.addWidget(button_group)
# ---- Connections
button_group.buttons()["ok_pushbutton"].clicked.connect(
self.accept)
class ErrorHandlerDeveloper_old:
def __init__(self, app, logger):
self.app = app
def excepthook(self, etype, value, tb):
error_msg_developer = ''.join(traceback.format_exception(etype, value, tb))
message_box = qtw.QMessageBox(qtw.QMessageBox.Warning,
"Error :(",
error_msg_developer +
"\n\nThis event may be logged unless ignore is chosen.",
)
message_box.addButton(qtw.QMessageBox.Ignore)
close_button = message_box.addButton(qtw.QMessageBox.Close)
message_box.setEscapeButton(qtw.QMessageBox.Ignore)
message_box.setDefaultButton(qtw.QMessageBox.Close)
close_button.clicked.connect(logger.warning(error_msg_developer))
message_box.exec()
class ErrorPopup(qtw.QErrorMessage):
def __init__(self, parent, error_msg):
if isinstance(parent, qtw.QApplication):
parent = parent.activeWindow()
elif parent is not None and not isinstance(parent, qtw.QWidget):
parent = None # parent is likely top level QApplication
else:
parent = parent
super().__init__(parent=parent)
self.setModal(True)
self.setMaximumWidth(640)
self.setMinimumHeight(480)
self.showMessage(error_msg)
class ErrorHandler:
def __init__(self, parent, logger, developer=False):
self.developer = developer
self.parent = parent
self.logger = logger
def excepthook(self, etype, value, tb):
error_msg_developer = ''.join(traceback.format_exception(etype, value, tb))
error_info = traceback.format_exception(etype, value, tb)
if isinstance(error_info, list) and len(error_info) > 2:
error_msg_short = error_info[-1]
# bad solution
else:
error_msg_short = error_info
if self.developer:
self.logger.warning(error_msg_developer)
ErrorPopup(self.parent, error_msg_developer)
else:
self.logger.warning(error_msg_short)
ErrorPopup(self.parent, error_msg_short)
class ErrorHandlerUser(ErrorHandler):
def __init__(self, parent, logger):
super().__init__(parent, logger, developer=False)
class ErrorHandlerDeveloper(ErrorHandler):
def __init__(self, parent, logger):
super().__init__(parent, logger, developer=True)
class LoadSaveEngine:
"""
Data to save for the graph in general:
Plot title: sget_title
Plot x label: sget_xlabel
Plot y label: sget_ylabel
x linear/log: sget_xscale
y linear/log: sget_yscale
Data to save per curve item:
Curve object in CurveAnalyze.curves
Line2D:
style: sget_linestyle
draw style: sget_drawstyle
width: sget_linewidth
color: sget_color
Line2D marker:
style: sget_marker
size: sget_markersize
face col: sget_markerfacecolor
edge col: sget_markeredgecolor
"""
def collect_graph_info(self, ax):
graph_info = {"title": ax.get_title(),
"xlabel": ax.get_xlabel(),
"ylabel": ax.get_ylabel(),
"xscale": ax.get_xscale(),
"yscale": ax.get_yscale(),
}
return graph_info
def collect_line2d_info(self, line):
line_info = {"style": line.get_style(),
"drawstyle": line.get_drawstyle(),
"width": line.get_width(),
"color": line.get_color(),
"marker": line.get_marker(),
"markersize": line.get_markersize(),
"markerfacecolor": line.get_markerfacecolor(),
"markeredgecolor": line.get_markeredgecolor(),
}
return line_info
def collect_curve_info(self, curve):
curve_info = {"visible": curve.is_visible(),
"identification": curve._identification,
"x": tuple(curve.get_x()),
"y": tuple(curve.get_y()),
}
return curve_info
def collect_all_info(self, ax, lines, curves):
graph_info = self.collect_graph_info(ax)
lines_info = []
curves_info = []
for line, curve in zip(lines, curves):
lines_info.append(self.collect_line2d_info(line))
curves_info.append(self.collect_curve_info(curve))
package = pickle.dumps([graph_info, lines_info, curves_info], protocol=5)
return package