pymagnetos 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymagnetos/__init__.py +15 -0
- pymagnetos/cli.py +40 -0
- pymagnetos/core/__init__.py +19 -0
- pymagnetos/core/_config.py +340 -0
- pymagnetos/core/_data.py +132 -0
- pymagnetos/core/_processor.py +905 -0
- pymagnetos/core/config_models.py +57 -0
- pymagnetos/core/gui/__init__.py +6 -0
- pymagnetos/core/gui/_base_mainwindow.py +819 -0
- pymagnetos/core/gui/widgets/__init__.py +19 -0
- pymagnetos/core/gui/widgets/_batch_processing.py +319 -0
- pymagnetos/core/gui/widgets/_configuration.py +167 -0
- pymagnetos/core/gui/widgets/_files.py +129 -0
- pymagnetos/core/gui/widgets/_graphs.py +93 -0
- pymagnetos/core/gui/widgets/_param_content.py +20 -0
- pymagnetos/core/gui/widgets/_popup_progressbar.py +29 -0
- pymagnetos/core/gui/widgets/_text_logger.py +32 -0
- pymagnetos/core/signal_processing.py +1004 -0
- pymagnetos/core/utils.py +85 -0
- pymagnetos/log.py +126 -0
- pymagnetos/py.typed +0 -0
- pymagnetos/pytdo/__init__.py +6 -0
- pymagnetos/pytdo/_config.py +24 -0
- pymagnetos/pytdo/_config_models.py +59 -0
- pymagnetos/pytdo/_tdoprocessor.py +1052 -0
- pymagnetos/pytdo/assets/config_default.toml +84 -0
- pymagnetos/pytdo/gui/__init__.py +26 -0
- pymagnetos/pytdo/gui/_worker.py +106 -0
- pymagnetos/pytdo/gui/main.py +617 -0
- pymagnetos/pytdo/gui/widgets/__init__.py +8 -0
- pymagnetos/pytdo/gui/widgets/_buttons.py +66 -0
- pymagnetos/pytdo/gui/widgets/_configuration.py +78 -0
- pymagnetos/pytdo/gui/widgets/_graphs.py +280 -0
- pymagnetos/pytdo/gui/widgets/_param_content.py +137 -0
- pymagnetos/pyuson/__init__.py +7 -0
- pymagnetos/pyuson/_config.py +26 -0
- pymagnetos/pyuson/_config_models.py +71 -0
- pymagnetos/pyuson/_echoprocessor.py +1901 -0
- pymagnetos/pyuson/assets/config_default.toml +92 -0
- pymagnetos/pyuson/gui/__init__.py +26 -0
- pymagnetos/pyuson/gui/_worker.py +135 -0
- pymagnetos/pyuson/gui/main.py +767 -0
- pymagnetos/pyuson/gui/widgets/__init__.py +7 -0
- pymagnetos/pyuson/gui/widgets/_buttons.py +95 -0
- pymagnetos/pyuson/gui/widgets/_configuration.py +85 -0
- pymagnetos/pyuson/gui/widgets/_graphs.py +248 -0
- pymagnetos/pyuson/gui/widgets/_param_content.py +193 -0
- pymagnetos-0.1.0.dist-info/METADATA +23 -0
- pymagnetos-0.1.0.dist-info/RECORD +51 -0
- pymagnetos-0.1.0.dist-info/WHEEL +4 -0
- pymagnetos-0.1.0.dist-info/entry_points.txt +7 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Qt base widgets to re-use and customise."""
|
|
2
|
+
|
|
3
|
+
from ._batch_processing import BatchProcessingWidget
|
|
4
|
+
from ._configuration import BaseConfigurationWidget
|
|
5
|
+
from ._files import FileBrowserWidget
|
|
6
|
+
from ._graphs import BaseGraphsWidget
|
|
7
|
+
from ._param_content import BaseParamContent
|
|
8
|
+
from ._popup_progressbar import PopupProgressBar
|
|
9
|
+
from ._text_logger import TextLoggerWidget
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseConfigurationWidget",
|
|
13
|
+
"BaseGraphsWidget",
|
|
14
|
+
"BatchProcessingWidget",
|
|
15
|
+
"BaseParamContent",
|
|
16
|
+
"FileBrowserWidget",
|
|
17
|
+
"PopupProgressBar",
|
|
18
|
+
"TextLoggerWidget",
|
|
19
|
+
]
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A widget for batch-processing.
|
|
3
|
+
|
|
4
|
+
It includes a 3 lists of files:
|
|
5
|
+
- a "available" list,
|
|
6
|
+
- a "todo" list,
|
|
7
|
+
- a "done" list.
|
|
8
|
+
Items can be moved across the three lists. Text fields allow to filter available files.
|
|
9
|
+
Those lists are DropListWidget defined in this module.
|
|
10
|
+
|
|
11
|
+
Some buttons are still specific to the `pyuson` module, but it will be made more generic
|
|
12
|
+
in the future.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import glob
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from PyQt6 import QtWidgets
|
|
20
|
+
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DropListWidget(QtWidgets.QListWidget):
|
|
24
|
+
"""A list widget with drag & drop."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
|
|
29
|
+
self.setAcceptDrops(True)
|
|
30
|
+
self.setDragEnabled(True)
|
|
31
|
+
self.setDragDropOverwriteMode(False)
|
|
32
|
+
self.setSelectionMode(
|
|
33
|
+
QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection
|
|
34
|
+
)
|
|
35
|
+
self.setDefaultDropAction(Qt.DropAction.MoveAction)
|
|
36
|
+
self.setSortingEnabled(True)
|
|
37
|
+
|
|
38
|
+
def move_item_from_other(
|
|
39
|
+
self, item: QtWidgets.QListWidgetItem, otherList: QtWidgets.QListWidget
|
|
40
|
+
):
|
|
41
|
+
"""Move `item` from `otherList` to this list."""
|
|
42
|
+
if not self.findItems(item.text(), Qt.MatchFlag.MatchExactly):
|
|
43
|
+
otherList.takeItem(otherList.indexFromItem(item).row())
|
|
44
|
+
self.addItem(item)
|
|
45
|
+
|
|
46
|
+
def add_selected_items_from_other(self, otherList: QtWidgets.QListWidget):
|
|
47
|
+
"""Take selected items from `otherList` list and add it to this list."""
|
|
48
|
+
selectedItems = otherList.selectedItems()
|
|
49
|
+
for item in selectedItems:
|
|
50
|
+
self.move_item_from_other(item, otherList)
|
|
51
|
+
|
|
52
|
+
def add_all_items_from_other(self, otherList: QtWidgets.QListWidget):
|
|
53
|
+
"""Take all items from `other` list and add it to this list."""
|
|
54
|
+
while otherList.count() != 0:
|
|
55
|
+
item = otherList.takeItem(0)
|
|
56
|
+
if item is not None and (
|
|
57
|
+
not self.findItems(item.text(), Qt.MatchFlag.MatchExactly)
|
|
58
|
+
):
|
|
59
|
+
self.addItem(item)
|
|
60
|
+
|
|
61
|
+
def get_list_of_items(self) -> list[str]:
|
|
62
|
+
"""Get the list of items as strings."""
|
|
63
|
+
return [self.item(idx).text() for idx in range(self.count())] # ty:ignore[possibly-missing-attribute]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BatchProcessingWidget(QtWidgets.QWidget):
|
|
67
|
+
"""
|
|
68
|
+
List widgets that can drag & drop items between each other.
|
|
69
|
+
|
|
70
|
+
pyqtSignals
|
|
71
|
+
-------
|
|
72
|
+
sig_batch_process : emits when the "Batch process" is clicked.
|
|
73
|
+
sig_echo_index_changed : emits when the echo index changed.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
sig_batch_process = pyqtSignal()
|
|
77
|
+
sig_echo_index_changed = pyqtSignal()
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
self._current_directory = ""
|
|
83
|
+
|
|
84
|
+
layout = QtWidgets.QGridLayout()
|
|
85
|
+
|
|
86
|
+
self.left_list = DropListWidget()
|
|
87
|
+
self.right_list = DropListWidget()
|
|
88
|
+
self.done_list = DropListWidget()
|
|
89
|
+
self.wbuttons = self.init_buttons()
|
|
90
|
+
|
|
91
|
+
left_list_title = QtWidgets.QLabel("Available files", self)
|
|
92
|
+
left_list_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
93
|
+
|
|
94
|
+
right_list_title = QtWidgets.QLabel("Files to process", self)
|
|
95
|
+
right_list_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
96
|
+
|
|
97
|
+
done_list_title = QtWidgets.QLabel("Processed files", self)
|
|
98
|
+
|
|
99
|
+
readd_layout = QtWidgets.QHBoxLayout()
|
|
100
|
+
readd_layout.addWidget(done_list_title, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
101
|
+
readd_layout.addWidget(
|
|
102
|
+
self.init_readd_buttons(), alignment=Qt.AlignmentFlag.AlignRight
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
layout.addWidget(self.init_help_text(), 0, 0, 1, 2)
|
|
106
|
+
layout.addWidget(self.init_edits_widgets(), 1, 0, 1, 2)
|
|
107
|
+
layout.addWidget(left_list_title, 2, 0)
|
|
108
|
+
layout.addWidget(self.left_list, 3, 0, 3, 1)
|
|
109
|
+
layout.addWidget(right_list_title, 2, 1)
|
|
110
|
+
layout.addWidget(self.right_list, 3, 1)
|
|
111
|
+
layout.addLayout(readd_layout, 4, 1)
|
|
112
|
+
layout.addWidget(self.done_list, 5, 1)
|
|
113
|
+
layout.addWidget(self.wbuttons, 6, 0, 1, 2)
|
|
114
|
+
|
|
115
|
+
self.setLayout(layout)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def prefix(self) -> str:
|
|
119
|
+
return self.line_prefix.text()
|
|
120
|
+
|
|
121
|
+
@prefix.setter
|
|
122
|
+
def prefix(self, value: str):
|
|
123
|
+
self.line_prefix.setText(value)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def suffix(self) -> str:
|
|
127
|
+
return self.line_suffix.text()
|
|
128
|
+
|
|
129
|
+
@suffix.setter
|
|
130
|
+
def suffix(self, value: str):
|
|
131
|
+
self.line_suffix.setText(value)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def echo_index(self) -> int:
|
|
135
|
+
return self.spinbox_echo_index.value()
|
|
136
|
+
|
|
137
|
+
@echo_index.setter
|
|
138
|
+
def echo_index(self, value: int):
|
|
139
|
+
self.spinbox_echo_index.setValue(value)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def current_directory(self) -> str:
|
|
143
|
+
return self._current_directory
|
|
144
|
+
|
|
145
|
+
@current_directory.setter
|
|
146
|
+
def current_directory(self, value: str):
|
|
147
|
+
self._current_directory = value
|
|
148
|
+
self.list_files_in_dir()
|
|
149
|
+
|
|
150
|
+
def init_help_text(self) -> QtWidgets.QLabel:
|
|
151
|
+
"""Set up the help string on top of the tab."""
|
|
152
|
+
help_str = (
|
|
153
|
+
"Drag & drop files from the left (right) to the right (left) to add"
|
|
154
|
+
" (remove, respectively) files to the batch processor. Use the file name "
|
|
155
|
+
"prefix and suffix to filter the available files. The 'Batch process' will "
|
|
156
|
+
"process selected files with the same settings, including echo index."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
help_widget = QtWidgets.QLabel(help_str, self)
|
|
160
|
+
help_widget.setWordWrap(True)
|
|
161
|
+
|
|
162
|
+
return help_widget
|
|
163
|
+
|
|
164
|
+
def init_edits_widgets(self) -> QtWidgets.QWidget:
|
|
165
|
+
"""Set up editable fields to filter files."""
|
|
166
|
+
prefix_label = QtWidgets.QLabel("Prefix", self)
|
|
167
|
+
self.line_prefix = QtWidgets.QLineEdit("", self)
|
|
168
|
+
self.line_prefix.textChanged.connect(self.list_files_in_dir)
|
|
169
|
+
|
|
170
|
+
suffix_label = QtWidgets.QLabel("Suffix", self)
|
|
171
|
+
self.line_suffix = QtWidgets.QLineEdit(".bin", self) # TODO : generic default
|
|
172
|
+
self.line_suffix.textChanged.connect(self.list_files_in_dir)
|
|
173
|
+
|
|
174
|
+
# TODO : move to pyuson
|
|
175
|
+
echo_index_label = QtWidgets.QLabel("Echo index", self)
|
|
176
|
+
self.spinbox_echo_index = QtWidgets.QSpinBox()
|
|
177
|
+
self.spinbox_echo_index.setValue(1)
|
|
178
|
+
self.spinbox_echo_index.valueChanged.connect(self.sig_echo_index_changed)
|
|
179
|
+
|
|
180
|
+
self.checkbox_save_as_csv = QtWidgets.QCheckBox("Save as CSV", self)
|
|
181
|
+
self.checkbox_save_as_csv.setChecked(True)
|
|
182
|
+
|
|
183
|
+
grid = QtWidgets.QHBoxLayout()
|
|
184
|
+
grid.addWidget(prefix_label)
|
|
185
|
+
grid.addWidget(self.line_prefix)
|
|
186
|
+
grid.addWidget(suffix_label)
|
|
187
|
+
grid.addWidget(self.line_suffix)
|
|
188
|
+
grid.addWidget(echo_index_label)
|
|
189
|
+
grid.addWidget(self.spinbox_echo_index)
|
|
190
|
+
grid.addWidget(self.checkbox_save_as_csv)
|
|
191
|
+
|
|
192
|
+
edits_widget = QtWidgets.QWidget(self)
|
|
193
|
+
edits_widget.setLayout(grid)
|
|
194
|
+
|
|
195
|
+
return edits_widget
|
|
196
|
+
|
|
197
|
+
def init_buttons(self) -> QtWidgets.QWidget:
|
|
198
|
+
"""Set up buttons to manipulate lists."""
|
|
199
|
+
self.button_add_selected = QtWidgets.QPushButton("Add selected", self)
|
|
200
|
+
self.button_add_selected.clicked.connect(self.add_selected)
|
|
201
|
+
|
|
202
|
+
self.button_add_all = QtWidgets.QPushButton("Add all", self)
|
|
203
|
+
self.button_add_all.clicked.connect(self.add_all)
|
|
204
|
+
|
|
205
|
+
self.button_clear_all = QtWidgets.QPushButton("Clear all", self)
|
|
206
|
+
self.button_clear_all.clicked.connect(self.clear_all)
|
|
207
|
+
|
|
208
|
+
self.button_run = QtWidgets.QPushButton("Batch process", self)
|
|
209
|
+
self.button_run.clicked.connect(self.sig_batch_process.emit)
|
|
210
|
+
|
|
211
|
+
# TODO : move to pyuson
|
|
212
|
+
self.checkbox_rolling_average = QtWidgets.QCheckBox("Rolling average", self)
|
|
213
|
+
|
|
214
|
+
self.buttons_grid = QtWidgets.QGridLayout()
|
|
215
|
+
self.buttons_grid.addWidget(self.button_add_selected, 0, 0)
|
|
216
|
+
self.buttons_grid.addWidget(self.button_add_all, 0, 1)
|
|
217
|
+
self.buttons_grid.addWidget(self.button_clear_all, 0, 2)
|
|
218
|
+
self.buttons_grid.addWidget(self.button_run, 0, 3, 1, 2)
|
|
219
|
+
self.buttons_grid.addWidget(self.checkbox_rolling_average, 1, 3)
|
|
220
|
+
|
|
221
|
+
buttons_widget = QtWidgets.QWidget(self)
|
|
222
|
+
buttons_widget.setLayout(self.buttons_grid)
|
|
223
|
+
|
|
224
|
+
return buttons_widget
|
|
225
|
+
|
|
226
|
+
def add_findf0_checkbox(self) -> None:
|
|
227
|
+
"""Add a "Find f0" checkbox below the buttons."""
|
|
228
|
+
# TODO : move to pyuson
|
|
229
|
+
self.checkbox_find_f0 = QtWidgets.QCheckBox("Find f0", self)
|
|
230
|
+
self.checkbox_find_f0.setChecked(True)
|
|
231
|
+
self.buttons_grid.addWidget(self.checkbox_find_f0, 1, 4)
|
|
232
|
+
self.wbuttons.setLayout(self.buttons_grid)
|
|
233
|
+
|
|
234
|
+
def init_readd_buttons(self) -> QtWidgets.QWidget:
|
|
235
|
+
"""Set up buttons to read processed files back to the queue."""
|
|
236
|
+
self.button_readd_all = QtWidgets.QPushButton("⇈", self)
|
|
237
|
+
self.button_readd_all.clicked.connect(self.readd_all)
|
|
238
|
+
|
|
239
|
+
self.button_readd_selected = QtWidgets.QPushButton("↑")
|
|
240
|
+
self.button_readd_selected.clicked.connect(self.readd_selected)
|
|
241
|
+
|
|
242
|
+
grid = QtWidgets.QHBoxLayout()
|
|
243
|
+
grid.addWidget(self.button_readd_selected)
|
|
244
|
+
grid.addWidget(self.button_readd_all)
|
|
245
|
+
|
|
246
|
+
readd_widget = QtWidgets.QWidget()
|
|
247
|
+
readd_widget.setLayout(grid)
|
|
248
|
+
|
|
249
|
+
return readd_widget
|
|
250
|
+
|
|
251
|
+
@pyqtSlot()
|
|
252
|
+
def list_files_in_dir(self) -> None:
|
|
253
|
+
"""List files filtered with suffix and prefix."""
|
|
254
|
+
directory = self.current_directory
|
|
255
|
+
|
|
256
|
+
pattern = os.path.join(str(directory), self.prefix + "*" + self.suffix)
|
|
257
|
+
new_items = [
|
|
258
|
+
Path(filepath).name
|
|
259
|
+
for filepath in glob.glob(pattern)
|
|
260
|
+
if "-pickup" not in filepath
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
self.left_list.clear()
|
|
264
|
+
self.left_list.addItems(new_items)
|
|
265
|
+
|
|
266
|
+
@pyqtSlot()
|
|
267
|
+
def add_selected(self) -> None:
|
|
268
|
+
"""Add selected files in the left list to the right list."""
|
|
269
|
+
self.right_list.add_selected_items_from_other(self.left_list)
|
|
270
|
+
|
|
271
|
+
@pyqtSlot()
|
|
272
|
+
def add_all(self) -> None:
|
|
273
|
+
"""Add all items in the left list to the right list."""
|
|
274
|
+
self.right_list.add_all_items_from_other(self.left_list)
|
|
275
|
+
|
|
276
|
+
@pyqtSlot()
|
|
277
|
+
def readd_selected(self) -> None:
|
|
278
|
+
"""Add selected files in the done list to the right list."""
|
|
279
|
+
self.right_list.add_selected_items_from_other(self.done_list)
|
|
280
|
+
|
|
281
|
+
@pyqtSlot()
|
|
282
|
+
def readd_all(self) -> None:
|
|
283
|
+
"""Add all files in the done list to the right list."""
|
|
284
|
+
self.right_list.add_all_items_from_other(self.done_list)
|
|
285
|
+
|
|
286
|
+
@pyqtSlot()
|
|
287
|
+
def clear_all(self) -> None:
|
|
288
|
+
"""Remove all items from the right list and refresh the left list."""
|
|
289
|
+
self.right_list.clear()
|
|
290
|
+
self.list_files_in_dir()
|
|
291
|
+
|
|
292
|
+
def move_to_done(self, file: str) -> None:
|
|
293
|
+
"""Move an item from the Files to process list to the Processed files list."""
|
|
294
|
+
filename = Path(file).name
|
|
295
|
+
item = self.right_list.findItems(filename, Qt.MatchFlag.MatchExactly)[0]
|
|
296
|
+
self.done_list.move_item_from_other(item, self.right_list)
|
|
297
|
+
|
|
298
|
+
def get_files_to_process(self) -> list[str]:
|
|
299
|
+
"""List full paths to the files to process."""
|
|
300
|
+
names_list = self.right_list.get_list_of_items()
|
|
301
|
+
return [os.path.join(self.current_directory, name) for name in names_list]
|
|
302
|
+
|
|
303
|
+
def enable_buttons(self) -> None:
|
|
304
|
+
"""Enable all buttons."""
|
|
305
|
+
self.button_add_selected.setEnabled(True)
|
|
306
|
+
self.button_add_all.setEnabled(True)
|
|
307
|
+
self.button_clear_all.setEnabled(True)
|
|
308
|
+
self.button_readd_selected.setEnabled(True)
|
|
309
|
+
self.button_readd_all.setEnabled(True)
|
|
310
|
+
self.button_run.setEnabled(True)
|
|
311
|
+
|
|
312
|
+
def disable_buttons(self) -> None:
|
|
313
|
+
"""Disable all buttons."""
|
|
314
|
+
self.button_add_selected.setEnabled(False)
|
|
315
|
+
self.button_add_all.setEnabled(False)
|
|
316
|
+
self.button_clear_all.setEnabled(False)
|
|
317
|
+
self.button_readd_selected.setEnabled(False)
|
|
318
|
+
self.button_readd_all.setEnabled(False)
|
|
319
|
+
self.button_run.setEnabled(False)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Configuration panel widget.
|
|
3
|
+
|
|
4
|
+
Contains the pyqtgraph ParameterTree that represents a configuration file, hosting all
|
|
5
|
+
the parameters and settings for the analysis.
|
|
6
|
+
|
|
7
|
+
It requires a ParamContent class which lists all the parameters to include in the tree.
|
|
8
|
+
See the `_param_content.py` module.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from functools import partial
|
|
14
|
+
|
|
15
|
+
from PyQt6 import QtWidgets
|
|
16
|
+
from PyQt6.QtCore import pyqtSignal
|
|
17
|
+
from pyqtgraph.parametertree import Parameter, ParameterTree
|
|
18
|
+
|
|
19
|
+
from ._param_content import BaseParamContent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseConfigurationWidget(QtWidgets.QWidget):
|
|
23
|
+
"""
|
|
24
|
+
Initialize a PyQtGraph ParameterTree.
|
|
25
|
+
|
|
26
|
+
pyqtSignals
|
|
27
|
+
-------
|
|
28
|
+
sig_file_changed : emits when the file parameter changed.
|
|
29
|
+
sig_expid_changed : emits when the "Reload data" button is clicked or the experiment
|
|
30
|
+
ID parameter changed.
|
|
31
|
+
sig_autoload_changed : emits when the autoload checkbox in the File section is
|
|
32
|
+
changed.
|
|
33
|
+
sig_reload_config : emits when the "Reload config" button is clicked.
|
|
34
|
+
sig_save_config : emits when the "Save config" button is clicked.
|
|
35
|
+
sig_parameter_changed : emits when any other parameter in the tree is changed. Emits
|
|
36
|
+
the parameter name and the scope ("parameters", "settings").
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
sig_file_changed = pyqtSignal()
|
|
40
|
+
sig_expid_changed = pyqtSignal()
|
|
41
|
+
sig_autoload_changed = pyqtSignal()
|
|
42
|
+
|
|
43
|
+
sig_save_config = pyqtSignal()
|
|
44
|
+
sig_reload_config = pyqtSignal()
|
|
45
|
+
|
|
46
|
+
sig_parameter_changed = pyqtSignal(str, str)
|
|
47
|
+
|
|
48
|
+
def __init__(self, param_content: type[BaseParamContent]) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._param_content = param_content
|
|
51
|
+
self.parameters_to_parse = self._param_content.PARAMS_TO_PARSE
|
|
52
|
+
|
|
53
|
+
# Create trees and buttons
|
|
54
|
+
self.init_files_tree()
|
|
55
|
+
self.init_buttons()
|
|
56
|
+
self.init_configuration_tree()
|
|
57
|
+
|
|
58
|
+
# Connect
|
|
59
|
+
self.connect_to_signals()
|
|
60
|
+
|
|
61
|
+
# Create layout
|
|
62
|
+
layout = QtWidgets.QVBoxLayout()
|
|
63
|
+
layout.addWidget(self.files_tree, stretch=3)
|
|
64
|
+
layout.addWidget(self.widget_buttons, stretch=1)
|
|
65
|
+
layout.addWidget(self.config_tree, stretch=10)
|
|
66
|
+
|
|
67
|
+
self.setLayout(layout)
|
|
68
|
+
|
|
69
|
+
def init_files_tree(self) -> None:
|
|
70
|
+
"""Create the File section."""
|
|
71
|
+
# Files section
|
|
72
|
+
self.files_parameters = Parameter.create(
|
|
73
|
+
name="Files", type="group", children=self._param_content.children_files
|
|
74
|
+
)
|
|
75
|
+
self.files_tree = ParameterTree(showHeader=False)
|
|
76
|
+
self.files_tree.setParameters(self.files_parameters)
|
|
77
|
+
|
|
78
|
+
def init_buttons(self) -> None:
|
|
79
|
+
"""Create and connect the file-related buttons."""
|
|
80
|
+
# Create buttons
|
|
81
|
+
self.button_reload_data = QtWidgets.QPushButton("Reload data", self)
|
|
82
|
+
self.button_reload_config = QtWidgets.QPushButton("Reload config", self)
|
|
83
|
+
self.button_save_config = QtWidgets.QPushButton("Save config", self)
|
|
84
|
+
|
|
85
|
+
# Connect
|
|
86
|
+
self.button_reload_data.clicked.connect(self.sig_expid_changed.emit)
|
|
87
|
+
self.button_reload_config.clicked.connect(self.sig_reload_config.emit)
|
|
88
|
+
self.button_save_config.clicked.connect(self.sig_save_config)
|
|
89
|
+
|
|
90
|
+
# Create a layout
|
|
91
|
+
layout_buttons = QtWidgets.QHBoxLayout(self)
|
|
92
|
+
layout_buttons.addWidget(self.button_reload_data)
|
|
93
|
+
layout_buttons.addWidget(self.button_reload_config)
|
|
94
|
+
layout_buttons.addWidget(self.button_save_config)
|
|
95
|
+
self.widget_buttons = QtWidgets.QWidget(self)
|
|
96
|
+
self.widget_buttons.setLayout(layout_buttons)
|
|
97
|
+
|
|
98
|
+
def init_configuration_tree(self) -> None:
|
|
99
|
+
"""Create the ParameterTree, with the Parameters and Settings sections."""
|
|
100
|
+
# Parameters section
|
|
101
|
+
self.param_parameters = Parameter.create(
|
|
102
|
+
name="Parameters",
|
|
103
|
+
type="group",
|
|
104
|
+
children=self._param_content.children_parameters,
|
|
105
|
+
)
|
|
106
|
+
# Settings section
|
|
107
|
+
self.settings_parameters = Parameter.create(
|
|
108
|
+
name="Settings",
|
|
109
|
+
type="group",
|
|
110
|
+
children=self._param_content.children_settings,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Host Tree
|
|
114
|
+
self.host_parameters = Parameter.create(
|
|
115
|
+
name="Configuration",
|
|
116
|
+
type="group",
|
|
117
|
+
children=[
|
|
118
|
+
self.param_parameters,
|
|
119
|
+
self.settings_parameters,
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
self.config_tree = ParameterTree(showHeader=False)
|
|
123
|
+
self.config_tree.setParameters(self.host_parameters)
|
|
124
|
+
|
|
125
|
+
def get_numbers_from_text(self, inds: str | Iterable) -> list[float] | str:
|
|
126
|
+
"""Parse input as list of numbers, or the other way around."""
|
|
127
|
+
if isinstance(inds, str):
|
|
128
|
+
pattern = r"-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?"
|
|
129
|
+
return [float(el) for el in re.findall(pattern, inds)]
|
|
130
|
+
elif isinstance(inds, Iterable):
|
|
131
|
+
return ", ".join(f"{int(e)}" if int(e) == e else f"{e:.4f}" for e in inds)
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError(f"Can't parse numbers nor text from type {type(inds)}")
|
|
134
|
+
|
|
135
|
+
def connect_to_signals(self) -> None:
|
|
136
|
+
"""Connect changes to signals."""
|
|
137
|
+
self.files_parameters.child("file").sigValueChanged.connect(
|
|
138
|
+
self.sig_file_changed.emit
|
|
139
|
+
)
|
|
140
|
+
self.files_parameters.child("expid").sigValueChanged.connect(
|
|
141
|
+
self.sig_expid_changed.emit
|
|
142
|
+
)
|
|
143
|
+
self.files_parameters.child("autoload").sigValueChanged.connect(
|
|
144
|
+
self.sig_autoload_changed.emit
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
for p in self.param_parameters:
|
|
148
|
+
self.param_parameters.child(p.name()).sigValueChanged.connect(
|
|
149
|
+
partial(self.sig_parameter_changed.emit, p.name(), "parameters")
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
for p in self.settings_parameters:
|
|
153
|
+
self.settings_parameters.child(p.name()).sigValueChanged.connect(
|
|
154
|
+
partial(self.sig_parameter_changed.emit, p.name(), "settings")
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def enable_buttons(self) -> None:
|
|
158
|
+
"""Enable all buttons."""
|
|
159
|
+
self.button_reload_data.setEnabled(True)
|
|
160
|
+
self.button_reload_config.setEnabled(True)
|
|
161
|
+
self.button_save_config.setEnabled(True)
|
|
162
|
+
|
|
163
|
+
def disable_buttons(self) -> None:
|
|
164
|
+
"""Disable all buttons."""
|
|
165
|
+
self.button_reload_data.setEnabled(False)
|
|
166
|
+
self.button_reload_config.setEnabled(False)
|
|
167
|
+
self.button_save_config.setEnabled(False)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A simple file browser with a folder selection button and a checkbox.
|
|
3
|
+
|
|
4
|
+
Files can be selected, when they are, a signal is emitted.
|
|
5
|
+
|
|
6
|
+
This module includes the QTreeView itself (FileBrowser), and the host widget that adds
|
|
7
|
+
the folder selection button (FileBrowserWidget).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from PyQt6 import QtWidgets
|
|
13
|
+
from PyQt6.QtCore import QSortFilterProxyModel, pyqtSignal, pyqtSlot
|
|
14
|
+
from PyQt6.QtGui import QFileSystemModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileBrowser(QtWidgets.QTreeView):
|
|
18
|
+
"""File browser and picker."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, startDir: str | Path = "") -> None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
self.fsModel = QFileSystemModel()
|
|
24
|
+
self.fsModel.setRootPath("")
|
|
25
|
+
|
|
26
|
+
self.proxyModel = QSortFilterProxyModel()
|
|
27
|
+
self.proxyModel.setSourceModel(self.fsModel)
|
|
28
|
+
|
|
29
|
+
self.setModel(self.proxyModel)
|
|
30
|
+
|
|
31
|
+
start_dir = Path(startDir)
|
|
32
|
+
if start_dir.is_file():
|
|
33
|
+
start_dir = str(start_dir.parent)
|
|
34
|
+
elif start_dir.is_dir():
|
|
35
|
+
start_dir = str(start_dir)
|
|
36
|
+
else:
|
|
37
|
+
start_dir = ""
|
|
38
|
+
|
|
39
|
+
index = self.fsModel.index(start_dir)
|
|
40
|
+
proxyIndex = self.proxyModel.mapFromSource(index)
|
|
41
|
+
self.setRootIndex(proxyIndex)
|
|
42
|
+
|
|
43
|
+
def set_directory(self, dirname: str | Path) -> None:
|
|
44
|
+
"""Set the current folder in the tree view."""
|
|
45
|
+
dirname = str(dirname)
|
|
46
|
+
|
|
47
|
+
index = self.fsModel.index(dirname)
|
|
48
|
+
proxyIndex = self.proxyModel.mapFromSource(index)
|
|
49
|
+
self.setRootIndex(proxyIndex)
|
|
50
|
+
|
|
51
|
+
def get_current_file(self) -> str:
|
|
52
|
+
"""Return the currently selected file."""
|
|
53
|
+
index = self.currentIndex()
|
|
54
|
+
sourceIndex = self.proxyModel.mapToSource(index)
|
|
55
|
+
|
|
56
|
+
return self.fsModel.filePath(sourceIndex)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FileBrowserWidget(QtWidgets.QWidget):
|
|
60
|
+
"""
|
|
61
|
+
File browser and picker with a folder selection button and a checkbox.
|
|
62
|
+
|
|
63
|
+
pyqtSignals
|
|
64
|
+
-------
|
|
65
|
+
sig_file_selected : emits when an item is double-clicked. Emits True if the file is
|
|
66
|
+
a TOML file, False otherwise, and the file path.
|
|
67
|
+
sig_checkbox_changed : emits when the checkbox is changed.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
sig_file_selected = pyqtSignal(bool, str)
|
|
71
|
+
sig_checkbox_changed = pyqtSignal()
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
super().__init__()
|
|
75
|
+
|
|
76
|
+
# Build the push button
|
|
77
|
+
self.button_open_folder = QtWidgets.QPushButton("Open Folder...", self)
|
|
78
|
+
self.button_open_folder.clicked.connect(self.open_folder)
|
|
79
|
+
|
|
80
|
+
# Build the load data checkbox
|
|
81
|
+
self.checkbox_autoload_data = QtWidgets.QCheckBox(
|
|
82
|
+
"Load data automatically", self
|
|
83
|
+
)
|
|
84
|
+
self.checkbox_autoload_data.setChecked(True)
|
|
85
|
+
self.checkbox_autoload_data.stateChanged.connect(self.sig_checkbox_changed)
|
|
86
|
+
|
|
87
|
+
# Build the file browser tree view
|
|
88
|
+
self.file_browser = FileBrowser()
|
|
89
|
+
self.file_browser.doubleClicked.connect(self.select_file)
|
|
90
|
+
|
|
91
|
+
# Define layout
|
|
92
|
+
grid = QtWidgets.QGridLayout()
|
|
93
|
+
grid.addWidget(self.button_open_folder, 0, 0)
|
|
94
|
+
grid.addWidget(self.checkbox_autoload_data, 0, 1)
|
|
95
|
+
grid.addWidget(self.file_browser, 1, 0, -1, 2)
|
|
96
|
+
|
|
97
|
+
# Create widget
|
|
98
|
+
self.setLayout(grid)
|
|
99
|
+
|
|
100
|
+
@pyqtSlot()
|
|
101
|
+
def open_folder(self) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Open a folder picker.
|
|
104
|
+
|
|
105
|
+
Callback for the "Open Folder" button in the file browser.
|
|
106
|
+
"""
|
|
107
|
+
dirname = QtWidgets.QFileDialog.getExistingDirectory(
|
|
108
|
+
self,
|
|
109
|
+
"Open folder...",
|
|
110
|
+
)
|
|
111
|
+
if dirname:
|
|
112
|
+
self.file_browser.set_directory(dirname)
|
|
113
|
+
|
|
114
|
+
@pyqtSlot()
|
|
115
|
+
def select_file(self) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Load the file selected in the file browser tab.
|
|
118
|
+
|
|
119
|
+
Callback for when a file is double-clicked in the file browser tab.
|
|
120
|
+
"""
|
|
121
|
+
filepath = self.file_browser.get_current_file()
|
|
122
|
+
|
|
123
|
+
if filepath.endswith(".toml"):
|
|
124
|
+
# This is a configuration file
|
|
125
|
+
self.sig_file_selected.emit(True, filepath)
|
|
126
|
+
else:
|
|
127
|
+
# Not really sure but let's assume it is a data file used to change the
|
|
128
|
+
# experiment ID
|
|
129
|
+
self.sig_file_selected.emit(False, filepath)
|