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