boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -36
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  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 +16 -34
  23. boris/config.py +102 -50
  24. boris/config_file.py +55 -64
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2108 -1275
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +27 -7
  34. boris/dialog.py +461 -242
  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 +405 -281
  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 +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  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 +304 -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 +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +128 -35
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +298 -123
  79. boris/preferences_ui.py +664 -225
  80. boris/project.py +293 -270
  81. boris/project_functions.py +610 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +42 -26
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +77 -89
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.1.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.1.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  123. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.1.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,
@@ -40,11 +43,9 @@ from PyQt5.QtWidgets import (
40
43
  QPushButton,
41
44
  QSizePolicy,
42
45
  QSpacerItem,
46
+ QTableWidget,
43
47
  QTableWidgetItem,
44
48
  QVBoxLayout,
45
- QColorDialog,
46
- QTableWidget,
47
- QAbstractItemView,
48
49
  )
49
50
 
50
51
  from . import add_modifier
@@ -81,12 +82,12 @@ class BehavioralCategories(QDialog):
81
82
  # add categories
82
83
  self.lw.setColumnCount(2)
83
84
  self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
84
- # self.lw.verticalHeader().hide()
85
85
  self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
86
86
 
87
- # self.lw.setSelectionBehavior(QAbstractItemView.SelectRows)
88
87
  self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
89
88
 
89
+ behavioral_categories: list = []
90
+
90
91
  if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
91
92
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
92
93
  behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
@@ -94,7 +95,7 @@ class BehavioralCategories(QDialog):
94
95
  # name
95
96
  item = QTableWidgetItem()
96
97
  item.setText(behav_cat[key]["name"])
97
- # item.setFlags(Qt.ItemIsEnabled)
98
+ behavioral_categories.append(behav_cat[key]["name"])
98
99
  self.lw.setItem(idx, 0, item)
99
100
  # color
100
101
  item = QTableWidgetItem()
@@ -102,23 +103,21 @@ class BehavioralCategories(QDialog):
102
103
  if behav_cat[key].get(cfg.COLOR, ""):
103
104
  item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
104
105
  else:
105
- item.setBackground(QColor(230, 230, 230))
106
- # item.setFlags(Qt.ItemIsEnabled)
106
+ item.setBackground(self.not_editable_column_color())
107
107
  self.lw.setItem(idx, 1, item)
108
108
  else:
109
109
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
110
110
  for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
111
+ # name
111
112
  item = QTableWidgetItem()
112
113
  item.setText(category)
113
- # item.setFlags(Qt.ItemIsEnabled)
114
+ behavioral_categories.append(category)
114
115
  self.lw.setItem(idx, 0, item)
115
-
116
+ # color
116
117
  item = QTableWidgetItem()
117
118
  item.setText("")
118
- # item.setFlags(Qt.ItemIsEnabled)
119
- self.lw.setItem(idx, 1, item)
120
119
 
121
- # self.lw.addItem(QListWidgetItem(category))
120
+ self.lw.setItem(idx, 1, item)
122
121
 
123
122
  self.vbox.addWidget(self.lw)
124
123
 
@@ -135,8 +134,8 @@ class BehavioralCategories(QDialog):
135
134
  self.vbox.addLayout(self.hbox0)
136
135
 
137
136
  hbox1 = QHBoxLayout()
138
- self.pbOK = QPushButton("OK", clicked=self.accept)
139
- 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)
140
139
 
141
140
  spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
142
141
  hbox1.addItem(spacerItem)
@@ -146,6 +145,54 @@ class BehavioralCategories(QDialog):
146
145
 
147
146
  self.setLayout(self.vbox)
148
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
+
149
196
  def lw_double_clicked(self, row: int, column: int):
150
197
  """
151
198
  change color
@@ -165,7 +212,7 @@ class BehavioralCategories(QDialog):
165
212
  color = col_diag.currentColor()
166
213
  if color.name() == "#000000": # black -> delete color
167
214
  self.lw.item(row, 1).setText("")
168
- self.lw.item(row, 1).setBackground(QColor(230, 230, 230))
215
+ self.lw.item(row, 1).setBackground(self.not_editable_column_color())
169
216
  elif color.isValid():
170
217
  self.lw.item(row, 1).setText(color.name())
171
218
  self.lw.item(row, 1).setBackground(color)
@@ -253,9 +300,7 @@ class BehavioralCategories(QDialog):
253
300
  flag_rename = (
254
301
  dialog.MessageDialog(
255
302
  cfg.programName,
256
- ("Some behavior belong to the <b>{1}</b> to rename:<br>" "{0}<br>").format(
257
- "<br>".join(behaviors_in_category), category_to_rename
258
- ),
303
+ (f"Some behavior belong to the <b>{category_to_rename}</b> to rename:<br>{'<br>'.join(behaviors_in_category)}<br>"),
259
304
  ["Rename category", cfg.CANCEL],
260
305
  )
261
306
  == "Rename category"
@@ -271,12 +316,11 @@ class BehavioralCategories(QDialog):
271
316
  self.lw.item(self.lw.indexFromItem(selected_item).row(), 0).setText(new_category_name)
272
317
  # check behaviors belonging to the renamed category
273
318
  self.renamed = [category_to_rename, new_category_name]
274
- self.accept()
319
+ # self.accept()
275
320
 
276
321
 
277
322
  class projectDialog(QDialog, Ui_dlgProject):
278
323
  def __init__(self, parent=None):
279
-
280
324
  super().__init__()
281
325
 
282
326
  self.setupUi(self)
@@ -292,23 +336,23 @@ class projectDialog(QDialog, Ui_dlgProject):
292
336
  "remove all|Remove all behaviors",
293
337
  "lower|Convert keys to lower case",
294
338
  ]
295
- menu = QMenu()
296
- menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
297
- self.add_button_menu(behavior_button_items, menu)
298
- 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)
299
343
 
300
344
  import_button_items = [
301
345
  "boris|from a BORIS project",
302
- "spreadsheet|from a spreadsheet file (XLSX)",
346
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
303
347
  "jwatcher|from a JWatcher project",
304
348
  "text|from a text file (CSV or TSV)",
305
349
  "clipboard|from the clipboard",
306
350
  "repository|from the BORIS repository",
307
351
  ]
308
- menu = QMenu()
309
- menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
310
- self.add_button_menu(import_button_items, menu)
311
- 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)
312
356
 
313
357
  self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
314
358
 
@@ -332,21 +376,21 @@ class projectDialog(QDialog, Ui_dlgProject):
332
376
  "lower|Convert keys to lower case",
333
377
  ]
334
378
 
335
- menu = QMenu()
336
- menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
337
- self.add_button_menu(subjects_button_items, menu)
338
- 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)
339
383
 
340
384
  subjects_import_button_items = [
341
385
  "boris|from a BORIS project",
342
- "spreadsheet|from a spreadsheet file (XLSX)",
386
+ "spreadsheet|from a spreadsheet file (XLSX/ODS)",
343
387
  "text|from a text file (CSV or TSV)",
344
388
  "clipboard|from the clipboard",
345
389
  ]
346
- menu = QMenu()
347
- menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
348
- self.add_button_menu(subjects_import_button_items, menu)
349
- 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)
350
394
 
351
395
  self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
352
396
 
@@ -416,6 +460,17 @@ class projectDialog(QDialog, Ui_dlgProject):
416
460
  self.twSubjects.horizontalHeader().sortIndicatorChanged.connect(self.sort_twSubjects)
417
461
  self.twVariables.horizontalHeader().sortIndicatorChanged.connect(self.sort_twVariables)
418
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
+
419
474
  def add_button_menu(self, data, menu_obj):
420
475
  """
421
476
  add menu option from dictionary
@@ -529,20 +584,14 @@ class projectDialog(QDialog, Ui_dlgProject):
529
584
 
530
585
  # check if some keys will be duplicated after conversion
531
586
  try:
532
- all_keys = [
533
- self.twBehaviors.item(row, cfg.behavioursFields["key"]).text()
534
- for row in range(self.twBehaviors.rowCount())
535
- ]
587
+ all_keys = [self.twBehaviors.item(row, cfg.behavioursFields["key"]).text() for row in range(self.twBehaviors.rowCount())]
536
588
  except Exception:
537
589
  pass
538
590
  if all_keys == [x.lower() for x in all_keys]:
539
591
  QMessageBox.information(self, cfg.programName, "All keys are already lower case")
540
592
  return
541
593
 
542
- if (
543
- dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
544
- == cfg.CANCEL
545
- ):
594
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
546
595
  return
547
596
 
548
597
  if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
@@ -564,9 +613,8 @@ class projectDialog(QDialog, Ui_dlgProject):
564
613
 
565
614
  # convert modifier shortcuts
566
615
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text():
567
-
568
616
  modifiers_dict = (
569
- eval(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
617
+ json.loads(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
570
618
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text()
571
619
  else {}
572
620
  )
@@ -576,16 +624,12 @@ class projectDialog(QDialog, Ui_dlgProject):
576
624
  for idx2, value in enumerate(modifiers_dict[modifier_set]["values"]):
577
625
  if re.findall(r"\((\w+)\)", value):
578
626
  modifiers_dict[modifier_set]["values"][idx2] = (
579
- value.split("(")[0]
580
- + "("
581
- + re.findall(r"\((\w+)\)", value)[0].lower()
582
- + ")"
583
- + value.split(")")[-1]
627
+ value.split("(")[0] + "(" + re.findall(r"\((\w+)\)", value)[0].lower() + ")" + value.split(")")[-1]
584
628
  )
585
629
  except Exception:
586
630
  logging.warning("error during conversion of modifier short cut to lower case")
587
631
 
588
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(str(modifiers_dict))
632
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(json.dumps(modifiers_dict))
589
633
 
590
634
  def convert_subjects_keys_to_lower_case(self):
591
635
  """
@@ -593,20 +637,14 @@ class projectDialog(QDialog, Ui_dlgProject):
593
637
  """
594
638
  # check if some keys will be duplicated after conversion
595
639
  try:
596
- all_keys = [
597
- self.twSubjects.item(row, cfg.subjectsFields.index("key")).text()
598
- for row in range(self.twSubjects.rowCount())
599
- ]
640
+ all_keys = [self.twSubjects.item(row, cfg.subjectsFields.index("key")).text() for row in range(self.twSubjects.rowCount())]
600
641
  except Exception:
601
642
  pass
602
643
  if all_keys == [x.lower() for x in all_keys]:
603
644
  QMessageBox.information(self, cfg.programName, "All keys are already lower case")
604
645
  return
605
646
 
606
- if (
607
- dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
608
- == cfg.CANCEL
609
- ):
647
+ if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
610
648
  return
611
649
 
612
650
  if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
@@ -631,47 +669,45 @@ class projectDialog(QDialog, Ui_dlgProject):
631
669
  Add a behaviors coding map from file
632
670
  """
633
671
 
634
- fn = QFileDialog().getOpenFileName(
672
+ file_name, _ = QFileDialog().getOpenFileName(
635
673
  self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
636
674
  )
637
- file_name = fn[0] if type(fn) is tuple else fn
638
- if file_name:
639
- try:
640
- bcm = json.loads(open(file_name, "r").read())
641
- except Exception:
642
- QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
643
- 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
644
682
 
645
- if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
646
- QMessageBox.critical(
647
- self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map."
648
- )
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.")
649
685
 
650
- if cfg.BEHAVIORS_CODING_MAP not in self.pj:
651
- self.pj[cfg.BEHAVIORS_CODING_MAP] = []
686
+ if cfg.BEHAVIORS_CODING_MAP not in self.pj:
687
+ self.pj[cfg.BEHAVIORS_CODING_MAP] = []
652
688
 
653
- bcm_code_not_found = []
654
- existing_codes = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
655
- for code in [bcm["areas"][key][cfg.BEHAVIOR_CODE] for key in bcm["areas"]]:
656
- if code not in existing_codes:
657
- 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)
658
694
 
659
- if bcm_code_not_found:
660
- QMessageBox.warning(
661
- self,
662
- cfg.programName,
663
- ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
664
- "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
665
- ),
666
- )
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
+ )
667
703
 
668
- self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
704
+ self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
669
705
 
670
- self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
706
+ self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
671
707
 
672
- self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
673
- codes = ", ".join([bcm["areas"][idx][cfg.BEHAVIOR_CODE] for idx in bcm["areas"]])
674
- 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))
675
711
 
676
712
  def remove_behaviors_coding_map(self):
677
713
  """
@@ -680,27 +716,28 @@ class projectDialog(QDialog, Ui_dlgProject):
680
716
  if not self.twBehavCodingMap.selectedIndexes():
681
717
  QMessageBox.warning(self, cfg.programName, "Select a behaviors coding map")
682
718
  else:
683
- if (
684
- dialog.MessageDialog(
685
- cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]
686
- )
687
- == cfg.YES
688
- ):
719
+ if dialog.MessageDialog(cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
689
720
  del self.pj[cfg.BEHAVIORS_CODING_MAP][self.twBehavCodingMap.selectedIndexes()[0].row()]
690
721
  self.twBehavCodingMap.removeRow(self.twBehavCodingMap.selectedIndexes()[0].row())
691
722
 
692
723
  def leLabel_changed(self):
693
-
724
+ """
725
+ independent variable label changed
726
+ """
694
727
  if self.selected_twvariables_row != -1:
695
728
  self.twVariables.item(self.selected_twvariables_row, 0).setText(self.leLabel.text())
696
729
 
697
730
  def leDescription_changed(self):
698
-
731
+ """
732
+ independent variable description changed
733
+ """
699
734
  if self.selected_twvariables_row != -1:
700
735
  self.twVariables.item(self.selected_twvariables_row, 1).setText(self.leDescription.text())
701
736
 
702
737
  def lePredefined_changed(self):
703
-
738
+ """
739
+ independent variable predefined value changed
740
+ """
704
741
  if self.selected_twvariables_row != -1:
705
742
  self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
706
743
  if not self.lePredefined.hasFocus():
@@ -709,15 +746,19 @@ class projectDialog(QDialog, Ui_dlgProject):
709
746
  QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
710
747
 
711
748
  def leSetValues_changed(self):
712
-
749
+ """
750
+ independent variable available values changed
751
+ """
713
752
  if self.selected_twvariables_row != -1:
714
753
  self.twVariables.item(self.selected_twvariables_row, 4).setText(self.leSetValues.text())
715
754
 
716
755
  def dte_default_date_changed(self):
717
-
756
+ """
757
+ independent variable default timestamp changed
758
+ """
718
759
  if self.selected_twvariables_row != -1:
719
760
  self.twVariables.item(self.selected_twvariables_row, 3).setText(
720
- self.dte_default_date.dateTime().toString(Qt.ISODate)
761
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
721
762
  )
722
763
 
723
764
  def pbBehaviorsCategories_clicked(self):
@@ -763,13 +804,8 @@ class projectDialog(QDialog, Ui_dlgProject):
763
804
  if bc.renamed:
764
805
  for row in range(self.twBehaviors.rowCount()):
765
806
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]):
766
- if (
767
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text()
768
- == bc.renamed[0]
769
- ):
770
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(
771
- bc.renamed[1]
772
- )
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])
773
809
 
774
810
  def twBehaviors_cellDoubleClicked(self, row: int, column: int) -> None:
775
811
  """
@@ -785,20 +821,18 @@ class projectDialog(QDialog, Ui_dlgProject):
785
821
  column (int): column double-clicked
786
822
  """
787
823
 
788
- # check if double click on excluded column
789
- if column == cfg.behavioursFields["excluded"]:
824
+ # excluded column
825
+ if column == cfg.behavioursFields[cfg.EXCLUDED]:
790
826
  self.exclusion_matrix()
791
827
 
792
- # check if double click on 'coding map' column
793
- if column == cfg.behavioursFields["coding map"]:
828
+ # coding map
829
+ if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
794
830
  if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
795
- self.behaviorTypeChanged(row)
831
+ self.behavior_type_changed(row)
796
832
  else:
797
- QMessageBox.information(
798
- self, cfg.programName, "Change the behavior type on first column to select a coding map"
799
- )
833
+ QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
800
834
 
801
- # check if double click on category
835
+ # behavior type
802
836
  if column == cfg.behavioursFields["type"]:
803
837
  self.behavior_type_doubleclicked(row)
804
838
 
@@ -810,30 +844,30 @@ class projectDialog(QDialog, Ui_dlgProject):
810
844
  if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
811
845
  self.category_doubleclicked(row)
812
846
 
847
+ # modifiers
813
848
  if column == cfg.behavioursFields[cfg.MODIFIERS]:
814
849
  # check if behavior has coding map
815
850
  if (
816
- self.twBehaviors.item(row, cfg.behavioursFields["coding map"]) is not None
817
- 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()
818
853
  ):
819
854
  QMessageBox.warning(self, cfg.programName, "Use the coding map to set/modify the areas")
820
855
  else:
821
856
  subjects_list = []
822
857
  for subject_row in range(self.twSubjects.rowCount()):
823
858
  key = self.twSubjects.item(subject_row, 0).text() if self.twSubjects.item(subject_row, 0) else ""
824
- subjectName = (
825
- self.twSubjects.item(subject_row, 1).text().strip()
826
- if self.twSubjects.item(subject_row, 1)
827
- else ""
828
- )
859
+ subjectName = self.twSubjects.item(subject_row, 1).text().strip() if self.twSubjects.item(subject_row, 1) else ""
829
860
  subjects_list.append((subjectName, key))
830
861
 
831
862
  addModifierWindow = add_modifier.addModifierDialog(
832
- 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,
833
866
  )
834
867
  addModifierWindow.setWindowTitle(f'Set modifiers for "{self.twBehaviors.item(row, 2).text()}" behavior')
868
+
835
869
  if addModifierWindow.exec_():
836
- self.twBehaviors.item(row, column).setText(addModifierWindow.getModifiers())
870
+ self.twBehaviors.item(row, column).setText(addModifierWindow.get_modifiers())
837
871
 
838
872
  def behavior_type_doubleclicked(self, row):
839
873
  """
@@ -845,14 +879,12 @@ class projectDialog(QDialog, Ui_dlgProject):
845
879
  else:
846
880
  selected = 0
847
881
 
848
- new_type, ok = QInputDialog.getItem(
849
- self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False
850
- )
882
+ new_type, ok = QInputDialog.getItem(self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False)
851
883
 
852
884
  if ok and new_type:
853
885
  self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText(new_type)
854
886
 
855
- self.behaviorTypeChanged(row)
887
+ self.behavior_type_changed(row)
856
888
 
857
889
  def color_doubleclicked(self, row: int) -> None:
858
890
  """
@@ -866,16 +898,17 @@ class projectDialog(QDialog, Ui_dlgProject):
866
898
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
867
899
  current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
868
900
  if current_color.isValid():
901
+ print(f"{current_color=}")
869
902
  col_diag.setCurrentColor(current_color)
870
903
 
871
- if col_diag.exec_():
904
+ if col_diag.exec():
872
905
  color = col_diag.currentColor()
873
906
  if color.name() == "#000000": # black -> delete color
874
907
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
875
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(230, 230, 230))
908
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
876
909
  elif color.isValid():
910
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
877
911
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
878
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(color)
879
912
 
880
913
  def category_doubleclicked(self, row):
881
914
  """
@@ -889,9 +922,7 @@ class projectDialog(QDialog, Ui_dlgProject):
889
922
  else:
890
923
  selected = 0
891
924
 
892
- category, ok = QInputDialog.getItem(
893
- self, "Select a behavioral category", "Behavioral categories", categories, selected, False
894
- )
925
+ category, ok = QInputDialog.getItem(self, "Select a behavioral category", "Behavioral categories", categories, selected, False)
895
926
 
896
927
  if ok and category:
897
928
  if category == "None":
@@ -920,22 +951,15 @@ class projectDialog(QDialog, Ui_dlgProject):
920
951
 
921
952
  if self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentText() == cfg.SET_OF_VALUES:
922
953
  if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() == "NA":
923
- self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText(
924
- "Double-click to add values"
925
- )
954
+ self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("Double-click to add values")
926
955
  else:
927
956
  # check if set of values defined
928
957
  if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() not in [
929
958
  "NA",
930
959
  "Double-click to add values",
931
960
  ]:
932
- if (
933
- dialog.MessageDialog(cfg.programName, "Erase the set of values?", [cfg.YES, cfg.CANCEL])
934
- == cfg.CANCEL
935
- ):
936
- self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).setCurrentIndex(
937
- cfg.SET_OF_VALUES_idx
938
- )
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)
939
963
  return
940
964
  else:
941
965
  self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
@@ -964,7 +988,6 @@ class projectDialog(QDialog, Ui_dlgProject):
964
988
 
965
989
  existing_var = []
966
990
  for r in range(self.twVariables.rowCount()):
967
-
968
991
  if self.twVariables.item(r, 0).text().strip().upper() in existing_var:
969
992
  return (
970
993
  False,
@@ -1002,7 +1025,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1002
1025
  return True, "OK"
1003
1026
 
1004
1027
  def cbtype_changed(self):
1005
-
1006
1028
  self.leSetValues.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
1007
1029
  self.label_5.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
1008
1030
 
@@ -1012,10 +1034,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1012
1034
  self.label_4.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
1013
1035
 
1014
1036
  def cbtype_activated(self):
1015
-
1016
1037
  if self.cbType.currentText() == cfg.TIMESTAMP:
1017
1038
  self.twVariables.item(self.selected_twvariables_row, 3).setText(
1018
- self.dte_default_date.dateTime().toString(Qt.ISODate)
1039
+ self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
1019
1040
  )
1020
1041
  self.twVariables.item(self.selected_twvariables_row, 4).setText("")
1021
1042
  else:
@@ -1125,12 +1146,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1125
1146
  )
1126
1147
 
1127
1148
  for r in range(self.twBehaviors.rowCount()):
1128
-
1129
1149
  if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
1130
-
1131
1150
  if include_point_events == cfg.YES or (
1132
- include_point_events == cfg.NO
1133
- 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()
1134
1152
  ):
1135
1153
  allBehaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1136
1154
 
@@ -1185,9 +1203,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1185
1203
 
1186
1204
  if c_name != r_name:
1187
1205
  ex.checkboxes[f"{r_name}|{c_name}"] = QCheckBox()
1188
- ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet(
1189
- "text-align: center; margin-left:50%; margin-right:50%;"
1190
- )
1206
+ ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet("text-align: center; margin-left:50%; margin-right:50%;")
1191
1207
 
1192
1208
  if flag_left_bottom:
1193
1209
  # hide if cell in left-bottom part of table
@@ -1215,18 +1231,15 @@ class projectDialog(QDialog, Ui_dlgProject):
1215
1231
 
1216
1232
  # update excluded field
1217
1233
  for r in range(self.twBehaviors.rowCount()):
1218
- if include_point_events == cfg.YES or (
1219
- include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()
1220
- ):
1234
+ if include_point_events == cfg.YES or (include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()):
1221
1235
  for e in excl:
1222
1236
  if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
1223
1237
  item = QTableWidgetItem(",".join(new_excl[e]))
1224
1238
  item.setFlags(Qt.ItemIsEnabled)
1225
- item.setBackground(QColor(230, 230, 230))
1239
+ item.setBackground(self.not_editable_column_color())
1226
1240
  self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
1227
1241
 
1228
1242
  def remove_all_behaviors(self):
1229
-
1230
1243
  if not self.twBehaviors.rowCount():
1231
1244
  QMessageBox.critical(
1232
1245
  None,
@@ -1282,7 +1295,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1282
1295
  self.lbObservationsState.setText("")
1283
1296
 
1284
1297
  for r in range(self.twBehaviors.rowCount()):
1285
-
1286
1298
  # check key
1287
1299
  if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX):
1288
1300
  key = self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX).text()
@@ -1327,13 +1339,14 @@ class projectDialog(QDialog, Ui_dlgProject):
1327
1339
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
1328
1340
  if field in (cfg.TYPE, "category", "excluded", "coding map", "modifiers"):
1329
1341
  item.setFlags(Qt.ItemIsEnabled)
1330
- item.setBackground(QColor(230, 230, 230))
1342
+ item.setBackground(self.not_editable_column_color())
1331
1343
  if field == cfg.COLOR:
1332
1344
  item.setFlags(Qt.ItemIsEnabled)
1333
1345
  if QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()).isValid():
1334
1346
  item.setBackground(QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()))
1335
1347
  else:
1336
- item.setBackground(QColor(230, 230, 230))
1348
+ item.setBackground(self.not_editable_column_color())
1349
+
1337
1350
  self.twBehaviors.scrollToBottom()
1338
1351
 
1339
1352
  def remove_behavior(self):
@@ -1353,26 +1366,22 @@ class projectDialog(QDialog, Ui_dlgProject):
1353
1366
 
1354
1367
  if not self.twBehaviors.selectedIndexes():
1355
1368
  QMessageBox.warning(self, cfg.programName, "Select a behaviour to be removed")
1356
- else:
1357
- if dialog.MessageDialog(cfg.programName, "Remove the selected behavior?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1358
-
1359
- # check if behavior already used in observations
1360
- codeToDelete = self.twBehaviors.item(self.twBehaviors.selectedIndexes()[0].row(), 2).text()
1361
- for obs_id in self.pj[cfg.OBSERVATIONS]:
1362
- if codeToDelete in [
1363
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
1364
- ]:
1365
- if (
1366
- dialog.MessageDialog(
1367
- cfg.programName, "The code to remove is used in observations!", [cfg.REMOVE, cfg.CANCEL]
1368
- )
1369
- == cfg.CANCEL
1370
- ):
1371
- return
1372
- break
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
1373
1382
 
1374
- self.twBehaviors.removeRow(self.twBehaviors.selectedIndexes()[0].row())
1375
- self.twBehaviors_cellChanged(0, 0)
1383
+ self.twBehaviors.removeRow(self.twBehaviors.selectedIndexes()[0].row())
1384
+ self.twBehaviors_cellChanged(0, 0)
1376
1385
 
1377
1386
  def add_behavior(self):
1378
1387
  """
@@ -1388,36 +1397,34 @@ class projectDialog(QDialog, Ui_dlgProject):
1388
1397
  # no manual editing, gray back ground
1389
1398
  if field_type in (cfg.TYPE, cfg.COLOR, "category", cfg.MODIFIERS, "modifiers", "excluded", "coding map"):
1390
1399
  item.setFlags(Qt.ItemIsEnabled)
1391
- item.setBackground(QColor(230, 230, 230))
1400
+ # item.setBackground(QColor(230, 230, 230))
1401
+ item.setBackground(self.not_editable_column_color())
1392
1402
  self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
1393
1403
  self.twBehaviors.scrollToBottom()
1394
1404
 
1395
- def behaviorTypeChanged(self, row):
1405
+ def behavior_type_changed(self, row: int) -> None:
1396
1406
  """
1397
1407
  event type combobox changed
1398
1408
  """
1399
1409
 
1400
- 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():
1401
1411
  # let user select a coding maop
1402
- fn = QFileDialog().getOpenFileName(
1412
+ file_name, _ = QFileDialog().getOpenFileName(
1403
1413
  self,
1404
- "Select a coding map for "
1405
- 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",
1406
1415
  "",
1407
1416
  "BORIS map files (*.boris_map);;All files (*)",
1408
1417
  )
1409
- fileName = fn[0] if type(fn) is tuple else fn
1410
-
1411
- if fileName:
1418
+ if file_name:
1412
1419
  try:
1413
- new_map = json.loads(open(fileName, "r").read())
1420
+ new_map = json.loads(open(file_name, "r").read())
1414
1421
  except Exception:
1415
- QMessageBox.critical(self, cfg.programName, "Error reding the file")
1422
+ QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
1416
1423
  return
1417
1424
  self.pj[cfg.CODING_MAP][new_map["name"]] = new_map
1418
1425
 
1419
1426
  # add modifiers from coding map areas
1420
- modifstr = str(
1427
+ modifstr = json.dumps(
1421
1428
  {
1422
1429
  "0": {
1423
1430
  "name": new_map["name"],
@@ -1433,9 +1440,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1433
1440
  else:
1434
1441
  # if coding map already exists do not reset the behavior type if no filename selected
1435
1442
  if not self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text():
1436
- QMessageBox.critical(
1437
- self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" '
1438
- )
1443
+ QMessageBox.critical(self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" ')
1439
1444
  self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText("Point event")
1440
1445
  else:
1441
1446
  self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText("")
@@ -1461,12 +1466,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1461
1466
  QMessageBox.warning(self, cfg.programName, "Select a subject to remove")
1462
1467
  else:
1463
1468
  if dialog.MessageDialog(cfg.programName, "Remove the selected subject?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
1464
-
1465
1469
  flagDel = False
1466
1470
  if self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1):
1467
- subjectToDelete = self.twSubjects.item(
1468
- self.twSubjects.selectedIndexes()[0].row(), 1
1469
- ).text() # 1: subject name
1471
+ subjectToDelete = self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1).text() # 1: subject name
1470
1472
 
1471
1473
  subjectsInObs = []
1472
1474
  for obs in self.pj[cfg.OBSERVATIONS]:
@@ -1546,19 +1548,18 @@ class projectDialog(QDialog, Ui_dlgProject):
1546
1548
 
1547
1549
  self.twSubjects_cellChanged(0, 0)
1548
1550
 
1549
- def twSubjects_cellChanged(self, row: int, column: int):
1551
+ def twSubjects_cellChanged(self, row: int, column: int) -> None:
1550
1552
  """
1551
1553
  check if subject not unique
1552
1554
  """
1553
1555
 
1554
- subjects, keys = [], []
1556
+ subjects: list = []
1557
+ """keys: list = []"""
1555
1558
  self.lbSubjectsState.setText("")
1556
1559
 
1557
1560
  for r in range(self.twSubjects.rowCount()):
1558
-
1559
1561
  # check key
1560
1562
  if self.twSubjects.item(r, 0):
1561
-
1562
1563
  # check key length
1563
1564
  if (
1564
1565
  self.twSubjects.item(r, 0).text().upper() not in list(cfg.function_keys.values())
@@ -1573,11 +1574,14 @@ class projectDialog(QDialog, Ui_dlgProject):
1573
1574
  )
1574
1575
  return
1575
1576
 
1577
+ # control of duplicated key removed 2024-01-29
1578
+ """
1576
1579
  if self.twSubjects.item(r, 0).text() in keys:
1577
1580
  self.lbSubjectsState.setText(f'<font color="red">Key duplicated at row # {r + 1}</font>')
1578
1581
  else:
1579
1582
  if self.twSubjects.item(r, 0).text():
1580
1583
  keys.append(self.twSubjects.item(r, 0).text())
1584
+ """
1581
1585
 
1582
1586
  # check subject
1583
1587
  if self.twSubjects.item(r, 1):
@@ -1596,14 +1600,14 @@ class projectDialog(QDialog, Ui_dlgProject):
1596
1600
  logging.debug(f"selected row: {self.selected_twvariables_row}")
1597
1601
 
1598
1602
  if self.selected_twvariables_row == -1:
1599
- for widget in [
1603
+ for widget in (
1600
1604
  self.leLabel,
1601
1605
  self.leDescription,
1602
1606
  self.cbType,
1603
1607
  self.lePredefined,
1604
1608
  self.dte_default_date,
1605
1609
  self.leSetValues,
1606
- ]:
1610
+ ):
1607
1611
  widget.setEnabled(False)
1608
1612
  self.leLabel.setText("")
1609
1613
  self.leDescription.setText("")
@@ -1614,20 +1618,27 @@ class projectDialog(QDialog, Ui_dlgProject):
1614
1618
  return
1615
1619
 
1616
1620
  # enable widget for indep var setting
1617
- for widget in [
1621
+ for widget in (
1618
1622
  self.leLabel,
1619
1623
  self.leDescription,
1620
1624
  self.cbType,
1621
1625
  self.lePredefined,
1622
1626
  self.dte_default_date,
1623
1627
  self.leSetValues,
1624
- ]:
1628
+ ):
1625
1629
  widget.setEnabled(True)
1626
1630
 
1627
1631
  self.leLabel.setText(self.twVariables.item(row, 0).text())
1628
1632
  self.leDescription.setText(self.twVariables.item(row, 1).text())
1629
1633
  self.lePredefined.setText(self.twVariables.item(row, 3).text())
1630
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))
1631
1642
 
1632
1643
  self.cbType.clear()
1633
1644
  self.cbType.addItems(cfg.AVAILABLE_INDEP_VAR_TYPES)
@@ -1637,12 +1648,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1637
1648
 
1638
1649
  def pbCancel_clicked(self):
1639
1650
  if self.flag_modified:
1640
- if (
1641
- dialog.MessageDialog(
1642
- "BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]
1643
- )
1644
- == cfg.OK
1645
- ):
1651
+ if dialog.MessageDialog("BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]) == cfg.OK:
1646
1652
  self.reject()
1647
1653
  else:
1648
1654
  self.reject()
@@ -1650,12 +1656,12 @@ class projectDialog(QDialog, Ui_dlgProject):
1650
1656
  def check_ethogram(self) -> dict:
1651
1657
  """
1652
1658
  check ethogram for various parameter
1653
- returns ethogram dict or {cfg.CANCEL: True"} in case of error
1659
+ returns ethogram dict or {cfg.CANCEL: True} in case of error
1654
1660
 
1655
1661
  """
1656
1662
  # store behaviors
1657
- missing_data = []
1658
- checked_ethogram = {}
1663
+ missing_data: list = []
1664
+ checked_ethogram: dict = {}
1659
1665
 
1660
1666
  # Ethogram
1661
1667
  # coding maps in ethogram
@@ -1663,18 +1669,19 @@ class projectDialog(QDialog, Ui_dlgProject):
1663
1669
  # check for leading/trailing space in behaviors and modifiers
1664
1670
  code_with_leading_trailing_spaces, modifiers_with_leading_trailing_spaces = [], []
1665
1671
  for r in range(self.twBehaviors.rowCount()):
1666
-
1667
1672
  if (
1668
1673
  self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
1669
1674
  != self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text().strip()
1670
1675
  ):
1671
- code_with_leading_trailing_spaces.append(
1672
- self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
1673
- )
1676
+ code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
1674
1677
 
1675
1678
  if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text():
1676
1679
  try:
1677
- 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
+ )
1678
1685
  for k in modifiers_dict:
1679
1686
  for value in modifiers_dict[k]["values"]:
1680
1687
  modif_code = value.split(" (")[0]
@@ -1724,12 +1731,8 @@ class projectDialog(QDialog, Ui_dlgProject):
1724
1731
  row = {}
1725
1732
  for field in cfg.behavioursFields:
1726
1733
  if self.twBehaviors.item(r, cfg.behavioursFields[field]):
1727
-
1728
1734
  # check for | char in code
1729
- if (
1730
- field == cfg.BEHAVIOR_CODE
1731
- and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
1732
- ):
1735
+ if field == cfg.BEHAVIOR_CODE and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
1733
1736
  QMessageBox.warning(
1734
1737
  self,
1735
1738
  cfg.programName,
@@ -1746,10 +1749,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1746
1749
  row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
1747
1750
 
1748
1751
  if field == "modifiers" and row["modifiers"]:
1749
-
1750
1752
  if remove_leading_trailing_spaces_in_modifiers == cfg.YES:
1751
1753
  try:
1752
- modifiers_dict = eval(row["modifiers"])
1754
+ modifiers_dict = json.loads(row["modifiers"]) if row["modifiers"] else {}
1753
1755
  for k in modifiers_dict:
1754
1756
  for idx, value in enumerate(modifiers_dict[k]["values"]):
1755
1757
  modif_code = value.split(" (")[0]
@@ -1760,15 +1762,12 @@ class projectDialog(QDialog, Ui_dlgProject):
1760
1762
 
1761
1763
  row["modifiers"] = dict(modifiers_dict)
1762
1764
  except Exception:
1763
-
1764
1765
  logging.critical("Error removing leading/trailing spaces in modifiers")
1765
1766
 
1766
- QMessageBox.critical(
1767
- self, cfg.programName, "Error removing leading/trailing spaces in modifiers"
1768
- )
1767
+ QMessageBox.critical(self, cfg.programName, "Error removing leading/trailing spaces in modifiers")
1769
1768
 
1770
1769
  else:
1771
- row["modifiers"] = eval(row["modifiers"])
1770
+ row["modifiers"] = json.loads(row["modifiers"]) if row["modifiers"] else {}
1772
1771
  else:
1773
1772
  row[field] = ""
1774
1773
 
@@ -1794,27 +1793,40 @@ class projectDialog(QDialog, Ui_dlgProject):
1794
1793
  return {cfg.CANCEL: True}
1795
1794
 
1796
1795
  # check if behavior belong to category that is not in categories list
1797
- behavior_category = []
1796
+ missing_behavior_category: list = []
1798
1797
  for idx in checked_ethogram:
1799
1798
  if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
1800
1799
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
1801
1800
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
1802
- behavior_category.append(
1801
+ missing_behavior_category.append(
1803
1802
  (checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
1804
1803
  )
1805
- if behavior_category:
1806
-
1804
+ if missing_behavior_category:
1807
1805
  response = dialog.MessageDialog(
1808
1806
  f"{cfg.programName} - Behavioral categories",
1809
1807
  (
1810
- "The behavioral categorie(s) "
1811
- f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in behavior_category]))} "
1812
- "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>"
1813
1811
  ),
1814
- ["Add behavioral category/ies", "Ignore", cfg.CANCEL],
1812
+ ["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
1815
1813
  )
1816
1814
  if response == "Add behavioral category/ies":
1817
- [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
+
1818
1830
  if response == cfg.CANCEL:
1819
1831
  return {cfg.CANCEL: True}
1820
1832
 
@@ -1845,7 +1857,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1845
1857
  self.pj[cfg.TIME_FORMAT] = cfg.HHMMSS
1846
1858
 
1847
1859
  # store subjects
1848
- self.subjects_conf = {}
1860
+ self.subjects_conf: dict = {}
1849
1861
 
1850
1862
  # check for leading/trailing spaces in subjects names
1851
1863
  subjects_name_with_leading_trailing_spaces = ""
@@ -1856,7 +1868,6 @@ class projectDialog(QDialog, Ui_dlgProject):
1856
1868
 
1857
1869
  remove_leading_trailing_spaces = cfg.NO
1858
1870
  if subjects_name_with_leading_trailing_spaces:
1859
-
1860
1871
  remove_leading_trailing_spaces = dialog.MessageDialog(
1861
1872
  cfg.programName,
1862
1873
  (
@@ -1872,10 +1883,9 @@ class projectDialog(QDialog, Ui_dlgProject):
1872
1883
  # check subjects
1873
1884
  for row in range(self.twSubjects.rowCount()):
1874
1885
  # check key
1886
+ key: str = ""
1875
1887
  if self.twSubjects.item(row, 0):
1876
1888
  key = self.twSubjects.item(row, 0).text()
1877
- else:
1878
- key = ""
1879
1889
 
1880
1890
  # check subject name
1881
1891
  if self.twSubjects.item(row, 1):
@@ -1886,9 +1896,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1886
1896
 
1887
1897
  # check if subject name is empty
1888
1898
  if subjectName == "":
1889
- QMessageBox.warning(
1890
- self, cfg.programName, f"The subject name can not be empty (check row #{row + 1})."
1891
- )
1899
+ QMessageBox.warning(self, cfg.programName, f"The subject name can not be empty (check row #{row + 1}).")
1892
1900
  return
1893
1901
 
1894
1902
  if "|" in subjectName:
@@ -1899,13 +1907,11 @@ class projectDialog(QDialog, Ui_dlgProject):
1899
1907
  )
1900
1908
  return
1901
1909
  else:
1902
- QMessageBox.warning(
1903
- self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}"
1904
- )
1910
+ QMessageBox.warning(self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}")
1905
1911
  return
1906
1912
 
1907
1913
  # description
1908
- subjectDescription = ""
1914
+ subjectDescription: str = ""
1909
1915
  if self.twSubjects.item(row, 2):
1910
1916
  subjectDescription = self.twSubjects.item(row, 2).text().strip()
1911
1917
 
@@ -1915,6 +1921,25 @@ class projectDialog(QDialog, Ui_dlgProject):
1915
1921
  "description": subjectDescription,
1916
1922
  }
1917
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
+
1918
1943
  self.pj[cfg.SUBJECTS] = dict(self.subjects_conf)
1919
1944
 
1920
1945
  # check ethogram
@@ -1972,9 +1997,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1972
1997
  for converter in sorted(self.converters.keys()):
1973
1998
  self.tw_converters.setRowCount(self.tw_converters.rowCount() + 1)
1974
1999
  self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 0, QTableWidgetItem(converter)) # id / name
1975
- self.tw_converters.setItem(
1976
- self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"])
1977
- )
2000
+ self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"]))
1978
2001
  self.tw_converters.setItem(
1979
2002
  self.tw_converters.rowCount() - 1,
1980
2003
  2,