boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  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 +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +25 -33
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +627 -236
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +95 -29
  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.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/project.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -24,10 +24,13 @@ import json
24
24
  import logging
25
25
  import re
26
26
 
27
- from PyQt5.QtCore import Qt
28
- from PyQt5.QtGui import QColor
29
- from PyQt5.QtWidgets import (
27
+ from PySide6.QtCore import Qt, QDateTime
28
+ from PySide6.QtGui import QColor
29
+ from PySide6.QtWidgets import (
30
+ QAbstractItemView,
31
+ QApplication,
30
32
  QCheckBox,
33
+ QColorDialog,
31
34
  QDialog,
32
35
  QFileDialog,
33
36
  QHBoxLayout,
@@ -35,13 +38,12 @@ from PyQt5.QtWidgets import (
35
38
  QInputDialog,
36
39
  QLabel,
37
40
  QLineEdit,
38
- QListWidget,
39
- QListWidgetItem,
40
41
  QMenu,
41
42
  QMessageBox,
42
43
  QPushButton,
43
44
  QSizePolicy,
44
45
  QSpacerItem,
46
+ QTableWidget,
45
47
  QTableWidgetItem,
46
48
  QVBoxLayout,
47
49
  )
@@ -73,11 +75,49 @@ class BehavioralCategories(QDialog):
73
75
  self.label.setText("Behavioral categories")
74
76
  self.vbox.addWidget(self.label)
75
77
 
76
- self.lw = QListWidget()
78
+ # self.lw = QListWidget()
79
+ self.lw = QTableWidget()
80
+ self.lw.cellDoubleClicked[int, int].connect(self.lw_double_clicked)
77
81
 
78
82
  # add categories
79
- for category in sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])):
80
- self.lw.addItem(QListWidgetItem(category))
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)
81
121
 
82
122
  self.vbox.addWidget(self.lw)
83
123
 
@@ -94,8 +134,8 @@ class BehavioralCategories(QDialog):
94
134
  self.vbox.addLayout(self.hbox0)
95
135
 
96
136
  hbox1 = QHBoxLayout()
97
- self.pbOK = QPushButton("OK", clicked=self.accept)
98
- self.pbCancel = QPushButton("Cancel", clicked=self.accept)
137
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
138
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.accept)
99
139
 
100
140
  spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
101
141
  hbox1.addItem(spacerItem)
@@ -105,23 +145,111 @@ class BehavioralCategories(QDialog):
105
145
 
106
146
  self.setLayout(self.vbox)
107
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
+
108
220
  def add_behavioral_category(self):
109
221
  """
110
222
  add a behavioral category
111
223
  """
112
224
  category, ok = QInputDialog.getText(self, "New behavioral category", "Category name:")
113
225
  if ok:
114
- self.lw.addItem(QListWidgetItem(category))
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)
115
233
 
116
234
  def remove_behavioral_category(self):
117
235
  """
118
236
  remove the selected behavioral category
119
237
  """
120
238
 
121
- for SelectedItem in self.lw.selectedItems():
239
+ for selected_item in self.lw.selectedItems():
122
240
  # check if behavioral category is in use
123
- category_to_remove = self.lw.item(self.lw.row(SelectedItem)).text().strip()
124
- behaviors_in_category = []
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 = []
125
253
  for idx in self.pj[cfg.ETHOGRAM]:
126
254
  if (
127
255
  cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
@@ -130,16 +258,15 @@ class BehavioralCategories(QDialog):
130
258
  behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
131
259
  flag_remove = False
132
260
  if behaviors_in_category:
133
-
134
261
  flag_remove = (
135
262
  dialog.MessageDialog(
136
263
  cfg.programName,
137
264
  (
138
- "Some behavior belong to the <b>{1}</b> to remove:<br>"
139
- "{0}<br>"
265
+ f"Some behavior belong to the <b>{category_to_remove}</b> to remove:<br>"
266
+ f"{'<br>'.join(behaviors_in_category)}<br>"
140
267
  "<br>Some features may not be available anymore.<br>"
141
- ).format("<br>".join(behaviors_in_category), category_to_remove),
142
- ["Remove category", cfg.CANCEL],
268
+ ),
269
+ ("Remove category", cfg.CANCEL),
143
270
  )
144
271
  == "Remove category"
145
272
  )
@@ -148,17 +275,18 @@ class BehavioralCategories(QDialog):
148
275
  flag_remove = True
149
276
 
150
277
  if flag_remove:
151
- self.lw.takeItem(self.lw.row(SelectedItem))
278
+ self.lw.removeRow(self.lw.row(selected_item))
152
279
  self.removed = category_to_remove
280
+
153
281
  self.accept()
154
282
 
155
- def pb_rename_category_clicked(self):
283
+ def pb_rename_category_clicked(self, row: int):
156
284
  """
157
285
  rename the selected behavioral category
158
286
  """
159
- for SelectedItem in self.lw.selectedItems():
287
+ for selected_item in self.lw.selectedItems():
160
288
  # check if behavioral category is in use
161
- category_to_rename = self.lw.item(self.lw.row(SelectedItem)).text().strip()
289
+ category_to_rename = self.lw.item(self.lw.row(selected_item), 0).text().strip()
162
290
  behaviors_in_category = []
163
291
  for idx in self.pj[cfg.ETHOGRAM]:
164
292
  if (
@@ -172,9 +300,7 @@ class BehavioralCategories(QDialog):
172
300
  flag_rename = (
173
301
  dialog.MessageDialog(
174
302
  cfg.programName,
175
- ("Some behavior belong to the <b>{1}</b> to rename:<br>" "{0}<br>").format(
176
- "<br>".join(behaviors_in_category), category_to_rename
177
- ),
303
+ (f"Some behavior belong to the <b>{category_to_rename}</b> to rename:<br>{'<br>'.join(behaviors_in_category)}<br>"),
178
304
  ["Rename category", cfg.CANCEL],
179
305
  )
180
306
  == "Rename category"
@@ -187,15 +313,14 @@ class BehavioralCategories(QDialog):
187
313
  self, "Rename behavioral category", "New category name:", QLineEdit.Normal, category_to_rename
188
314
  )
189
315
  if ok:
190
- self.lw.item(self.lw.indexFromItem(SelectedItem).row()).setText(new_category_name)
316
+ self.lw.item(self.lw.indexFromItem(selected_item).row(), 0).setText(new_category_name)
191
317
  # check behaviors belonging to the renamed category
192
318
  self.renamed = [category_to_rename, new_category_name]
193
- self.accept()
319
+ # self.accept()
194
320
 
195
321
 
196
322
  class projectDialog(QDialog, Ui_dlgProject):
197
323
  def __init__(self, parent=None):
198
-
199
324
  super().__init__()
200
325
 
201
326
  self.setupUi(self)
@@ -211,23 +336,23 @@ class projectDialog(QDialog, Ui_dlgProject):
211
336
  "remove all|Remove all behaviors",
212
337
  "lower|Convert keys to lower case",
213
338
  ]
214
- menu = QMenu()
215
- menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
216
- self.add_button_menu(behavior_button_items, menu)
217
- self.pb_behavior.setMenu(menu)
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)
218
343
 
219
344
  import_button_items = [
220
345
  "boris|from a BORIS project",
221
- "spreadsheet|from a spreadsheet file (XLSX)",
346
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
222
347
  "jwatcher|from a JWatcher project",
223
348
  "text|from a text file (CSV or TSV)",
224
349
  "clipboard|from the clipboard",
225
350
  "repository|from the BORIS repository",
226
351
  ]
227
- menu = QMenu()
228
- menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
229
- self.add_button_menu(import_button_items, menu)
230
- self.pb_import.setMenu(menu)
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)
231
356
 
232
357
  self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
233
358
 
@@ -251,21 +376,21 @@ class projectDialog(QDialog, Ui_dlgProject):
251
376
  "lower|Convert keys to lower case",
252
377
  ]
253
378
 
254
- menu = QMenu()
255
- menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
256
- self.add_button_menu(subjects_button_items, menu)
257
- self.pb_subjects.setMenu(menu)
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)
258
383
 
259
384
  subjects_import_button_items = [
260
385
  "boris|from a BORIS project",
261
- "spreadsheet|from a spreadsheet file (XLSX)",
386
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
262
387
  "text|from a text file (CSV or TSV)",
263
388
  "clipboard|from the clipboard",
264
389
  ]
265
- menu = QMenu()
266
- menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
267
- self.add_button_menu(subjects_import_button_items, menu)
268
- self.pbImportSubjectsFromProject.setMenu(menu)
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)
269
394
 
270
395
  self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
271
396
 
@@ -311,30 +436,41 @@ class projectDialog(QDialog, Ui_dlgProject):
311
436
  self.row_in_modification = -1
312
437
  self.flag_modified = False
313
438
 
314
- for w in [
439
+ for w in (
315
440
  self.le_converter_name,
316
441
  self.le_converter_description,
317
442
  self.pteCode,
318
443
  self.pb_save_converter,
319
444
  self.pb_cancel_converter,
320
- ]:
445
+ ):
321
446
  w.setEnabled(False)
322
447
 
323
448
  # disable widget for indep var setting
324
- for widget in [
449
+ for widget in (
325
450
  self.leLabel,
326
451
  self.le_converter_description,
327
452
  self.cbType,
328
453
  self.lePredefined,
329
454
  self.dte_default_date,
330
455
  self.leSetValues,
331
- ]:
456
+ ):
332
457
  widget.setEnabled(False)
333
458
 
334
459
  self.twBehaviors.horizontalHeader().sortIndicatorChanged.connect(self.sort_twBehaviors)
335
460
  self.twSubjects.horizontalHeader().sortIndicatorChanged.connect(self.sort_twSubjects)
336
461
  self.twVariables.horizontalHeader().sortIndicatorChanged.connect(self.sort_twVariables)
337
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
+
338
474
  def add_button_menu(self, data, menu_obj):
339
475
  """
340
476
  add menu option from dictionary
@@ -448,20 +584,14 @@ class projectDialog(QDialog, Ui_dlgProject):
448
584
 
449
585
  # check if some keys will be duplicated after conversion
450
586
  try:
451
- all_keys = [
452
- self.twBehaviors.item(row, cfg.behavioursFields["key"]).text()
453
- for row in range(self.twBehaviors.rowCount())
454
- ]
587
+ all_keys = [self.twBehaviors.item(row, cfg.behavioursFields["key"]).text() for row in range(self.twBehaviors.rowCount())]
455
588
  except Exception:
456
589
  pass
457
590
  if all_keys == [x.lower() for x in all_keys]:
458
591
  QMessageBox.information(self, cfg.programName, "All keys are already lower case")
459
592
  return
460
593
 
461
- if (
462
- dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
463
- == cfg.CANCEL
464
- ):
594
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
465
595
  return
466
596
 
467
597
  if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
@@ -482,11 +612,10 @@ class projectDialog(QDialog, Ui_dlgProject):
482
612
  )
483
613
 
484
614
  # convert modifier shortcuts
485
- if self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).text():
486
-
615
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text():
487
616
  modifiers_dict = (
488
- eval(self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).text())
489
- if self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).text()
617
+ json.loads(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
618
+ if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text()
490
619
  else {}
491
620
  )
492
621
 
@@ -495,16 +624,12 @@ class projectDialog(QDialog, Ui_dlgProject):
495
624
  for idx2, value in enumerate(modifiers_dict[modifier_set]["values"]):
496
625
  if re.findall(r"\((\w+)\)", value):
497
626
  modifiers_dict[modifier_set]["values"][idx2] = (
498
- value.split("(")[0]
499
- + "("
500
- + re.findall(r"\((\w+)\)", value)[0].lower()
501
- + ")"
502
- + value.split(")")[-1]
627
+ value.split("(")[0] + "(" + re.findall(r"\((\w+)\)", value)[0].lower() + ")" + value.split(")")[-1]
503
628
  )
504
629
  except Exception:
505
630
  logging.warning("error during conversion of modifier short cut to lower case")
506
631
 
507
- self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).setText(str(modifiers_dict))
632
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(json.dumps(modifiers_dict))
508
633
 
509
634
  def convert_subjects_keys_to_lower_case(self):
510
635
  """
@@ -512,20 +637,14 @@ class projectDialog(QDialog, Ui_dlgProject):
512
637
  """
513
638
  # check if some keys will be duplicated after conversion
514
639
  try:
515
- all_keys = [
516
- self.twSubjects.item(row, cfg.subjectsFields.index("key")).text()
517
- for row in range(self.twSubjects.rowCount())
518
- ]
640
+ all_keys = [self.twSubjects.item(row, cfg.subjectsFields.index("key")).text() for row in range(self.twSubjects.rowCount())]
519
641
  except Exception:
520
642
  pass
521
643
  if all_keys == [x.lower() for x in all_keys]:
522
644
  QMessageBox.information(self, cfg.programName, "All keys are already lower case")
523
645
  return
524
646
 
525
- if (
526
- dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
527
- == cfg.CANCEL
528
- ):
647
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
529
648
  return
530
649
 
531
650
  if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
@@ -550,47 +669,45 @@ class projectDialog(QDialog, Ui_dlgProject):
550
669
  Add a behaviors coding map from file
551
670
  """
552
671
 
553
- fn = QFileDialog().getOpenFileName(
672
+ file_name, _ = QFileDialog().getOpenFileName(
554
673
  self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
555
674
  )
556
- file_name = fn[0] if type(fn) is tuple else fn
557
- if file_name:
558
- try:
559
- bcm = json.loads(open(file_name, "r").read())
560
- except Exception:
561
- QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
562
- return
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
563
682
 
564
- if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
565
- QMessageBox.critical(
566
- self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map."
567
- )
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.")
568
685
 
569
- if cfg.BEHAVIORS_CODING_MAP not in self.pj:
570
- self.pj[cfg.BEHAVIORS_CODING_MAP] = []
686
+ if cfg.BEHAVIORS_CODING_MAP not in self.pj:
687
+ self.pj[cfg.BEHAVIORS_CODING_MAP] = []
571
688
 
572
- bcm_code_not_found = []
573
- existing_codes = [self.pj[cfg.ETHOGRAM][key]["code"] for key in self.pj[cfg.ETHOGRAM]]
574
- for code in [bcm["areas"][key]["code"] for key in bcm["areas"]]:
575
- if code not in existing_codes:
576
- bcm_code_not_found.append(code)
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)
577
694
 
578
- if bcm_code_not_found:
579
- QMessageBox.warning(
580
- self,
581
- cfg.programName,
582
- ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
583
- "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
584
- ),
585
- )
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
+ )
586
703
 
587
- self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
704
+ self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
588
705
 
589
- self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
706
+ self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
590
707
 
591
- self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
592
- codes = ", ".join([bcm["areas"][idx]["code"] for idx in bcm["areas"]])
593
- self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 1, QTableWidgetItem(codes))
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))
594
711
 
595
712
  def remove_behaviors_coding_map(self):
596
713
  """
@@ -599,27 +716,28 @@ class projectDialog(QDialog, Ui_dlgProject):
599
716
  if not self.twBehavCodingMap.selectedIndexes():
600
717
  QMessageBox.warning(self, cfg.programName, "Select a behaviors coding map")
601
718
  else:
602
- if (
603
- dialog.MessageDialog(
604
- cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]
605
- )
606
- == cfg.YES
607
- ):
719
+ if dialog.MessageDialog(cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
608
720
  del self.pj[cfg.BEHAVIORS_CODING_MAP][self.twBehavCodingMap.selectedIndexes()[0].row()]
609
721
  self.twBehavCodingMap.removeRow(self.twBehavCodingMap.selectedIndexes()[0].row())
610
722
 
611
723
  def leLabel_changed(self):
612
-
724
+ """
725
+ independent variable label changed
726
+ """
613
727
  if self.selected_twvariables_row != -1:
614
728
  self.twVariables.item(self.selected_twvariables_row, 0).setText(self.leLabel.text())
615
729
 
616
730
  def leDescription_changed(self):
617
-
731
+ """
732
+ independent variable description changed
733
+ """
618
734
  if self.selected_twvariables_row != -1:
619
735
  self.twVariables.item(self.selected_twvariables_row, 1).setText(self.leDescription.text())
620
736
 
621
737
  def lePredefined_changed(self):
622
-
738
+ """
739
+ independent variable predefined value changed
740
+ """
623
741
  if self.selected_twvariables_row != -1:
624
742
  self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
625
743
  if not self.lePredefined.hasFocus():
@@ -628,15 +746,19 @@ class projectDialog(QDialog, Ui_dlgProject):
628
746
  QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
629
747
 
630
748
  def leSetValues_changed(self):
631
-
749
+ """
750
+ independent variable available values changed
751
+ """
632
752
  if self.selected_twvariables_row != -1:
633
753
  self.twVariables.item(self.selected_twvariables_row, 4).setText(self.leSetValues.text())
634
754
 
635
755
  def dte_default_date_changed(self):
636
-
756
+ """
757
+ independent variable default timestamp changed
758
+ """
637
759
  if self.selected_twvariables_row != -1:
638
760
  self.twVariables.item(self.selected_twvariables_row, 3).setText(
639
- self.dte_default_date.dateTime().toString(Qt.ISODate)
761
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
640
762
  )
641
763
 
642
764
  def pbBehaviorsCategories_clicked(self):
@@ -648,8 +770,13 @@ class projectDialog(QDialog, Ui_dlgProject):
648
770
 
649
771
  if bc.exec_():
650
772
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
651
- for index in range(bc.lw.count()):
652
- self.pj[cfg.BEHAVIORAL_CATEGORIES].append(bc.lw.item(index).text().strip())
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
+ }
653
780
 
654
781
  # sort
655
782
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = sorted(self.pj[cfg.BEHAVIORAL_CATEGORIES])
@@ -657,14 +784,15 @@ class projectDialog(QDialog, Ui_dlgProject):
657
784
  # check if behavior belong to removed category
658
785
  if bc.removed:
659
786
  for row in range(self.twBehaviors.rowCount()):
660
- if self.twBehaviors.item(row, cfg.behavioursFields["category"]):
661
- if self.twBehaviors.item(row, cfg.behavioursFields["category"]).text() == bc.removed:
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:
662
789
  if (
663
790
  dialog.MessageDialog(
664
791
  cfg.programName,
665
792
  (
666
793
  f"The <b>{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()}</b> behavior belongs "
667
- f"to a behavioral category <b>{self.twBehaviors.item(row, cfg.behavioursFields['category']).text()}</b> "
794
+ "to a behavioral category "
795
+ f"<b>{self.twBehaviors.item(row, cfg.behavioursFields['category']).text()}</b> "
668
796
  "that is no more in the behavioral categories list.<br><br>"
669
797
  "Remove the behavior from category?"
670
798
  ),
@@ -672,16 +800,17 @@ class projectDialog(QDialog, Ui_dlgProject):
672
800
  )
673
801
  == cfg.YES
674
802
  ):
675
- self.twBehaviors.item(row, cfg.behavioursFields["category"]).setText("")
803
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText("")
676
804
  if bc.renamed:
677
805
  for row in range(self.twBehaviors.rowCount()):
678
- if self.twBehaviors.item(row, cfg.behavioursFields["category"]):
679
- if self.twBehaviors.item(row, cfg.behavioursFields["category"]).text() == bc.renamed[0]:
680
- self.twBehaviors.item(row, cfg.behavioursFields["category"]).setText(bc.renamed[1])
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])
681
809
 
682
- def twBehaviors_cellDoubleClicked(self, row, column):
810
+ def twBehaviors_cellDoubleClicked(self, row: int, column: int) -> None:
683
811
  """
684
812
  manage double-click on ethogram table:
813
+ * color
685
814
  * behavioral category
686
815
  * modifiers
687
816
  * exclusion
@@ -692,51 +821,53 @@ class projectDialog(QDialog, Ui_dlgProject):
692
821
  column (int): column double-clicked
693
822
  """
694
823
 
695
- # check if double click on excluded column
696
- if column == cfg.behavioursFields["excluded"]:
824
+ # excluded column
825
+ if column == cfg.behavioursFields[cfg.EXCLUDED]:
697
826
  self.exclusion_matrix()
698
827
 
699
- # check if double click on 'coding map' column
700
- if column == cfg.behavioursFields["coding map"]:
828
+ # coding map
829
+ if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
701
830
  if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
702
- self.behaviorTypeChanged(row)
831
+ self.behavior_type_changed(row)
703
832
  else:
704
- QMessageBox.information(
705
- self, cfg.programName, "Change the behavior type on first column to select a coding map"
706
- )
833
+ QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
707
834
 
708
- # check if double click on category
835
+ # behavior type
709
836
  if column == cfg.behavioursFields["type"]:
710
837
  self.behavior_type_doubleclicked(row)
711
838
 
839
+ # color
840
+ if column == cfg.behavioursFields[cfg.COLOR]:
841
+ self.color_doubleclicked(row)
842
+
712
843
  # behavioral category
713
- if column == cfg.behavioursFields["category"]:
844
+ if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
714
845
  self.category_doubleclicked(row)
715
846
 
716
- if column == cfg.behavioursFields["modifiers"]:
847
+ # modifiers
848
+ if column == cfg.behavioursFields[cfg.MODIFIERS]:
717
849
  # check if behavior has coding map
718
850
  if (
719
- self.twBehaviors.item(row, cfg.behavioursFields["coding map"]) is not None
720
- and self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text()
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()
721
853
  ):
722
854
  QMessageBox.warning(self, cfg.programName, "Use the coding map to set/modify the areas")
723
855
  else:
724
856
  subjects_list = []
725
857
  for subject_row in range(self.twSubjects.rowCount()):
726
858
  key = self.twSubjects.item(subject_row, 0).text() if self.twSubjects.item(subject_row, 0) else ""
727
- subjectName = (
728
- self.twSubjects.item(subject_row, 1).text().strip()
729
- if self.twSubjects.item(subject_row, 1)
730
- else ""
731
- )
859
+ subjectName = self.twSubjects.item(subject_row, 1).text().strip() if self.twSubjects.item(subject_row, 1) else ""
732
860
  subjects_list.append((subjectName, key))
733
861
 
734
862
  addModifierWindow = add_modifier.addModifierDialog(
735
- self.twBehaviors.item(row, column).text(), subjects=subjects_list
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,
736
866
  )
737
867
  addModifierWindow.setWindowTitle(f'Set modifiers for "{self.twBehaviors.item(row, 2).text()}" behavior')
868
+
738
869
  if addModifierWindow.exec_():
739
- self.twBehaviors.item(row, column).setText(addModifierWindow.getModifiers())
870
+ self.twBehaviors.item(row, column).setText(addModifierWindow.get_modifiers())
740
871
 
741
872
  def behavior_type_doubleclicked(self, row):
742
873
  """
@@ -748,14 +879,36 @@ class projectDialog(QDialog, Ui_dlgProject):
748
879
  else:
749
880
  selected = 0
750
881
 
751
- new_type, ok = QInputDialog.getItem(
752
- self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False
753
- )
882
+ new_type, ok = QInputDialog.getItem(self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False)
754
883
 
755
884
  if ok and new_type:
756
885
  self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText(new_type)
757
886
 
758
- self.behaviorTypeChanged(row)
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())
759
912
 
760
913
  def category_doubleclicked(self, row):
761
914
  """
@@ -764,19 +917,17 @@ class projectDialog(QDialog, Ui_dlgProject):
764
917
 
765
918
  categories = ["None"] + self.pj[cfg.BEHAVIORAL_CATEGORIES] if cfg.BEHAVIORAL_CATEGORIES in self.pj else ["None"]
766
919
 
767
- if self.twBehaviors.item(row, cfg.behavioursFields["category"]).text() in categories:
768
- selected = categories.index(self.twBehaviors.item(row, cfg.behavioursFields["category"]).text())
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())
769
922
  else:
770
923
  selected = 0
771
924
 
772
- category, ok = QInputDialog.getItem(
773
- self, "Select a behavioral category", "Behavioral categories", categories, selected, False
774
- )
925
+ category, ok = QInputDialog.getItem(self, "Select a behavioral category", "Behavioral categories", categories, selected, False)
775
926
 
776
927
  if ok and category:
777
928
  if category == "None":
778
929
  category = ""
779
- self.twBehaviors.item(row, cfg.behavioursFields["category"]).setText(category)
930
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(category)
780
931
 
781
932
  def check_variable_default_value(self, txt, varType):
782
933
  """
@@ -800,22 +951,15 @@ class projectDialog(QDialog, Ui_dlgProject):
800
951
 
801
952
  if self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentText() == cfg.SET_OF_VALUES:
802
953
  if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() == "NA":
803
- self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText(
804
- "Double-click to add values"
805
- )
954
+ self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("Double-click to add values")
806
955
  else:
807
956
  # check if set of values defined
808
957
  if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() not in [
809
958
  "NA",
810
959
  "Double-click to add values",
811
960
  ]:
812
- if (
813
- dialog.MessageDialog(cfg.programName, "Erase the set of values?", [cfg.YES, cfg.CANCEL])
814
- == cfg.CANCEL
815
- ):
816
- self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).setCurrentIndex(
817
- cfg.SET_OF_VALUES_idx
818
- )
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)
819
963
  return
820
964
  else:
821
965
  self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
@@ -844,7 +988,6 @@ class projectDialog(QDialog, Ui_dlgProject):
844
988
 
845
989
  existing_var = []
846
990
  for r in range(self.twVariables.rowCount()):
847
-
848
991
  if self.twVariables.item(r, 0).text().strip().upper() in existing_var:
849
992
  return (
850
993
  False,
@@ -882,7 +1025,6 @@ class projectDialog(QDialog, Ui_dlgProject):
882
1025
  return True, "OK"
883
1026
 
884
1027
  def cbtype_changed(self):
885
-
886
1028
  self.leSetValues.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
887
1029
  self.label_5.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
888
1030
 
@@ -892,10 +1034,9 @@ class projectDialog(QDialog, Ui_dlgProject):
892
1034
  self.label_4.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
893
1035
 
894
1036
  def cbtype_activated(self):
895
-
896
1037
  if self.cbType.currentText() == cfg.TIMESTAMP:
897
1038
  self.twVariables.item(self.selected_twvariables_row, 3).setText(
898
- self.dte_default_date.dateTime().toString(Qt.ISODate)
1039
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
899
1040
  )
900
1041
  self.twVariables.item(self.selected_twvariables_row, 4).setText("")
901
1042
  else:
@@ -975,7 +1116,7 @@ class projectDialog(QDialog, Ui_dlgProject):
975
1116
  return
976
1117
 
977
1118
  for row in range(self.twBehaviors.rowCount()):
978
- if not self.twBehaviors.item(row, cfg.behavioursFields["code"]).text():
1119
+ if not self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
979
1120
  QMessageBox.critical(
980
1121
  None,
981
1122
  cfg.programName,
@@ -1005,12 +1146,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1005
1146
  )
1006
1147
 
1007
1148
  for r in range(self.twBehaviors.rowCount()):
1008
-
1009
1149
  if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
1010
-
1011
1150
  if include_point_events == cfg.YES or (
1012
- include_point_events == cfg.NO
1013
- and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
1151
+ include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
1014
1152
  ):
1015
1153
  allBehaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1016
1154
 
@@ -1065,9 +1203,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1065
1203
 
1066
1204
  if c_name != r_name:
1067
1205
  ex.checkboxes[f"{r_name}|{c_name}"] = QCheckBox()
1068
- ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet(
1069
- "text-align: center; margin-left:50%; margin-right:50%;"
1070
- )
1206
+ ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet("text-align: center; margin-left:50%; margin-right:50%;")
1071
1207
 
1072
1208
  if flag_left_bottom:
1073
1209
  # hide if cell in left-bottom part of table
@@ -1095,18 +1231,15 @@ class projectDialog(QDialog, Ui_dlgProject):
1095
1231
 
1096
1232
  # update excluded field
1097
1233
  for r in range(self.twBehaviors.rowCount()):
1098
- if include_point_events == cfg.YES or (
1099
- include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()
1100
- ):
1234
+ if include_point_events == cfg.YES or (include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()):
1101
1235
  for e in excl:
1102
1236
  if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
1103
1237
  item = QTableWidgetItem(",".join(new_excl[e]))
1104
1238
  item.setFlags(Qt.ItemIsEnabled)
1105
- item.setBackground(QColor(230, 230, 230))
1239
+ item.setBackground(self.not_editable_column_color())
1106
1240
  self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
1107
1241
 
1108
1242
  def remove_all_behaviors(self):
1109
-
1110
1243
  if not self.twBehaviors.rowCount():
1111
1244
  QMessageBox.critical(
1112
1245
  None,
@@ -1162,7 +1295,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1162
1295
  self.lbObservationsState.setText("")
1163
1296
 
1164
1297
  for r in range(self.twBehaviors.rowCount()):
1165
-
1166
1298
  # check key
1167
1299
  if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX):
1168
1300
  key = self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX).text()
@@ -1205,9 +1337,16 @@ class projectDialog(QDialog, Ui_dlgProject):
1205
1337
  for field in cfg.behavioursFields:
1206
1338
  item = QTableWidgetItem(self.twBehaviors.item(row, cfg.behavioursFields[field]))
1207
1339
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
1208
- if field in [cfg.TYPE, "category", "excluded", "coding map", "modifiers"]:
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:
1209
1344
  item.setFlags(Qt.ItemIsEnabled)
1210
- item.setBackground(QColor(230, 230, 230))
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
+
1211
1350
  self.twBehaviors.scrollToBottom()
1212
1351
 
1213
1352
  def remove_behavior(self):
@@ -1227,27 +1366,22 @@ class projectDialog(QDialog, Ui_dlgProject):
1227
1366
 
1228
1367
  if not self.twBehaviors.selectedIndexes():
1229
1368
  QMessageBox.warning(self, cfg.programName, "Select a behaviour to be removed")
1230
- else:
1231
- if dialog.MessageDialog(cfg.programName, "Remove the selected behavior?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1232
-
1233
- # check if behavior already used in observations
1234
- flag_break = False
1235
- codeToDelete = self.twBehaviors.item(self.twBehaviors.selectedIndexes()[0].row(), 2).text()
1236
- for obs_id in self.pj[cfg.OBSERVATIONS]:
1237
- if codeToDelete in [
1238
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
1239
- ]:
1240
- if (
1241
- dialog.MessageDialog(
1242
- cfg.programName, "The code to remove is used in observations!", [cfg.REMOVE, cfg.CANCEL]
1243
- )
1244
- == cfg.CANCEL
1245
- ):
1246
- return
1247
- break
1369
+ return
1248
1370
 
1249
- self.twBehaviors.removeRow(self.twBehaviors.selectedIndexes()[0].row())
1250
- self.twBehaviors_cellChanged(0, 0)
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)
1251
1385
 
1252
1386
  def add_behavior(self):
1253
1387
  """
@@ -1261,38 +1395,36 @@ class projectDialog(QDialog, Ui_dlgProject):
1261
1395
  if field_type == cfg.TYPE:
1262
1396
  item.setText("Point event")
1263
1397
  # no manual editing, gray back ground
1264
- if field_type in [cfg.TYPE, "category", "modifiers", "excluded", "coding map"]:
1398
+ if field_type in (cfg.TYPE, cfg.COLOR, "category", cfg.MODIFIERS, "modifiers", "excluded", "coding map"):
1265
1399
  item.setFlags(Qt.ItemIsEnabled)
1266
- item.setBackground(QColor(230, 230, 230))
1400
+ # item.setBackground(QColor(230, 230, 230))
1401
+ item.setBackground(self.not_editable_column_color())
1267
1402
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
1268
1403
  self.twBehaviors.scrollToBottom()
1269
1404
 
1270
- def behaviorTypeChanged(self, row):
1405
+ def behavior_type_changed(self, row: int) -> None:
1271
1406
  """
1272
1407
  event type combobox changed
1273
1408
  """
1274
1409
 
1275
- if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
1410
+ if cfg.CODING_MAP_sp in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
1276
1411
  # let user select a coding maop
1277
- fn = QFileDialog().getOpenFileName(
1412
+ file_name, _ = QFileDialog().getOpenFileName(
1278
1413
  self,
1279
- "Select a coding map for "
1280
- f"{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1414
+ f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1281
1415
  "",
1282
1416
  "BORIS map files (*.boris_map);;All files (*)",
1283
1417
  )
1284
- fileName = fn[0] if type(fn) is tuple else fn
1285
-
1286
- if fileName:
1418
+ if file_name:
1287
1419
  try:
1288
- new_map = json.loads(open(fileName, "r").read())
1420
+ new_map = json.loads(open(file_name, "r").read())
1289
1421
  except Exception:
1290
- QMessageBox.critical(self, cfg.programName, "Error reding the file")
1422
+ QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
1291
1423
  return
1292
1424
  self.pj[cfg.CODING_MAP][new_map["name"]] = new_map
1293
1425
 
1294
1426
  # add modifiers from coding map areas
1295
- modifstr = str(
1427
+ modifstr = json.dumps(
1296
1428
  {
1297
1429
  "0": {
1298
1430
  "name": new_map["name"],
@@ -1308,9 +1440,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1308
1440
  else:
1309
1441
  # if coding map already exists do not reset the behavior type if no filename selected
1310
1442
  if not self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text():
1311
- QMessageBox.critical(
1312
- self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" '
1313
- )
1443
+ QMessageBox.critical(self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" ')
1314
1444
  self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText("Point event")
1315
1445
  else:
1316
1446
  self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText("")
@@ -1336,12 +1466,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1336
1466
  QMessageBox.warning(self, cfg.programName, "Select a subject to remove")
1337
1467
  else:
1338
1468
  if dialog.MessageDialog(cfg.programName, "Remove the selected subject?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1339
-
1340
1469
  flagDel = False
1341
1470
  if self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1):
1342
- subjectToDelete = self.twSubjects.item(
1343
- self.twSubjects.selectedIndexes()[0].row(), 1
1344
- ).text() # 1: subject name
1471
+ subjectToDelete = self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1).text() # 1: subject name
1345
1472
 
1346
1473
  subjectsInObs = []
1347
1474
  for obs in self.pj[cfg.OBSERVATIONS]:
@@ -1421,19 +1548,18 @@ class projectDialog(QDialog, Ui_dlgProject):
1421
1548
 
1422
1549
  self.twSubjects_cellChanged(0, 0)
1423
1550
 
1424
- def twSubjects_cellChanged(self, row: int, column: int):
1551
+ def twSubjects_cellChanged(self, row: int, column: int) -> None:
1425
1552
  """
1426
1553
  check if subject not unique
1427
1554
  """
1428
1555
 
1429
- subjects, keys = [], []
1556
+ subjects: list = []
1557
+ """keys: list = []"""
1430
1558
  self.lbSubjectsState.setText("")
1431
1559
 
1432
1560
  for r in range(self.twSubjects.rowCount()):
1433
-
1434
1561
  # check key
1435
1562
  if self.twSubjects.item(r, 0):
1436
-
1437
1563
  # check key length
1438
1564
  if (
1439
1565
  self.twSubjects.item(r, 0).text().upper() not in list(cfg.function_keys.values())
@@ -1448,11 +1574,14 @@ class projectDialog(QDialog, Ui_dlgProject):
1448
1574
  )
1449
1575
  return
1450
1576
 
1577
+ # control of duplicated key removed 2024-01-29
1578
+ """
1451
1579
  if self.twSubjects.item(r, 0).text() in keys:
1452
1580
  self.lbSubjectsState.setText(f'<font color="red">Key duplicated at row # {r + 1}</font>')
1453
1581
  else:
1454
1582
  if self.twSubjects.item(r, 0).text():
1455
1583
  keys.append(self.twSubjects.item(r, 0).text())
1584
+ """
1456
1585
 
1457
1586
  # check subject
1458
1587
  if self.twSubjects.item(r, 1):
@@ -1471,14 +1600,14 @@ class projectDialog(QDialog, Ui_dlgProject):
1471
1600
  logging.debug(f"selected row: {self.selected_twvariables_row}")
1472
1601
 
1473
1602
  if self.selected_twvariables_row == -1:
1474
- for widget in [
1603
+ for widget in (
1475
1604
  self.leLabel,
1476
1605
  self.leDescription,
1477
1606
  self.cbType,
1478
1607
  self.lePredefined,
1479
1608
  self.dte_default_date,
1480
1609
  self.leSetValues,
1481
- ]:
1610
+ ):
1482
1611
  widget.setEnabled(False)
1483
1612
  self.leLabel.setText("")
1484
1613
  self.leDescription.setText("")
@@ -1489,20 +1618,27 @@ class projectDialog(QDialog, Ui_dlgProject):
1489
1618
  return
1490
1619
 
1491
1620
  # enable widget for indep var setting
1492
- for widget in [
1621
+ for widget in (
1493
1622
  self.leLabel,
1494
1623
  self.leDescription,
1495
1624
  self.cbType,
1496
1625
  self.lePredefined,
1497
1626
  self.dte_default_date,
1498
1627
  self.leSetValues,
1499
- ]:
1628
+ ):
1500
1629
  widget.setEnabled(True)
1501
1630
 
1502
1631
  self.leLabel.setText(self.twVariables.item(row, 0).text())
1503
1632
  self.leDescription.setText(self.twVariables.item(row, 1).text())
1504
1633
  self.lePredefined.setText(self.twVariables.item(row, 3).text())
1505
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))
1506
1642
 
1507
1643
  self.cbType.clear()
1508
1644
  self.cbType.addItems(cfg.AVAILABLE_INDEP_VAR_TYPES)
@@ -1512,12 +1648,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1512
1648
 
1513
1649
  def pbCancel_clicked(self):
1514
1650
  if self.flag_modified:
1515
- if (
1516
- dialog.MessageDialog(
1517
- "BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]
1518
- )
1519
- == cfg.OK
1520
- ):
1651
+ if dialog.MessageDialog("BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]) == cfg.OK:
1521
1652
  self.reject()
1522
1653
  else:
1523
1654
  self.reject()
@@ -1525,12 +1656,12 @@ class projectDialog(QDialog, Ui_dlgProject):
1525
1656
  def check_ethogram(self) -> dict:
1526
1657
  """
1527
1658
  check ethogram for various parameter
1528
- returns ethogram dict or {cfg.CANCEL: True"} in case of error
1659
+ returns ethogram dict or {cfg.CANCEL: True} in case of error
1529
1660
 
1530
1661
  """
1531
1662
  # store behaviors
1532
- missing_data = []
1533
- checked_ethogram = {}
1663
+ missing_data: list = []
1664
+ checked_ethogram: dict = {}
1534
1665
 
1535
1666
  # Ethogram
1536
1667
  # coding maps in ethogram
@@ -1538,16 +1669,19 @@ class projectDialog(QDialog, Ui_dlgProject):
1538
1669
  # check for leading/trailing space in behaviors and modifiers
1539
1670
  code_with_leading_trailing_spaces, modifiers_with_leading_trailing_spaces = [], []
1540
1671
  for r in range(self.twBehaviors.rowCount()):
1541
-
1542
1672
  if (
1543
- self.twBehaviors.item(r, cfg.behavioursFields["code"]).text()
1544
- != self.twBehaviors.item(r, cfg.behavioursFields["code"]).text().strip()
1673
+ self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
1674
+ != self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text().strip()
1545
1675
  ):
1546
- code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields["code"]).text())
1676
+ code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1547
1677
 
1548
1678
  if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text():
1549
1679
  try:
1550
- modifiers_dict = eval(self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text())
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
+ )
1551
1685
  for k in modifiers_dict:
1552
1686
  for value in modifiers_dict[k]["values"]:
1553
1687
  modif_code = value.split(" (")[0]
@@ -1563,7 +1697,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1563
1697
  (
1564
1698
  "<b>Warning!</b> Some leading and/or trailing spaces are present"
1565
1699
  " in the following behaviors code(s):<br>"
1566
- f"<b>{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '&#9608;') for x in code_with_leading_trailing_spaces])}</b><br><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>"
1567
1703
  "Do you want to remove the leading and trailing spaces (visualized as black boxes) from behaviors?<br><br>"
1568
1704
  """<font color="red"><b>Be careful with this option"""
1569
1705
  """ if you have already done observations!</b></font>"""
@@ -1579,13 +1715,13 @@ class projectDialog(QDialog, Ui_dlgProject):
1579
1715
  cfg.programName,
1580
1716
  (
1581
1717
  "<b>Warning!</b> Some leading and/or trailing spaces are present"
1582
- " in the following modifier(s):<br>"
1583
- f"<b>{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '&#9608;') for x in set(modifiers_with_leading_trailing_spaces)])}</b><br><br>"
1584
- "Do you want to remove the leading and trailing spaces (visualized as black boxes) from modifiers?<br><br>"
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>"
1585
1721
  """<font color="red"><b>Be careful with this option"""
1586
1722
  """ if you have already done observations!</b></font>"""
1587
1723
  ),
1588
- [cfg.YES, cfg.NO, cfg.CANCEL],
1724
+ (cfg.YES, cfg.NO, cfg.CANCEL),
1589
1725
  )
1590
1726
  if remove_leading_trailing_spaces_in_modifiers == cfg.CANCEL:
1591
1727
  return {cfg.CANCEL: True}
@@ -1595,9 +1731,8 @@ class projectDialog(QDialog, Ui_dlgProject):
1595
1731
  row = {}
1596
1732
  for field in cfg.behavioursFields:
1597
1733
  if self.twBehaviors.item(r, cfg.behavioursFields[field]):
1598
-
1599
1734
  # check for | char in code
1600
- if field == "code" and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
1735
+ if field == cfg.BEHAVIOR_CODE and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
1601
1736
  QMessageBox.warning(
1602
1737
  self,
1603
1738
  cfg.programName,
@@ -1614,10 +1749,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1614
1749
  row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
1615
1750
 
1616
1751
  if field == "modifiers" and row["modifiers"]:
1617
-
1618
1752
  if remove_leading_trailing_spaces_in_modifiers == cfg.YES:
1619
1753
  try:
1620
- modifiers_dict = eval(row["modifiers"])
1754
+ modifiers_dict = json.loads(row["modifiers"]) if row["modifiers"] else {}
1621
1755
  for k in modifiers_dict:
1622
1756
  for idx, value in enumerate(modifiers_dict[k]["values"]):
1623
1757
  modif_code = value.split(" (")[0]
@@ -1628,19 +1762,16 @@ class projectDialog(QDialog, Ui_dlgProject):
1628
1762
 
1629
1763
  row["modifiers"] = dict(modifiers_dict)
1630
1764
  except Exception:
1631
-
1632
1765
  logging.critical("Error removing leading/trailing spaces in modifiers")
1633
1766
 
1634
- QMessageBox.critical(
1635
- self, cfg.programName, "Error removing leading/trailing spaces in modifiers"
1636
- )
1767
+ QMessageBox.critical(self, cfg.programName, "Error removing leading/trailing spaces in modifiers")
1637
1768
 
1638
1769
  else:
1639
- row["modifiers"] = eval(row["modifiers"])
1770
+ row["modifiers"] = json.loads(row["modifiers"]) if row["modifiers"] else {}
1640
1771
  else:
1641
1772
  row[field] = ""
1642
1773
 
1643
- if (row["type"]) and (row["code"]):
1774
+ if (row["type"]) and (row[cfg.BEHAVIOR_CODE]):
1644
1775
  checked_ethogram[str(len(checked_ethogram))] = row
1645
1776
  else:
1646
1777
  missing_data.append(str(r + 1))
@@ -1662,27 +1793,40 @@ class projectDialog(QDialog, Ui_dlgProject):
1662
1793
  return {cfg.CANCEL: True}
1663
1794
 
1664
1795
  # check if behavior belong to category that is not in categories list
1665
- behavior_category = []
1796
+ missing_behavior_category: list = []
1666
1797
  for idx in checked_ethogram:
1667
1798
  if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
1668
1799
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
1669
1800
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
1670
- behavior_category.append(
1801
+ missing_behavior_category.append(
1671
1802
  (checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
1672
1803
  )
1673
- if behavior_category:
1674
-
1804
+ if missing_behavior_category:
1675
1805
  response = dialog.MessageDialog(
1676
1806
  f"{cfg.programName} - Behavioral categories",
1677
1807
  (
1678
- "The behavioral categorie(s) "
1679
- f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in behavior_category]))} "
1680
- "are no more defined in behavioral categories list"
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>"
1681
1811
  ),
1682
- ["Add behavioral category/ies", "Ignore", cfg.CANCEL],
1812
+ ["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
1683
1813
  )
1684
1814
  if response == "Add behavioral category/ies":
1685
- [self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1) for x1 in set(x[1] for x in behavior_category)]
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
+
1686
1830
  if response == cfg.CANCEL:
1687
1831
  return {cfg.CANCEL: True}
1688
1832
 
@@ -1713,7 +1857,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1713
1857
  self.pj[cfg.TIME_FORMAT] = cfg.HHMMSS
1714
1858
 
1715
1859
  # store subjects
1716
- self.subjects_conf = {}
1860
+ self.subjects_conf: dict = {}
1717
1861
 
1718
1862
  # check for leading/trailing spaces in subjects names
1719
1863
  subjects_name_with_leading_trailing_spaces = ""
@@ -1724,7 +1868,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1724
1868
 
1725
1869
  remove_leading_trailing_spaces = cfg.NO
1726
1870
  if subjects_name_with_leading_trailing_spaces:
1727
-
1728
1871
  remove_leading_trailing_spaces = dialog.MessageDialog(
1729
1872
  cfg.programName,
1730
1873
  (
@@ -1740,10 +1883,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1740
1883
  # check subjects
1741
1884
  for row in range(self.twSubjects.rowCount()):
1742
1885
  # check key
1886
+ key: str = ""
1743
1887
  if self.twSubjects.item(row, 0):
1744
1888
  key = self.twSubjects.item(row, 0).text()
1745
- else:
1746
- key = ""
1747
1889
 
1748
1890
  # check subject name
1749
1891
  if self.twSubjects.item(row, 1):
@@ -1754,9 +1896,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1754
1896
 
1755
1897
  # check if subject name is empty
1756
1898
  if subjectName == "":
1757
- QMessageBox.warning(
1758
- self, cfg.programName, f"The subject name can not be empty (check row #{row + 1})."
1759
- )
1899
+ QMessageBox.warning(self, cfg.programName, f"The subject name can not be empty (check row #{row + 1}).")
1760
1900
  return
1761
1901
 
1762
1902
  if "|" in subjectName:
@@ -1767,13 +1907,11 @@ class projectDialog(QDialog, Ui_dlgProject):
1767
1907
  )
1768
1908
  return
1769
1909
  else:
1770
- QMessageBox.warning(
1771
- self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}"
1772
- )
1910
+ QMessageBox.warning(self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}")
1773
1911
  return
1774
1912
 
1775
1913
  # description
1776
- subjectDescription = ""
1914
+ subjectDescription: str = ""
1777
1915
  if self.twSubjects.item(row, 2):
1778
1916
  subjectDescription = self.twSubjects.item(row, 2).text().strip()
1779
1917
 
@@ -1783,6 +1921,25 @@ class projectDialog(QDialog, Ui_dlgProject):
1783
1921
  "description": subjectDescription,
1784
1922
  }
1785
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
+
1786
1943
  self.pj[cfg.SUBJECTS] = dict(self.subjects_conf)
1787
1944
 
1788
1945
  # check ethogram
@@ -1820,7 +1977,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1820
1977
  self.pj[cfg.INDEPENDENT_VARIABLES] = dict(self.indVar)
1821
1978
 
1822
1979
  # converters
1823
- converters = {}
1980
+ converters: dict = {}
1824
1981
  for row in range(self.tw_converters.rowCount()):
1825
1982
  converters[self.tw_converters.item(row, 0).text()] = {
1826
1983
  "name": self.tw_converters.item(row, 0).text(),
@@ -1840,9 +1997,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1840
1997
  for converter in sorted(self.converters.keys()):
1841
1998
  self.tw_converters.setRowCount(self.tw_converters.rowCount() + 1)
1842
1999
  self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 0, QTableWidgetItem(converter)) # id / name
1843
- self.tw_converters.setItem(
1844
- self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"])
1845
- )
2000
+ self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"]))
1846
2001
  self.tw_converters.setItem(
1847
2002
  self.tw_converters.rowCount() - 1,
1848
2003
  2,