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/event_operations.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
@@ -22,20 +22,25 @@ Copyright 2012-2023 Olivier Friard
22
22
  """
23
23
 
24
24
  import logging
25
+ import copy
26
+ import time
25
27
  from decimal import Decimal as dec
26
28
  from decimal import InvalidOperation
27
29
  from decimal import ROUND_DOWN
28
- from typing import Union, Optional, List, Tuple, Dict
30
+ from typing import Union
31
+
29
32
 
30
33
  from . import config as cfg
31
34
  from . import utilities as util
32
35
  from . import dialog
33
36
  from . import select_subj_behav
34
37
  from . import select_modifiers
38
+ from . import write_event
35
39
  from .edit_event import DlgEditEvent, EditSelectedEvents
36
40
 
37
- from PyQt5.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
38
- from PyQt5.QtCore import QTime, Qt
41
+ from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
42
+ from PySide6.QtCore import QTime, Qt
43
+ from PySide6.QtGui import QClipboard
39
44
 
40
45
 
41
46
  def add_event(self):
@@ -49,11 +54,10 @@ def add_event(self):
49
54
 
50
55
  if self.pause_before_addevent:
51
56
  # pause media
52
- if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
53
- if self.playerType == cfg.MEDIA:
54
- memState = self.is_playing()
55
- if memState:
56
- self.pause_video()
57
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
58
+ memState = self.is_playing()
59
+ if memState:
60
+ self.pause_video()
57
61
 
58
62
  if not self.pj[cfg.ETHOGRAM]:
59
63
  QMessageBox.warning(self, cfg.programName, "The ethogram is not set!")
@@ -66,7 +70,7 @@ def add_event(self):
66
70
 
67
71
  editWindow = DlgEditEvent(
68
72
  observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
69
- time_value=0,
73
+ time_value=dec("NaN"),
70
74
  image_idx=0,
71
75
  current_time=current_time,
72
76
  time_format=self.timeFormat,
@@ -74,31 +78,48 @@ def add_event(self):
74
78
  )
75
79
  editWindow.setWindowTitle("Add a new event")
76
80
 
77
- sortedSubjects = [""] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
81
+ sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
78
82
 
79
83
  editWindow.cobSubject.addItems(sortedSubjects)
80
- editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
84
+ if self.currentSubject:
85
+ editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
81
86
 
82
87
  sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
83
88
 
84
89
  editWindow.cobCode.addItems(sortedCodes)
85
90
 
86
91
  if editWindow.exec_(): # button OK
87
-
88
92
  # MEDIA / LIVE
89
93
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
90
94
  newTime = editWindow.time_widget.get_time()
95
+ if newTime is None:
96
+ QMessageBox.warning(
97
+ self,
98
+ cfg.programName,
99
+ ("Select a time format"),
100
+ )
101
+ return
91
102
 
92
103
  for idx in self.pj[cfg.ETHOGRAM]:
93
104
  if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
94
-
95
105
  event = self.full_event(idx)
96
106
 
97
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
107
+ event[cfg.SUBJECT] = (
108
+ "" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
109
+ )
98
110
  if editWindow.leComment.toPlainText():
99
111
  event[cfg.COMMENT] = editWindow.leComment.toPlainText()
100
112
 
101
- self.write_event(event, newTime)
113
+ # determine the frame index
114
+ if self.playerType == cfg.MEDIA:
115
+ mem_time = self.getLaps()
116
+ if not self.seek_mediaplayer(newTime):
117
+ time.sleep(0.1)
118
+ frame_idx = self.get_frame_index()
119
+ event[cfg.FRAME_INDEX] = frame_idx
120
+ self.seek_mediaplayer(mem_time)
121
+
122
+ write_event.write_event(self, event, newTime)
102
123
  break
103
124
 
104
125
  self.update_realtime_plot(force_plot=True)
@@ -126,13 +147,17 @@ def add_event(self):
126
147
  # IMAGES
127
148
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
128
149
  new_index = editWindow.sb_image_idx.value()
150
+ if new_index == 0:
151
+ QMessageBox.warning(self, cfg.programName, "The image index cannot be null")
152
+ return
129
153
 
130
154
  for idx in self.pj[cfg.ETHOGRAM]:
131
155
  if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
132
-
133
156
  event = self.full_event(idx)
134
157
 
135
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
158
+ event[cfg.SUBJECT] = (
159
+ "" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
160
+ )
136
161
  if editWindow.leComment.toPlainText():
137
162
  event[cfg.COMMENT] = editWindow.leComment.toPlainText()
138
163
 
@@ -146,16 +171,18 @@ def add_event(self):
146
171
  time_ = dec("NaN")
147
172
  if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
148
173
  if self.playerType != cfg.VIEWER_IMAGES:
149
- if self.extract_exif_DateTimeOriginal(self.images_list[new_index]) != -1:
150
- time_ = (
151
- self.extract_exif_DateTimeOriginal(self.images_list[new_index])
152
- - self.image_time_ref
153
- )
174
+ exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[new_index])
175
+ if exif_date_time != -1:
176
+ time_ = exif_date_time
177
+
178
+ # check if first value must be substracted
179
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.SUBSTRACT_FIRST_EXIF_DATE, True):
180
+ time_ -= self.image_time_ref
154
181
 
155
182
  elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
156
183
  time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
157
184
 
158
- self.write_event(event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
185
+ write_event.write_event(self, event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
159
186
 
160
187
  break
161
188
 
@@ -173,13 +200,28 @@ def find_events(self):
173
200
 
174
201
  self.find_dialog = dialog.FindInEvents()
175
202
  # list of rows to find
176
- self.find_dialog.rowsToFind = set([item.row() for item in self.twEvents.selectedIndexes()])
203
+ self.find_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
177
204
  self.find_dialog.currentIdx = -1
178
205
  self.find_dialog.clickSignal.connect(self.click_signal_find_in_events)
179
206
  self.find_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
180
207
  self.find_dialog.show()
181
208
 
182
209
 
210
+ def find_replace_events(self):
211
+ """
212
+ find and replace in events
213
+ """
214
+ fill_events_undo_list(self, "Undo Find/Replace operations")
215
+ self.find_replace_dialog = dialog.FindReplaceEvents()
216
+ self.find_replace_dialog.currentIdx = -1
217
+ self.find_replace_dialog.currentIdx_idx = -1
218
+ # list of rows to find/replace
219
+ self.find_replace_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
220
+ self.find_replace_dialog.clickSignal.connect(self.click_signal_find_replace_in_events)
221
+ self.find_replace_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
222
+ self.find_replace_dialog.show()
223
+
224
+
183
225
  def filter_events(self):
184
226
  """
185
227
  filter coded events and subjects
@@ -188,11 +230,8 @@ def filter_events(self):
188
230
  parameters = select_subj_behav.choose_obs_subj_behav_category(
189
231
  self,
190
232
  selected_observations=[], # empty selection of observations for selecting all subjects and behaviors
191
- start_coding=dec("NaN"),
192
- end_coding=dec("NaN"),
193
- maxTime=None,
194
- flagShowIncludeModifiers=False,
195
- flagShowExcludeBehaviorsWoEvents=False,
233
+ show_include_modifiers=False,
234
+ show_exclude_non_coded_behaviors=False,
196
235
  by_category=False,
197
236
  )
198
237
  if parameters == {}:
@@ -223,7 +262,10 @@ def fill_events_undo_list(self, operation_description: str) -> None:
223
262
  """
224
263
  fill the undo events list for Undo function (CTRL + Z)
225
264
  """
226
- self.undo_queue.append(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][:])
265
+ logging.debug("fill_events_undo_list function")
266
+
267
+ self.undo_queue.append(copy.deepcopy(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]))
268
+
227
269
  self.undo_description.append(operation_description)
228
270
 
229
271
  self.actionUndo.setText(operation_description)
@@ -234,17 +276,22 @@ def fill_events_undo_list(self, operation_description: str) -> None:
234
276
  if len(self.undo_queue) > cfg.MAX_UNDO_QUEUE:
235
277
  self.undo_queue.popleft()
236
278
  self.undo_description.popleft()
237
- logging.debug(f"Max events undo ")
279
+ logging.debug("Max events undo ")
238
280
 
239
281
 
240
282
  def undo_event_operation(self) -> None:
241
283
  """
242
284
  undo operation on event(s)
243
285
  """
286
+
287
+ logging.debug("Undo event operation function")
288
+
244
289
  if len(self.undo_queue) == 0:
245
- self.statusbar.showMessage(f"The Undo buffer is empty", 5000)
290
+ self.statusbar.showMessage("The Undo buffer is empty", 5000)
246
291
  return
292
+
247
293
  events = self.undo_queue.pop()
294
+
248
295
  operation_description = self.undo_description.pop()
249
296
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = events[:]
250
297
  self.project_changed()
@@ -274,32 +321,25 @@ def delete_all_events(self):
274
321
  self.no_observation()
275
322
  return
276
323
 
277
- if not self.twEvents.rowCount():
324
+ if not self.tv_idx2events_idx:
278
325
  QMessageBox.warning(self, cfg.programName, "No events to delete")
279
326
  return
280
327
 
281
328
  if (
282
329
  dialog.MessageDialog(
283
330
  cfg.programName,
284
- ("Confirm the deletion of all (filtered) events in the current observation?<br>" "Filters do not apply!"),
331
+ ("Confirm the deletion of all (filtered) events in the current observation?<br>Filters do not apply!"),
285
332
  [cfg.YES, cfg.NO],
286
333
  )
287
334
  == cfg.YES
288
335
  ):
289
-
290
336
  # fill the undo list
291
337
  fill_events_undo_list(self, "Undo 'Delete all events'")
292
338
 
293
- rows_to_delete: list = []
294
- for row in range(self.twEvents.rowCount()):
295
- rows_to_delete.append(
296
- self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole)
297
- )
298
-
299
339
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
300
340
  event
301
341
  for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
302
- if event_idx not in rows_to_delete
342
+ if event_idx not in self.tv_idx2events_idx
303
343
  ]
304
344
 
305
345
  self.update_realtime_plot(force_plot=True)
@@ -319,7 +359,7 @@ def delete_selected_events(self):
319
359
 
320
360
  logging.debug("begin function delete_selected_events")
321
361
 
322
- if not self.twEvents.selectedIndexes():
362
+ if not self.tv_events.selectedIndexes():
323
363
  QMessageBox.warning(self, cfg.programName, "No event selected!")
324
364
  else:
325
365
  # list of rows to delete (set for unique)
@@ -327,10 +367,8 @@ def delete_selected_events(self):
327
367
  fill_events_undo_list(self, "Undo 'Delete selected events'")
328
368
 
329
369
  rows_to_delete: list = []
330
- for row in set([item.row() for item in self.twEvents.selectedIndexes()]):
331
- rows_to_delete.append(
332
- self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole)
333
- )
370
+ for row in set([item.row() for item in self.tv_events.selectedIndexes()]):
371
+ rows_to_delete.append(self.tv_idx2events_idx[row])
334
372
 
335
373
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
336
374
  event
@@ -367,7 +405,7 @@ def select_events_between_activated(self):
367
405
  return None
368
406
  return timeSeconds
369
407
 
370
- if not self.twEvents.rowCount():
408
+ if not self.tv_idx2events_idx:
371
409
  QMessageBox.warning(self, cfg.programName, "There are no events to select")
372
410
  return
373
411
 
@@ -380,7 +418,6 @@ def select_events_between_activated(self):
380
418
  )
381
419
 
382
420
  if ok and text != "":
383
-
384
421
  if "-" not in text:
385
422
  QMessageBox.critical(self, cfg.programName, "Use minus sign (-) to separate initial value from final value")
386
423
  return
@@ -400,15 +437,76 @@ def select_events_between_activated(self):
400
437
  if to_sec < from_sec:
401
438
  QMessageBox.critical(self, cfg.programName, "The initial time is greater than the final time")
402
439
  return
403
- self.twEvents.clearSelection()
404
- self.twEvents.setSelectionMode(QAbstractItemView.MultiSelection)
405
- for r in range(self.twEvents.rowCount()):
406
- if ":" in self.twEvents.item(r, cfg.TW_EVENTS_FIELDS[self.playerType][cfg.TIME]).text():
407
- time = util.time2seconds(self.twEvents.item(r, cfg.TW_EVENTS_FIELDS[self.playerType][cfg.TIME]).text())
408
- else:
409
- time = dec(self.twEvents.item(r, cfg.TW_EVENTS_FIELDS[self.playerType][cfg.TIME]).text())
440
+
441
+ self.tv_events.clearSelection()
442
+ self.tv_events.setSelectionMode(QAbstractItemView.MultiSelection)
443
+
444
+ # for r in range(self.tv_events.rowCount()):
445
+ # for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
446
+ for tv_idx in range(len(self.tv_idx2events_idx)):
447
+ time = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][self.tv_idx2events_idx[tv_idx]][
448
+ cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]
449
+ ]
450
+
410
451
  if from_sec <= time <= to_sec:
411
- self.twEvents.selectRow(r)
452
+ self.tv_events.selectRow(tv_idx)
453
+
454
+ self.tv_events.setSelectionMode(QAbstractItemView.ExtendedSelection)
455
+
456
+
457
+ def add_comment(self):
458
+ """
459
+ add a comment to the selected events
460
+ operation can be undone with Undo
461
+ """
462
+ tvevents_rows_to_edit = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
463
+ if not len(tvevents_rows_to_edit):
464
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
465
+ return
466
+
467
+ comment_str: str = ""
468
+ if len(tvevents_rows_to_edit) == 1:
469
+ pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
470
+ comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
471
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
472
+ ]
473
+ else:
474
+ # check if comment is the same in all selected events
475
+
476
+ if (
477
+ len(
478
+ set(
479
+ [
480
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][self.tv_idx2events_idx[tvevents_row]][
481
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
482
+ ]
483
+ for tvevents_row in tvevents_rows_to_edit
484
+ ]
485
+ )
486
+ )
487
+ == 1
488
+ ):
489
+ pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
490
+ comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
491
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
492
+ ]
493
+
494
+ new_comment, ok = QInputDialog.getText(self, "Add/Edit a comment", "Comment:", text=comment_str)
495
+ if not ok:
496
+ return
497
+
498
+ # fill the undo list
499
+ fill_events_undo_list(self, "Undo last comment operation")
500
+
501
+ for tvevents_row in tvevents_rows_to_edit:
502
+ pj_event_idx = self.tv_idx2events_idx[tvevents_row]
503
+
504
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
505
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
506
+ ] = new_comment
507
+
508
+ # reload all events in tw
509
+ self.load_tw_events(self.observationId)
412
510
 
413
511
 
414
512
  def edit_selected_events(self):
@@ -416,58 +514,46 @@ def edit_selected_events(self):
416
514
  edit one or more selected events for subject, behavior and/or comment
417
515
  """
418
516
  # list of rows to edit
419
- twEvents_rows_to_edit = set([item.row() for item in self.twEvents.selectedIndexes()])
420
517
 
421
- if not len(twEvents_rows_to_edit):
518
+ tvevents_rows_to_edit = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
519
+
520
+ if not len(tvevents_rows_to_edit):
422
521
  QMessageBox.warning(self, cfg.programName, "No event selected!")
423
- elif len(twEvents_rows_to_edit) == 1: # 1 event selected
522
+
523
+ elif len(tvevents_rows_to_edit) == 1: # 1 event selected
424
524
  edit_event(self)
525
+
425
526
  else: # editing of more events
426
- dialogWindow = EditSelectedEvents()
427
- dialogWindow.all_behaviors = sorted(
428
- [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]]
429
- )
527
+ dialog_window = EditSelectedEvents()
528
+ dialog_window.all_behaviors = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
430
529
 
431
- dialogWindow.all_subjects = [
432
- self.pj[cfg.SUBJECTS][str(k)][cfg.SUBJECT_NAME]
433
- for k in sorted([int(x) for x in self.pj[cfg.SUBJECTS].keys()])
530
+ dialog_window.all_subjects = [cfg.NO_FOCAL_SUBJECT] + [
531
+ self.pj[cfg.SUBJECTS][str(k)][cfg.SUBJECT_NAME] for k in sorted([int(x) for x in self.pj[cfg.SUBJECTS].keys()])
434
532
  ]
435
533
 
436
- if dialogWindow.exec_():
437
-
534
+ if dialog_window.exec_():
438
535
  # fill the undo list
439
536
  fill_events_undo_list(self, "Undo 'Edit selected event(s)'")
440
537
 
441
538
  tsb_to_edit: list = []
442
- for row in twEvents_rows_to_edit:
443
- tsb_to_edit.append(
444
- self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole)
445
- )
446
- """
447
- tsb_to_edit.append(
448
- [
449
- util.time2seconds(self.read_tw_event_field(row, self.playerType, cfg.TIME))
450
- if self.timeFormat == cfg.HHMMSS
451
- else dec(self.read_tw_event_field(row, self.playerType, cfg.TIME)),
452
- self.read_tw_event_field(row, self.playerType, cfg.SUBJECT),
453
- self.read_tw_event_field(row, self.playerType, cfg.BEHAVIOR_CODE),
454
- ]
455
- )
456
- """
539
+ for row in tvevents_rows_to_edit:
540
+ tsb_to_edit.append(self.tv_idx2events_idx[row])
457
541
 
458
- behavior_codes = []
459
- modifiers_mem = []
460
- mem_event_idx = []
542
+ behavior_codes: list = []
543
+ modifiers_mem: list = []
544
+ mem_event_idx: list = []
461
545
  for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
462
546
  if idx in tsb_to_edit:
463
-
464
547
  new_event = list(event)
465
- if dialogWindow.rbSubject.isChecked():
466
- new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = dialogWindow.newText.selectedItems()[0].text()
467
- if dialogWindow.rbBehavior.isChecked():
468
- new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] = dialogWindow.newText.selectedItems()[0].text()
469
- if dialogWindow.rbComment.isChecked():
470
- new_event[cfg.EVENT_COMMENT_FIELD_IDX] = dialogWindow.commentText.text()
548
+ if dialog_window.rbSubject.isChecked():
549
+ if dialog_window.newText.selectedItems()[0].text() == cfg.NO_FOCAL_SUBJECT:
550
+ new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = ""
551
+ else:
552
+ new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = dialog_window.newText.selectedItems()[0].text()
553
+ if dialog_window.rbBehavior.isChecked():
554
+ new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] = dialog_window.newText.selectedItems()[0].text()
555
+ if dialog_window.rbComment.isChecked():
556
+ new_event[cfg.EVENT_COMMENT_FIELD_IDX] = dialog_window.commentText.text()
471
557
 
472
558
  if new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behavior_codes:
473
559
  behavior_codes.append(new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
@@ -481,7 +567,6 @@ def edit_selected_events(self):
481
567
 
482
568
  # check if behavior is unique for editing modifiers
483
569
  if len(behavior_codes) == 1:
484
-
485
570
  # get behavior index
486
571
  for idx in self.pj[cfg.ETHOGRAM]:
487
572
  if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == behavior_codes[0]:
@@ -497,9 +582,7 @@ def edit_selected_events(self):
497
582
  current_modifier = ""
498
583
 
499
584
  if event["modifiers"]:
500
- modifiers_selector = select_modifiers.ModifiersList(
501
- behavior_codes[0], eval(str(event["modifiers"])), current_modifier
502
- )
585
+ modifiers_selector = select_modifiers.ModifiersList(behavior_codes[0], eval(str(event["modifiers"])), current_modifier)
503
586
 
504
587
  r = modifiers_selector.exec_()
505
588
  if r:
@@ -520,9 +603,7 @@ def edit_selected_events(self):
520
603
  # set new modifier
521
604
  for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
522
605
  if idx in mem_event_idx:
523
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][
524
- cfg.EVENT_MODIFIER_FIELD_IDX
525
- ] = modifier_str
606
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.EVENT_MODIFIER_FIELD_IDX] = modifier_str
526
607
 
527
608
  self.load_tw_events(self.observationId)
528
609
 
@@ -531,20 +612,27 @@ def edit_selected_events(self):
531
612
 
532
613
  def edit_event(self):
533
614
  """
534
- edit event corresponding to the selected row in twEvents
615
+ edit event corresponding to the selected row in tv_events
535
616
  """
536
617
 
537
618
  if not self.observationId:
538
619
  self.no_observation()
539
620
  return
540
621
 
541
- if not self.twEvents.selectedItems():
622
+ if not self.tv_events.selectionModel().selectedIndexes():
542
623
  QMessageBox.warning(self, cfg.programName, "Select an event to edit")
543
624
  return
544
625
 
545
- twEvents_row = self.twEvents.selectedItems()[0].row()
626
+ if self.pause_before_addevent:
627
+ # pause media
628
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
629
+ player_mem_state = self.is_playing()
630
+ if player_mem_state:
631
+ self.pause_video()
632
+
633
+ tvevents_row = self.tv_events.selectionModel().selectedIndexes()[0].row()
546
634
 
547
- pj_event_idx = self.twEvents.item(twEvents_row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole)
635
+ pj_event_idx = self.tv_idx2events_idx[tvevents_row]
548
636
 
549
637
  time_value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
550
638
  cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
@@ -556,140 +644,159 @@ def edit_event(self):
556
644
  cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.IMAGE_INDEX]
557
645
  ]
558
646
 
559
- """
560
- elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
561
- value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
562
- cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
563
- ]
564
- else:
565
- QMessageBox.warning(
566
- self,
567
- cfg.programName,
568
- f"Observation type {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]} not recognized",
569
- )
570
- return
571
- """
572
-
573
647
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
574
648
  current_value = self.image_idx + 1
575
649
  else:
576
650
  current_value = self.getLaps()
577
651
 
578
- editWindow = DlgEditEvent(
652
+ # get exif date time
653
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
654
+ exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
655
+ else:
656
+ exif_date_time = None
657
+
658
+ edit_window = DlgEditEvent(
579
659
  observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
580
660
  time_value=time_value,
581
661
  image_idx=image_idx,
582
662
  current_time=current_value,
583
663
  time_format=self.timeFormat,
584
664
  show_set_current_time=True,
665
+ exif_date_time=exif_date_time,
585
666
  )
586
- editWindow.setWindowTitle("Edit event")
587
-
588
- #
589
- if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
590
- editWindow.pb_set_to_current_time.setVisible(False)
667
+ edit_window.setWindowTitle("Edit event")
591
668
 
592
- sortedSubjects = [""] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
593
-
594
- editWindow.cobSubject.addItems(sortedSubjects)
669
+ # time
670
+ if time_value.is_nan():
671
+ edit_window.cb_set_time_na.setChecked(True)
595
672
 
596
- if (
597
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]
598
- in sortedSubjects
599
- ):
600
- editWindow.cobSubject.setCurrentIndex(
601
- sortedSubjects.index(
602
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]
603
- )
673
+ # remove visibility of 'set current time' widget if VIEWER mode
674
+ if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
675
+ edit_window.pb_set_to_current_time.setVisible(False)
676
+
677
+ # subjects
678
+ sorted_subjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
679
+ edit_window.cobSubject.addItems(sorted_subjects)
680
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX] == "": # no focal subject
681
+ edit_window.cobSubject.setCurrentIndex(sorted_subjects.index(cfg.NO_FOCAL_SUBJECT))
682
+ elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX] in sorted_subjects:
683
+ edit_window.cobSubject.setCurrentIndex(
684
+ sorted_subjects.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX])
604
685
  )
605
686
  else:
606
687
  QMessageBox.warning(
607
688
  self,
608
689
  cfg.programName,
609
690
  (
610
- f"The subject <b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
691
+ "The subject "
692
+ f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
611
693
  "does not exist more in the subject's list"
612
694
  ),
613
695
  )
614
- editWindow.cobSubject.setCurrentIndex(0)
696
+ edit_window.cobSubject.setCurrentIndex(0)
615
697
 
698
+ # behaviors
616
699
  sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
617
- editWindow.cobCode.addItems(sortedCodes)
618
-
700
+ edit_window.cobCode.addItems(sortedCodes)
619
701
  # check if selected code is in code's list (no modification of codes)
620
- if (
621
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]
622
- in sortedCodes
623
- ):
624
- editWindow.cobCode.setCurrentIndex(
625
- sortedCodes.index(
626
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]
627
- )
702
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX] in sortedCodes:
703
+ edit_window.cobCode.setCurrentIndex(
704
+ sortedCodes.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX])
628
705
  )
629
706
  else:
630
- logging.warning(
631
- (
632
- f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
633
- "does not exist more in the ethogram"
634
- )
707
+ msg: str = (
708
+ f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
709
+ "does not exist longer in the ethogram"
635
710
  )
711
+ logging.warning(msg)
712
+
636
713
  QMessageBox.warning(
637
714
  self,
638
715
  cfg.programName,
639
- (
640
- f"The behaviour <b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> "
641
- "does not exist more in the ethogram"
642
- ),
716
+ msg,
643
717
  )
644
- editWindow.cobCode.setCurrentIndex(0)
718
+ edit_window.cobCode.setCurrentIndex(0)
645
719
 
646
720
  logging.debug(
647
721
  f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
648
722
  )
649
723
 
650
- # frame index
651
- frame_idx = read_event_field(
652
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
653
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
654
- cfg.FRAME_INDEX,
655
- )
656
- editWindow.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
657
- if frame_idx in (cfg.NA, None):
658
- editWindow.cb_set_frame_idx_na.setChecked(True)
724
+ # # frame index
725
+ # frame_idx = read_event_field(
726
+ # self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
727
+ # self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
728
+ # cfg.FRAME_INDEX,
729
+ # )
730
+ # edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
731
+ # if frame_idx in (cfg.NA, None):
732
+ # edit_window.cb_set_frame_idx_na.setChecked(True)
659
733
 
660
734
  # comment
661
- editWindow.leComment.setPlainText(
662
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX]
663
- )
735
+ edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
664
736
 
665
737
  flag_ok = False # for looping until event is OK or Cancel pressed
666
738
  while True:
667
- if editWindow.exec_(): # button OK
668
-
739
+ if edit_window.exec(): # button OK
669
740
  self.project_changed()
670
741
 
742
+ new_time = edit_window.time_widget.get_time()
743
+
744
+ if edit_window.cb_set_time_na.isChecked():
745
+ new_time = dec("NaN")
746
+
671
747
  # MEDIA / LIVE
672
748
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
749
+ if new_time.is_nan():
750
+ QMessageBox.warning(
751
+ self,
752
+ cfg.programName,
753
+ ("Select a time format"),
754
+ )
755
+ continue
673
756
 
674
- new_time = editWindow.time_widget.get_time()
675
757
  for key in self.pj[cfg.ETHOGRAM]:
676
- if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
758
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
677
759
  event = self.full_event(key)
678
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
679
- event[cfg.COMMENT] = editWindow.leComment.toPlainText()
760
+ # subject
761
+ event[cfg.SUBJECT] = (
762
+ "" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
763
+ )
764
+
765
+ event[cfg.COMMENT] = edit_window.leComment.toPlainText()
680
766
 
767
+ # determine the new frame index
681
768
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
682
- if not editWindow.sb_frame_idx.value() or editWindow.cb_set_frame_idx_na.isChecked():
683
- event[cfg.FRAME_INDEX] = cfg.NA
684
- else:
685
- event[cfg.FRAME_INDEX] = editWindow.sb_frame_idx.value()
769
+ if self.playerType == cfg.MEDIA:
770
+ mem_time = self.getLaps()
771
+ if not self.seek_mediaplayer(new_time):
772
+ time.sleep(0.1)
773
+ frame_idx = self.get_frame_index()
774
+ event[cfg.FRAME_INDEX] = frame_idx
775
+ self.seek_mediaplayer(mem_time)
776
+
777
+ # if not edit_window.sb_frame_idx.value() or edit_window.cb_set_frame_idx_na.isChecked():
778
+ # event[cfg.FRAME_INDEX] = cfg.NA
779
+ # else:
780
+ # if self.playerType == cfg.MEDIA:
781
+ # mem_time = self.getLaps()
782
+ # if not self.seek_mediaplayer(new_time):
783
+ # frame_idx = self.get_frame_index()
784
+ # event[cfg.FRAME_INDEX] = frame_idx
785
+ # self.seek_mediaplayer(mem_time)
786
+ #
787
+ # # event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
686
788
 
687
789
  event["row"] = pj_event_idx
688
- event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][
689
- pj_event_idx
690
- ][cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]]
790
+ event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
791
+ cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
792
+ ]
793
+
794
+ r = write_event.write_event(self, event, new_time)
795
+
796
+ # scroll tv events
797
+ index = self.tv_events.model().index(pj_event_idx, 0)
798
+ self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
691
799
 
692
- r = self.write_event(event, new_time)
693
800
  if r == 1: # same event already present
694
801
  continue
695
802
  if not r:
@@ -700,22 +807,21 @@ def edit_event(self):
700
807
 
701
808
  # IMAGES
702
809
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
703
- new_index = editWindow.sb_image_idx.value()
810
+ new_index = edit_window.sb_image_idx.value()
704
811
 
705
812
  for key in self.pj[cfg.ETHOGRAM]:
706
- if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
813
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
707
814
  event = self.full_event(key)
708
- if editWindow.time_value == cfg.NA or (editWindow.cb_set_time_na.isChecked()):
709
- event[cfg.TIME] = dec("NaN")
710
- else:
711
- event[cfg.TIME] = editWindow.time_widget.get_time()
815
+ event[cfg.TIME] = new_time
712
816
 
713
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
714
- event[cfg.COMMENT] = editWindow.leComment.toPlainText()
817
+ event[cfg.SUBJECT] = (
818
+ "" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
819
+ )
820
+ event[cfg.COMMENT] = edit_window.leComment.toPlainText()
715
821
  event["row"] = pj_event_idx
716
- event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][
717
- pj_event_idx
718
- ][cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]]
822
+ event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
823
+ cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
824
+ ]
719
825
 
720
826
  # not editable yet. Read previous value
721
827
  event[cfg.IMAGE_PATH] = read_event_field(
@@ -724,36 +830,13 @@ def edit_event(self):
724
830
  cfg.IMAGE_PATH,
725
831
  )
726
832
 
727
- """
728
- try:
729
- event[cfg.IMAGE_PATH] = self.images_list[new_index]
730
- except IndexError:
731
- event[cfg.IMAGE_PATH] = ""
732
- """
733
833
  event[cfg.IMAGE_INDEX] = new_index
734
834
 
735
- """
736
- time_ = dec("NaN")
737
- if (
738
- self.playerType != cfg.VIEWER_IMAGES
739
- and self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False)
740
- and self.extract_exif_DateTimeOriginal(self.images_list[new_index]) != -1
741
- ):
742
- time_ = (
743
- self.extract_exif_DateTimeOriginal(self.images_list[new_index]) - self.image_time_ref
744
- )
745
-
746
- elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
747
- time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
748
- """
749
-
750
- r = self.write_event(event, event[cfg.TIME].quantize(dec("0.001"), rounding=ROUND_DOWN))
751
-
835
+ r = write_event.write_event(self, event, event[cfg.TIME].quantize(dec("0.001"), rounding=ROUND_DOWN))
752
836
  if r == 1: # same event already present
753
837
  continue
754
838
  if not r:
755
839
  flag_ok = True
756
-
757
840
  break
758
841
 
759
842
  else: # Cancel button
@@ -762,78 +845,81 @@ def edit_event(self):
762
845
  if flag_ok:
763
846
  break
764
847
 
848
+ if self.pause_before_addevent:
849
+ # restart media
850
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
851
+ if player_mem_state:
852
+ self.play_video()
853
+
765
854
 
766
855
  def edit_time_selected_events(self):
767
856
  """
768
- edit time of one or more selected events
857
+ shift time of one or more selected events
769
858
  """
770
- # list of rows to edit
771
- twEvents_rows_to_shift = set([item.row() for item in self.twEvents.selectedIndexes()])
772
859
 
773
- if not len(twEvents_rows_to_shift):
860
+ tvevents_rows_to_shift = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
861
+ if not len(tvevents_rows_to_shift):
774
862
  QMessageBox.warning(self, cfg.programName, "No event selected!")
775
863
  return
776
864
 
777
- d, ok = QInputDialog.getDouble(
778
- self, "Time value", "Value to add or substract (use negative value):", 0, -86400, 86400, 3
779
- )
780
- if ok and d:
781
- if (
782
- dialog.MessageDialog(
783
- cfg.programName,
784
- (
785
- f"Confirm the {'addition' if d > 0 else 'subtraction'} of {abs(d)} seconds "
786
- "to all selected events in the current observation?"
787
- ),
788
- [cfg.YES, cfg.NO],
789
- )
790
- == cfg.NO
791
- ):
792
- return
865
+ w = dialog.Ask_time(0)
866
+ w.setWindowTitle("Shift time of selected event(s)")
867
+ w.label.setText("Amount of time to add or substract")
793
868
 
794
- # fill the undo list
795
- fill_events_undo_list(self, "Undo 'Edit time'")
869
+ if not w.exec_():
870
+ return
796
871
 
797
- for tw_event_idx in twEvents_rows_to_shift:
798
- pj_event_idx = self.twEvents.item(tw_event_idx, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(
799
- Qt.UserRole
800
- )
801
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
802
- cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]
803
- ] += dec(f"{d:.3f}")
804
- self.project_changed()
872
+ d = w.time_widget.get_time()
873
+ if d.is_nan() or not d:
874
+ return
805
875
 
806
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(
807
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
876
+ if ":" in util.smart_time_format(abs(d)):
877
+ smart_d = f"{util.smart_time_format(abs(d))}"
878
+ else:
879
+ smart_d = f"{d} seconds"
880
+
881
+ if (
882
+ dialog.MessageDialog(
883
+ cfg.programName,
884
+ (f"Confirm the {'addition' if d > 0 else 'subtraction'} of {smart_d} to all selected events in the current observation?"),
885
+ [cfg.YES, cfg.NO],
808
886
  )
809
- self.load_tw_events(self.observationId)
887
+ == cfg.NO
888
+ ):
889
+ return
810
890
 
811
- self.update_realtime_plot(force_plot=True)
891
+ # fill the undo list
892
+ fill_events_undo_list(self, "Undo 'Edit time'")
812
893
 
894
+ mem_time = self.getLaps()
895
+ for tw_event_idx in tvevents_rows_to_shift:
896
+ pj_event_idx = self.tv_idx2events_idx[tw_event_idx]
813
897
 
814
- '''
815
- def copy_selected_events(self):
816
- """
817
- copy selected events to clipboard
818
- """
819
- twEvents_rows_to_copy = set([item.row() for item in self.twEvents.selectedIndexes()])
820
- if not len(twEvents_rows_to_copy):
821
- QMessageBox.warning(self, cfg.programName, "No event selected!")
822
- return
898
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]] += dec(
899
+ f"{d:.3f}"
900
+ )
901
+ # set new frame index
902
+ if self.playerType == cfg.MEDIA:
903
+ if not self.seek_mediaplayer(
904
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]]
905
+ ):
906
+ # determine the new frame index
907
+ time.sleep(0.1)
908
+ frame_idx = self.get_frame_index()
909
+ if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx]) == 6:
910
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][-1] = frame_idx
911
+ elif len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx]) == 5:
912
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx].append(frame_idx)
823
913
 
824
- tsb_to_copy: list = []
825
- for row in twEvents_rows_to_copy:
826
- tsb_to_copy.append(self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole))
914
+ self.project_changed()
827
915
 
828
- copied_events: list = []
829
- for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
830
- if idx in tsb_to_copy:
831
- copied_events.append("\t".join([str(x) for x in event]))
916
+ if self.playerType == cfg.MEDIA:
917
+ self.seek_mediaplayer(mem_time)
832
918
 
833
- cb = QApplication.clipboard()
834
- cb.clear(mode=cb.Clipboard)
835
- cb.setText("\n".join(copied_events), mode=cb.Clipboard)
836
- '''
919
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
920
+ self.load_tw_events(self.observationId)
921
+
922
+ self.update_realtime_plot(force_plot=True)
837
923
 
838
924
 
839
925
  def copy_selected_events(self):
@@ -843,16 +929,12 @@ def copy_selected_events(self):
843
929
 
844
930
  logging.debug("Copy selected events to clipboard")
845
931
 
846
- twEvents_rows_to_copy = set([item.row() for item in self.twEvents.selectedIndexes()])
847
- if not len(twEvents_rows_to_copy):
932
+ tvevents_rows_to_copy = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
933
+ if not len(tvevents_rows_to_copy):
848
934
  QMessageBox.warning(self, cfg.programName, "No event selected!")
849
935
  return
850
936
 
851
- pj_event_idx_to_copy: list = []
852
- for row in twEvents_rows_to_copy:
853
- pj_event_idx_to_copy.append(
854
- self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.TIME]).data(Qt.UserRole)
855
- )
937
+ pj_event_idx_to_copy: list = [self.tv_idx2events_idx[row] for row in tvevents_rows_to_copy]
856
938
 
857
939
  copied_events: list = []
858
940
  for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
@@ -863,8 +945,8 @@ def copy_selected_events(self):
863
945
  copied_events.append("\t".join([str(x) for x in event]))
864
946
 
865
947
  cb = QApplication.clipboard()
866
- cb.clear(mode=cb.Clipboard)
867
- cb.setText("\n".join(copied_events), mode=cb.Clipboard)
948
+ cb.clear(mode=QClipboard.Mode.Clipboard)
949
+ cb.setText("\n".join(copied_events), mode=QClipboard.Mode.Clipboard)
868
950
 
869
951
  logging.debug("Selected events copied in clipboard")
870
952
 
@@ -879,24 +961,35 @@ def paste_clipboard_to_events(self):
879
961
  cb_text_splitted = cb_text.split("\n")
880
962
  length: list = []
881
963
  content: list = []
882
- for l in cb_text_splitted:
883
- length.append(len(l.split("\t")))
884
- content.append(l.split("\t"))
964
+ for line in cb_text_splitted:
965
+ length.append(len(line.split("\t")))
966
+ content.append(line.split("\t"))
885
967
 
886
968
  if set(length) != set([len(cfg.PJ_EVENTS_FIELDS[self.playerType])]):
887
- QMessageBox.warning(
888
- self,
969
+ msg_box = QMessageBox(
970
+ QMessageBox.Warning,
889
971
  cfg.programName,
890
972
  (
891
- "The clipboard does not contain events!\n"
973
+ "The clipboard does not contain events!<br>"
892
974
  f"For an observation from <b>{self.playerType}</b> "
893
- f"the events must be organized in {len(cfg.PJ_EVENTS_FIELDS[self.playerType])} columns separated by TAB character"
975
+ f"the events must be organized in {len(cfg.PJ_EVENTS_FIELDS[self.playerType])} columns separated by <TAB> character"
894
976
  ),
895
977
  )
978
+ msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint)
979
+ msg_box.exec()
980
+
896
981
  return
897
982
 
898
983
  for event in content:
984
+ # convert time in decimal
899
985
  event[cfg.EVENT_TIME_FIELD_IDX] = dec(event[cfg.EVENT_TIME_FIELD_IDX])
986
+ for idx, _ in enumerate(event):
987
+ if cfg.PJ_EVENTS_FIELDS[self.playerType][idx] in (cfg.FRAME_INDEX, cfg.IMAGE_INDEX):
988
+ try:
989
+ event[idx] = int(event[idx])
990
+ except ValueError:
991
+ pass
992
+
900
993
  # skip if event already present
901
994
  if event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
902
995
  continue
@@ -905,9 +998,7 @@ def paste_clipboard_to_events(self):
905
998
 
906
999
  self.project_changed()
907
1000
 
908
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(
909
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
910
- )
1001
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
911
1002
  self.load_tw_events(self.observationId)
912
1003
 
913
1004
  self.update_realtime_plot(force_plot=True)
@@ -923,3 +1014,27 @@ def read_event_field(event: list, player_type: str, field_type: str) -> Union[st
923
1014
  return event[cfg.PJ_OBS_FIELDS[player_type][field_type]]
924
1015
  else:
925
1016
  return cfg.NA
1017
+
1018
+
1019
+ def add_frame_indexes(self):
1020
+ """
1021
+ add frame indexes for all events
1022
+ """
1023
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] != cfg.MEDIA:
1024
+ return
1025
+ if self.playerType != cfg.MEDIA:
1026
+ return
1027
+
1028
+ mem_time = self.getLaps()
1029
+ for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
1030
+ if event[0] == "NA":
1031
+ continue
1032
+ if not self.seek_mediaplayer(event[0]):
1033
+ time.sleep(0.1)
1034
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.PJ_OBS_FIELDS[cfg.MEDIA][cfg.FRAME_INDEX]] = (
1035
+ self.get_frame_index()
1036
+ )
1037
+
1038
+ self.seek_mediaplayer(mem_time)
1039
+
1040
+ self.load_tw_events(self.observationId)