boris-behav-obs 9.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- 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 +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from PySide6.QtCore import Qt, QEvent
|
|
26
|
+
from PySide6.QtWidgets import (
|
|
27
|
+
QDialog,
|
|
28
|
+
QVBoxLayout,
|
|
29
|
+
QLabel,
|
|
30
|
+
QListWidget,
|
|
31
|
+
QHBoxLayout,
|
|
32
|
+
QLineEdit,
|
|
33
|
+
QPushButton,
|
|
34
|
+
QListWidgetItem,
|
|
35
|
+
QSizePolicy,
|
|
36
|
+
QSpacerItem,
|
|
37
|
+
QAbstractItemView,
|
|
38
|
+
QMessageBox,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from . import config as cfg
|
|
42
|
+
from . import utilities as util
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ModifiersList(QDialog):
|
|
46
|
+
"""
|
|
47
|
+
class for selection the modifier(s)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, code: str, modifiers_dict: dict, currentModifier: str):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.setWindowTitle(cfg.programName)
|
|
53
|
+
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
54
|
+
|
|
55
|
+
self.modifiers_dict = dict(modifiers_dict)
|
|
56
|
+
currentModifierList = currentModifier.split("|")
|
|
57
|
+
|
|
58
|
+
V1layout = QVBoxLayout()
|
|
59
|
+
label = QLabel()
|
|
60
|
+
label.setText(f"Choose the modifier{'s' * (len(self.modifiers_dict) > 1)} for <b>{code}</b> behavior")
|
|
61
|
+
V1layout.addWidget(label)
|
|
62
|
+
|
|
63
|
+
Hlayout = QHBoxLayout()
|
|
64
|
+
self.modifiersSetNumber = 0
|
|
65
|
+
|
|
66
|
+
for idx in util.sorted_keys(modifiers_dict):
|
|
67
|
+
if self.modifiers_dict[idx]["type"] not in (
|
|
68
|
+
cfg.SINGLE_SELECTION,
|
|
69
|
+
cfg.MULTI_SELECTION,
|
|
70
|
+
cfg.NUMERIC_MODIFIER,
|
|
71
|
+
):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
V2layout = QVBoxLayout()
|
|
75
|
+
|
|
76
|
+
self.modifiersSetNumber += 1
|
|
77
|
+
|
|
78
|
+
if self.modifiers_dict[idx].get("name", ""):
|
|
79
|
+
lb1 = QLabel(f"Modifier <b>{self.modifiers_dict[idx].get('name', '')}</b>")
|
|
80
|
+
V2layout.addWidget(lb1)
|
|
81
|
+
|
|
82
|
+
if self.modifiers_dict[idx].get("description", ""):
|
|
83
|
+
lb2 = QLabel(f"<small>{self.modifiers_dict[idx].get('description', '')[:50]}</small>")
|
|
84
|
+
V2layout.addWidget(lb2)
|
|
85
|
+
|
|
86
|
+
if self.modifiers_dict[idx]["type"] in (
|
|
87
|
+
cfg.SINGLE_SELECTION,
|
|
88
|
+
cfg.MULTI_SELECTION,
|
|
89
|
+
):
|
|
90
|
+
lw = QListWidget()
|
|
91
|
+
self.modifiers_dict[idx]["widget"] = lw
|
|
92
|
+
lw.setObjectName(f"lw_modifiers_({self.modifiers_dict[idx]['type']})")
|
|
93
|
+
lw.installEventFilter(self)
|
|
94
|
+
|
|
95
|
+
if self.modifiers_dict[idx]["type"] == cfg.SINGLE_SELECTION:
|
|
96
|
+
item = QListWidgetItem("None")
|
|
97
|
+
lw.addItem(item)
|
|
98
|
+
item.setSelected(True)
|
|
99
|
+
|
|
100
|
+
for modifier in self.modifiers_dict[idx]["values"]:
|
|
101
|
+
item = QListWidgetItem(modifier)
|
|
102
|
+
if self.modifiers_dict[idx]["type"] == cfg.MULTI_SELECTION:
|
|
103
|
+
item.setCheckState(Qt.Unchecked)
|
|
104
|
+
|
|
105
|
+
# previously selected
|
|
106
|
+
try:
|
|
107
|
+
if currentModifierList != [""] and re.sub(r" \(.\)", "", modifier) in currentModifierList[int(idx)].split(","):
|
|
108
|
+
item.setCheckState(Qt.Checked)
|
|
109
|
+
except Exception: # for old projects due to a fixed bug
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
lw.addItem(item)
|
|
113
|
+
|
|
114
|
+
if self.modifiers_dict[idx]["type"] == cfg.SINGLE_SELECTION:
|
|
115
|
+
try:
|
|
116
|
+
if currentModifierList != [""] and re.sub(r" \(.\)", "", modifier) == currentModifierList[int(idx)]:
|
|
117
|
+
item.setSelected(True)
|
|
118
|
+
except Exception: # for old projects due to a fixed bug
|
|
119
|
+
pass
|
|
120
|
+
V2layout.addWidget(lw)
|
|
121
|
+
|
|
122
|
+
if self.modifiers_dict[idx]["type"] in [cfg.NUMERIC_MODIFIER]:
|
|
123
|
+
le = QLineEdit()
|
|
124
|
+
self.modifiers_dict[idx]["widget"] = le
|
|
125
|
+
le.setObjectName(f"le_modifiers_({self.modifiers_dict[idx]['type']})")
|
|
126
|
+
|
|
127
|
+
if currentModifierList != [""] and currentModifierList[int(idx)] != "None":
|
|
128
|
+
le.setText(currentModifierList[int(idx)])
|
|
129
|
+
|
|
130
|
+
V2layout.addWidget(le)
|
|
131
|
+
|
|
132
|
+
# vertical spacer
|
|
133
|
+
spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
|
|
134
|
+
V2layout.addItem(spacerItem)
|
|
135
|
+
|
|
136
|
+
Hlayout.addLayout(V2layout)
|
|
137
|
+
|
|
138
|
+
V1layout.addLayout(Hlayout)
|
|
139
|
+
|
|
140
|
+
H2layout = QHBoxLayout()
|
|
141
|
+
H2layout.addStretch(1)
|
|
142
|
+
|
|
143
|
+
pbCancel = QPushButton(cfg.CANCEL)
|
|
144
|
+
pbCancel.clicked.connect(self.reject)
|
|
145
|
+
H2layout.addWidget(pbCancel)
|
|
146
|
+
|
|
147
|
+
pbOK = QPushButton(cfg.OK)
|
|
148
|
+
pbOK.setDefault(True)
|
|
149
|
+
pbOK.clicked.connect(self.pbOK_clicked)
|
|
150
|
+
H2layout.addWidget(pbOK)
|
|
151
|
+
|
|
152
|
+
V1layout.addLayout(H2layout)
|
|
153
|
+
self.setLayout(V1layout)
|
|
154
|
+
|
|
155
|
+
self.installEventFilter(self)
|
|
156
|
+
self.setMaximumSize(1024, 960)
|
|
157
|
+
|
|
158
|
+
def eventFilter(self, receiver, event):
|
|
159
|
+
"""
|
|
160
|
+
send event (if keypress) to main window
|
|
161
|
+
"""
|
|
162
|
+
if event.type() == QEvent.KeyPress:
|
|
163
|
+
ek, ek_text = event.key(), event.text()
|
|
164
|
+
|
|
165
|
+
# reject and close dialog if escape pressed
|
|
166
|
+
if ek == Qt.Key_Escape: # close
|
|
167
|
+
self.reject()
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
# accept and close dialog if enter pressed
|
|
171
|
+
if ek == Qt.Key_Enter or ek == Qt.Key_Return: # enter or enter from numeric pad
|
|
172
|
+
if not self.pbOK_clicked():
|
|
173
|
+
return False
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
modifiersSetIndex = 0
|
|
177
|
+
for widget in self.children():
|
|
178
|
+
if "_modifiers" in widget.objectName():
|
|
179
|
+
modifiersSetIndex = modifiersSetIndex + 1
|
|
180
|
+
if "lw_modifiers" in widget.objectName():
|
|
181
|
+
if self.modifiersSetNumber == 1:
|
|
182
|
+
# check if modifiers have code
|
|
183
|
+
for index in range(widget.count()):
|
|
184
|
+
if "(" in widget.item(index).text():
|
|
185
|
+
break
|
|
186
|
+
else:
|
|
187
|
+
# modifiers have no associated code: the modifier starting with hit key will be selected
|
|
188
|
+
if ek not in (Qt.Key_Down, Qt.Key_Up):
|
|
189
|
+
if ek == Qt.Key_Space and f"({cfg.MULTI_SELECTION})" in widget.objectName(): # checking using SPACE bar
|
|
190
|
+
if widget.item(widget.currentRow()).checkState() == Qt.Checked:
|
|
191
|
+
widget.item(widget.currentRow()).setCheckState(Qt.Unchecked)
|
|
192
|
+
else:
|
|
193
|
+
widget.item(widget.currentRow()).setCheckState(Qt.Checked)
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
for index in range(widget.count()):
|
|
197
|
+
if widget.item(index).text().upper().startswith(ek_text.upper()):
|
|
198
|
+
widget.setCurrentRow(index)
|
|
199
|
+
widget.scrollToItem(
|
|
200
|
+
widget.item(index),
|
|
201
|
+
QAbstractItemView.EnsureVisible,
|
|
202
|
+
)
|
|
203
|
+
return True
|
|
204
|
+
else: # up / down keys
|
|
205
|
+
try:
|
|
206
|
+
if ek == Qt.Key_Down and widget.currentRow() < widget.count() - 1:
|
|
207
|
+
widget.setCurrentRow(widget.currentRow() + 1)
|
|
208
|
+
if ek == Qt.Key_Up and widget.currentRow() > 0:
|
|
209
|
+
widget.setCurrentRow(widget.currentRow() - 1)
|
|
210
|
+
except Exception:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
for index in range(widget.count()):
|
|
214
|
+
# check function kesy (F1, F2...)
|
|
215
|
+
if ek in cfg.function_keys:
|
|
216
|
+
if f"({cfg.function_keys[ek]})" in widget.item(index).text().upper():
|
|
217
|
+
if f"({cfg.SINGLE_SELECTION})" in widget.objectName():
|
|
218
|
+
widget.item(index).setSelected(True)
|
|
219
|
+
# close dialog if one set of modifiers
|
|
220
|
+
if self.modifiersSetNumber == 1:
|
|
221
|
+
self.accept()
|
|
222
|
+
return True
|
|
223
|
+
# else move to next set of mofifiers
|
|
224
|
+
elif modifiersSetIndex != self.modifiersSetNumber:
|
|
225
|
+
widget.parent().focusNextChild()
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
if f"({cfg.MULTI_SELECTION})" in widget.objectName():
|
|
229
|
+
if widget.item(index).checkState() == Qt.Checked:
|
|
230
|
+
widget.item(index).setCheckState(Qt.Unchecked)
|
|
231
|
+
else:
|
|
232
|
+
widget.item(index).setCheckState(Qt.Checked)
|
|
233
|
+
|
|
234
|
+
if ek < 1114112 and f"({ek_text})" in widget.item(index).text():
|
|
235
|
+
if f"({cfg.SINGLE_SELECTION})" in widget.objectName():
|
|
236
|
+
widget.item(index).setSelected(True)
|
|
237
|
+
# close dialog if one set of modifiers
|
|
238
|
+
if self.modifiersSetNumber == 1:
|
|
239
|
+
self.accept()
|
|
240
|
+
return True
|
|
241
|
+
# else move to next set of mofifiers
|
|
242
|
+
elif modifiersSetIndex != self.modifiersSetNumber:
|
|
243
|
+
widget.parent().focusNextChild()
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
if f"({cfg.MULTI_SELECTION})" in widget.objectName():
|
|
247
|
+
if widget.item(index).checkState() == Qt.Checked:
|
|
248
|
+
widget.item(index).setCheckState(Qt.Unchecked)
|
|
249
|
+
else:
|
|
250
|
+
widget.item(index).setCheckState(Qt.Checked)
|
|
251
|
+
|
|
252
|
+
return True
|
|
253
|
+
else:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def get_modifiers(self):
|
|
257
|
+
"""
|
|
258
|
+
get modifiers
|
|
259
|
+
returns list of selected modifiers
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
for idx in util.sorted_keys(self.modifiers_dict):
|
|
263
|
+
if self.modifiers_dict[idx]["type"] in [
|
|
264
|
+
cfg.SINGLE_SELECTION,
|
|
265
|
+
cfg.MULTI_SELECTION,
|
|
266
|
+
cfg.NUMERIC_MODIFIER,
|
|
267
|
+
]:
|
|
268
|
+
self.modifiers_dict[idx]["selected"] = []
|
|
269
|
+
|
|
270
|
+
if self.modifiers_dict[idx]["type"] == cfg.MULTI_SELECTION:
|
|
271
|
+
for j in range(self.modifiers_dict[idx]["widget"].count()):
|
|
272
|
+
if self.modifiers_dict[idx]["widget"].item(j).checkState() == Qt.Checked:
|
|
273
|
+
self.modifiers_dict[idx]["selected"].append(
|
|
274
|
+
re.sub(
|
|
275
|
+
r" \(.*\)",
|
|
276
|
+
"",
|
|
277
|
+
self.modifiers_dict[idx]["widget"].item(j).text(),
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not self.modifiers_dict[idx]["selected"]:
|
|
282
|
+
self.modifiers_dict[idx]["selected"].append("None")
|
|
283
|
+
|
|
284
|
+
if self.modifiers_dict[idx]["type"] == cfg.SINGLE_SELECTION:
|
|
285
|
+
for item in self.modifiers_dict[idx]["widget"].selectedItems():
|
|
286
|
+
self.modifiers_dict[idx]["selected"].append(re.sub(r" \(.*\)", "", item.text()))
|
|
287
|
+
|
|
288
|
+
if self.modifiers_dict[idx]["type"] == cfg.NUMERIC_MODIFIER:
|
|
289
|
+
self.modifiers_dict[idx]["selected"] = (
|
|
290
|
+
self.modifiers_dict[idx]["widget"].text() if self.modifiers_dict[idx]["widget"].text() else "None"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return self.modifiers_dict
|
|
294
|
+
|
|
295
|
+
def pbOK_clicked(self):
|
|
296
|
+
"""
|
|
297
|
+
OK button is clicked
|
|
298
|
+
"""
|
|
299
|
+
for idx in util.sorted_keys(self.modifiers_dict):
|
|
300
|
+
if self.modifiers_dict[idx]["type"] == cfg.NUMERIC_MODIFIER:
|
|
301
|
+
if self.modifiers_dict[idx]["widget"].text():
|
|
302
|
+
try:
|
|
303
|
+
_ = float(self.modifiers_dict[idx]["widget"].text())
|
|
304
|
+
except Exception:
|
|
305
|
+
QMessageBox.warning(
|
|
306
|
+
self,
|
|
307
|
+
cfg.programName,
|
|
308
|
+
f"<b>{self.modifiers_dict[idx]['widget'].text()}</b> is not a numeric value",
|
|
309
|
+
)
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
self.accept()
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Tuple
|
|
26
|
+
|
|
27
|
+
from PySide6.QtCore import Qt
|
|
28
|
+
from PySide6.QtWidgets import QAbstractItemView
|
|
29
|
+
|
|
30
|
+
from . import config as cfg
|
|
31
|
+
from . import gui_utilities, observations_list, project_functions
|
|
32
|
+
from . import utilities as util
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def select_observations2(self, mode: str, windows_title: str = "") -> Tuple[str, list]:
|
|
36
|
+
"""
|
|
37
|
+
allow user to select observations
|
|
38
|
+
mode: accepted values: OPEN, EDIT, SINGLE, MULTIPLE, SELECT1
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
pj (dict): BORIS project dictionary
|
|
42
|
+
mode (str): mode for selection: OPEN, EDIT, SINGLE, MULTIPLE, SELECT1
|
|
43
|
+
windows_title (str): title for windows
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: selected mode: OPEN, EDIT, VIEW
|
|
47
|
+
list: list of selected observations
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
pj = self.pj
|
|
51
|
+
|
|
52
|
+
fields_list = ["id", "date", "description", "subjects", "observation duration", "exhaustivity %", "media"]
|
|
53
|
+
indep_var_header, column_type = [], [cfg.TEXT, cfg.TEXT, cfg.TEXT, cfg.TEXT, cfg.NUMERIC, cfg.NUMERIC, cfg.TEXT]
|
|
54
|
+
|
|
55
|
+
if cfg.INDEPENDENT_VARIABLES in pj:
|
|
56
|
+
for idx in util.sorted_keys(pj[cfg.INDEPENDENT_VARIABLES]):
|
|
57
|
+
indep_var_header.append(pj[cfg.INDEPENDENT_VARIABLES][idx]["label"])
|
|
58
|
+
column_type.append(pj[cfg.INDEPENDENT_VARIABLES][idx]["type"])
|
|
59
|
+
|
|
60
|
+
state_events_list = util.state_behavior_codes(pj[cfg.ETHOGRAM])
|
|
61
|
+
|
|
62
|
+
data: list = []
|
|
63
|
+
not_paired: list = []
|
|
64
|
+
|
|
65
|
+
# check if observations changed
|
|
66
|
+
if hash(str(self.pj[cfg.OBSERVATIONS])) != self.mem_hash_obs:
|
|
67
|
+
logging.debug("observations changed")
|
|
68
|
+
|
|
69
|
+
for obs in sorted(list(pj[cfg.OBSERVATIONS].keys())):
|
|
70
|
+
date = pj[cfg.OBSERVATIONS][obs]["date"].replace("T", " ")
|
|
71
|
+
descr = util.eol2space(pj[cfg.OBSERVATIONS][obs][cfg.DESCRIPTION])
|
|
72
|
+
|
|
73
|
+
# subjects
|
|
74
|
+
observed_subjects = [cfg.NO_FOCAL_SUBJECT if x == "" else x for x in project_functions.extract_observed_subjects(pj, [obs])]
|
|
75
|
+
|
|
76
|
+
subjectsList = ", ".join(observed_subjects)
|
|
77
|
+
|
|
78
|
+
# observed time interval
|
|
79
|
+
interval = project_functions.observed_interval(pj[cfg.OBSERVATIONS][obs])
|
|
80
|
+
observed_interval_str = str(round(interval[1] - interval[0], 3))
|
|
81
|
+
|
|
82
|
+
# media
|
|
83
|
+
media: str = ""
|
|
84
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] == cfg.MEDIA:
|
|
85
|
+
media_list: list = []
|
|
86
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.FILE]:
|
|
87
|
+
for player in sorted(pj[cfg.OBSERVATIONS][obs][cfg.FILE].keys()):
|
|
88
|
+
for media in pj[cfg.OBSERVATIONS][obs][cfg.FILE][player]:
|
|
89
|
+
media_list.append(f"#{player}: {media}")
|
|
90
|
+
|
|
91
|
+
if len(media_list) > 8:
|
|
92
|
+
media = " ".join(media_list)
|
|
93
|
+
else:
|
|
94
|
+
media = "\n".join(media_list)
|
|
95
|
+
|
|
96
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] == cfg.LIVE:
|
|
97
|
+
media = cfg.LIVE
|
|
98
|
+
|
|
99
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] == cfg.IMAGES:
|
|
100
|
+
dir_list: list = []
|
|
101
|
+
for dir_path in pj[cfg.OBSERVATIONS][obs].get(cfg.DIRECTORIES_LIST, []):
|
|
102
|
+
dir_list.append(dir_path)
|
|
103
|
+
media = "; ".join(dir_list)
|
|
104
|
+
|
|
105
|
+
# independent variables
|
|
106
|
+
indepvar: list = []
|
|
107
|
+
if cfg.INDEPENDENT_VARIABLES in pj[cfg.OBSERVATIONS][obs]:
|
|
108
|
+
for var_label in indep_var_header:
|
|
109
|
+
if var_label in pj[cfg.OBSERVATIONS][obs][cfg.INDEPENDENT_VARIABLES]:
|
|
110
|
+
indepvar.append(pj[cfg.OBSERVATIONS][obs][cfg.INDEPENDENT_VARIABLES][var_label])
|
|
111
|
+
else:
|
|
112
|
+
indepvar.append("")
|
|
113
|
+
|
|
114
|
+
# check unpaired events
|
|
115
|
+
ok, _ = project_functions.check_state_events_obs(obs, pj[cfg.ETHOGRAM], pj[cfg.OBSERVATIONS][obs], cfg.HHMMSS)
|
|
116
|
+
if not ok:
|
|
117
|
+
not_paired.append(obs)
|
|
118
|
+
|
|
119
|
+
# exhaustivity
|
|
120
|
+
if pj[cfg.OBSERVATIONS][obs][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
121
|
+
# check exhaustivity of observation
|
|
122
|
+
exhaustivity = project_functions.check_observation_exhaustivity(pj[cfg.OBSERVATIONS][obs][cfg.EVENTS], state_events_list)
|
|
123
|
+
elif pj[cfg.OBSERVATIONS][obs][cfg.TYPE] == cfg.IMAGES:
|
|
124
|
+
exhaustivity = project_functions.check_observation_exhaustivity_pictures(pj[cfg.OBSERVATIONS][obs])
|
|
125
|
+
|
|
126
|
+
data.append([obs, date, descr, subjectsList, observed_interval_str, str(exhaustivity), media] + indepvar)
|
|
127
|
+
|
|
128
|
+
obsList = observations_list.observationsList_widget(
|
|
129
|
+
data, header=fields_list + indep_var_header, column_type=column_type, not_paired=not_paired
|
|
130
|
+
)
|
|
131
|
+
self.data = data
|
|
132
|
+
self.not_paired = not_paired
|
|
133
|
+
self.mem_hash_obs = hash(str(self.pj[cfg.OBSERVATIONS]))
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
obsList = observations_list.observationsList_widget(
|
|
137
|
+
self.data, header=fields_list + indep_var_header, column_type=column_type, not_paired=self.not_paired
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if windows_title:
|
|
141
|
+
obsList.setWindowTitle(windows_title)
|
|
142
|
+
|
|
143
|
+
obsList.pbOpen.setVisible(False)
|
|
144
|
+
obsList.pbView.setVisible(False)
|
|
145
|
+
obsList.pbEdit.setVisible(False)
|
|
146
|
+
obsList.pbOk.setVisible(False)
|
|
147
|
+
obsList.pbSelectAll.setVisible(False)
|
|
148
|
+
obsList.pbUnSelectAll.setVisible(False)
|
|
149
|
+
obsList.mode = mode
|
|
150
|
+
|
|
151
|
+
if mode == cfg.OPEN:
|
|
152
|
+
obsList.view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
153
|
+
obsList.pbOpen.setVisible(True)
|
|
154
|
+
|
|
155
|
+
if mode == cfg.VIEW:
|
|
156
|
+
obsList.view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
157
|
+
obsList.pbView.setVisible(True)
|
|
158
|
+
|
|
159
|
+
if mode == cfg.EDIT:
|
|
160
|
+
obsList.view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
161
|
+
obsList.pbEdit.setVisible(True)
|
|
162
|
+
|
|
163
|
+
if mode == cfg.SINGLE:
|
|
164
|
+
obsList.view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
165
|
+
obsList.pbOpen.setVisible(True)
|
|
166
|
+
obsList.pbView.setVisible(True)
|
|
167
|
+
obsList.pbEdit.setVisible(True)
|
|
168
|
+
|
|
169
|
+
if mode == cfg.MULTIPLE:
|
|
170
|
+
obsList.view.setSelectionMode(QAbstractItemView.MultiSelection)
|
|
171
|
+
obsList.pbOk.setVisible(True)
|
|
172
|
+
obsList.pbSelectAll.setVisible(True)
|
|
173
|
+
obsList.pbUnSelectAll.setVisible(True)
|
|
174
|
+
|
|
175
|
+
if mode == cfg.SELECT1:
|
|
176
|
+
obsList.view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
177
|
+
obsList.pbOk.setVisible(True)
|
|
178
|
+
|
|
179
|
+
# restore window geometry
|
|
180
|
+
gui_utilities.restore_geometry(obsList, "observations list", (900, 600))
|
|
181
|
+
|
|
182
|
+
obsList.view.sortItems(0, Qt.AscendingOrder)
|
|
183
|
+
for row in range(obsList.view.rowCount()):
|
|
184
|
+
obsList.view.resizeRowToContents(row)
|
|
185
|
+
|
|
186
|
+
selected_observations = []
|
|
187
|
+
|
|
188
|
+
result = obsList.exec_()
|
|
189
|
+
|
|
190
|
+
# saving window geometry in ini file
|
|
191
|
+
gui_utilities.save_geometry(obsList, "observations list")
|
|
192
|
+
|
|
193
|
+
if result:
|
|
194
|
+
if obsList.view.selectedIndexes():
|
|
195
|
+
for idx in obsList.view.selectedIndexes():
|
|
196
|
+
if idx.column() == 0: # first column
|
|
197
|
+
selected_observations.append(idx.data())
|
|
198
|
+
|
|
199
|
+
if result == 0: # cancel
|
|
200
|
+
resultStr = ""
|
|
201
|
+
if result == 1: # select
|
|
202
|
+
resultStr = "ok"
|
|
203
|
+
if result == 2: # open
|
|
204
|
+
resultStr = cfg.OPEN
|
|
205
|
+
if result == 3: # edit
|
|
206
|
+
resultStr = cfg.EDIT
|
|
207
|
+
if result == 4: # view
|
|
208
|
+
resultStr = cfg.VIEW
|
|
209
|
+
|
|
210
|
+
return resultStr, selected_observations
|