boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.2__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 -40
  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 +108 -49
  24. boris/config_file.py +58 -67
  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 +2106 -1277
  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 +134 -0
  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 +127 -36
  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 +304 -123
  79. boris/preferences_ui.py +684 -227
  80. boris/project.py +293 -270
  81. boris/project_functions.py +618 -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 +25 -17
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +71 -86
  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.2.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.2.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.2.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.6.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
  123. boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.2.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.2.dist-info}/top_level.txt +0 -0
boris/write_event.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
 
7
7
  This program is free software; you can redistribute it and/or modify
@@ -25,6 +25,7 @@ import bisect
25
25
  import logging
26
26
  from decimal import Decimal as dec
27
27
  import re
28
+ import pathlib as pl
28
29
 
29
30
  from . import config as cfg
30
31
  from . import dialog
@@ -32,10 +33,6 @@ from . import utilities as util
32
33
  from . import select_modifiers
33
34
  from . import event_operations
34
35
 
35
- from PyQt5.QtWidgets import (
36
- QAbstractItemView,
37
- )
38
-
39
36
 
40
37
  def write_event(self, event: dict, mem_time: dec) -> int:
41
38
  """
@@ -54,37 +51,35 @@ def write_event(self, event: dict, mem_time: dec) -> int:
54
51
  logging.debug(f"write event - event: {event} memtime: {mem_time}")
55
52
 
56
53
  if event is None:
57
- return
58
-
59
- # live observation finished (end of time interval reached)
60
- if not self.liveObservationStarted and mem_time.is_nan():
61
- _ = dialog.MessageDialog(
62
- cfg.programName,
63
- (
64
- "The live observation is finished.<br>"
65
- "The observation interval is "
66
- f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
67
- f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
68
- ),
69
- (cfg.OK,),
70
- )
71
- return
72
-
73
- if (
74
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE
75
- and mem_time < self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]
76
- ):
77
- _ = dialog.MessageDialog(
78
- cfg.programName,
79
- (
80
- "The live observation has not began.<br>"
81
- "The observation interval is "
82
- f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
83
- f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
84
- ),
85
- (cfg.OK,),
86
- )
87
- return
54
+ return 1
55
+
56
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
57
+ # live observation finished (end of time interval reached)
58
+ if not self.liveObservationStarted and mem_time.is_nan():
59
+ _ = dialog.MessageDialog(
60
+ cfg.programName,
61
+ (
62
+ "The live observation is finished.<br>"
63
+ "The observation interval is "
64
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
65
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
66
+ ),
67
+ (cfg.OK,),
68
+ )
69
+ return 1
70
+
71
+ if mem_time < self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]:
72
+ _ = dialog.MessageDialog(
73
+ cfg.programName,
74
+ (
75
+ "The live observation has not began.<br>"
76
+ "The observation interval is "
77
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
78
+ f"{self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
79
+ ),
80
+ (cfg.OK,),
81
+ )
82
+ return 1
88
83
 
89
84
  editing_event = "row" in event
90
85
 
@@ -94,6 +89,33 @@ def write_event(self, event: dict, mem_time: dec) -> int:
94
89
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
95
90
  mem_time += dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET]).quantize(dec(".001"))
96
91
 
92
+ # add media creation time
93
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
94
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
95
+ media_file_name = self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"]
96
+
97
+ logging.debug(f"{media_file_name=}")
98
+
99
+ media_file_name_posix = pl.Path(media_file_name).as_posix()
100
+
101
+ logging.debug(f"{media_file_name_posix=}")
102
+
103
+ # add media creation date/time
104
+
105
+ mem_time += dec(
106
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_file_name_posix]
107
+ )
108
+
109
+ # check if time > 2**31 - 1 (2147483647)
110
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.VIEWER_MEDIA, cfg.LIVE, cfg.VIEWER_LIVE):
111
+ if (mem_time < -2147483647) or (mem_time > 2147483647):
112
+ _ = dialog.MessageDialog(
113
+ cfg.programName,
114
+ (f"The timestamp must be between -2147483647 and 2147483647.<br>The current timestamp is {mem_time}"),
115
+ (cfg.OK,),
116
+ )
117
+ return 1
118
+
97
119
  # remove key code from modifiers
98
120
  subject = event.get(cfg.SUBJECT, self.currentSubject)
99
121
  comment = event.get(cfg.COMMENT, "")
@@ -101,6 +123,9 @@ def write_event(self, event: dict, mem_time: dec) -> int:
101
123
  if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
102
124
  image_idx = event.get(cfg.IMAGE_INDEX, "")
103
125
  image_path = event.get(cfg.IMAGE_PATH, "")
126
+ # check if pictures dir is relative
127
+ if str(pl.Path(image_path).parent) not in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
128
+ image_path = str(pl.Path(image_path).relative_to(pl.Path(self.projectFileName).parent))
104
129
 
105
130
  if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA):
106
131
  frame_idx = event.get(cfg.FRAME_INDEX, cfg.NA)
@@ -115,9 +140,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
115
140
  subject,
116
141
  event[cfg.BEHAVIOR_CODE],
117
142
  ):
118
- _ = dialog.MessageDialog(
119
- cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,)
120
- )
143
+ _ = dialog.MessageDialog(cfg.programName, "The same event already exists (same time, behavior code and subject).", (cfg.OK,))
121
144
  return 1
122
145
 
123
146
  # modifying event and time was changed
@@ -154,9 +177,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
154
177
  if (
155
178
  editing_event
156
179
  and image_idx
157
- != self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][
158
- cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]
159
- ]
180
+ != self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]][cfg.PJ_OBS_FIELDS[cfg.IMAGES][cfg.IMAGE_INDEX]]
160
181
  ):
161
182
  if self.checkSameEvent(
162
183
  self.observationId,
@@ -171,77 +192,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
171
192
  )
172
193
  return 1
173
194
 
174
- if "from map" not in event: # modifiers only for behaviors without coding map
175
- # check if event has modifiers
176
- modifier_str = ""
177
-
178
- if event[cfg.MODIFIERS]:
179
- selected_modifiers, modifiers_external_data = {}, {}
180
- # check if modifiers are from external data
181
- for idx in event[cfg.MODIFIERS]:
182
- if event[cfg.MODIFIERS][idx]["type"] == cfg.EXTERNAL_DATA_MODIFIER:
183
- if "row" not in event: # no edit
184
- for idx2 in self.plot_data:
185
- if self.plot_data[idx2].y_label.upper() == event[cfg.MODIFIERS][idx]["name"].upper():
186
- modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
187
- modifiers_external_data[idx]["selected"] = self.plot_data[idx2].lb_value.text()
188
- else: # edit
189
- original_modifiers_list = event.get("original_modifiers", "").split("|")
190
- modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
191
- modifiers_external_data[idx]["selected"] = original_modifiers_list[int(idx)]
192
-
193
- # check if modifiers are in single, multiple or numeric
194
- if [x for x in event[cfg.MODIFIERS] if event[cfg.MODIFIERS][x]["type"] != cfg.EXTERNAL_DATA_MODIFIER]:
195
- # pause media
196
- if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in [cfg.MEDIA]:
197
- if self.playerType == cfg.MEDIA:
198
- if self.dw_player[0].player.pause:
199
- memState = "paused"
200
- elif self.dw_player[0].player.time_pos is not None:
201
- memState = "playing"
202
- else:
203
- memState = "stopped"
204
- if memState == "playing":
205
- self.pause_video()
206
-
207
- # check if editing (original_modifiers key)
208
- currentModifiers = event.get("original_modifiers", "")
209
-
210
- modifiers_selector = select_modifiers.ModifiersList(
211
- event["code"], eval(str(event[cfg.MODIFIERS])), currentModifiers
212
- )
213
-
214
- r = modifiers_selector.exec_()
215
- if r:
216
- selected_modifiers = modifiers_selector.get_modifiers()
217
-
218
- # restart media
219
- if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
220
- if self.playerType == cfg.MEDIA:
221
- if memState == "playing":
222
- self.play_video()
223
- if not r: # cancel button pressed
224
- return
225
-
226
- all_modifiers = {**selected_modifiers, **modifiers_external_data}
227
-
228
- modifier_str = ""
229
- for idx in util.sorted_keys(all_modifiers):
230
- if modifier_str:
231
- modifier_str += "|"
232
- if all_modifiers[idx]["type"] in [cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION]:
233
- modifier_str += ",".join(all_modifiers[idx].get("selected", ""))
234
- if all_modifiers[idx]["type"] in [cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER]:
235
- modifier_str += all_modifiers[idx].get("selected", "NA")
236
-
237
- else:
238
- modifier_str = event["from map"]
239
-
240
- modifier_str = re.sub(" \(.*\)", "", modifier_str)
241
-
242
- # update current state
243
- # TODO: verify event["subject"] / self.currentSubject
244
-
245
195
  # extract State events
246
196
  state_behaviors_codes = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
247
197
 
@@ -253,7 +203,7 @@ def write_event(self, event: dict, mem_time: dec) -> int:
253
203
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
254
204
  position = dec(image_idx) # decimal to pass to util.get_current_states_modifiers_by_subject
255
205
 
256
- current_states = util.get_current_states_modifiers_by_subject(
206
+ current_states: dict = util.get_current_states_modifiers_by_subject(
257
207
  state_behaviors_codes,
258
208
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
259
209
  dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}),
@@ -261,19 +211,117 @@ def write_event(self, event: dict, mem_time: dec) -> int:
261
211
  include_modifiers=False,
262
212
  )
263
213
 
214
+ # check if ask modifiers at stop is enabled
215
+ flag_ask_at_stop = False
216
+ if event["type"] == cfg.STATE_EVENT:
217
+ for idx in event[cfg.MODIFIERS]:
218
+ if event[cfg.MODIFIERS][idx].get("ask at stop", False):
219
+ flag_ask_at_stop = True
220
+ break
221
+
222
+ flag_ask_modifier = False
223
+ if flag_ask_at_stop:
224
+ # TODO: check if new event is a STOP one
225
+
226
+ idx_subject: str = ""
227
+ for idx in current_states:
228
+ if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
229
+ idx_subject = idx
230
+ break
231
+
232
+ if event[cfg.BEHAVIOR_CODE] in current_states[idx_subject]:
233
+ flag_ask_modifier = True
234
+ else:
235
+ flag_ask_modifier = True
236
+
237
+ if flag_ask_modifier:
238
+ if "from map" in event: # modifiers only for behaviors without coding map
239
+ modifier_str = event["from map"]
240
+ else:
241
+ # check if event has modifiers
242
+ modifier_str: str = ""
243
+
244
+ if event[cfg.MODIFIERS]:
245
+ selected_modifiers: dict = {}
246
+ modifiers_external_data: dict = {}
247
+ # check if modifiers are from external data
248
+ for idx in event[cfg.MODIFIERS]:
249
+ if event[cfg.MODIFIERS][idx]["type"] == cfg.EXTERNAL_DATA_MODIFIER:
250
+ if editing_event:
251
+ original_modifiers_list = event.get("original_modifiers", "").split("|")
252
+ modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
253
+ modifiers_external_data[idx]["selected"] = original_modifiers_list[int(idx)]
254
+
255
+ else: # no edit
256
+ for idx2 in self.plot_data:
257
+ if self.plot_data[idx2].y_label.upper() == event[cfg.MODIFIERS][idx]["name"].upper():
258
+ modifiers_external_data[idx] = dict(event[cfg.MODIFIERS][idx])
259
+ modifiers_external_data[idx]["selected"] = self.plot_data[idx2].lb_value.text()
260
+
261
+ # check if modifiers are in single, multiple or numeric
262
+ if [x for x in event[cfg.MODIFIERS] if event[cfg.MODIFIERS][x]["type"] != cfg.EXTERNAL_DATA_MODIFIER]:
263
+ # pause media
264
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
265
+ if self.playerType == cfg.MEDIA:
266
+ if self.dw_player[0].player.pause:
267
+ memState = cfg.PAUSED
268
+ elif self.dw_player[0].player.time_pos is not None:
269
+ memState = cfg.PLAYING
270
+ else:
271
+ memState = cfg.STOPPED
272
+ if memState == cfg.PLAYING:
273
+ self.pause_video()
274
+
275
+ # check if editing (original_modifiers key)
276
+ currentModifiers = event.get("original_modifiers", "")
277
+
278
+ modifiers_selector = select_modifiers.ModifiersList(
279
+ event[cfg.BEHAVIOR_CODE], eval(str(event[cfg.MODIFIERS])), currentModifiers
280
+ )
281
+
282
+ r = modifiers_selector.exec_()
283
+ if r:
284
+ selected_modifiers = modifiers_selector.get_modifiers()
285
+
286
+ # restart media
287
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
288
+ if self.playerType == cfg.MEDIA:
289
+ if memState == cfg.PLAYING:
290
+ self.play_video()
291
+ if not r: # cancel button pressed
292
+ return
293
+
294
+ all_modifiers = {**selected_modifiers, **modifiers_external_data}
295
+
296
+ modifier_str: str = ""
297
+ for idx in util.sorted_keys(all_modifiers):
298
+ if modifier_str:
299
+ modifier_str += "|"
300
+ if all_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
301
+ modifier_str += ",".join(all_modifiers[idx].get("selected", ""))
302
+ if all_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
303
+ modifier_str += all_modifiers[idx].get("selected", "NA")
304
+
305
+ modifier_str = re.sub(r" \(.*\)", "", modifier_str)
306
+ else: # do not ask modifier
307
+ modifier_str = ""
308
+
309
+ # update current state
310
+ # TODO: verify event["subject"] / self.currentSubject
311
+
312
+ # print(f"{current_states=}")
313
+
264
314
  # logging.debug(f"self.currentSubject {self.currentSubject}")
265
315
  # logging.debug(f"current_states {current_states}")
266
316
 
267
317
  # fill the undo list
268
- event_operations.fill_events_undo_list(
269
- self, "Undo last event edition" if editing_event else "Undo last event insertion"
270
- )
318
+ event_operations.fill_events_undo_list(self, "Undo last event edition" if editing_event else "Undo last event insertion")
271
319
 
272
320
  logging.debug("save list of events for undo operation")
273
321
 
274
322
  if not editing_event:
275
323
  if self.currentSubject:
276
- csj: list = []
324
+ csj: list = [] # list of current state for the current subject
277
325
  for idx in current_states:
278
326
  if idx in self.pj[cfg.SUBJECTS] and self.pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == self.currentSubject:
279
327
  csj = current_states[idx]
@@ -287,6 +335,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
287
335
 
288
336
  logging.debug(f"csj {csj}")
289
337
 
338
+ # print(f"{csj=}")
339
+
290
340
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
291
341
  check_index = cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
292
342
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
@@ -302,6 +352,21 @@ def write_event(self, event: dict, mem_time: dec) -> int:
302
352
  if ev[cfg.EVENT_BEHAVIOR_FIELD_IDX] == cs:
303
353
  cm[cs] = ev[cfg.EVENT_MODIFIER_FIELD_IDX]
304
354
 
355
+ if flag_ask_at_stop:
356
+ # set modifier to START behavior
357
+ if event[cfg.BEHAVIOR_CODE] in csj:
358
+ mem_idx = -1
359
+ for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
360
+ # print(f"{e=}")
361
+ if e[0] >= mem_time:
362
+ break
363
+ # same behavior, same subject and modifier(s) not set
364
+ if e[2] == event[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
365
+ mem_idx = idx
366
+ if mem_idx != -1:
367
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = modifier_str
368
+ csj.remove(event[cfg.BEHAVIOR_CODE])
369
+
305
370
  for cs in csj:
306
371
  # close state if same state without modifier
307
372
  if (
@@ -312,25 +377,74 @@ def write_event(self, event: dict, mem_time: dec) -> int:
312
377
  modifier_str = cm[cs]
313
378
  continue
314
379
 
315
- if (event["excluded"] and cs in event["excluded"].split(",")) or (
380
+ if (event[cfg.EXCLUDED] and cs in event[cfg.EXCLUDED].split(",")) or (
316
381
  event[cfg.BEHAVIOR_CODE] == cs and cm[cs] != modifier_str
317
382
  ):
383
+ # check if behavior to stop is a 'ask modifier at stop'
384
+ behavior_to_stop = [
385
+ self.pj[cfg.ETHOGRAM][x]
386
+ for x in self.pj[cfg.ETHOGRAM]
387
+ if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == cs
388
+ and self.pj[cfg.ETHOGRAM][x]["type"] in (cfg.STATE_EVENT, cfg.STATE_EVENT_WITH_CODING_MAP)
389
+ ]
390
+ if behavior_to_stop:
391
+ behavior_to_stop = behavior_to_stop[0]
392
+
393
+ flag_behavior_ask_at_stop = False
394
+ for idx in behavior_to_stop[cfg.MODIFIERS]:
395
+ if behavior_to_stop[cfg.MODIFIERS][idx].get("ask at stop", False):
396
+ flag_behavior_ask_at_stop = True
397
+ break
398
+ if flag_behavior_ask_at_stop:
399
+ modifiers_selector = select_modifiers.ModifiersList(
400
+ behavior_to_stop[cfg.BEHAVIOR_CODE],
401
+ eval(str(behavior_to_stop[cfg.MODIFIERS])),
402
+ currentModifier="",
403
+ )
404
+
405
+ r = modifiers_selector.exec_()
406
+ if r:
407
+ selected_modifiers = modifiers_selector.get_modifiers()
408
+
409
+ behavior_to_stop_modifier_str: str = ""
410
+ for idx in util.sorted_keys(selected_modifiers):
411
+ if behavior_to_stop_modifier_str:
412
+ behavior_to_stop_modifier_str += "|"
413
+ if selected_modifiers[idx]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
414
+ behavior_to_stop_modifier_str += ",".join(selected_modifiers[idx].get("selected", ""))
415
+ if selected_modifiers[idx]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
416
+ behavior_to_stop_modifier_str += selected_modifiers[idx].get("selected", "NA")
417
+
418
+ # set the start modifier
419
+ mem_idx = -1
420
+ for idx, e in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
421
+ if e[0] >= mem_time - dec("0.001"):
422
+ break
423
+ # same behavior, same subject and modifier(s) not set
424
+ if e[2] == behavior_to_stop[cfg.BEHAVIOR_CODE] and e[1] == self.currentSubject and e[3] == "":
425
+ mem_idx = idx
426
+ if mem_idx != -1:
427
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][mem_idx][3] = behavior_to_stop_modifier_str
428
+
429
+ else:
430
+ behavior_to_stop_modifier_str = cm[cs]
431
+
318
432
  # add excluded state event to observations (= STOP them)
319
433
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE):
320
434
  bisect.insort(
321
435
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
322
- [mem_time - dec("0.001"), self.currentSubject, cs, cm[cs], ""],
436
+ [mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, ""],
323
437
  )
324
438
 
325
439
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA):
326
440
  bisect.insort(
327
441
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
328
- [mem_time - dec("0.001"), self.currentSubject, cs, cm[cs], "", cfg.NA],
442
+ [mem_time - dec("0.001"), self.currentSubject, cs, behavior_to_stop_modifier_str, "", cfg.NA],
329
443
  )
330
444
 
331
445
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
332
446
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(
333
- [mem_time, self.currentSubject, cs, cm[cs], "", image_idx, image_path]
447
+ [mem_time, self.currentSubject, cs, behavior_to_stop_modifier_str, "", image_idx, image_path]
334
448
  )
335
449
 
336
450
  # order by image index ASC
@@ -349,8 +463,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
349
463
  comment,
350
464
  frame_idx,
351
465
  ]
352
- # order by image index ASC
353
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
466
+ # order events list using time, subject, behavior
467
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
354
468
 
355
469
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
356
470
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
@@ -360,8 +474,8 @@ def write_event(self, event: dict, mem_time: dec) -> int:
360
474
  modifier_str,
361
475
  comment,
362
476
  ]
363
- # order by image index ASC
364
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
477
+ # order events list using time, subject, behavior
478
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
365
479
 
366
480
  elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
367
481
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][event["row"]] = [
@@ -402,16 +516,6 @@ def write_event(self, event: dict, mem_time: dec) -> int:
402
516
  # reload all events in tw
403
517
  self.load_tw_events(self.observationId)
404
518
 
405
- if self.playerType in (cfg.MEDIA, cfg.LIVE):
406
- position_in_events = [
407
- i for i, t in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]) if t[0] == mem_time
408
- ][0]
409
-
410
- if position_in_events == len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]) - 1:
411
- self.twEvents.scrollToBottom()
412
- else:
413
- self.twEvents.scrollToItem(self.twEvents.item(position_in_events, 0), QAbstractItemView.EnsureVisible)
414
-
415
519
  self.project_changed()
416
520
 
417
521
  self.get_events_current_row()
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: boris-behav-obs
3
+ Version: 9.7.2
4
+ Summary: BORIS - Behavioral Observation Research Interactive Software
5
+ Author-email: Olivier Friard <olivier.friard@unito.it>
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, http://www.boris.unito.it
8
+ Project-URL: Documentation, https://boris.readthedocs.io/en/latest/
9
+ Project-URL: Change_log, https://github.com/olivierfriard/BORIS/wiki/BORIS-change-log-v.8
10
+ Project-URL: Source_code, https://github.com/olivierfriard/BORIS
11
+ Project-URL: Issues, https://github.com/olivierfriard/BORIS/issues
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.TXT
21
+ Requires-Dist: exifread==3.5.1
22
+ Requires-Dist: numpy==2.3.2
23
+ Requires-Dist: matplotlib==3.10.5
24
+ Requires-Dist: pandas==2.3.2
25
+ Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]==3.8.0
26
+ Requires-Dist: pyreadr==0.5.3
27
+ Requires-Dist: pyside6==6.10
28
+ Requires-Dist: hachoir==3.3.0
29
+ Requires-Dist: scipy==1.16.1
30
+ Requires-Dist: scikit-learn==1.7.1
31
+ Provides-Extra: dev
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Provides-Extra: r
36
+ Requires-Dist: rpy2>=3.6.1; extra == "r"
37
+ Dynamic: license-file
38
+
39
+ BORIS (Behavioral Observation Research Interactive Software)
40
+ ===============================================================
41
+
42
+
43
+ ![BORIS logo](https://github.com/olivierfriard/BORIS/blob/master/boris/icons/logo_boris.png?raw=true)
44
+
45
+ BORIS is an easy-to-use event logging software for video/audio coding or live observations.
46
+
47
+ BORIS is a free and open-source software available for GNU/Linux and Windows.
48
+ You can not longer run BORIS natively on MacOS (since v.8). Some alternatives to run the last version of BORIS are available, see [BORIS on MacOS](https://www.boris.unito.it/download_mac).
49
+
50
+ It provides also some analysis tools like time budget and some plotting functions.
51
+
52
+ <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
53
+
54
+
55
+ The BORIS paper has more than 2407 citations in peer-reviewed scientific publications.
56
+
57
+
58
+
59
+
60
+ See the official [BORIS web site](https://www.boris.unito.it).
61
+
62
+ <a href="https://www.boris.unito.it" target="_blank"><img alt="Website" src="https://img.shields.io/website?url=https%3A%2F%2Fwww.boris.unito.it"></a>
63
+ <a href="https://www.boris.unito.it/user_guide/" target="_blank"><img alt="User guide" src="https://img.shields.io/badge/Documentation-orange"></a>
64
+ [![Python web site](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org)
65
+ ![Python versions](https://img.shields.io/pypi/pyversions/boris-behav-obs)
66
+ ![BORIS license](https://img.shields.io/pypi/l/boris-behav-obs)
67
+ [![PyPI version](https://img.shields.io/pypi/v/boris-behav-obs.svg)](https://pypi.org/project/boris-behav-obs/)
68
+
69
+ [![Number of downloads](https://static.pepy.tech/personalized-badge/boris-behav-obs?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/boris-behav-obs)
70
+ ![commit-activity](https://img.shields.io/github/commit-activity/m/olivierfriard/BORIS)
71
+ ![GitHub last commit](https://img.shields.io/github/last-commit/olivierfriard/BORIS)
72
+
73
+ ![BORIS scopus citations badge](https://penelope.unito.it/friard/boris_scopus_citations.svg)
74
+
75
+
76
+ ![GitHub Repo stars](https://img.shields.io/github/stars/olivierfriard/BORIS?style=flat&label=Stars)
77
+ [![Please Star](https://img.shields.io/badge/⭐-Star%20this%20repo-blue?style=flat-square)](https://github.com/olivierfriard/BORIS/stargazers)
78
+
79
+ # Documentation
80
+
81
+
82
+
83
+ The [user guide](https://www.boris.unito.it/user_guide/) provides a good starting point for learning how to use BORIS.
84
+
85
+ Some [video tutorials](https://www.boris.unito.it/video_tutorials/) are available.
86
+
87
+
88
+
89
+
90
+
91
+ # Bug reports and feature requests
92
+
93
+
94
+ To search for bugs, report them or request a feature, please use the [GitHub issues tracker](https://github.com/olivierfriard/BORIS/issues)
95
+
96
+
97
+
98
+
99
+
100
+ # Citing BORIS
101
+
102
+
103
+ Please acknowledge and cite the use of this software and its authors when
104
+ results are used in publications or published elsewhere. You can use the
105
+ following BibTex entry
106
+
107
+ ```
108
+ @article {MEE3:MEE312584,
109
+ author = {Friard, Olivier and Gamba, Marco},
110
+ title = {BORIS: a free, versatile open-source event-logging software for video/audio coding and live observations},
111
+ journal = {Methods in Ecology and Evolution},
112
+ issn = {2041-210X},
113
+ url = {http://dx.doi.org/10.1111/2041-210X.12584},
114
+ doi = {10.1111/2041-210X.12584},
115
+ pages = {1324--1330},
116
+ year = {2016},
117
+ }
118
+ ```
119
+
120
+ You can also send us a nice postcard! See the [user testimonials](https://www.boris.unito.it/postcards).
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+ # Licence
130
+
131
+
132
+ This program is distributed in the hope that it will be useful,
133
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
134
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
135
+ GNU General Public License for more details.
136
+
137
+
138
+ Distributed with a [GPL v.3 license](LICENSE.TXT).
139
+
140
+ Copyright (C) 2012-2025 Olivier Friard