boris-behav-obs 8.16.6__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.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +24 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +235 -131
- boris/advanced_event_filtering.py +23 -29
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +16 -34
- boris/config.py +101 -49
- boris/config_file.py +55 -64
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2108 -1275
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +134 -0
- boris/dialog.py +461 -242
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +405 -281
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +180 -203
- boris/export_observation.py +60 -73
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +427 -218
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +304 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +16 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv2.py +127 -36
- boris/observation.py +493 -210
- boris/observation_operations.py +1010 -391
- boris/observation_ui.py +573 -363
- boris/observations_list.py +51 -58
- boris/otx_parser.py +74 -68
- boris/param_panel.py +45 -59
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +91 -56
- boris/plot_data_module.py +18 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +80 -56
- boris/plot_waveform_rt.py +23 -48
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +298 -123
- boris/preferences_ui.py +664 -225
- boris/project.py +293 -270
- boris/project_functions.py +610 -537
- boris/project_import_export.py +204 -213
- boris/project_ui.py +673 -441
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +62 -90
- boris/select_observations.py +19 -197
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +51 -33
- boris/subjects_pad.py +6 -8
- boris/synthetic_time_budget.py +25 -17
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +71 -86
- boris/transitions.py +41 -41
- boris/utilities.py +562 -222
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +78 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +240 -136
- boris_behav_obs-9.7.1.dist-info/METADATA +140 -0
- boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.1.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -37
- boris/core.ui +0 -1571
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -982
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1074
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
- boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.6.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-
|
|
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
|
|
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
|
|
39
|
-
from
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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=
|
|
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 = [
|
|
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
|
-
|
|
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] =
|
|
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] =
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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>
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
400
|
-
self.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
669
|
+
# time
|
|
670
|
+
if time_value.is_nan():
|
|
671
|
+
edit_window.cb_set_time_na.setChecked(True)
|
|
588
672
|
|
|
589
|
-
if
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
if frame_idx in (cfg.NA, None):
|
|
651
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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] ==
|
|
758
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
668
759
|
event = self.full_event(key)
|
|
669
|
-
|
|
670
|
-
event[cfg.
|
|
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
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
681
|
-
]
|
|
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 =
|
|
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] ==
|
|
813
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
698
814
|
event = self.full_event(key)
|
|
699
|
-
|
|
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] =
|
|
705
|
-
|
|
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
|
-
|
|
709
|
-
]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
786
|
-
|
|
869
|
+
if not w.exec_():
|
|
870
|
+
return
|
|
787
871
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
887
|
+
== cfg.NO
|
|
888
|
+
):
|
|
889
|
+
return
|
|
801
890
|
|
|
802
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
|
-
|
|
820
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
838
|
-
if not len(
|
|
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=
|
|
858
|
-
cb.setText("\n".join(copied_events), mode=
|
|
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
|
|
874
|
-
length.append(len(
|
|
875
|
-
content.append(
|
|
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
|
|
879
|
-
|
|
969
|
+
msg_box = QMessageBox(
|
|
970
|
+
QMessageBox.Warning,
|
|
880
971
|
cfg.programName,
|
|
881
972
|
(
|
|
882
|
-
"The clipboard does not contain events
|
|
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)
|