boris-behav-obs 9.7.7__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.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/project.py ADDED
@@ -0,0 +1,2007 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import json
24
+ import logging
25
+ import re
26
+
27
+ from PySide6.QtCore import Qt, QDateTime
28
+ from PySide6.QtGui import QColor
29
+ from PySide6.QtWidgets import (
30
+ QAbstractItemView,
31
+ QApplication,
32
+ QCheckBox,
33
+ QColorDialog,
34
+ QDialog,
35
+ QFileDialog,
36
+ QHBoxLayout,
37
+ QHeaderView,
38
+ QInputDialog,
39
+ QLabel,
40
+ QLineEdit,
41
+ QMenu,
42
+ QMessageBox,
43
+ QPushButton,
44
+ QSizePolicy,
45
+ QSpacerItem,
46
+ QTableWidget,
47
+ QTableWidgetItem,
48
+ QVBoxLayout,
49
+ )
50
+
51
+ from . import add_modifier
52
+ from . import config as cfg
53
+ from . import utilities as util
54
+ from . import converters, dialog, exclusion_matrix, project_import_export
55
+ from .project_ui import Ui_dlgProject
56
+
57
+
58
+ class BehavioralCategories(QDialog):
59
+ """
60
+ Class for managing the behavioral categories
61
+ """
62
+
63
+ def __init__(self, pj):
64
+ super().__init__()
65
+
66
+ self.pj = pj
67
+ self.setWindowTitle("Behavioral categories")
68
+
69
+ self.renamed = None
70
+ self.removed = None
71
+
72
+ self.vbox = QVBoxLayout(self)
73
+
74
+ self.label = QLabel()
75
+ self.label.setText("Behavioral categories")
76
+ self.vbox.addWidget(self.label)
77
+
78
+ # self.lw = QListWidget()
79
+ self.lw = QTableWidget()
80
+ self.lw.cellDoubleClicked[int, int].connect(self.lw_double_clicked)
81
+
82
+ # add categories
83
+ self.lw.setColumnCount(2)
84
+ self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
85
+ self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
86
+
87
+ self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
88
+
89
+ behavioral_categories: list = []
90
+
91
+ if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
92
+ self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
93
+ behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
94
+ for idx, key in enumerate(behav_cat.keys()):
95
+ # name
96
+ item = QTableWidgetItem()
97
+ item.setText(behav_cat[key]["name"])
98
+ behavioral_categories.append(behav_cat[key]["name"])
99
+ self.lw.setItem(idx, 0, item)
100
+ # color
101
+ item = QTableWidgetItem()
102
+ item.setText(behav_cat[key].get(cfg.COLOR, ""))
103
+ if behav_cat[key].get(cfg.COLOR, ""):
104
+ item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
105
+ else:
106
+ item.setBackground(self.not_editable_column_color())
107
+ self.lw.setItem(idx, 1, item)
108
+ else:
109
+ self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
110
+ for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
111
+ # name
112
+ item = QTableWidgetItem()
113
+ item.setText(category)
114
+ behavioral_categories.append(category)
115
+ self.lw.setItem(idx, 0, item)
116
+ # color
117
+ item = QTableWidgetItem()
118
+ item.setText("")
119
+
120
+ self.lw.setItem(idx, 1, item)
121
+
122
+ self.vbox.addWidget(self.lw)
123
+
124
+ self.hbox0 = QHBoxLayout()
125
+ self.pbAddCategory = QPushButton("Add category", clicked=self.add_behavioral_category)
126
+ self.pbRemoveCategory = QPushButton("Remove category", clicked=self.remove_behavioral_category)
127
+ self.pb_rename_category = QPushButton("Rename category", clicked=self.pb_rename_category_clicked)
128
+
129
+ spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
130
+ self.hbox0.addItem(spacerItem)
131
+ self.hbox0.addWidget(self.pb_rename_category)
132
+ self.hbox0.addWidget(self.pbRemoveCategory)
133
+ self.hbox0.addWidget(self.pbAddCategory)
134
+ self.vbox.addLayout(self.hbox0)
135
+
136
+ hbox1 = QHBoxLayout()
137
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
138
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.accept)
139
+
140
+ spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
141
+ hbox1.addItem(spacerItem)
142
+ hbox1.addWidget(self.pbCancel)
143
+ hbox1.addWidget(self.pbOK)
144
+ self.vbox.addLayout(hbox1)
145
+
146
+ self.setLayout(self.vbox)
147
+
148
+ # check if behavioral categories are present in events
149
+ behavioral_categories_in_ethogram = set(
150
+ sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
151
+ )
152
+
153
+ if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
154
+ set(behavioral_categories)
155
+ ) != {""}:
156
+ if (
157
+ dialog.MessageDialog(
158
+ cfg.programName,
159
+ (
160
+ "There are behavioral categories that are present in ethogram but not defined.<br>"
161
+ f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
162
+ "<br>"
163
+ "Do you want to add them in the behavioral categories list?"
164
+ ),
165
+ [cfg.YES, cfg.NO],
166
+ )
167
+ == cfg.YES
168
+ ):
169
+ # add behavioral categories present in ethogram in behavioal categories list
170
+ rc = self.lw.rowCount()
171
+ self.lw.setRowCount(rc + len(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))
172
+ for idx, category in enumerate(sorted(list(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))):
173
+ print(category)
174
+ # name
175
+ item = QTableWidgetItem()
176
+ item.setText(category)
177
+ # behavioral_categories.append(category)
178
+ self.lw.setItem(rc + idx, 0, item)
179
+ # color
180
+ item = QTableWidgetItem()
181
+ item.setText("")
182
+
183
+ self.lw.setItem(rc + idx, 1, item)
184
+
185
+ def not_editable_column_color(self):
186
+ """
187
+ return a color for the not editable column
188
+ """
189
+ window_color = QApplication.instance().palette().window().color()
190
+ return QColor(
191
+ window_color.red() - cfg.DARKER_DIFFERENCE,
192
+ window_color.green() - cfg.DARKER_DIFFERENCE,
193
+ window_color.blue() - cfg.DARKER_DIFFERENCE,
194
+ )
195
+
196
+ def lw_double_clicked(self, row: int, column: int):
197
+ """
198
+ change color
199
+ """
200
+
201
+ if column != 1:
202
+ return
203
+ col_diag = QColorDialog()
204
+ col_diag.setOptions(QColorDialog.DontUseNativeDialog)
205
+
206
+ if self.lw.item(row, 1).text():
207
+ current_color = QColor(self.lw.item(row, 1).text())
208
+ if current_color.isValid():
209
+ col_diag.setCurrentColor(current_color)
210
+
211
+ if col_diag.exec_():
212
+ color = col_diag.currentColor()
213
+ if color.name() == "#000000": # black -> delete color
214
+ self.lw.item(row, 1).setText("")
215
+ self.lw.item(row, 1).setBackground(self.not_editable_column_color())
216
+ elif color.isValid():
217
+ self.lw.item(row, 1).setText(color.name())
218
+ self.lw.item(row, 1).setBackground(color)
219
+
220
+ def add_behavioral_category(self):
221
+ """
222
+ add a behavioral category
223
+ """
224
+ category, ok = QInputDialog.getText(self, "New behavioral category", "Category name:")
225
+ if ok:
226
+ self.lw.insertRow(self.lw.rowCount())
227
+ item = QTableWidgetItem(category)
228
+ self.lw.setItem(self.lw.rowCount() - 1, 0, item)
229
+
230
+ item = QTableWidgetItem("")
231
+ # item.setFlags(Qt.ItemIsEnabled)
232
+ self.lw.setItem(self.lw.rowCount() - 1, 1, item)
233
+
234
+ def remove_behavioral_category(self):
235
+ """
236
+ remove the selected behavioral category
237
+ """
238
+
239
+ for selected_item in self.lw.selectedItems():
240
+ # check if behavioral category is in use
241
+ if (
242
+ dialog.MessageDialog(
243
+ cfg.programName,
244
+ ("Confirm deletion of the behavioral category"),
245
+ ("Confirm", cfg.CANCEL),
246
+ )
247
+ == cfg.CANCEL
248
+ ):
249
+ continue
250
+
251
+ category_to_remove = self.lw.item(self.lw.row(selected_item), 0).text().strip()
252
+ behaviors_in_category: list = []
253
+ for idx in self.pj[cfg.ETHOGRAM]:
254
+ if (
255
+ cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
256
+ and self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] == category_to_remove
257
+ ):
258
+ behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
259
+ flag_remove = False
260
+ if behaviors_in_category:
261
+ flag_remove = (
262
+ dialog.MessageDialog(
263
+ cfg.programName,
264
+ (
265
+ f"Some behavior belong to the <b>{category_to_remove}</b> to remove:<br>"
266
+ f"{'<br>'.join(behaviors_in_category)}<br>"
267
+ "<br>Some features may not be available anymore.<br>"
268
+ ),
269
+ ("Remove category", cfg.CANCEL),
270
+ )
271
+ == "Remove category"
272
+ )
273
+
274
+ else:
275
+ flag_remove = True
276
+
277
+ if flag_remove:
278
+ self.lw.removeRow(self.lw.row(selected_item))
279
+ self.removed = category_to_remove
280
+
281
+ self.accept()
282
+
283
+ def pb_rename_category_clicked(self, row: int):
284
+ """
285
+ rename the selected behavioral category
286
+ """
287
+ for selected_item in self.lw.selectedItems():
288
+ # check if behavioral category is in use
289
+ category_to_rename = self.lw.item(self.lw.row(selected_item), 0).text().strip()
290
+ behaviors_in_category = []
291
+ for idx in self.pj[cfg.ETHOGRAM]:
292
+ if (
293
+ cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
294
+ and self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] == category_to_rename
295
+ ):
296
+ behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
297
+
298
+ flag_rename = False
299
+ if behaviors_in_category:
300
+ flag_rename = (
301
+ dialog.MessageDialog(
302
+ cfg.programName,
303
+ (f"Some behavior belong to the <b>{category_to_rename}</b> to rename:<br>{'<br>'.join(behaviors_in_category)}<br>"),
304
+ ["Rename category", cfg.CANCEL],
305
+ )
306
+ == "Rename category"
307
+ )
308
+ else:
309
+ flag_rename = True
310
+
311
+ if flag_rename:
312
+ new_category_name, ok = QInputDialog.getText(
313
+ self, "Rename behavioral category", "New category name:", QLineEdit.Normal, category_to_rename
314
+ )
315
+ if ok:
316
+ self.lw.item(self.lw.indexFromItem(selected_item).row(), 0).setText(new_category_name)
317
+ # check behaviors belonging to the renamed category
318
+ self.renamed = [category_to_rename, new_category_name]
319
+ # self.accept()
320
+
321
+
322
+ class projectDialog(QDialog, Ui_dlgProject):
323
+ def __init__(self, parent=None):
324
+ super().__init__()
325
+
326
+ self.setupUi(self)
327
+
328
+ self.lbObservationsState.setText("")
329
+ self.lbSubjectsState.setText("")
330
+
331
+ # ethogram tab
332
+ behavior_button_items = [
333
+ "new|Add new behavior",
334
+ "clone|Clone behavior",
335
+ "remove|Remove behavior",
336
+ "remove all|Remove all behaviors",
337
+ "lower|Convert keys to lower case",
338
+ ]
339
+ self.behavior_menu = QMenu()
340
+ self.behavior_menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
341
+ self.add_button_menu(behavior_button_items, self.behavior_menu)
342
+ self.pb_behavior.setMenu(self.behavior_menu)
343
+
344
+ import_button_items = [
345
+ "boris|from a BORIS project",
346
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
347
+ "jwatcher|from a JWatcher project",
348
+ "text|from a text file (CSV or TSV)",
349
+ "clipboard|from the clipboard",
350
+ "repository|from the BORIS repository",
351
+ ]
352
+ self.import_behaviors_menu = QMenu()
353
+ self.import_behaviors_menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
354
+ self.add_button_menu(import_button_items, self.import_behaviors_menu)
355
+ self.pb_import.setMenu(self.import_behaviors_menu)
356
+
357
+ self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
358
+
359
+ self.pb_exclusion_matrix.clicked.connect(self.exclusion_matrix)
360
+
361
+ self.pbExportEthogram.clicked.connect(lambda: project_import_export.export_ethogram(self))
362
+
363
+ self.twBehaviors.cellChanged[int, int].connect(self.twBehaviors_cellChanged)
364
+ self.twBehaviors.cellDoubleClicked[int, int].connect(self.twBehaviors_cellDoubleClicked)
365
+
366
+ # left align table header
367
+ for i in range(self.twBehaviors.columnCount()):
368
+ self.twBehaviors.horizontalHeaderItem(i).setTextAlignment(Qt.AlignLeft)
369
+
370
+ # subjects
371
+ subjects_button_items = [
372
+ "new|Add a new subject",
373
+ # "clone|Clone subject",
374
+ "remove|Remove subject",
375
+ "remove all|Remove all subjects",
376
+ "lower|Convert keys to lower case",
377
+ ]
378
+
379
+ self.subject_menu = QMenu()
380
+ self.subject_menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
381
+ self.add_button_menu(subjects_button_items, self.subject_menu)
382
+ self.pb_subjects.setMenu(self.subject_menu)
383
+
384
+ subjects_import_button_items = [
385
+ "boris|from a BORIS project",
386
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
387
+ "text|from a text file (CSV or TSV)",
388
+ "clipboard|from the clipboard",
389
+ ]
390
+ self.import_subjects_menu = QMenu()
391
+ self.import_subjects_menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
392
+ self.add_button_menu(subjects_import_button_items, self.import_subjects_menu)
393
+ self.pbImportSubjectsFromProject.setMenu(self.import_subjects_menu)
394
+
395
+ self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
396
+
397
+ self.twSubjects.cellChanged[int, int].connect(self.twSubjects_cellChanged)
398
+
399
+ # independent variables tab
400
+ self.pbAddVariable.clicked.connect(self.pbAddVariable_clicked)
401
+ self.pbRemoveVariable.clicked.connect(self.pbRemoveVariable_clicked)
402
+
403
+ self.leLabel.textChanged.connect(self.leLabel_changed)
404
+ self.leDescription.textChanged.connect(self.leDescription_changed)
405
+ self.lePredefined.textChanged.connect(self.lePredefined_changed)
406
+ self.leSetValues.textChanged.connect(self.leSetValues_changed)
407
+ self.dte_default_date.dateTimeChanged.connect(self.dte_default_date_changed)
408
+
409
+ self.twVariables.cellClicked[int, int].connect(self.twVariables_cellClicked)
410
+
411
+ self.cbType.currentIndexChanged.connect(self.cbtype_changed)
412
+ self.cbType.activated.connect(self.cbtype_activated)
413
+
414
+ self.pbImportVarFromProject.clicked.connect(self.pbImportVarFromProject_clicked)
415
+
416
+ self.pbOK.clicked.connect(self.pbOK_clicked)
417
+ self.pbCancel.clicked.connect(self.pbCancel_clicked)
418
+
419
+ self.selected_twvariables_row = -1
420
+
421
+ self.pbAddBehaviorsCodingMap.clicked.connect(self.add_behaviors_coding_map)
422
+ self.pbRemoveBehaviorsCodingMap.clicked.connect(self.remove_behaviors_coding_map)
423
+
424
+ # converters tab
425
+ self.pb_add_converter.clicked.connect(lambda: converters.add_converter(self))
426
+ self.pb_modify_converter.clicked.connect(lambda: converters.modify_converter(self))
427
+ self.pb_save_converter.clicked.connect(lambda: converters.save_converter(self))
428
+ self.pb_cancel_converter.clicked.connect(lambda: converters.cancel_converter(self))
429
+ self.pb_delete_converter.clicked.connect(lambda: converters.delete_converter(self))
430
+
431
+ self.pb_load_from_file.clicked.connect(lambda: converters.load_converters_from_file_repo(self, mode="file"))
432
+ self.pb_load_from_repo.clicked.connect(lambda: converters.load_converters_from_file_repo(self, mode="repo"))
433
+
434
+ self.pb_code_help.clicked.connect(lambda: converters.pb_code_help_clicked(self))
435
+
436
+ self.row_in_modification = -1
437
+ self.flag_modified = False
438
+
439
+ for w in (
440
+ self.le_converter_name,
441
+ self.le_converter_description,
442
+ self.pteCode,
443
+ self.pb_save_converter,
444
+ self.pb_cancel_converter,
445
+ ):
446
+ w.setEnabled(False)
447
+
448
+ # disable widget for indep var setting
449
+ for widget in (
450
+ self.leLabel,
451
+ self.le_converter_description,
452
+ self.cbType,
453
+ self.lePredefined,
454
+ self.dte_default_date,
455
+ self.leSetValues,
456
+ ):
457
+ widget.setEnabled(False)
458
+
459
+ self.twBehaviors.horizontalHeader().sortIndicatorChanged.connect(self.sort_twBehaviors)
460
+ self.twSubjects.horizontalHeader().sortIndicatorChanged.connect(self.sort_twSubjects)
461
+ self.twVariables.horizontalHeader().sortIndicatorChanged.connect(self.sort_twVariables)
462
+
463
+ def not_editable_column_color(self):
464
+ """
465
+ return a color for the not editable column
466
+ """
467
+ window_color = QApplication.instance().palette().window().color()
468
+ return QColor(
469
+ window_color.red() - cfg.DARKER_DIFFERENCE,
470
+ window_color.green() - cfg.DARKER_DIFFERENCE,
471
+ window_color.blue() - cfg.DARKER_DIFFERENCE,
472
+ )
473
+
474
+ def add_button_menu(self, data, menu_obj):
475
+ """
476
+ add menu option from dictionary
477
+ """
478
+ if isinstance(data, dict):
479
+ for k, v in data.items():
480
+ sub_menu = QMenu(k, menu_obj)
481
+ menu_obj.addMenu(sub_menu)
482
+ self.add_button_menu(v, sub_menu)
483
+ elif isinstance(data, list):
484
+ for element in data:
485
+ self.add_button_menu(element, menu_obj)
486
+ else:
487
+ action = menu_obj.addAction(data.split("|")[1])
488
+ # tips are used to discriminate the menu option
489
+ action.setStatusTip(data.split("|")[0])
490
+ action.setIconVisibleInMenu(False)
491
+
492
+ def behavior(self, action: str):
493
+ """
494
+ behavior menu
495
+ """
496
+ if action == "new":
497
+ self.add_behavior()
498
+ if action == "clone":
499
+ self.clone_behavior()
500
+ if action == "remove":
501
+ self.remove_behavior()
502
+ if action == "remove all":
503
+ self.remove_all_behaviors()
504
+ if action == "lower":
505
+ self.convert_behaviors_keys_to_lower_case()
506
+
507
+ def import_ethogram(self, action: str):
508
+ """
509
+ import behaviors
510
+ """
511
+ if action == "boris":
512
+ project_import_export.import_behaviors_from_project(self)
513
+ if action == "jwatcher":
514
+ project_import_export.import_behaviors_from_JWatcher(self)
515
+ if action == "text":
516
+ project_import_export.import_behaviors_from_text_file(self)
517
+ if action == "spreadsheet":
518
+ project_import_export.import_behaviors_from_spreadsheet(self)
519
+ if action == "clipboard":
520
+ project_import_export.import_behaviors_from_clipboard(self)
521
+ if action == "repository":
522
+ project_import_export.import_behaviors_from_repository(self)
523
+
524
+ def import_subjects(self, action: str):
525
+ """
526
+ import subjects
527
+ """
528
+ if action == "boris":
529
+ project_import_export.import_subjects_from_project(self)
530
+ if action == "text":
531
+ project_import_export.import_subjects_from_text_file(self)
532
+ if action == "spreadsheet":
533
+ project_import_export.import_subjects_from_spreadsheet(self)
534
+ if action == "clipboard":
535
+ project_import_export.import_subjects_from_clipboard(self)
536
+
537
+ def subjects(self, action: str):
538
+ """
539
+ subjects menu
540
+ """
541
+ if action == "new":
542
+ self.add_subject()
543
+ # if action == "clone":
544
+ # self.clone_behavior()
545
+ if action == "remove":
546
+ self.remove_subject()
547
+ if action == "remove all":
548
+ self.remove_all_subjects()
549
+ if action == "lower":
550
+ self.convert_subjects_keys_to_lower_case()
551
+
552
+ def sort_twBehaviors(self, index, order):
553
+ """
554
+ order ethogram table
555
+ """
556
+ self.twBehaviors.sortItems(index, order)
557
+
558
+ def sort_twSubjects(self, index, order):
559
+ """
560
+ order subjects table
561
+ """
562
+ self.twSubjects.sortItems(index, order)
563
+
564
+ def sort_twVariables(self, index, order):
565
+ """
566
+ order variables table
567
+ """
568
+ self.twVariables.sortItems(index, order)
569
+
570
+ def convert_behaviors_keys_to_lower_case(self):
571
+ """
572
+ convert behaviors key to lower case to help to migrate to v. 7
573
+ """
574
+
575
+ if not self.twBehaviors.rowCount():
576
+ QMessageBox.critical(
577
+ None,
578
+ cfg.programName,
579
+ "The ethogram is empty",
580
+ QMessageBox.Ok | QMessageBox.Default,
581
+ QMessageBox.NoButton,
582
+ )
583
+ return
584
+
585
+ # check if some keys will be duplicated after conversion
586
+ try:
587
+ all_keys = [self.twBehaviors.item(row, cfg.behavioursFields["key"]).text() for row in range(self.twBehaviors.rowCount())]
588
+ except Exception:
589
+ pass
590
+ if all_keys == [x.lower() for x in all_keys]:
591
+ QMessageBox.information(self, cfg.programName, "All keys are already lower case")
592
+ return
593
+
594
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
595
+ return
596
+
597
+ if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
598
+ if (
599
+ dialog.MessageDialog(
600
+ cfg.programName,
601
+ "Some keys will be duplicated after conversion. Proceed?",
602
+ [cfg.YES, cfg.CANCEL],
603
+ )
604
+ == cfg.CANCEL
605
+ ):
606
+ return
607
+
608
+ for row in range(self.twBehaviors.rowCount()):
609
+ if self.twBehaviors.item(row, cfg.behavioursFields["key"]).text():
610
+ self.twBehaviors.item(row, cfg.behavioursFields["key"]).setText(
611
+ self.twBehaviors.item(row, cfg.behavioursFields["key"]).text().lower()
612
+ )
613
+
614
+ # convert modifier shortcuts
615
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text():
616
+ modifiers_dict = (
617
+ json.loads(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
618
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text()
619
+ else {}
620
+ )
621
+
622
+ for modifier_set in modifiers_dict:
623
+ try:
624
+ for idx2, value in enumerate(modifiers_dict[modifier_set]["values"]):
625
+ if re.findall(r"\((\w+)\)", value):
626
+ modifiers_dict[modifier_set]["values"][idx2] = (
627
+ value.split("(")[0] + "(" + re.findall(r"\((\w+)\)", value)[0].lower() + ")" + value.split(")")[-1]
628
+ )
629
+ except Exception:
630
+ logging.warning("error during conversion of modifier short cut to lower case")
631
+
632
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(json.dumps(modifiers_dict))
633
+
634
+ def convert_subjects_keys_to_lower_case(self):
635
+ """
636
+ convert subjects key to lower case to help to migrate to v. 7
637
+ """
638
+ # check if some keys will be duplicated after conversion
639
+ try:
640
+ all_keys = [self.twSubjects.item(row, cfg.subjectsFields.index("key")).text() for row in range(self.twSubjects.rowCount())]
641
+ except Exception:
642
+ pass
643
+ if all_keys == [x.lower() for x in all_keys]:
644
+ QMessageBox.information(self, cfg.programName, "All keys are already lower case")
645
+ return
646
+
647
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
648
+ return
649
+
650
+ if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
651
+ if (
652
+ dialog.MessageDialog(
653
+ cfg.programName,
654
+ "Some keys will be duplicated after conversion. Proceed?",
655
+ [cfg.YES, cfg.CANCEL],
656
+ )
657
+ == cfg.CANCEL
658
+ ):
659
+ return
660
+
661
+ for row in range(self.twSubjects.rowCount()):
662
+ if self.twSubjects.item(row, cfg.subjectsFields.index("key")).text():
663
+ self.twSubjects.item(row, cfg.subjectsFields.index("key")).setText(
664
+ self.twSubjects.item(row, cfg.subjectsFields.index("key")).text().lower()
665
+ )
666
+
667
+ def add_behaviors_coding_map(self):
668
+ """
669
+ Add a behaviors coding map from file
670
+ """
671
+
672
+ file_name, _ = QFileDialog().getOpenFileName(
673
+ self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
674
+ )
675
+ if not file_name:
676
+ return
677
+ try:
678
+ bcm = json.loads(open(file_name, "r").read())
679
+ except Exception:
680
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
681
+ return
682
+
683
+ if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
684
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map.")
685
+
686
+ if cfg.BEHAVIORS_CODING_MAP not in self.pj:
687
+ self.pj[cfg.BEHAVIORS_CODING_MAP] = []
688
+
689
+ bcm_code_not_found = []
690
+ existing_codes = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
691
+ for code in [bcm["areas"][key][cfg.BEHAVIOR_CODE] for key in bcm["areas"]]:
692
+ if code not in existing_codes:
693
+ bcm_code_not_found.append(code)
694
+
695
+ if bcm_code_not_found:
696
+ QMessageBox.warning(
697
+ self,
698
+ cfg.programName,
699
+ ("The following behavior{} are not defined in the ethogram:<br>{}").format(
700
+ "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
701
+ ),
702
+ )
703
+
704
+ self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
705
+
706
+ self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
707
+
708
+ self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
709
+ codes = ", ".join([bcm["areas"][idx][cfg.BEHAVIOR_CODE] for idx in bcm["areas"]])
710
+ self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 1, QTableWidgetItem(codes))
711
+
712
+ def remove_behaviors_coding_map(self):
713
+ """
714
+ remove the first selected behaviors coding map
715
+ """
716
+ if not self.twBehavCodingMap.selectedIndexes():
717
+ QMessageBox.warning(self, cfg.programName, "Select a behaviors coding map")
718
+ else:
719
+ if dialog.MessageDialog(cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
720
+ del self.pj[cfg.BEHAVIORS_CODING_MAP][self.twBehavCodingMap.selectedIndexes()[0].row()]
721
+ self.twBehavCodingMap.removeRow(self.twBehavCodingMap.selectedIndexes()[0].row())
722
+
723
+ def leLabel_changed(self):
724
+ """
725
+ independent variable label changed
726
+ """
727
+ if self.selected_twvariables_row != -1:
728
+ self.twVariables.item(self.selected_twvariables_row, 0).setText(self.leLabel.text())
729
+
730
+ def leDescription_changed(self):
731
+ """
732
+ independent variable description changed
733
+ """
734
+ if self.selected_twvariables_row != -1:
735
+ self.twVariables.item(self.selected_twvariables_row, 1).setText(self.leDescription.text())
736
+
737
+ def lePredefined_changed(self):
738
+ """
739
+ independent variable predefined value changed
740
+ """
741
+ if self.selected_twvariables_row != -1:
742
+ self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
743
+ if not self.lePredefined.hasFocus():
744
+ r, msg = self.check_indep_var_config()
745
+ if not r:
746
+ QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
747
+
748
+ def leSetValues_changed(self):
749
+ """
750
+ independent variable available values changed
751
+ """
752
+ if self.selected_twvariables_row != -1:
753
+ self.twVariables.item(self.selected_twvariables_row, 4).setText(self.leSetValues.text())
754
+
755
+ def dte_default_date_changed(self):
756
+ """
757
+ independent variable default timestamp changed
758
+ """
759
+ if self.selected_twvariables_row != -1:
760
+ self.twVariables.item(self.selected_twvariables_row, 3).setText(
761
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
762
+ )
763
+
764
+ def pbBehaviorsCategories_clicked(self):
765
+ """
766
+ behavioral categories manager
767
+ """
768
+
769
+ bc = BehavioralCategories(self.pj)
770
+
771
+ if bc.exec_():
772
+ self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
773
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
774
+ for index in range(bc.lw.rowCount()):
775
+ self.pj[cfg.BEHAVIORAL_CATEGORIES].append(bc.lw.item(index, 0).text().strip())
776
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][str(index)] = {
777
+ "name": bc.lw.item(index, 0).text().strip(),
778
+ cfg.COLOR: bc.lw.item(index, 1).text(),
779
+ }
780
+
781
+ # sort
782
+ self.pj[cfg.BEHAVIORAL_CATEGORIES] = sorted(self.pj[cfg.BEHAVIORAL_CATEGORIES])
783
+
784
+ # check if behavior belong to removed category
785
+ if bc.removed:
786
+ for row in range(self.twBehaviors.rowCount()):
787
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]):
788
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() == bc.removed:
789
+ if (
790
+ dialog.MessageDialog(
791
+ cfg.programName,
792
+ (
793
+ f"The <b>{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()}</b> behavior belongs "
794
+ "to a behavioral category "
795
+ f"<b>{self.twBehaviors.item(row, cfg.behavioursFields['category']).text()}</b> "
796
+ "that is no more in the behavioral categories list.<br><br>"
797
+ "Remove the behavior from category?"
798
+ ),
799
+ [cfg.YES, cfg.CANCEL],
800
+ )
801
+ == cfg.YES
802
+ ):
803
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText("")
804
+ if bc.renamed:
805
+ for row in range(self.twBehaviors.rowCount()):
806
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]):
807
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() == bc.renamed[0]:
808
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(bc.renamed[1])
809
+
810
+ def twBehaviors_cellDoubleClicked(self, row: int, column: int) -> None:
811
+ """
812
+ manage double-click on ethogram table:
813
+ * color
814
+ * behavioral category
815
+ * modifiers
816
+ * exclusion
817
+ * modifiers coding map
818
+
819
+ Args:
820
+ row (int): row double-clicked
821
+ column (int): column double-clicked
822
+ """
823
+
824
+ # excluded column
825
+ if column == cfg.behavioursFields[cfg.EXCLUDED]:
826
+ self.exclusion_matrix()
827
+
828
+ # coding map
829
+ if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
830
+ if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
831
+ self.behavior_type_changed(row)
832
+ else:
833
+ QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
834
+
835
+ # behavior type
836
+ if column == cfg.behavioursFields["type"]:
837
+ self.behavior_type_doubleclicked(row)
838
+
839
+ # color
840
+ if column == cfg.behavioursFields[cfg.COLOR]:
841
+ self.color_doubleclicked(row)
842
+
843
+ # behavioral category
844
+ if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
845
+ self.category_doubleclicked(row)
846
+
847
+ # modifiers
848
+ if column == cfg.behavioursFields[cfg.MODIFIERS]:
849
+ # check if behavior has coding map
850
+ if (
851
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.CODING_MAP_sp]) is not None
852
+ and self.twBehaviors.item(row, cfg.behavioursFields[cfg.CODING_MAP_sp]).text()
853
+ ):
854
+ QMessageBox.warning(self, cfg.programName, "Use the coding map to set/modify the areas")
855
+ else:
856
+ subjects_list = []
857
+ for subject_row in range(self.twSubjects.rowCount()):
858
+ key = self.twSubjects.item(subject_row, 0).text() if self.twSubjects.item(subject_row, 0) else ""
859
+ subjectName = self.twSubjects.item(subject_row, 1).text().strip() if self.twSubjects.item(subject_row, 1) else ""
860
+ subjects_list.append((subjectName, key))
861
+
862
+ addModifierWindow = add_modifier.addModifierDialog(
863
+ self.twBehaviors.item(row, column).text(),
864
+ subjects=subjects_list,
865
+ ask_at_stop_enabled=self.twBehaviors.item(row, cfg.behavioursFields["type"]).text() == cfg.STATE_EVENT,
866
+ )
867
+ addModifierWindow.setWindowTitle(f'Set modifiers for "{self.twBehaviors.item(row, 2).text()}" behavior')
868
+
869
+ if addModifierWindow.exec_():
870
+ self.twBehaviors.item(row, column).setText(addModifierWindow.get_modifiers())
871
+
872
+ def behavior_type_doubleclicked(self, row):
873
+ """
874
+ select type for behavior
875
+ """
876
+
877
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text() in cfg.BEHAVIOR_TYPES:
878
+ selected = cfg.BEHAVIOR_TYPES.index(self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text())
879
+ else:
880
+ selected = 0
881
+
882
+ new_type, ok = QInputDialog.getItem(self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False)
883
+
884
+ if ok and new_type:
885
+ self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText(new_type)
886
+
887
+ self.behavior_type_changed(row)
888
+
889
+ def color_doubleclicked(self, row: int) -> None:
890
+ """
891
+ select a color for behavior
892
+ Selecting black delete the color
893
+ """
894
+
895
+ col_diag = QColorDialog()
896
+ col_diag.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
897
+
898
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
899
+ current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
900
+ if current_color.isValid():
901
+ print(f"{current_color=}")
902
+ col_diag.setCurrentColor(current_color)
903
+
904
+ if col_diag.exec():
905
+ color = col_diag.currentColor()
906
+ if color.name() == "#000000": # black -> delete color
907
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
908
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
909
+ elif color.isValid():
910
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
911
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
912
+
913
+ def category_doubleclicked(self, row):
914
+ """
915
+ select category for behavior
916
+ """
917
+
918
+ categories = ["None"] + self.pj[cfg.BEHAVIORAL_CATEGORIES] if cfg.BEHAVIORAL_CATEGORIES in self.pj else ["None"]
919
+
920
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() in categories:
921
+ selected = categories.index(self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text())
922
+ else:
923
+ selected = 0
924
+
925
+ category, ok = QInputDialog.getItem(self, "Select a behavioral category", "Behavioral categories", categories, selected, False)
926
+
927
+ if ok and category:
928
+ if category == "None":
929
+ category = ""
930
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(category)
931
+
932
+ def check_variable_default_value(self, txt, varType):
933
+ """
934
+ check if variable default value is compatible with variable type
935
+ """
936
+ # check for numeric type
937
+ if varType == cfg.NUMERIC:
938
+ try:
939
+ if txt:
940
+ float(txt)
941
+ return True
942
+ except Exception:
943
+ return False
944
+
945
+ return True
946
+
947
+ def variableTypeChanged(self, row):
948
+ """
949
+ variable type combobox changed
950
+ """
951
+
952
+ if self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentText() == cfg.SET_OF_VALUES:
953
+ if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() == "NA":
954
+ self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("Double-click to add values")
955
+ else:
956
+ # check if set of values defined
957
+ if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() not in [
958
+ "NA",
959
+ "Double-click to add values",
960
+ ]:
961
+ if dialog.MessageDialog(cfg.programName, "Erase the set of values?", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
962
+ self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).setCurrentIndex(cfg.SET_OF_VALUES_idx)
963
+ return
964
+ else:
965
+ self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
966
+ else:
967
+ self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
968
+
969
+ # check compatibility between variable type and default value
970
+ if not self.check_variable_default_value(
971
+ self.twVariables.item(row, cfg.tw_indVarFields.index("default value")).text(),
972
+ self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentIndex(),
973
+ ):
974
+ QMessageBox.warning(
975
+ self,
976
+ cfg.programName + " - Independent variables error",
977
+ (
978
+ f"The default value ({self.twVariables.item(row, cfg.tw_indVarFields.index('default value')).text()}) "
979
+ f"of variable <b>{self.twVariables.item(row, cfg.tw_indVarFields.index('label')).text()}</b> "
980
+ "is not compatible with variable type"
981
+ ),
982
+ )
983
+
984
+ def check_indep_var_config(self) -> tuple:
985
+ """
986
+ check if default type is compatible with var type
987
+ """
988
+
989
+ existing_var = []
990
+ for r in range(self.twVariables.rowCount()):
991
+ if self.twVariables.item(r, 0).text().strip().upper() in existing_var:
992
+ return (
993
+ False,
994
+ f"Row: {r + 1} - The variable label <b>{self.twVariables.item(r, 0).text()}</b> is already in use.",
995
+ )
996
+
997
+ # check if same lables
998
+ existing_var.append(self.twVariables.item(r, 0).text().strip().upper())
999
+
1000
+ # check default value
1001
+ if self.twVariables.item(r, 2).text() != cfg.TIMESTAMP and not self.check_variable_default_value(
1002
+ self.twVariables.item(r, 3).text(), self.twVariables.item(r, 2).text()
1003
+ ):
1004
+ return False, (
1005
+ f"Row: {r + 1} - "
1006
+ f"The default value ({self.twVariables.item(r, 3).text()}) is not compatible "
1007
+ f"with the variable type ({self.twVariables.item(r, 2).text()})"
1008
+ )
1009
+
1010
+ # check if default value in set of values
1011
+ if self.twVariables.item(r, 2).text() == cfg.SET_OF_VALUES and self.twVariables.item(r, 4).text() == "":
1012
+ return False, "No values were defined in set"
1013
+
1014
+ if (
1015
+ self.twVariables.item(r, 2).text() == cfg.SET_OF_VALUES
1016
+ and self.twVariables.item(r, 4).text()
1017
+ and self.twVariables.item(r, 3).text()
1018
+ and self.twVariables.item(r, 3).text() not in self.twVariables.item(r, 4).text().split(",")
1019
+ ):
1020
+ return (
1021
+ False,
1022
+ f"The default value ({self.twVariables.item(r, 3).text()}) is not contained in set of values",
1023
+ )
1024
+
1025
+ return True, "OK"
1026
+
1027
+ def cbtype_changed(self):
1028
+ self.leSetValues.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
1029
+ self.label_5.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
1030
+
1031
+ self.dte_default_date.setVisible(self.cbType.currentText() == cfg.TIMESTAMP)
1032
+ self.label_9.setVisible(self.cbType.currentText() == cfg.TIMESTAMP)
1033
+ self.lePredefined.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
1034
+ self.label_4.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
1035
+
1036
+ def cbtype_activated(self):
1037
+ if self.cbType.currentText() == cfg.TIMESTAMP:
1038
+ self.twVariables.item(self.selected_twvariables_row, 3).setText(
1039
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
1040
+ )
1041
+ self.twVariables.item(self.selected_twvariables_row, 4).setText("")
1042
+ else:
1043
+ self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
1044
+ self.twVariables.item(self.selected_twvariables_row, 4).setText("")
1045
+
1046
+ # remove spaces after and before comma
1047
+ if self.cbType.currentText() == cfg.SET_OF_VALUES:
1048
+ self.twVariables.item(self.selected_twvariables_row, 4).setText(
1049
+ ",".join([x.strip() for x in self.leSetValues.text().split(",")])
1050
+ )
1051
+
1052
+ self.twVariables.item(self.selected_twvariables_row, 2).setText(self.cbType.currentText())
1053
+
1054
+ r, msg = self.check_indep_var_config()
1055
+
1056
+ if not r:
1057
+ QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
1058
+
1059
+ def pbAddVariable_clicked(self):
1060
+ """
1061
+ add an independent variable
1062
+ """
1063
+
1064
+ logging.debug("add an independent variable")
1065
+ self.twVariables.setRowCount(self.twVariables.rowCount() + 1)
1066
+ self.selected_twvariables_row = self.twVariables.rowCount() - 1
1067
+
1068
+ for idx, field in enumerate(cfg.tw_indVarFields):
1069
+ if field == "type":
1070
+ item = QTableWidgetItem("numeric")
1071
+ else:
1072
+ item = QTableWidgetItem("")
1073
+ self.twVariables.setItem(self.twVariables.rowCount() - 1, idx, item)
1074
+
1075
+ self.twVariables.setCurrentCell(self.twVariables.rowCount() - 1, 0)
1076
+
1077
+ self.twVariables_cellClicked(self.twVariables.rowCount() - 1, 0)
1078
+
1079
+ def pbRemoveVariable_clicked(self):
1080
+ """
1081
+ remove the selected independent variable
1082
+ """
1083
+ logging.debug("remove selected independent variable")
1084
+
1085
+ if not self.twVariables.selectedIndexes():
1086
+ QMessageBox.warning(self, cfg.programName, "Select a variable to remove")
1087
+ else:
1088
+ if dialog.MessageDialog(cfg.programName, "Remove the selected variable?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1089
+ self.twVariables.removeRow(self.twVariables.selectedIndexes()[0].row())
1090
+
1091
+ if self.twVariables.selectedIndexes():
1092
+ self.twVariables_cellClicked(self.twVariables.selectedIndexes()[0].row(), 0)
1093
+ else:
1094
+ self.twVariables_cellClicked(-1, 0)
1095
+
1096
+ def pbImportVarFromProject_clicked(self):
1097
+ """
1098
+ import independent variables from another project
1099
+ """
1100
+
1101
+ project_import_export.import_indep_variables_from_project(self)
1102
+
1103
+ def exclusion_matrix(self):
1104
+ """
1105
+ activate exclusion matrix window
1106
+ """
1107
+
1108
+ if not self.twBehaviors.rowCount():
1109
+ QMessageBox.critical(
1110
+ None,
1111
+ cfg.programName,
1112
+ "The ethogram is empty",
1113
+ QMessageBox.Ok | QMessageBox.Default,
1114
+ QMessageBox.NoButton,
1115
+ )
1116
+ return
1117
+
1118
+ for row in range(self.twBehaviors.rowCount()):
1119
+ if not self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
1120
+ QMessageBox.critical(
1121
+ None,
1122
+ cfg.programName,
1123
+ f"A behavior code is empty at row {row + 1}",
1124
+ QMessageBox.Ok | QMessageBox.Default,
1125
+ QMessageBox.NoButton,
1126
+ )
1127
+ return
1128
+
1129
+ ex = exclusion_matrix.ExclusionMatrix()
1130
+
1131
+ state_behaviors, point_behaviors, allBehaviors, excl, new_excl = [], [], [], {}, {}
1132
+
1133
+ # list of point events
1134
+ for r in range(self.twBehaviors.rowCount()):
1135
+ if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
1136
+ if "Point" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text():
1137
+ point_behaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1138
+
1139
+ # check if point are present and if user want to include them in exclusion matrix
1140
+ include_point_events = cfg.NO
1141
+ if point_behaviors:
1142
+ include_point_events = dialog.MessageDialog(
1143
+ cfg.programName,
1144
+ "Do you want to include the point events in the exclusion matrix?",
1145
+ [cfg.YES, cfg.NO],
1146
+ )
1147
+
1148
+ for r in range(self.twBehaviors.rowCount()):
1149
+ if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
1150
+ if include_point_events == cfg.YES or (
1151
+ include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
1152
+ ):
1153
+ allBehaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1154
+
1155
+ excl[self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()] = (
1156
+ self.twBehaviors.item(r, cfg.behavioursFields["excluded"]).text().split(",")
1157
+ )
1158
+ new_excl[self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()] = []
1159
+
1160
+ if "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text():
1161
+ state_behaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1162
+
1163
+ logging.debug(f"point behaviors: {point_behaviors}")
1164
+ logging.debug(f"state behaviors: {state_behaviors}")
1165
+
1166
+ if not state_behaviors:
1167
+ QMessageBox.critical(
1168
+ None,
1169
+ cfg.programName,
1170
+ "No state events were defined in ethogram",
1171
+ QMessageBox.Ok | QMessageBox.Default,
1172
+ QMessageBox.NoButton,
1173
+ )
1174
+ return
1175
+
1176
+ logging.debug(f"exclusion matrix {excl}")
1177
+
1178
+ # first row contain state events
1179
+ ex.twExclusions.setColumnCount(len(state_behaviors))
1180
+ ex.twExclusions.setHorizontalHeaderLabels(state_behaviors)
1181
+ ex.twExclusions.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
1182
+
1183
+ # first column contains all events: point + state
1184
+ ex.twExclusions.setRowCount(len(point_behaviors + state_behaviors))
1185
+ for idx, header in enumerate(point_behaviors + state_behaviors):
1186
+ item = QTableWidgetItem(header)
1187
+ if header in point_behaviors:
1188
+ item.setBackground(QColor(0, 200, 200))
1189
+ ex.twExclusions.setVerticalHeaderItem(idx, item)
1190
+ ex.twExclusions.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
1191
+
1192
+ ex.allBehaviors = allBehaviors
1193
+ ex.stateBehaviors = state_behaviors
1194
+ ex.point_behaviors = point_behaviors
1195
+
1196
+ ex.checkboxes = {}
1197
+
1198
+ for c, c_name in enumerate(state_behaviors):
1199
+ flag_left_bottom = False
1200
+ for r, r_name in enumerate(point_behaviors + state_behaviors):
1201
+ if c_name == r_name:
1202
+ flag_left_bottom = True
1203
+
1204
+ if c_name != r_name:
1205
+ ex.checkboxes[f"{r_name}|{c_name}"] = QCheckBox()
1206
+ ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet("text-align: center; margin-left:50%; margin-right:50%;")
1207
+
1208
+ if flag_left_bottom:
1209
+ # hide if cell in left-bottom part of table
1210
+ ex.checkboxes[f"{r_name}|{c_name}"].setEnabled(False)
1211
+
1212
+ # connect function when a CB is clicked
1213
+ ex.checkboxes[f"{r_name}|{c_name}"].clicked.connect(ex.cb_clicked)
1214
+ if c_name in excl[r_name]:
1215
+ ex.checkboxes[f"{r_name}|{c_name}"].setChecked(True)
1216
+ ex.twExclusions.setCellWidget(r, c, ex.checkboxes[f"{r_name}|{c_name}"])
1217
+
1218
+ ex.twExclusions.resizeColumnsToContents()
1219
+ # check corresponding checkbox
1220
+ ex.cb_clicked()
1221
+
1222
+ if ex.exec_():
1223
+ for c, c_name in enumerate(state_behaviors):
1224
+ for r, r_name in enumerate(point_behaviors + state_behaviors):
1225
+ if c_name != r_name:
1226
+ if ex.twExclusions.cellWidget(r, c).isChecked():
1227
+ if c_name not in new_excl[r_name]:
1228
+ new_excl[r_name].append(c_name)
1229
+
1230
+ logging.debug(f"new exclusion matrix {new_excl}")
1231
+
1232
+ # update excluded field
1233
+ for r in range(self.twBehaviors.rowCount()):
1234
+ if include_point_events == cfg.YES or (include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()):
1235
+ for e in excl:
1236
+ if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
1237
+ item = QTableWidgetItem(",".join(new_excl[e]))
1238
+ item.setFlags(Qt.ItemIsEnabled)
1239
+ item.setBackground(self.not_editable_column_color())
1240
+ self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
1241
+
1242
+ def remove_all_behaviors(self):
1243
+ if not self.twBehaviors.rowCount():
1244
+ QMessageBox.critical(
1245
+ None,
1246
+ cfg.programName,
1247
+ "The ethogram is empty",
1248
+ QMessageBox.Ok | QMessageBox.Default,
1249
+ QMessageBox.NoButton,
1250
+ )
1251
+ return
1252
+
1253
+ if dialog.MessageDialog(cfg.programName, "Remove all behaviors?", [cfg.YES, cfg.CANCEL]) != cfg.YES:
1254
+ return
1255
+
1256
+ # delete ethogram rows without behavior code
1257
+ for r in range(self.twBehaviors.rowCount() - 1, -1, -1):
1258
+ if not self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
1259
+ self.twBehaviors.removeRow(r)
1260
+
1261
+ # extract all codes to delete
1262
+ codesToDelete = []
1263
+ row_mem = {}
1264
+ for r in range(self.twBehaviors.rowCount() - 1, -1, -1):
1265
+ if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
1266
+ codesToDelete.append(self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text())
1267
+ row_mem[self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text()] = r
1268
+
1269
+ # extract all codes used in observations
1270
+ codesInObs = []
1271
+ for obs in self.pj[cfg.OBSERVATIONS]:
1272
+ events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
1273
+ for event in events:
1274
+ codesInObs.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
1275
+
1276
+ for codeToDelete in codesToDelete:
1277
+ # if code to delete used in obs ask confirmation
1278
+ if codeToDelete in codesInObs:
1279
+ response = dialog.MessageDialog(
1280
+ cfg.programName,
1281
+ f"The code <b>{codeToDelete}</b> is used in observations!",
1282
+ ["Remove", cfg.CANCEL],
1283
+ )
1284
+ if response == "Remove":
1285
+ self.twBehaviors.removeRow(row_mem[codeToDelete])
1286
+ else: # remove without asking
1287
+ self.twBehaviors.removeRow(row_mem[codeToDelete])
1288
+
1289
+ def twBehaviors_cellChanged(self, row, column):
1290
+ """
1291
+ check ethogram
1292
+ """
1293
+
1294
+ keys, codes = [], []
1295
+ self.lbObservationsState.setText("")
1296
+
1297
+ for r in range(self.twBehaviors.rowCount()):
1298
+ # check key
1299
+ if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX):
1300
+ key = self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX).text()
1301
+ # check key length
1302
+ if key.upper() not in list(cfg.function_keys.values()) and len(key) > 1:
1303
+ self.lbObservationsState.setText('<font color="red">Key length &gt; 1</font>')
1304
+ return
1305
+
1306
+ keys.append(key)
1307
+
1308
+ # check code
1309
+ if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX):
1310
+ if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text() in codes:
1311
+ self.lbObservationsState.setText(f'<font color="red">Code duplicated at line {r + 1} </font>')
1312
+ else:
1313
+ if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
1314
+ codes.append(self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text())
1315
+
1316
+ def clone_behavior(self):
1317
+ """
1318
+ clone the selected behavior
1319
+ """
1320
+
1321
+ if not self.twBehaviors.rowCount():
1322
+ QMessageBox.critical(
1323
+ None,
1324
+ cfg.programName,
1325
+ "The ethogram is empty",
1326
+ QMessageBox.Ok | QMessageBox.Default,
1327
+ QMessageBox.NoButton,
1328
+ )
1329
+ return
1330
+
1331
+ if not self.twBehaviors.selectedIndexes():
1332
+ QMessageBox.about(self, cfg.programName, "First select a behavior")
1333
+ else:
1334
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
1335
+
1336
+ row = self.twBehaviors.selectedIndexes()[0].row()
1337
+ for field in cfg.behavioursFields:
1338
+ item = QTableWidgetItem(self.twBehaviors.item(row, cfg.behavioursFields[field]))
1339
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
1340
+ if field in (cfg.TYPE, "category", "excluded", "coding map", "modifiers"):
1341
+ item.setFlags(Qt.ItemIsEnabled)
1342
+ item.setBackground(self.not_editable_column_color())
1343
+ if field == cfg.COLOR:
1344
+ item.setFlags(Qt.ItemIsEnabled)
1345
+ if QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()).isValid():
1346
+ item.setBackground(QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()))
1347
+ else:
1348
+ item.setBackground(self.not_editable_column_color())
1349
+
1350
+ self.twBehaviors.scrollToBottom()
1351
+
1352
+ def remove_behavior(self):
1353
+ """
1354
+ remove behavior
1355
+ """
1356
+
1357
+ if not self.twBehaviors.rowCount():
1358
+ QMessageBox.critical(
1359
+ None,
1360
+ cfg.programName,
1361
+ "The ethogram is empty",
1362
+ QMessageBox.Ok | QMessageBox.Default,
1363
+ QMessageBox.NoButton,
1364
+ )
1365
+ return
1366
+
1367
+ if not self.twBehaviors.selectedIndexes():
1368
+ QMessageBox.warning(self, cfg.programName, "Select a behaviour to be removed")
1369
+ return
1370
+
1371
+ if dialog.MessageDialog(cfg.programName, "Remove the selected behavior?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1372
+ # check if behavior already used in observations
1373
+ codeToDelete = self.twBehaviors.item(self.twBehaviors.selectedIndexes()[0].row(), 2).text()
1374
+ for obs_id in self.pj[cfg.OBSERVATIONS]:
1375
+ if codeToDelete in [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]]:
1376
+ if (
1377
+ dialog.MessageDialog(cfg.programName, "The code to remove is used in observations!", [cfg.REMOVE, cfg.CANCEL])
1378
+ == cfg.CANCEL
1379
+ ):
1380
+ return
1381
+ break
1382
+
1383
+ self.twBehaviors.removeRow(self.twBehaviors.selectedIndexes()[0].row())
1384
+ self.twBehaviors_cellChanged(0, 0)
1385
+
1386
+ def add_behavior(self):
1387
+ """
1388
+ add new behavior to ethogram
1389
+ """
1390
+
1391
+ # Add behavior to table
1392
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
1393
+ for field_type in cfg.behavioursFields:
1394
+ item = QTableWidgetItem()
1395
+ if field_type == cfg.TYPE:
1396
+ item.setText("Point event")
1397
+ # no manual editing, gray back ground
1398
+ if field_type in (cfg.TYPE, cfg.COLOR, "category", cfg.MODIFIERS, "modifiers", "excluded", "coding map"):
1399
+ item.setFlags(Qt.ItemIsEnabled)
1400
+ # item.setBackground(QColor(230, 230, 230))
1401
+ item.setBackground(self.not_editable_column_color())
1402
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
1403
+ self.twBehaviors.scrollToBottom()
1404
+
1405
+ def behavior_type_changed(self, row: int) -> None:
1406
+ """
1407
+ event type combobox changed
1408
+ """
1409
+
1410
+ if cfg.CODING_MAP_sp in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
1411
+ # let user select a coding maop
1412
+ file_name, _ = QFileDialog().getOpenFileName(
1413
+ self,
1414
+ f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1415
+ "",
1416
+ "BORIS map files (*.boris_map);;All files (*)",
1417
+ )
1418
+ if file_name:
1419
+ try:
1420
+ new_map = json.loads(open(file_name, "r").read())
1421
+ except Exception:
1422
+ QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
1423
+ return
1424
+ self.pj[cfg.CODING_MAP][new_map["name"]] = new_map
1425
+
1426
+ # add modifiers from coding map areas
1427
+ modifstr = json.dumps(
1428
+ {
1429
+ "0": {
1430
+ "name": new_map["name"],
1431
+ "type": cfg.MULTI_SELECTION,
1432
+ "values": list(sorted(new_map["areas"].keys())),
1433
+ }
1434
+ }
1435
+ )
1436
+
1437
+ self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).setText(modifstr)
1438
+ self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText(new_map["name"])
1439
+
1440
+ else:
1441
+ # if coding map already exists do not reset the behavior type if no filename selected
1442
+ if not self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text():
1443
+ QMessageBox.critical(self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" ')
1444
+ self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText("Point event")
1445
+ else:
1446
+ self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText("")
1447
+
1448
+ def add_subject(self):
1449
+ """
1450
+ add a subject
1451
+ """
1452
+
1453
+ self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
1454
+ for col in range(len(cfg.subjectsFields)):
1455
+ item = QTableWidgetItem("")
1456
+ self.twSubjects.setItem(self.twSubjects.rowCount() - 1, col, item)
1457
+ self.twSubjects.scrollToBottom()
1458
+
1459
+ def remove_subject(self):
1460
+ """
1461
+ remove selected subject from subjects list
1462
+ control before if subject used in observations
1463
+ """
1464
+
1465
+ if not self.twSubjects.selectedIndexes():
1466
+ QMessageBox.warning(self, cfg.programName, "Select a subject to remove")
1467
+ else:
1468
+ if dialog.MessageDialog(cfg.programName, "Remove the selected subject?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1469
+ flagDel = False
1470
+ if self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1):
1471
+ subjectToDelete = self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1).text() # 1: subject name
1472
+
1473
+ subjectsInObs = []
1474
+ for obs in self.pj[cfg.OBSERVATIONS]:
1475
+ events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
1476
+ for event in events:
1477
+ subjectsInObs.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
1478
+ if subjectToDelete in subjectsInObs:
1479
+ if (
1480
+ dialog.MessageDialog(
1481
+ cfg.programName,
1482
+ "The subject to remove is used in observations!",
1483
+ [cfg.REMOVE, cfg.CANCEL],
1484
+ )
1485
+ == cfg.REMOVE
1486
+ ):
1487
+ flagDel = True
1488
+ else:
1489
+ # code not used
1490
+ flagDel = True
1491
+ else:
1492
+ flagDel = True
1493
+
1494
+ if flagDel:
1495
+ self.twSubjects.removeRow(self.twSubjects.selectedIndexes()[0].row())
1496
+
1497
+ self.twSubjects_cellChanged(0, 0)
1498
+
1499
+ def remove_all_subjects(self):
1500
+ """
1501
+ remove all subjects.
1502
+ Verify if they are used in observations
1503
+ """
1504
+
1505
+ if not self.twSubjects.rowCount():
1506
+ return
1507
+
1508
+ if dialog.MessageDialog(cfg.programName, "Remove all subjects?", [cfg.YES, cfg.CANCEL]) != cfg.YES:
1509
+ return
1510
+
1511
+ # delete ethogram rows without behavior code
1512
+ for r in range(self.twSubjects.rowCount() - 1, -1, -1):
1513
+ if not self.twSubjects.item(r, 1).text(): # no name
1514
+ self.twSubjects.removeRow(r)
1515
+
1516
+ # extract all subjects names to delete
1517
+ namesToDelete: list = []
1518
+ row_mem: dict = {}
1519
+ for r in range(self.twSubjects.rowCount() - 1, -1, -1):
1520
+ if self.twSubjects.item(r, 1).text():
1521
+ namesToDelete.append(self.twSubjects.item(r, 1).text())
1522
+ row_mem[self.twSubjects.item(r, 1).text()] = r
1523
+
1524
+ # extract all subjects name used in observations
1525
+ namesInObs: list = []
1526
+ for obs in self.pj[cfg.OBSERVATIONS]:
1527
+ events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
1528
+ for event in events:
1529
+ namesInObs.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
1530
+
1531
+ flag_force: bool = False
1532
+ for nameToDelete in namesToDelete:
1533
+ # if name to delete used in obs ask confirmation
1534
+ if nameToDelete in namesInObs and not flag_force:
1535
+ response = dialog.MessageDialog(
1536
+ cfg.programName,
1537
+ f"The subject <b>{nameToDelete}</b> is used in observations!",
1538
+ ["Force removing of all subjects", cfg.REMOVE, cfg.CANCEL],
1539
+ )
1540
+ if response == "Force removing of all subjects":
1541
+ flag_force = True
1542
+ self.twSubjects.removeRow(row_mem[nameToDelete])
1543
+
1544
+ if response == cfg.REMOVE:
1545
+ self.twSubjects.removeRow(row_mem[nameToDelete])
1546
+ else: # remove without asking
1547
+ self.twSubjects.removeRow(row_mem[nameToDelete])
1548
+
1549
+ self.twSubjects_cellChanged(0, 0)
1550
+
1551
+ def twSubjects_cellChanged(self, row: int, column: int) -> None:
1552
+ """
1553
+ check if subject not unique
1554
+ """
1555
+
1556
+ subjects: list = []
1557
+ """keys: list = []"""
1558
+ self.lbSubjectsState.setText("")
1559
+
1560
+ for r in range(self.twSubjects.rowCount()):
1561
+ # check key
1562
+ if self.twSubjects.item(r, 0):
1563
+ # check key length
1564
+ if (
1565
+ self.twSubjects.item(r, 0).text().upper() not in list(cfg.function_keys.values())
1566
+ and len(self.twSubjects.item(r, 0).text()) > 1
1567
+ ):
1568
+ self.lbSubjectsState.setText(
1569
+ (
1570
+ f'<font color="red">Error on key {self.twSubjects.item(r, 0).text()} for subject!</font>'
1571
+ "The key is too long (keys must be of one character"
1572
+ " except for function keys _F1, F2..._)"
1573
+ )
1574
+ )
1575
+ return
1576
+
1577
+ # control of duplicated key removed 2024-01-29
1578
+ """
1579
+ if self.twSubjects.item(r, 0).text() in keys:
1580
+ self.lbSubjectsState.setText(f'<font color="red">Key duplicated at row # {r + 1}</font>')
1581
+ else:
1582
+ if self.twSubjects.item(r, 0).text():
1583
+ keys.append(self.twSubjects.item(r, 0).text())
1584
+ """
1585
+
1586
+ # check subject
1587
+ if self.twSubjects.item(r, 1):
1588
+ if self.twSubjects.item(r, 1).text() in subjects:
1589
+ self.lbSubjectsState.setText(f'<font color="red">Subject duplicated at row # {r + 1}</font>')
1590
+ else:
1591
+ if self.twSubjects.item(r, 1).text():
1592
+ subjects.append(self.twSubjects.item(r, 1).text())
1593
+
1594
+ def twVariables_cellClicked(self, row, column):
1595
+ """
1596
+ check if variable default values are compatible with variable type
1597
+ """
1598
+
1599
+ self.selected_twvariables_row = row
1600
+ logging.debug(f"selected row: {self.selected_twvariables_row}")
1601
+
1602
+ if self.selected_twvariables_row == -1:
1603
+ for widget in (
1604
+ self.leLabel,
1605
+ self.leDescription,
1606
+ self.cbType,
1607
+ self.lePredefined,
1608
+ self.dte_default_date,
1609
+ self.leSetValues,
1610
+ ):
1611
+ widget.setEnabled(False)
1612
+ self.leLabel.setText("")
1613
+ self.leDescription.setText("")
1614
+ self.lePredefined.setText("")
1615
+ self.leSetValues.setText("")
1616
+
1617
+ self.cbType.clear()
1618
+ return
1619
+
1620
+ # enable widget for indep var setting
1621
+ for widget in (
1622
+ self.leLabel,
1623
+ self.leDescription,
1624
+ self.cbType,
1625
+ self.lePredefined,
1626
+ self.dte_default_date,
1627
+ self.leSetValues,
1628
+ ):
1629
+ widget.setEnabled(True)
1630
+
1631
+ self.leLabel.setText(self.twVariables.item(row, 0).text())
1632
+ self.leDescription.setText(self.twVariables.item(row, 1).text())
1633
+ self.lePredefined.setText(self.twVariables.item(row, 3).text())
1634
+ self.leSetValues.setText(self.twVariables.item(row, 4).text())
1635
+ if self.twVariables.item(row, 2).text() == cfg.TIMESTAMP:
1636
+ if len(self.twVariables.item(row, 3).text()) == len("yyyy-MM-ddTHH:mm:ss.zzz"):
1637
+ datetime_format = "yyyy-MM-ddThh:mm:ss.zzz"
1638
+ if len(self.twVariables.item(row, 3).text()) == len("yyyy-MM-ddTHH:mm:ss"):
1639
+ datetime_format = "yyyy-MM-ddThh:mm:ss"
1640
+
1641
+ self.dte_default_date.setDateTime(QDateTime.fromString(self.twVariables.item(row, 3).text(), datetime_format))
1642
+
1643
+ self.cbType.clear()
1644
+ self.cbType.addItems(cfg.AVAILABLE_INDEP_VAR_TYPES)
1645
+ self.cbType.setCurrentIndex(cfg.NUMERIC_idx)
1646
+
1647
+ self.cbType.setCurrentIndex(cfg.AVAILABLE_INDEP_VAR_TYPES.index(self.twVariables.item(row, 2).text()))
1648
+
1649
+ def pbCancel_clicked(self):
1650
+ if self.flag_modified:
1651
+ if dialog.MessageDialog("BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]) == cfg.OK:
1652
+ self.reject()
1653
+ else:
1654
+ self.reject()
1655
+
1656
+ def check_ethogram(self) -> dict:
1657
+ """
1658
+ check ethogram for various parameter
1659
+ returns ethogram dict or {cfg.CANCEL: True} in case of error
1660
+
1661
+ """
1662
+ # store behaviors
1663
+ missing_data: list = []
1664
+ checked_ethogram: dict = {}
1665
+
1666
+ # Ethogram
1667
+ # coding maps in ethogram
1668
+
1669
+ # check for leading/trailing space in behaviors and modifiers
1670
+ code_with_leading_trailing_spaces, modifiers_with_leading_trailing_spaces = [], []
1671
+ for r in range(self.twBehaviors.rowCount()):
1672
+ if (
1673
+ self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
1674
+ != self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text().strip()
1675
+ ):
1676
+ code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1677
+
1678
+ if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text():
1679
+ try:
1680
+ modifiers_dict = (
1681
+ json.loads(self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text())
1682
+ if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text()
1683
+ else {}
1684
+ )
1685
+ for k in modifiers_dict:
1686
+ for value in modifiers_dict[k]["values"]:
1687
+ modif_code = value.split(" (")[0]
1688
+ if modif_code.strip() != modif_code:
1689
+ modifiers_with_leading_trailing_spaces.append(modif_code)
1690
+ except Exception:
1691
+ logging.critical("error checking leading/trailing spaces in modifiers")
1692
+
1693
+ remove_leading_trailing_spaces = cfg.NO
1694
+ if code_with_leading_trailing_spaces:
1695
+ remove_leading_trailing_spaces = dialog.MessageDialog(
1696
+ cfg.programName,
1697
+ (
1698
+ "<b>Warning!</b> Some leading and/or trailing spaces are present"
1699
+ " in the following behaviors code(s):<br>"
1700
+ "<b>"
1701
+ f"{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '&#9608;') for x in code_with_leading_trailing_spaces])}"
1702
+ "</b><br><br>"
1703
+ "Do you want to remove the leading and trailing spaces (visualized as black boxes) from behaviors?<br><br>"
1704
+ """<font color="red"><b>Be careful with this option"""
1705
+ """ if you have already done observations!</b></font>"""
1706
+ ),
1707
+ [cfg.YES, cfg.NO, cfg.CANCEL],
1708
+ )
1709
+ if remove_leading_trailing_spaces == cfg.CANCEL:
1710
+ return {cfg.CANCEL: True}
1711
+
1712
+ remove_leading_trailing_spaces_in_modifiers = cfg.NO
1713
+ if modifiers_with_leading_trailing_spaces:
1714
+ remove_leading_trailing_spaces_in_modifiers = dialog.MessageDialog(
1715
+ cfg.programName,
1716
+ (
1717
+ "<b>Warning!</b> Some leading and/or trailing spaces are present"
1718
+ " in the following modifier(s):<br><b>"
1719
+ f"{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '&#9608;') for x in set(modifiers_with_leading_trailing_spaces)])}"
1720
+ "</b><br><br>Do you want to remove the leading and trailing spaces (visualized as black boxes) from modifiers?<br><br>"
1721
+ """<font color="red"><b>Be careful with this option"""
1722
+ """ if you have already done observations!</b></font>"""
1723
+ ),
1724
+ (cfg.YES, cfg.NO, cfg.CANCEL),
1725
+ )
1726
+ if remove_leading_trailing_spaces_in_modifiers == cfg.CANCEL:
1727
+ return {cfg.CANCEL: True}
1728
+
1729
+ codingMapsList = []
1730
+ for r in range(self.twBehaviors.rowCount()):
1731
+ row = {}
1732
+ for field in cfg.behavioursFields:
1733
+ if self.twBehaviors.item(r, cfg.behavioursFields[field]):
1734
+ # check for | char in code
1735
+ if field == cfg.BEHAVIOR_CODE and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
1736
+ QMessageBox.warning(
1737
+ self,
1738
+ cfg.programName,
1739
+ (
1740
+ "The pipe (|) character is not allowed in code "
1741
+ f"<b>{self.twBehaviors.item(r, cfg.behavioursFields[field]).text()}</b> !"
1742
+ ),
1743
+ )
1744
+ return {cfg.CANCEL: True}
1745
+
1746
+ if remove_leading_trailing_spaces == cfg.YES:
1747
+ row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text().strip()
1748
+ else:
1749
+ row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
1750
+
1751
+ if field == "modifiers" and row["modifiers"]:
1752
+ if remove_leading_trailing_spaces_in_modifiers == cfg.YES:
1753
+ try:
1754
+ modifiers_dict = json.loads(row["modifiers"]) if row["modifiers"] else {}
1755
+ for k in modifiers_dict:
1756
+ for idx, value in enumerate(modifiers_dict[k]["values"]):
1757
+ modif_code = value.split(" (")[0]
1758
+
1759
+ modifiers_dict[k]["values"][idx] = modifiers_dict[k]["values"][idx].replace(
1760
+ modif_code, modif_code.strip()
1761
+ )
1762
+
1763
+ row["modifiers"] = dict(modifiers_dict)
1764
+ except Exception:
1765
+ logging.critical("Error removing leading/trailing spaces in modifiers")
1766
+
1767
+ QMessageBox.critical(self, cfg.programName, "Error removing leading/trailing spaces in modifiers")
1768
+
1769
+ else:
1770
+ row["modifiers"] = json.loads(row["modifiers"]) if row["modifiers"] else {}
1771
+ else:
1772
+ row[field] = ""
1773
+
1774
+ if (row["type"]) and (row[cfg.BEHAVIOR_CODE]):
1775
+ checked_ethogram[str(len(checked_ethogram))] = row
1776
+ else:
1777
+ missing_data.append(str(r + 1))
1778
+
1779
+ if self.twBehaviors.item(r, cfg.behavioursFields["coding map"]).text():
1780
+ codingMapsList.append(self.twBehaviors.item(r, cfg.behavioursFields["coding map"]).text())
1781
+
1782
+ # remove coding map from project if not in ethogram
1783
+ cmToDelete = []
1784
+ for cm in self.pj[cfg.CODING_MAP]:
1785
+ if cm not in codingMapsList:
1786
+ cmToDelete.append(cm)
1787
+
1788
+ for cm in cmToDelete:
1789
+ del self.pj[cfg.CODING_MAP][cm]
1790
+
1791
+ if missing_data:
1792
+ QMessageBox.warning(self, cfg.programName, f"Missing data in ethogram at row {','.join(missing_data)} !")
1793
+ return {cfg.CANCEL: True}
1794
+
1795
+ # check if behavior belong to category that is not in categories list
1796
+ missing_behavior_category: list = []
1797
+ for idx in checked_ethogram:
1798
+ if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
1799
+ if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
1800
+ if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
1801
+ missing_behavior_category.append(
1802
+ (checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
1803
+ )
1804
+ if missing_behavior_category:
1805
+ response = dialog.MessageDialog(
1806
+ f"{cfg.programName} - Behavioral categories",
1807
+ (
1808
+ "The behavioral category/ies<br> "
1809
+ f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)<br>' for x in missing_behavior_category]))} "
1810
+ "are not defined in behavioral categories list.<br>"
1811
+ ),
1812
+ ["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
1813
+ )
1814
+ if response == "Add behavioral category/ies":
1815
+ if cfg.BEHAVIORAL_CATEGORIES_CONF not in self.pj:
1816
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
1817
+ for x1 in set(x[1] for x in missing_behavior_category):
1818
+ self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1)
1819
+
1820
+ if self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]:
1821
+ index = str(max([int(k) for k in self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]]) + 1)
1822
+ else:
1823
+ index = "0"
1824
+
1825
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][index] = {
1826
+ "name": x1,
1827
+ cfg.COLOR: "",
1828
+ }
1829
+
1830
+ if response == cfg.CANCEL:
1831
+ return {cfg.CANCEL: True}
1832
+
1833
+ # delete coding maps loaded in pj and not cited in ethogram
1834
+ return checked_ethogram
1835
+
1836
+ def pbOK_clicked(self):
1837
+ """
1838
+ verify project configuration
1839
+ """
1840
+
1841
+ if self.lbObservationsState.text():
1842
+ QMessageBox.warning(self, cfg.programName, self.lbObservationsState.text())
1843
+ return
1844
+
1845
+ if self.lbSubjectsState.text():
1846
+ QMessageBox.warning(self, cfg.programName, self.lbSubjectsState.text())
1847
+ return
1848
+
1849
+ self.pj[cfg.PROJECT_NAME] = self.leProjectName.text().strip()
1850
+ self.pj[cfg.PROJECT_DATE] = self.dteDate.dateTime().toString(Qt.ISODate)
1851
+ self.pj[cfg.PROJECT_DESCRIPTION] = self.teDescription.toPlainText()
1852
+
1853
+ # time format
1854
+ if self.rbSeconds.isChecked():
1855
+ self.pj[cfg.TIME_FORMAT] = cfg.S
1856
+ if self.rbHMS.isChecked():
1857
+ self.pj[cfg.TIME_FORMAT] = cfg.HHMMSS
1858
+
1859
+ # store subjects
1860
+ self.subjects_conf: dict = {}
1861
+
1862
+ # check for leading/trailing spaces in subjects names
1863
+ subjects_name_with_leading_trailing_spaces = ""
1864
+ for row in range(self.twSubjects.rowCount()):
1865
+ if self.twSubjects.item(row, 1):
1866
+ if self.twSubjects.item(row, 1).text() != self.twSubjects.item(row, 1).text().strip():
1867
+ subjects_name_with_leading_trailing_spaces += f'"{self.twSubjects.item(row, 1).text()}" '
1868
+
1869
+ remove_leading_trailing_spaces = cfg.NO
1870
+ if subjects_name_with_leading_trailing_spaces:
1871
+ remove_leading_trailing_spaces = dialog.MessageDialog(
1872
+ cfg.programName,
1873
+ (
1874
+ "Attention! Some leading and/or trailing spaces are present in the following <b>subject name(s)</b>:<br>"
1875
+ f"<b>{subjects_name_with_leading_trailing_spaces}</b><br><br>"
1876
+ "Do you want to remove the leading and trailing spaces?<br><br>"
1877
+ '<font color="red"><b>Be careful with this option'
1878
+ " if you have already done observations!</b></font>"
1879
+ ),
1880
+ [cfg.YES, cfg.NO],
1881
+ )
1882
+
1883
+ # check subjects
1884
+ for row in range(self.twSubjects.rowCount()):
1885
+ # check key
1886
+ key: str = ""
1887
+ if self.twSubjects.item(row, 0):
1888
+ key = self.twSubjects.item(row, 0).text()
1889
+
1890
+ # check subject name
1891
+ if self.twSubjects.item(row, 1):
1892
+ if remove_leading_trailing_spaces == cfg.YES:
1893
+ subjectName = self.twSubjects.item(row, 1).text().strip()
1894
+ else:
1895
+ subjectName = self.twSubjects.item(row, 1).text()
1896
+
1897
+ # check if subject name is empty
1898
+ if subjectName == "":
1899
+ QMessageBox.warning(self, cfg.programName, f"The subject name can not be empty (check row #{row + 1}).")
1900
+ return
1901
+
1902
+ if "|" in subjectName:
1903
+ QMessageBox.warning(
1904
+ self,
1905
+ cfg.programName,
1906
+ f"The pipe (|) character is not allowed in subject name <b>{subjectName}</b>",
1907
+ )
1908
+ return
1909
+ else:
1910
+ QMessageBox.warning(self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}")
1911
+ return
1912
+
1913
+ # description
1914
+ subjectDescription: str = ""
1915
+ if self.twSubjects.item(row, 2):
1916
+ subjectDescription = self.twSubjects.item(row, 2).text().strip()
1917
+
1918
+ self.subjects_conf[str(len(self.subjects_conf))] = {
1919
+ "key": key,
1920
+ "name": subjectName,
1921
+ "description": subjectDescription,
1922
+ }
1923
+
1924
+ # check if coded subjects are defined in the subjects list
1925
+ subjects_list: list = [self.subjects_conf[x]["name"] for x in self.subjects_conf]
1926
+ coded_subjects = set(
1927
+ util.flatten_list([[y[1] for y in self.pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in self.pj[cfg.OBSERVATIONS]])
1928
+ )
1929
+
1930
+ not_defined_subjects: list = []
1931
+ for subject in coded_subjects:
1932
+ if subject and subject not in subjects_list:
1933
+ not_defined_subjects.append(subject)
1934
+
1935
+ if not_defined_subjects:
1936
+ QMessageBox.warning(
1937
+ self,
1938
+ cfg.programName,
1939
+ f"The coded subject(s) <b>{', '.join(not_defined_subjects)}</b> is/are not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.",
1940
+ )
1941
+ return
1942
+
1943
+ self.pj[cfg.SUBJECTS] = dict(self.subjects_conf)
1944
+
1945
+ # check ethogram
1946
+ r = dict(self.check_ethogram())
1947
+ if cfg.CANCEL in r:
1948
+ return
1949
+ self.pj[cfg.ETHOGRAM] = dict(r)
1950
+
1951
+ # independent variables
1952
+ r, msg = self.check_indep_var_config()
1953
+ if not r:
1954
+ QMessageBox.warning(self, cfg.programName + " - Independent variables error", msg)
1955
+ return
1956
+
1957
+ self.indVar = {}
1958
+ for r in range(self.twVariables.rowCount()):
1959
+ row = {}
1960
+ for idx, field in enumerate(cfg.tw_indVarFields):
1961
+ if self.twVariables.item(r, idx):
1962
+ # check if label is empty
1963
+ if field == "label" and self.twVariables.item(r, idx).text() == "":
1964
+ QMessageBox.warning(
1965
+ self,
1966
+ cfg.programName,
1967
+ f"The label of an indipendent variable can not be empty (check row #{r + 1}).",
1968
+ )
1969
+ return
1970
+
1971
+ row[field] = self.twVariables.item(r, idx).text().strip()
1972
+ else:
1973
+ row[field] = ""
1974
+
1975
+ self.indVar[str(len(self.indVar))] = row
1976
+
1977
+ self.pj[cfg.INDEPENDENT_VARIABLES] = dict(self.indVar)
1978
+
1979
+ # converters
1980
+ converters: dict = {}
1981
+ for row in range(self.tw_converters.rowCount()):
1982
+ converters[self.tw_converters.item(row, 0).text()] = {
1983
+ "name": self.tw_converters.item(row, 0).text(),
1984
+ "description": self.tw_converters.item(row, 1).text(),
1985
+ "code": self.tw_converters.item(row, 2).text().replace("@", "\n"),
1986
+ }
1987
+ self.pj[cfg.CONVERTERS] = dict(converters)
1988
+
1989
+ self.accept()
1990
+
1991
+ def load_converters_in_table(self):
1992
+ """
1993
+ load converters in table
1994
+ """
1995
+ self.tw_converters.setRowCount(0)
1996
+
1997
+ for converter in sorted(self.converters.keys()):
1998
+ self.tw_converters.setRowCount(self.tw_converters.rowCount() + 1)
1999
+ self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 0, QTableWidgetItem(converter)) # id / name
2000
+ self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"]))
2001
+ self.tw_converters.setItem(
2002
+ self.tw_converters.rowCount() - 1,
2003
+ 2,
2004
+ QTableWidgetItem(self.converters[converter]["code"].replace("\n", "@")),
2005
+ )
2006
+
2007
+ [self.tw_converters.resizeColumnToContents(idx) for idx in [0, 1]]