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.
Files changed (51) hide show
  1. pymagnetos/__init__.py +15 -0
  2. pymagnetos/cli.py +40 -0
  3. pymagnetos/core/__init__.py +19 -0
  4. pymagnetos/core/_config.py +340 -0
  5. pymagnetos/core/_data.py +132 -0
  6. pymagnetos/core/_processor.py +905 -0
  7. pymagnetos/core/config_models.py +57 -0
  8. pymagnetos/core/gui/__init__.py +6 -0
  9. pymagnetos/core/gui/_base_mainwindow.py +819 -0
  10. pymagnetos/core/gui/widgets/__init__.py +19 -0
  11. pymagnetos/core/gui/widgets/_batch_processing.py +319 -0
  12. pymagnetos/core/gui/widgets/_configuration.py +167 -0
  13. pymagnetos/core/gui/widgets/_files.py +129 -0
  14. pymagnetos/core/gui/widgets/_graphs.py +93 -0
  15. pymagnetos/core/gui/widgets/_param_content.py +20 -0
  16. pymagnetos/core/gui/widgets/_popup_progressbar.py +29 -0
  17. pymagnetos/core/gui/widgets/_text_logger.py +32 -0
  18. pymagnetos/core/signal_processing.py +1004 -0
  19. pymagnetos/core/utils.py +85 -0
  20. pymagnetos/log.py +126 -0
  21. pymagnetos/py.typed +0 -0
  22. pymagnetos/pytdo/__init__.py +6 -0
  23. pymagnetos/pytdo/_config.py +24 -0
  24. pymagnetos/pytdo/_config_models.py +59 -0
  25. pymagnetos/pytdo/_tdoprocessor.py +1052 -0
  26. pymagnetos/pytdo/assets/config_default.toml +84 -0
  27. pymagnetos/pytdo/gui/__init__.py +26 -0
  28. pymagnetos/pytdo/gui/_worker.py +106 -0
  29. pymagnetos/pytdo/gui/main.py +617 -0
  30. pymagnetos/pytdo/gui/widgets/__init__.py +8 -0
  31. pymagnetos/pytdo/gui/widgets/_buttons.py +66 -0
  32. pymagnetos/pytdo/gui/widgets/_configuration.py +78 -0
  33. pymagnetos/pytdo/gui/widgets/_graphs.py +280 -0
  34. pymagnetos/pytdo/gui/widgets/_param_content.py +137 -0
  35. pymagnetos/pyuson/__init__.py +7 -0
  36. pymagnetos/pyuson/_config.py +26 -0
  37. pymagnetos/pyuson/_config_models.py +71 -0
  38. pymagnetos/pyuson/_echoprocessor.py +1901 -0
  39. pymagnetos/pyuson/assets/config_default.toml +92 -0
  40. pymagnetos/pyuson/gui/__init__.py +26 -0
  41. pymagnetos/pyuson/gui/_worker.py +135 -0
  42. pymagnetos/pyuson/gui/main.py +767 -0
  43. pymagnetos/pyuson/gui/widgets/__init__.py +7 -0
  44. pymagnetos/pyuson/gui/widgets/_buttons.py +95 -0
  45. pymagnetos/pyuson/gui/widgets/_configuration.py +85 -0
  46. pymagnetos/pyuson/gui/widgets/_graphs.py +248 -0
  47. pymagnetos/pyuson/gui/widgets/_param_content.py +193 -0
  48. pymagnetos-0.1.0.dist-info/METADATA +23 -0
  49. pymagnetos-0.1.0.dist-info/RECORD +51 -0
  50. pymagnetos-0.1.0.dist-info/WHEEL +4 -0
  51. 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)