boris-behav-obs 9.7.7__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 (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  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 +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  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.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1040 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+
7
+ This program is free software; you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation; either version 2 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program; if not, write to the Free Software
19
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ MA 02110-1301, USA.
21
+
22
+ """
23
+
24
+ import logging
25
+ import copy
26
+ import time
27
+ from decimal import Decimal as dec
28
+ from decimal import InvalidOperation
29
+ from decimal import ROUND_DOWN
30
+ from typing import Union
31
+
32
+
33
+ from . import config as cfg
34
+ from . import utilities as util
35
+ from . import dialog
36
+ from . import select_subj_behav
37
+ from . import select_modifiers
38
+ from . import write_event
39
+ from .edit_event import DlgEditEvent, EditSelectedEvents
40
+
41
+ from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
42
+ from PySide6.QtCore import QTime, Qt
43
+ from PySide6.QtGui import QClipboard
44
+
45
+
46
+ def add_event(self):
47
+ """
48
+ manually add event to observation
49
+ """
50
+
51
+ if not self.observationId:
52
+ self.no_observation()
53
+ return
54
+
55
+ if self.pause_before_addevent:
56
+ # pause media
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()
61
+
62
+ if not self.pj[cfg.ETHOGRAM]:
63
+ QMessageBox.warning(self, cfg.programName, "The ethogram is not set!")
64
+ return
65
+
66
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
67
+ current_time = self.image_idx + 1
68
+ else:
69
+ current_time = self.getLaps()
70
+
71
+ editWindow = DlgEditEvent(
72
+ observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
73
+ time_value=dec("NaN"),
74
+ image_idx=0,
75
+ current_time=current_time,
76
+ time_format=self.timeFormat,
77
+ show_set_current_time=True,
78
+ )
79
+ editWindow.setWindowTitle("Add a new event")
80
+
81
+ sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
82
+
83
+ editWindow.cobSubject.addItems(sortedSubjects)
84
+ if self.currentSubject:
85
+ editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
86
+
87
+ sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
88
+
89
+ editWindow.cobCode.addItems(sortedCodes)
90
+
91
+ if editWindow.exec_(): # button OK
92
+ # MEDIA / LIVE
93
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
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
102
+
103
+ for idx in self.pj[cfg.ETHOGRAM]:
104
+ if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
105
+ event = self.full_event(idx)
106
+
107
+ event[cfg.SUBJECT] = (
108
+ "" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
109
+ )
110
+ if editWindow.leComment.toPlainText():
111
+ event[cfg.COMMENT] = editWindow.leComment.toPlainText()
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
+
122
+ write_event.write_event(self, event, newTime)
123
+ break
124
+
125
+ self.update_realtime_plot(force_plot=True)
126
+ """
127
+ if hasattr(self, "plot_events"):
128
+ if not self.plot_events.visibleRegion().isEmpty():
129
+ self.plot_events.events_list = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
130
+ self.plot_events.plot_events(float(self.getLaps()))
131
+ """
132
+
133
+ """
134
+ # update subjects table
135
+ self.currentStates = util.get_current_states_modifiers_by_subject(
136
+ util.state_behavior_codes(self.pj[cfg.ETHOGRAM]),
137
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
138
+ dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}), # add no focal subject
139
+ newTime,
140
+ include_modifiers=True,
141
+ )
142
+ subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
143
+ self.lbCurrentStates.setText(", ".join(self.currentStates[subject_idx]))
144
+ self.show_current_states_in_subjects_table()
145
+ """
146
+
147
+ # IMAGES
148
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
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
153
+
154
+ for idx in self.pj[cfg.ETHOGRAM]:
155
+ if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
156
+ event = self.full_event(idx)
157
+
158
+ event[cfg.SUBJECT] = (
159
+ "" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
160
+ )
161
+ if editWindow.leComment.toPlainText():
162
+ event[cfg.COMMENT] = editWindow.leComment.toPlainText()
163
+
164
+ if self.playerType != cfg.VIEWER_IMAGES:
165
+ event[cfg.IMAGE_PATH] = self.images_list[new_index]
166
+ else:
167
+ event[cfg.IMAGE_PATH] = ""
168
+
169
+ event[cfg.IMAGE_INDEX] = new_index
170
+
171
+ time_ = dec("NaN")
172
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
173
+ if self.playerType != cfg.VIEWER_IMAGES:
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
181
+
182
+ elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
183
+ time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
184
+
185
+ write_event.write_event(self, event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
186
+
187
+ break
188
+
189
+ if self.pause_before_addevent:
190
+ # restart media
191
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
192
+ if memState:
193
+ self.play_video()
194
+
195
+
196
+ def find_events(self):
197
+ """
198
+ find in events
199
+ """
200
+
201
+ self.find_dialog = dialog.FindInEvents()
202
+ # list of rows to find
203
+ self.find_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
204
+ self.find_dialog.currentIdx = -1
205
+ self.find_dialog.clickSignal.connect(self.click_signal_find_in_events)
206
+ self.find_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
207
+ self.find_dialog.show()
208
+
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
+
225
+ def filter_events(self):
226
+ """
227
+ filter coded events and subjects
228
+ """
229
+
230
+ parameters = select_subj_behav.choose_obs_subj_behav_category(
231
+ self,
232
+ selected_observations=[], # empty selection of observations for selecting all subjects and behaviors
233
+ show_include_modifiers=False,
234
+ show_exclude_non_coded_behaviors=False,
235
+ by_category=False,
236
+ )
237
+ if parameters == {}:
238
+ return
239
+
240
+ self.filtered_subjects = parameters[cfg.SELECTED_SUBJECTS][:]
241
+ if cfg.NO_FOCAL_SUBJECT in self.filtered_subjects:
242
+ self.filtered_subjects.append("")
243
+ self.filtered_behaviors = parameters[cfg.SELECTED_BEHAVIORS][:]
244
+
245
+ logging.debug(f"self.filtered_behaviors: {self.filtered_behaviors}")
246
+
247
+ self.load_tw_events(self.observationId)
248
+ self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation (filtered)")
249
+
250
+
251
+ def show_all_events(self) -> None:
252
+ """
253
+ show all events (disable filter)
254
+ """
255
+ self.filtered_subjects = []
256
+ self.filtered_behaviors = []
257
+ self.load_tw_events(self.observationId)
258
+ self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation")
259
+
260
+
261
+ def fill_events_undo_list(self, operation_description: str) -> None:
262
+ """
263
+ fill the undo events list for Undo function (CTRL + Z)
264
+ """
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
+
269
+ self.undo_description.append(operation_description)
270
+
271
+ self.actionUndo.setText(operation_description)
272
+ self.actionUndo.setEnabled(True)
273
+
274
+ logging.debug(f"{operation_description} added to undo events list")
275
+
276
+ if len(self.undo_queue) > cfg.MAX_UNDO_QUEUE:
277
+ self.undo_queue.popleft()
278
+ self.undo_description.popleft()
279
+ logging.debug("Max events undo ")
280
+
281
+
282
+ def undo_event_operation(self) -> None:
283
+ """
284
+ undo operation on event(s)
285
+ """
286
+
287
+ logging.debug("Undo event operation function")
288
+
289
+ if len(self.undo_queue) == 0:
290
+ self.statusbar.showMessage("The Undo buffer is empty", 5000)
291
+ return
292
+
293
+ events = self.undo_queue.pop()
294
+
295
+ operation_description = self.undo_description.pop()
296
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = events[:]
297
+ self.project_changed()
298
+
299
+ self.statusbar.showMessage(operation_description, 5000)
300
+
301
+ logging.debug(operation_description)
302
+
303
+ # reload all events in tw
304
+ self.load_tw_events(self.observationId)
305
+
306
+ self.update_realtime_plot(force_plot=True)
307
+
308
+ if not len(self.undo_queue):
309
+ self.actionUndo.setText("Undo")
310
+ self.actionUndo.setEnabled(False)
311
+ else:
312
+ self.actionUndo.setText(self.undo_description[-1])
313
+
314
+
315
+ def delete_all_events(self):
316
+ """
317
+ delete all (filtered) events in current observation
318
+ """
319
+
320
+ if not self.observationId:
321
+ self.no_observation()
322
+ return
323
+
324
+ if not self.tv_idx2events_idx:
325
+ QMessageBox.warning(self, cfg.programName, "No events to delete")
326
+ return
327
+
328
+ if (
329
+ dialog.MessageDialog(
330
+ cfg.programName,
331
+ ("Confirm the deletion of all (filtered) events in the current observation?<br>Filters do not apply!"),
332
+ [cfg.YES, cfg.NO],
333
+ )
334
+ == cfg.YES
335
+ ):
336
+ # fill the undo list
337
+ fill_events_undo_list(self, "Undo 'Delete all events'")
338
+
339
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
340
+ event
341
+ for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
342
+ if event_idx not in self.tv_idx2events_idx
343
+ ]
344
+
345
+ self.update_realtime_plot(force_plot=True)
346
+
347
+ self.project_changed()
348
+ self.load_tw_events(self.observationId)
349
+
350
+
351
+ def delete_selected_events(self):
352
+ """
353
+ delete selected events
354
+ """
355
+
356
+ if not self.observationId:
357
+ self.no_observation()
358
+ return
359
+
360
+ logging.debug("begin function delete_selected_events")
361
+
362
+ if not self.tv_events.selectedIndexes():
363
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
364
+ else:
365
+ # list of rows to delete (set for unique)
366
+ # fill the undo list
367
+ fill_events_undo_list(self, "Undo 'Delete selected events'")
368
+
369
+ rows_to_delete: list = []
370
+ for row in set([item.row() for item in self.tv_events.selectedIndexes()]):
371
+ rows_to_delete.append(self.tv_idx2events_idx[row])
372
+
373
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
374
+ event
375
+ for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
376
+ if event_idx not in rows_to_delete
377
+ ]
378
+
379
+ self.update_realtime_plot(force_plot=True)
380
+
381
+ self.project_changed()
382
+ self.load_tw_events(self.observationId)
383
+
384
+
385
+ def select_events_between_activated(self):
386
+ """
387
+ select events between a time interval
388
+ """
389
+
390
+ def parseTime(txt):
391
+ """
392
+ parse time in string (should be 00:00:00.000 or in seconds)
393
+ """
394
+ if ":" in txt:
395
+ qtime = QTime.fromString(txt, "hh:mm:ss.zzz")
396
+
397
+ if qtime.toString():
398
+ timeSeconds = util.time2seconds(qtime.toString("hh:mm:ss.zzz"))
399
+ else:
400
+ return None
401
+ else:
402
+ try:
403
+ timeSeconds = dec(txt)
404
+ except InvalidOperation:
405
+ return None
406
+ return timeSeconds
407
+
408
+ if not self.tv_idx2events_idx:
409
+ QMessageBox.warning(self, cfg.programName, "There are no events to select")
410
+ return
411
+
412
+ text, ok = QInputDialog.getText(
413
+ self,
414
+ "Select events in time interval",
415
+ "Interval: (example: 12.5-14.7 or 02:45.780-03:15.120)",
416
+ QLineEdit.Normal,
417
+ "",
418
+ )
419
+
420
+ if ok and text != "":
421
+ if "-" not in text:
422
+ QMessageBox.critical(self, cfg.programName, "Use minus sign (-) to separate initial value from final value")
423
+ return
424
+
425
+ while " " in text:
426
+ text = text.replace(" ", "")
427
+
428
+ from_, to_ = text.split("-")[0:2]
429
+ from_sec = parseTime(from_)
430
+ if not from_sec:
431
+ QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {from_}")
432
+ return
433
+ to_sec = parseTime(to_)
434
+ if not to_sec:
435
+ QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {to_}")
436
+ return
437
+ if to_sec < from_sec:
438
+ QMessageBox.critical(self, cfg.programName, "The initial time is greater than the final time")
439
+ return
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
+
451
+ if from_sec <= time <= to_sec:
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)
510
+
511
+
512
+ def edit_selected_events(self):
513
+ """
514
+ edit one or more selected events for subject, behavior and/or comment
515
+ """
516
+ # list of rows to edit
517
+
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):
521
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
522
+
523
+ elif len(tvevents_rows_to_edit) == 1: # 1 event selected
524
+ edit_event(self)
525
+
526
+ else: # editing of more events
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]])
529
+
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()])
532
+ ]
533
+
534
+ if dialog_window.exec_():
535
+ # fill the undo list
536
+ fill_events_undo_list(self, "Undo 'Edit selected event(s)'")
537
+
538
+ tsb_to_edit: list = []
539
+ for row in tvevents_rows_to_edit:
540
+ tsb_to_edit.append(self.tv_idx2events_idx[row])
541
+
542
+ behavior_codes: list = []
543
+ modifiers_mem: list = []
544
+ mem_event_idx: list = []
545
+ for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
546
+ if idx in tsb_to_edit:
547
+ new_event = list(event)
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()
557
+
558
+ if new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behavior_codes:
559
+ behavior_codes.append(new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
560
+
561
+ if new_event[cfg.EVENT_MODIFIER_FIELD_IDX] not in modifiers_mem:
562
+ modifiers_mem.append(new_event[cfg.EVENT_MODIFIER_FIELD_IDX])
563
+
564
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx] = list(new_event)
565
+ mem_event_idx.append(idx)
566
+ self.project_changed()
567
+
568
+ # check if behavior is unique for editing modifiers
569
+ if len(behavior_codes) == 1:
570
+ # get behavior index
571
+ for idx in self.pj[cfg.ETHOGRAM]:
572
+ if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == behavior_codes[0]:
573
+ break
574
+ else:
575
+ return
576
+
577
+ event = self.full_event(idx)
578
+
579
+ if len(modifiers_mem) == 1:
580
+ current_modifier = modifiers_mem[0]
581
+ else:
582
+ current_modifier = ""
583
+
584
+ if event["modifiers"]:
585
+ modifiers_selector = select_modifiers.ModifiersList(behavior_codes[0], eval(str(event["modifiers"])), current_modifier)
586
+
587
+ r = modifiers_selector.exec_()
588
+ if r:
589
+ selected_modifiers = modifiers_selector.get_modifiers()
590
+
591
+ modifier_str = ""
592
+ for idx1 in util.sorted_keys(selected_modifiers):
593
+ if modifier_str:
594
+ modifier_str += "|"
595
+ if selected_modifiers[idx1]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
596
+ modifier_str += ",".join(selected_modifiers[idx1].get("selected", ""))
597
+ if selected_modifiers[idx1]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
598
+ modifier_str += selected_modifiers[idx1].get("selected", "NA")
599
+
600
+ else: # delete current modifier(s)
601
+ modifier_str = ""
602
+
603
+ # set new modifier
604
+ for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
605
+ if idx in mem_event_idx:
606
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.EVENT_MODIFIER_FIELD_IDX] = modifier_str
607
+
608
+ self.load_tw_events(self.observationId)
609
+
610
+ self.update_realtime_plot(force_plot=True)
611
+
612
+
613
+ def edit_event(self):
614
+ """
615
+ edit event corresponding to the selected row in tv_events
616
+ """
617
+
618
+ if not self.observationId:
619
+ self.no_observation()
620
+ return
621
+
622
+ if not self.tv_events.selectionModel().selectedIndexes():
623
+ QMessageBox.warning(self, cfg.programName, "Select an event to edit")
624
+ return
625
+
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()
634
+
635
+ pj_event_idx = self.tv_idx2events_idx[tvevents_row]
636
+
637
+ time_value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
638
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
639
+ ]
640
+
641
+ image_idx = None
642
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
643
+ image_idx = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
644
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.IMAGE_INDEX]
645
+ ]
646
+
647
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
648
+ current_value = self.image_idx + 1
649
+ else:
650
+ current_value = self.getLaps()
651
+
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(
659
+ observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
660
+ time_value=time_value,
661
+ image_idx=image_idx,
662
+ current_time=current_value,
663
+ time_format=self.timeFormat,
664
+ show_set_current_time=True,
665
+ exif_date_time=exif_date_time,
666
+ )
667
+ edit_window.setWindowTitle("Edit event")
668
+
669
+ # time
670
+ if time_value.is_nan():
671
+ edit_window.cb_set_time_na.setChecked(True)
672
+
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])
685
+ )
686
+ else:
687
+ QMessageBox.warning(
688
+ self,
689
+ cfg.programName,
690
+ (
691
+ "The subject "
692
+ f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
693
+ "does not exist more in the subject's list"
694
+ ),
695
+ )
696
+ edit_window.cobSubject.setCurrentIndex(0)
697
+
698
+ # behaviors
699
+ sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
700
+ edit_window.cobCode.addItems(sortedCodes)
701
+ # check if selected code is in code's list (no modification of codes)
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])
705
+ )
706
+ else:
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"
710
+ )
711
+ logging.warning(msg)
712
+
713
+ QMessageBox.warning(
714
+ self,
715
+ cfg.programName,
716
+ msg,
717
+ )
718
+ edit_window.cobCode.setCurrentIndex(0)
719
+
720
+ logging.debug(
721
+ f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
722
+ )
723
+
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)
733
+
734
+ # comment
735
+ edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
736
+
737
+ flag_ok = False # for looping until event is OK or Cancel pressed
738
+ while True:
739
+ if edit_window.exec(): # button OK
740
+ self.project_changed()
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
+
747
+ # MEDIA / LIVE
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
756
+
757
+ for key in self.pj[cfg.ETHOGRAM]:
758
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
759
+ event = self.full_event(key)
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()
766
+
767
+ # determine the new frame index
768
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
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()
788
+
789
+ event["row"] = pj_event_idx
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)
799
+
800
+ if r == 1: # same event already present
801
+ continue
802
+ if not r:
803
+ flag_ok = True
804
+ break
805
+
806
+ self.update_realtime_plot(force_plot=True)
807
+
808
+ # IMAGES
809
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
810
+ new_index = edit_window.sb_image_idx.value()
811
+
812
+ for key in self.pj[cfg.ETHOGRAM]:
813
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
814
+ event = self.full_event(key)
815
+ event[cfg.TIME] = new_time
816
+
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()
821
+ event["row"] = pj_event_idx
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
+ ]
825
+
826
+ # not editable yet. Read previous value
827
+ event[cfg.IMAGE_PATH] = read_event_field(
828
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
829
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
830
+ cfg.IMAGE_PATH,
831
+ )
832
+
833
+ event[cfg.IMAGE_INDEX] = new_index
834
+
835
+ r = write_event.write_event(self, event, event[cfg.TIME].quantize(dec("0.001"), rounding=ROUND_DOWN))
836
+ if r == 1: # same event already present
837
+ continue
838
+ if not r:
839
+ flag_ok = True
840
+ break
841
+
842
+ else: # Cancel button
843
+ flag_ok = True
844
+
845
+ if flag_ok:
846
+ break
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
+
854
+
855
+ def edit_time_selected_events(self):
856
+ """
857
+ shift time of one or more selected events
858
+ """
859
+
860
+ tvevents_rows_to_shift = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
861
+ if not len(tvevents_rows_to_shift):
862
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
863
+ return
864
+
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")
868
+
869
+ if not w.exec_():
870
+ return
871
+
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"
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],
886
+ )
887
+ == cfg.NO
888
+ ):
889
+ return
890
+
891
+ # fill the undo list
892
+ fill_events_undo_list(self, "Undo 'Edit time'")
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]
897
+
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)
913
+
914
+ self.project_changed()
915
+
916
+ if self.playerType == cfg.MEDIA:
917
+ self.seek_mediaplayer(mem_time)
918
+
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)
923
+
924
+
925
+ def copy_selected_events(self):
926
+ """
927
+ copy selected events from project to clipboard
928
+ """
929
+
930
+ logging.debug("Copy selected events to clipboard")
931
+
932
+ tvevents_rows_to_copy = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
933
+ if not len(tvevents_rows_to_copy):
934
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
935
+ return
936
+
937
+ pj_event_idx_to_copy: list = [self.tv_idx2events_idx[row] for row in tvevents_rows_to_copy]
938
+
939
+ copied_events: list = []
940
+ for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
941
+ if idx in pj_event_idx_to_copy:
942
+ if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA) and len(event) < len(cfg.MEDIA_PJ_EVENTS_FIELDS):
943
+ copied_events.append("\t".join([str(x) for x in event + [cfg.NA]]))
944
+ else:
945
+ copied_events.append("\t".join([str(x) for x in event]))
946
+
947
+ cb = QApplication.clipboard()
948
+ cb.clear(mode=QClipboard.Mode.Clipboard)
949
+ cb.setText("\n".join(copied_events), mode=QClipboard.Mode.Clipboard)
950
+
951
+ logging.debug("Selected events copied in clipboard")
952
+
953
+
954
+ def paste_clipboard_to_events(self):
955
+ """
956
+ paste clipboard to events
957
+ """
958
+
959
+ cb = QApplication.clipboard()
960
+ cb_text = cb.text()
961
+ cb_text_splitted = cb_text.split("\n")
962
+ length: list = []
963
+ content: list = []
964
+ for line in cb_text_splitted:
965
+ length.append(len(line.split("\t")))
966
+ content.append(line.split("\t"))
967
+
968
+ if set(length) != set([len(cfg.PJ_EVENTS_FIELDS[self.playerType])]):
969
+ msg_box = QMessageBox(
970
+ QMessageBox.Warning,
971
+ cfg.programName,
972
+ (
973
+ "The clipboard does not contain events!<br>"
974
+ f"For an observation from <b>{self.playerType}</b> "
975
+ f"the events must be organized in {len(cfg.PJ_EVENTS_FIELDS[self.playerType])} columns separated by <TAB> character"
976
+ ),
977
+ )
978
+ msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint)
979
+ msg_box.exec()
980
+
981
+ return
982
+
983
+ for event in content:
984
+ # convert time in decimal
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
+
993
+ # skip if event already present
994
+ if event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
995
+ continue
996
+
997
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(event)
998
+
999
+ self.project_changed()
1000
+
1001
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
1002
+ self.load_tw_events(self.observationId)
1003
+
1004
+ self.update_realtime_plot(force_plot=True)
1005
+
1006
+
1007
+ def read_event_field(event: list, player_type: str, field_type: str) -> Union[str, None, int, dec]:
1008
+ """
1009
+ return value of field for event or NA if not available
1010
+ """
1011
+ if field_type not in cfg.PJ_EVENTS_FIELDS[player_type]:
1012
+ return None
1013
+ if cfg.PJ_OBS_FIELDS[player_type][field_type] < len(event):
1014
+ return event[cfg.PJ_OBS_FIELDS[player_type][field_type]]
1015
+ else:
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)