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