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/project.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
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -24,10 +24,13 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
import re
|
|
26
26
|
|
|
27
|
-
from
|
|
28
|
-
from
|
|
29
|
-
from
|
|
27
|
+
from PySide6.QtCore import Qt, QDateTime
|
|
28
|
+
from PySide6.QtGui import QColor
|
|
29
|
+
from PySide6.QtWidgets import (
|
|
30
|
+
QAbstractItemView,
|
|
31
|
+
QApplication,
|
|
30
32
|
QCheckBox,
|
|
33
|
+
QColorDialog,
|
|
31
34
|
QDialog,
|
|
32
35
|
QFileDialog,
|
|
33
36
|
QHBoxLayout,
|
|
@@ -35,13 +38,12 @@ from PyQt5.QtWidgets import (
|
|
|
35
38
|
QInputDialog,
|
|
36
39
|
QLabel,
|
|
37
40
|
QLineEdit,
|
|
38
|
-
QListWidget,
|
|
39
|
-
QListWidgetItem,
|
|
40
41
|
QMenu,
|
|
41
42
|
QMessageBox,
|
|
42
43
|
QPushButton,
|
|
43
44
|
QSizePolicy,
|
|
44
45
|
QSpacerItem,
|
|
46
|
+
QTableWidget,
|
|
45
47
|
QTableWidgetItem,
|
|
46
48
|
QVBoxLayout,
|
|
47
49
|
)
|
|
@@ -73,11 +75,49 @@ class BehavioralCategories(QDialog):
|
|
|
73
75
|
self.label.setText("Behavioral categories")
|
|
74
76
|
self.vbox.addWidget(self.label)
|
|
75
77
|
|
|
76
|
-
self.lw = QListWidget()
|
|
78
|
+
# self.lw = QListWidget()
|
|
79
|
+
self.lw = QTableWidget()
|
|
80
|
+
self.lw.cellDoubleClicked[int, int].connect(self.lw_double_clicked)
|
|
77
81
|
|
|
78
82
|
# add categories
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
self.lw.setColumnCount(2)
|
|
84
|
+
self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
|
|
85
|
+
self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
86
|
+
|
|
87
|
+
self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
88
|
+
|
|
89
|
+
behavioral_categories: list = []
|
|
90
|
+
|
|
91
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
|
|
92
|
+
self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
|
|
93
|
+
behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
|
|
94
|
+
for idx, key in enumerate(behav_cat.keys()):
|
|
95
|
+
# name
|
|
96
|
+
item = QTableWidgetItem()
|
|
97
|
+
item.setText(behav_cat[key]["name"])
|
|
98
|
+
behavioral_categories.append(behav_cat[key]["name"])
|
|
99
|
+
self.lw.setItem(idx, 0, item)
|
|
100
|
+
# color
|
|
101
|
+
item = QTableWidgetItem()
|
|
102
|
+
item.setText(behav_cat[key].get(cfg.COLOR, ""))
|
|
103
|
+
if behav_cat[key].get(cfg.COLOR, ""):
|
|
104
|
+
item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
|
|
105
|
+
else:
|
|
106
|
+
item.setBackground(self.not_editable_column_color())
|
|
107
|
+
self.lw.setItem(idx, 1, item)
|
|
108
|
+
else:
|
|
109
|
+
self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
|
|
110
|
+
for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
|
|
111
|
+
# name
|
|
112
|
+
item = QTableWidgetItem()
|
|
113
|
+
item.setText(category)
|
|
114
|
+
behavioral_categories.append(category)
|
|
115
|
+
self.lw.setItem(idx, 0, item)
|
|
116
|
+
# color
|
|
117
|
+
item = QTableWidgetItem()
|
|
118
|
+
item.setText("")
|
|
119
|
+
|
|
120
|
+
self.lw.setItem(idx, 1, item)
|
|
81
121
|
|
|
82
122
|
self.vbox.addWidget(self.lw)
|
|
83
123
|
|
|
@@ -94,8 +134,8 @@ class BehavioralCategories(QDialog):
|
|
|
94
134
|
self.vbox.addLayout(self.hbox0)
|
|
95
135
|
|
|
96
136
|
hbox1 = QHBoxLayout()
|
|
97
|
-
self.pbOK = QPushButton(
|
|
98
|
-
self.pbCancel = QPushButton(
|
|
137
|
+
self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
|
|
138
|
+
self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.accept)
|
|
99
139
|
|
|
100
140
|
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
101
141
|
hbox1.addItem(spacerItem)
|
|
@@ -105,23 +145,111 @@ class BehavioralCategories(QDialog):
|
|
|
105
145
|
|
|
106
146
|
self.setLayout(self.vbox)
|
|
107
147
|
|
|
148
|
+
# check if behavioral categories are present in events
|
|
149
|
+
behavioral_categories_in_ethogram = set(
|
|
150
|
+
sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
|
|
154
|
+
set(behavioral_categories)
|
|
155
|
+
) != {""}:
|
|
156
|
+
if (
|
|
157
|
+
dialog.MessageDialog(
|
|
158
|
+
cfg.programName,
|
|
159
|
+
(
|
|
160
|
+
"There are behavioral categories that are present in ethogram but not defined.<br>"
|
|
161
|
+
f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
|
|
162
|
+
"<br>"
|
|
163
|
+
"Do you want to add them in the behavioral categories list?"
|
|
164
|
+
),
|
|
165
|
+
[cfg.YES, cfg.NO],
|
|
166
|
+
)
|
|
167
|
+
== cfg.YES
|
|
168
|
+
):
|
|
169
|
+
# add behavioral categories present in ethogram in behavioal categories list
|
|
170
|
+
rc = self.lw.rowCount()
|
|
171
|
+
self.lw.setRowCount(rc + len(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))
|
|
172
|
+
for idx, category in enumerate(sorted(list(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))):
|
|
173
|
+
print(category)
|
|
174
|
+
# name
|
|
175
|
+
item = QTableWidgetItem()
|
|
176
|
+
item.setText(category)
|
|
177
|
+
# behavioral_categories.append(category)
|
|
178
|
+
self.lw.setItem(rc + idx, 0, item)
|
|
179
|
+
# color
|
|
180
|
+
item = QTableWidgetItem()
|
|
181
|
+
item.setText("")
|
|
182
|
+
|
|
183
|
+
self.lw.setItem(rc + idx, 1, item)
|
|
184
|
+
|
|
185
|
+
def not_editable_column_color(self):
|
|
186
|
+
"""
|
|
187
|
+
return a color for the not editable column
|
|
188
|
+
"""
|
|
189
|
+
window_color = QApplication.instance().palette().window().color()
|
|
190
|
+
return QColor(
|
|
191
|
+
window_color.red() - cfg.DARKER_DIFFERENCE,
|
|
192
|
+
window_color.green() - cfg.DARKER_DIFFERENCE,
|
|
193
|
+
window_color.blue() - cfg.DARKER_DIFFERENCE,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def lw_double_clicked(self, row: int, column: int):
|
|
197
|
+
"""
|
|
198
|
+
change color
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
if column != 1:
|
|
202
|
+
return
|
|
203
|
+
col_diag = QColorDialog()
|
|
204
|
+
col_diag.setOptions(QColorDialog.DontUseNativeDialog)
|
|
205
|
+
|
|
206
|
+
if self.lw.item(row, 1).text():
|
|
207
|
+
current_color = QColor(self.lw.item(row, 1).text())
|
|
208
|
+
if current_color.isValid():
|
|
209
|
+
col_diag.setCurrentColor(current_color)
|
|
210
|
+
|
|
211
|
+
if col_diag.exec_():
|
|
212
|
+
color = col_diag.currentColor()
|
|
213
|
+
if color.name() == "#000000": # black -> delete color
|
|
214
|
+
self.lw.item(row, 1).setText("")
|
|
215
|
+
self.lw.item(row, 1).setBackground(self.not_editable_column_color())
|
|
216
|
+
elif color.isValid():
|
|
217
|
+
self.lw.item(row, 1).setText(color.name())
|
|
218
|
+
self.lw.item(row, 1).setBackground(color)
|
|
219
|
+
|
|
108
220
|
def add_behavioral_category(self):
|
|
109
221
|
"""
|
|
110
222
|
add a behavioral category
|
|
111
223
|
"""
|
|
112
224
|
category, ok = QInputDialog.getText(self, "New behavioral category", "Category name:")
|
|
113
225
|
if ok:
|
|
114
|
-
self.lw.
|
|
226
|
+
self.lw.insertRow(self.lw.rowCount())
|
|
227
|
+
item = QTableWidgetItem(category)
|
|
228
|
+
self.lw.setItem(self.lw.rowCount() - 1, 0, item)
|
|
229
|
+
|
|
230
|
+
item = QTableWidgetItem("")
|
|
231
|
+
# item.setFlags(Qt.ItemIsEnabled)
|
|
232
|
+
self.lw.setItem(self.lw.rowCount() - 1, 1, item)
|
|
115
233
|
|
|
116
234
|
def remove_behavioral_category(self):
|
|
117
235
|
"""
|
|
118
236
|
remove the selected behavioral category
|
|
119
237
|
"""
|
|
120
238
|
|
|
121
|
-
for
|
|
239
|
+
for selected_item in self.lw.selectedItems():
|
|
122
240
|
# check if behavioral category is in use
|
|
123
|
-
|
|
124
|
-
|
|
241
|
+
if (
|
|
242
|
+
dialog.MessageDialog(
|
|
243
|
+
cfg.programName,
|
|
244
|
+
("Confirm deletion of the behavioral category"),
|
|
245
|
+
("Confirm", cfg.CANCEL),
|
|
246
|
+
)
|
|
247
|
+
== cfg.CANCEL
|
|
248
|
+
):
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
category_to_remove = self.lw.item(self.lw.row(selected_item), 0).text().strip()
|
|
252
|
+
behaviors_in_category: list = []
|
|
125
253
|
for idx in self.pj[cfg.ETHOGRAM]:
|
|
126
254
|
if (
|
|
127
255
|
cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
|
|
@@ -130,16 +258,15 @@ class BehavioralCategories(QDialog):
|
|
|
130
258
|
behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
|
|
131
259
|
flag_remove = False
|
|
132
260
|
if behaviors_in_category:
|
|
133
|
-
|
|
134
261
|
flag_remove = (
|
|
135
262
|
dialog.MessageDialog(
|
|
136
263
|
cfg.programName,
|
|
137
264
|
(
|
|
138
|
-
"Some behavior belong to the <b>{
|
|
139
|
-
"{
|
|
265
|
+
f"Some behavior belong to the <b>{category_to_remove}</b> to remove:<br>"
|
|
266
|
+
f"{'<br>'.join(behaviors_in_category)}<br>"
|
|
140
267
|
"<br>Some features may not be available anymore.<br>"
|
|
141
|
-
)
|
|
142
|
-
|
|
268
|
+
),
|
|
269
|
+
("Remove category", cfg.CANCEL),
|
|
143
270
|
)
|
|
144
271
|
== "Remove category"
|
|
145
272
|
)
|
|
@@ -148,17 +275,18 @@ class BehavioralCategories(QDialog):
|
|
|
148
275
|
flag_remove = True
|
|
149
276
|
|
|
150
277
|
if flag_remove:
|
|
151
|
-
self.lw.
|
|
278
|
+
self.lw.removeRow(self.lw.row(selected_item))
|
|
152
279
|
self.removed = category_to_remove
|
|
280
|
+
|
|
153
281
|
self.accept()
|
|
154
282
|
|
|
155
|
-
def pb_rename_category_clicked(self):
|
|
283
|
+
def pb_rename_category_clicked(self, row: int):
|
|
156
284
|
"""
|
|
157
285
|
rename the selected behavioral category
|
|
158
286
|
"""
|
|
159
|
-
for
|
|
287
|
+
for selected_item in self.lw.selectedItems():
|
|
160
288
|
# check if behavioral category is in use
|
|
161
|
-
category_to_rename = self.lw.item(self.lw.row(
|
|
289
|
+
category_to_rename = self.lw.item(self.lw.row(selected_item), 0).text().strip()
|
|
162
290
|
behaviors_in_category = []
|
|
163
291
|
for idx in self.pj[cfg.ETHOGRAM]:
|
|
164
292
|
if (
|
|
@@ -172,9 +300,7 @@ class BehavioralCategories(QDialog):
|
|
|
172
300
|
flag_rename = (
|
|
173
301
|
dialog.MessageDialog(
|
|
174
302
|
cfg.programName,
|
|
175
|
-
("Some behavior belong to the <b>{
|
|
176
|
-
"<br>".join(behaviors_in_category), category_to_rename
|
|
177
|
-
),
|
|
303
|
+
(f"Some behavior belong to the <b>{category_to_rename}</b> to rename:<br>{'<br>'.join(behaviors_in_category)}<br>"),
|
|
178
304
|
["Rename category", cfg.CANCEL],
|
|
179
305
|
)
|
|
180
306
|
== "Rename category"
|
|
@@ -187,15 +313,14 @@ class BehavioralCategories(QDialog):
|
|
|
187
313
|
self, "Rename behavioral category", "New category name:", QLineEdit.Normal, category_to_rename
|
|
188
314
|
)
|
|
189
315
|
if ok:
|
|
190
|
-
self.lw.item(self.lw.indexFromItem(
|
|
316
|
+
self.lw.item(self.lw.indexFromItem(selected_item).row(), 0).setText(new_category_name)
|
|
191
317
|
# check behaviors belonging to the renamed category
|
|
192
318
|
self.renamed = [category_to_rename, new_category_name]
|
|
193
|
-
self.accept()
|
|
319
|
+
# self.accept()
|
|
194
320
|
|
|
195
321
|
|
|
196
322
|
class projectDialog(QDialog, Ui_dlgProject):
|
|
197
323
|
def __init__(self, parent=None):
|
|
198
|
-
|
|
199
324
|
super().__init__()
|
|
200
325
|
|
|
201
326
|
self.setupUi(self)
|
|
@@ -211,23 +336,23 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
211
336
|
"remove all|Remove all behaviors",
|
|
212
337
|
"lower|Convert keys to lower case",
|
|
213
338
|
]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
self.add_button_menu(behavior_button_items,
|
|
217
|
-
self.pb_behavior.setMenu(
|
|
339
|
+
self.behavior_menu = QMenu()
|
|
340
|
+
self.behavior_menu.triggered.connect(lambda x: self.behavior(action=x.statusTip()))
|
|
341
|
+
self.add_button_menu(behavior_button_items, self.behavior_menu)
|
|
342
|
+
self.pb_behavior.setMenu(self.behavior_menu)
|
|
218
343
|
|
|
219
344
|
import_button_items = [
|
|
220
345
|
"boris|from a BORIS project",
|
|
221
|
-
"spreadsheet|from a spreadsheet file (XLSX)",
|
|
346
|
+
"spreadsheet|from a spreadsheet file (XLSX/ODS)",
|
|
222
347
|
"jwatcher|from a JWatcher project",
|
|
223
348
|
"text|from a text file (CSV or TSV)",
|
|
224
349
|
"clipboard|from the clipboard",
|
|
225
350
|
"repository|from the BORIS repository",
|
|
226
351
|
]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
self.add_button_menu(import_button_items,
|
|
230
|
-
self.pb_import.setMenu(
|
|
352
|
+
self.import_behaviors_menu = QMenu()
|
|
353
|
+
self.import_behaviors_menu.triggered.connect(lambda x: self.import_ethogram(action=x.statusTip()))
|
|
354
|
+
self.add_button_menu(import_button_items, self.import_behaviors_menu)
|
|
355
|
+
self.pb_import.setMenu(self.import_behaviors_menu)
|
|
231
356
|
|
|
232
357
|
self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
|
|
233
358
|
|
|
@@ -251,21 +376,21 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
251
376
|
"lower|Convert keys to lower case",
|
|
252
377
|
]
|
|
253
378
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
self.add_button_menu(subjects_button_items,
|
|
257
|
-
self.pb_subjects.setMenu(
|
|
379
|
+
self.subject_menu = QMenu()
|
|
380
|
+
self.subject_menu.triggered.connect(lambda x: self.subjects(action=x.statusTip()))
|
|
381
|
+
self.add_button_menu(subjects_button_items, self.subject_menu)
|
|
382
|
+
self.pb_subjects.setMenu(self.subject_menu)
|
|
258
383
|
|
|
259
384
|
subjects_import_button_items = [
|
|
260
385
|
"boris|from a BORIS project",
|
|
261
|
-
"spreadsheet|from a spreadsheet file (XLSX)",
|
|
386
|
+
"spreadsheet|from a spreadsheet file (XLSX/ODS)",
|
|
262
387
|
"text|from a text file (CSV or TSV)",
|
|
263
388
|
"clipboard|from the clipboard",
|
|
264
389
|
]
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self.add_button_menu(subjects_import_button_items,
|
|
268
|
-
self.pbImportSubjectsFromProject.setMenu(
|
|
390
|
+
self.import_subjects_menu = QMenu()
|
|
391
|
+
self.import_subjects_menu.triggered.connect(lambda x: self.import_subjects(action=x.statusTip()))
|
|
392
|
+
self.add_button_menu(subjects_import_button_items, self.import_subjects_menu)
|
|
393
|
+
self.pbImportSubjectsFromProject.setMenu(self.import_subjects_menu)
|
|
269
394
|
|
|
270
395
|
self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
|
|
271
396
|
|
|
@@ -311,30 +436,41 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
311
436
|
self.row_in_modification = -1
|
|
312
437
|
self.flag_modified = False
|
|
313
438
|
|
|
314
|
-
for w in
|
|
439
|
+
for w in (
|
|
315
440
|
self.le_converter_name,
|
|
316
441
|
self.le_converter_description,
|
|
317
442
|
self.pteCode,
|
|
318
443
|
self.pb_save_converter,
|
|
319
444
|
self.pb_cancel_converter,
|
|
320
|
-
|
|
445
|
+
):
|
|
321
446
|
w.setEnabled(False)
|
|
322
447
|
|
|
323
448
|
# disable widget for indep var setting
|
|
324
|
-
for widget in
|
|
449
|
+
for widget in (
|
|
325
450
|
self.leLabel,
|
|
326
451
|
self.le_converter_description,
|
|
327
452
|
self.cbType,
|
|
328
453
|
self.lePredefined,
|
|
329
454
|
self.dte_default_date,
|
|
330
455
|
self.leSetValues,
|
|
331
|
-
|
|
456
|
+
):
|
|
332
457
|
widget.setEnabled(False)
|
|
333
458
|
|
|
334
459
|
self.twBehaviors.horizontalHeader().sortIndicatorChanged.connect(self.sort_twBehaviors)
|
|
335
460
|
self.twSubjects.horizontalHeader().sortIndicatorChanged.connect(self.sort_twSubjects)
|
|
336
461
|
self.twVariables.horizontalHeader().sortIndicatorChanged.connect(self.sort_twVariables)
|
|
337
462
|
|
|
463
|
+
def not_editable_column_color(self):
|
|
464
|
+
"""
|
|
465
|
+
return a color for the not editable column
|
|
466
|
+
"""
|
|
467
|
+
window_color = QApplication.instance().palette().window().color()
|
|
468
|
+
return QColor(
|
|
469
|
+
window_color.red() - cfg.DARKER_DIFFERENCE,
|
|
470
|
+
window_color.green() - cfg.DARKER_DIFFERENCE,
|
|
471
|
+
window_color.blue() - cfg.DARKER_DIFFERENCE,
|
|
472
|
+
)
|
|
473
|
+
|
|
338
474
|
def add_button_menu(self, data, menu_obj):
|
|
339
475
|
"""
|
|
340
476
|
add menu option from dictionary
|
|
@@ -448,20 +584,14 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
448
584
|
|
|
449
585
|
# check if some keys will be duplicated after conversion
|
|
450
586
|
try:
|
|
451
|
-
all_keys = [
|
|
452
|
-
self.twBehaviors.item(row, cfg.behavioursFields["key"]).text()
|
|
453
|
-
for row in range(self.twBehaviors.rowCount())
|
|
454
|
-
]
|
|
587
|
+
all_keys = [self.twBehaviors.item(row, cfg.behavioursFields["key"]).text() for row in range(self.twBehaviors.rowCount())]
|
|
455
588
|
except Exception:
|
|
456
589
|
pass
|
|
457
590
|
if all_keys == [x.lower() for x in all_keys]:
|
|
458
591
|
QMessageBox.information(self, cfg.programName, "All keys are already lower case")
|
|
459
592
|
return
|
|
460
593
|
|
|
461
|
-
if (
|
|
462
|
-
dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
|
|
463
|
-
== cfg.CANCEL
|
|
464
|
-
):
|
|
594
|
+
if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
|
|
465
595
|
return
|
|
466
596
|
|
|
467
597
|
if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
|
|
@@ -482,11 +612,10 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
482
612
|
)
|
|
483
613
|
|
|
484
614
|
# convert modifier shortcuts
|
|
485
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
486
|
-
|
|
615
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text():
|
|
487
616
|
modifiers_dict = (
|
|
488
|
-
|
|
489
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
617
|
+
json.loads(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
|
|
618
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text()
|
|
490
619
|
else {}
|
|
491
620
|
)
|
|
492
621
|
|
|
@@ -495,16 +624,12 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
495
624
|
for idx2, value in enumerate(modifiers_dict[modifier_set]["values"]):
|
|
496
625
|
if re.findall(r"\((\w+)\)", value):
|
|
497
626
|
modifiers_dict[modifier_set]["values"][idx2] = (
|
|
498
|
-
value.split("(")[0]
|
|
499
|
-
+ "("
|
|
500
|
-
+ re.findall(r"\((\w+)\)", value)[0].lower()
|
|
501
|
-
+ ")"
|
|
502
|
-
+ value.split(")")[-1]
|
|
627
|
+
value.split("(")[0] + "(" + re.findall(r"\((\w+)\)", value)[0].lower() + ")" + value.split(")")[-1]
|
|
503
628
|
)
|
|
504
629
|
except Exception:
|
|
505
630
|
logging.warning("error during conversion of modifier short cut to lower case")
|
|
506
631
|
|
|
507
|
-
self.twBehaviors.item(row, cfg.behavioursFields[
|
|
632
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(json.dumps(modifiers_dict))
|
|
508
633
|
|
|
509
634
|
def convert_subjects_keys_to_lower_case(self):
|
|
510
635
|
"""
|
|
@@ -512,20 +637,14 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
512
637
|
"""
|
|
513
638
|
# check if some keys will be duplicated after conversion
|
|
514
639
|
try:
|
|
515
|
-
all_keys = [
|
|
516
|
-
self.twSubjects.item(row, cfg.subjectsFields.index("key")).text()
|
|
517
|
-
for row in range(self.twSubjects.rowCount())
|
|
518
|
-
]
|
|
640
|
+
all_keys = [self.twSubjects.item(row, cfg.subjectsFields.index("key")).text() for row in range(self.twSubjects.rowCount())]
|
|
519
641
|
except Exception:
|
|
520
642
|
pass
|
|
521
643
|
if all_keys == [x.lower() for x in all_keys]:
|
|
522
644
|
QMessageBox.information(self, cfg.programName, "All keys are already lower case")
|
|
523
645
|
return
|
|
524
646
|
|
|
525
|
-
if (
|
|
526
|
-
dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL])
|
|
527
|
-
== cfg.CANCEL
|
|
528
|
-
):
|
|
647
|
+
if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
|
|
529
648
|
return
|
|
530
649
|
|
|
531
650
|
if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
|
|
@@ -550,47 +669,45 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
550
669
|
Add a behaviors coding map from file
|
|
551
670
|
"""
|
|
552
671
|
|
|
553
|
-
|
|
672
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
554
673
|
self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
555
674
|
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
675
|
+
if not file_name:
|
|
676
|
+
return
|
|
677
|
+
try:
|
|
678
|
+
bcm = json.loads(open(file_name, "r").read())
|
|
679
|
+
except Exception:
|
|
680
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a behaviors coding map.")
|
|
681
|
+
return
|
|
563
682
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map."
|
|
567
|
-
)
|
|
683
|
+
if "coding_map_type" not in bcm or bcm["coding_map_type"] != "BORIS behaviors coding map":
|
|
684
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} is not a BORIS behaviors coding map.")
|
|
568
685
|
|
|
569
|
-
|
|
570
|
-
|
|
686
|
+
if cfg.BEHAVIORS_CODING_MAP not in self.pj:
|
|
687
|
+
self.pj[cfg.BEHAVIORS_CODING_MAP] = []
|
|
571
688
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
689
|
+
bcm_code_not_found = []
|
|
690
|
+
existing_codes = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
|
|
691
|
+
for code in [bcm["areas"][key][cfg.BEHAVIOR_CODE] for key in bcm["areas"]]:
|
|
692
|
+
if code not in existing_codes:
|
|
693
|
+
bcm_code_not_found.append(code)
|
|
577
694
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
695
|
+
if bcm_code_not_found:
|
|
696
|
+
QMessageBox.warning(
|
|
697
|
+
self,
|
|
698
|
+
cfg.programName,
|
|
699
|
+
("The following behavior{} are not defined in the ethogram:<br>{}").format(
|
|
700
|
+
"s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
|
|
701
|
+
),
|
|
702
|
+
)
|
|
586
703
|
|
|
587
|
-
|
|
704
|
+
self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
|
|
588
705
|
|
|
589
|
-
|
|
706
|
+
self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
|
|
590
707
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
708
|
+
self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 0, QTableWidgetItem(bcm["name"]))
|
|
709
|
+
codes = ", ".join([bcm["areas"][idx][cfg.BEHAVIOR_CODE] for idx in bcm["areas"]])
|
|
710
|
+
self.twBehavCodingMap.setItem(self.twBehavCodingMap.rowCount() - 1, 1, QTableWidgetItem(codes))
|
|
594
711
|
|
|
595
712
|
def remove_behaviors_coding_map(self):
|
|
596
713
|
"""
|
|
@@ -599,27 +716,28 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
599
716
|
if not self.twBehavCodingMap.selectedIndexes():
|
|
600
717
|
QMessageBox.warning(self, cfg.programName, "Select a behaviors coding map")
|
|
601
718
|
else:
|
|
602
|
-
if (
|
|
603
|
-
dialog.MessageDialog(
|
|
604
|
-
cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]
|
|
605
|
-
)
|
|
606
|
-
== cfg.YES
|
|
607
|
-
):
|
|
719
|
+
if dialog.MessageDialog(cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
608
720
|
del self.pj[cfg.BEHAVIORS_CODING_MAP][self.twBehavCodingMap.selectedIndexes()[0].row()]
|
|
609
721
|
self.twBehavCodingMap.removeRow(self.twBehavCodingMap.selectedIndexes()[0].row())
|
|
610
722
|
|
|
611
723
|
def leLabel_changed(self):
|
|
612
|
-
|
|
724
|
+
"""
|
|
725
|
+
independent variable label changed
|
|
726
|
+
"""
|
|
613
727
|
if self.selected_twvariables_row != -1:
|
|
614
728
|
self.twVariables.item(self.selected_twvariables_row, 0).setText(self.leLabel.text())
|
|
615
729
|
|
|
616
730
|
def leDescription_changed(self):
|
|
617
|
-
|
|
731
|
+
"""
|
|
732
|
+
independent variable description changed
|
|
733
|
+
"""
|
|
618
734
|
if self.selected_twvariables_row != -1:
|
|
619
735
|
self.twVariables.item(self.selected_twvariables_row, 1).setText(self.leDescription.text())
|
|
620
736
|
|
|
621
737
|
def lePredefined_changed(self):
|
|
622
|
-
|
|
738
|
+
"""
|
|
739
|
+
independent variable predefined value changed
|
|
740
|
+
"""
|
|
623
741
|
if self.selected_twvariables_row != -1:
|
|
624
742
|
self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
|
|
625
743
|
if not self.lePredefined.hasFocus():
|
|
@@ -628,15 +746,19 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
628
746
|
QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
|
|
629
747
|
|
|
630
748
|
def leSetValues_changed(self):
|
|
631
|
-
|
|
749
|
+
"""
|
|
750
|
+
independent variable available values changed
|
|
751
|
+
"""
|
|
632
752
|
if self.selected_twvariables_row != -1:
|
|
633
753
|
self.twVariables.item(self.selected_twvariables_row, 4).setText(self.leSetValues.text())
|
|
634
754
|
|
|
635
755
|
def dte_default_date_changed(self):
|
|
636
|
-
|
|
756
|
+
"""
|
|
757
|
+
independent variable default timestamp changed
|
|
758
|
+
"""
|
|
637
759
|
if self.selected_twvariables_row != -1:
|
|
638
760
|
self.twVariables.item(self.selected_twvariables_row, 3).setText(
|
|
639
|
-
self.dte_default_date.dateTime().toString(
|
|
761
|
+
self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
|
|
640
762
|
)
|
|
641
763
|
|
|
642
764
|
def pbBehaviorsCategories_clicked(self):
|
|
@@ -648,8 +770,13 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
648
770
|
|
|
649
771
|
if bc.exec_():
|
|
650
772
|
self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
|
|
651
|
-
|
|
652
|
-
|
|
773
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
|
|
774
|
+
for index in range(bc.lw.rowCount()):
|
|
775
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES].append(bc.lw.item(index, 0).text().strip())
|
|
776
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][str(index)] = {
|
|
777
|
+
"name": bc.lw.item(index, 0).text().strip(),
|
|
778
|
+
cfg.COLOR: bc.lw.item(index, 1).text(),
|
|
779
|
+
}
|
|
653
780
|
|
|
654
781
|
# sort
|
|
655
782
|
self.pj[cfg.BEHAVIORAL_CATEGORIES] = sorted(self.pj[cfg.BEHAVIORAL_CATEGORIES])
|
|
@@ -657,14 +784,15 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
657
784
|
# check if behavior belong to removed category
|
|
658
785
|
if bc.removed:
|
|
659
786
|
for row in range(self.twBehaviors.rowCount()):
|
|
660
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
661
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
787
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]):
|
|
788
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() == bc.removed:
|
|
662
789
|
if (
|
|
663
790
|
dialog.MessageDialog(
|
|
664
791
|
cfg.programName,
|
|
665
792
|
(
|
|
666
793
|
f"The <b>{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()}</b> behavior belongs "
|
|
667
|
-
|
|
794
|
+
"to a behavioral category "
|
|
795
|
+
f"<b>{self.twBehaviors.item(row, cfg.behavioursFields['category']).text()}</b> "
|
|
668
796
|
"that is no more in the behavioral categories list.<br><br>"
|
|
669
797
|
"Remove the behavior from category?"
|
|
670
798
|
),
|
|
@@ -672,16 +800,17 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
672
800
|
)
|
|
673
801
|
== cfg.YES
|
|
674
802
|
):
|
|
675
|
-
self.twBehaviors.item(row, cfg.behavioursFields[
|
|
803
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText("")
|
|
676
804
|
if bc.renamed:
|
|
677
805
|
for row in range(self.twBehaviors.rowCount()):
|
|
678
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
679
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
680
|
-
self.twBehaviors.item(row, cfg.behavioursFields[
|
|
806
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]):
|
|
807
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() == bc.renamed[0]:
|
|
808
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(bc.renamed[1])
|
|
681
809
|
|
|
682
|
-
def twBehaviors_cellDoubleClicked(self, row, column):
|
|
810
|
+
def twBehaviors_cellDoubleClicked(self, row: int, column: int) -> None:
|
|
683
811
|
"""
|
|
684
812
|
manage double-click on ethogram table:
|
|
813
|
+
* color
|
|
685
814
|
* behavioral category
|
|
686
815
|
* modifiers
|
|
687
816
|
* exclusion
|
|
@@ -692,51 +821,53 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
692
821
|
column (int): column double-clicked
|
|
693
822
|
"""
|
|
694
823
|
|
|
695
|
-
#
|
|
696
|
-
if column == cfg.behavioursFields[
|
|
824
|
+
# excluded column
|
|
825
|
+
if column == cfg.behavioursFields[cfg.EXCLUDED]:
|
|
697
826
|
self.exclusion_matrix()
|
|
698
827
|
|
|
699
|
-
#
|
|
700
|
-
if column == cfg.behavioursFields[
|
|
828
|
+
# coding map
|
|
829
|
+
if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
|
|
701
830
|
if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
702
|
-
self.
|
|
831
|
+
self.behavior_type_changed(row)
|
|
703
832
|
else:
|
|
704
|
-
QMessageBox.information(
|
|
705
|
-
self, cfg.programName, "Change the behavior type on first column to select a coding map"
|
|
706
|
-
)
|
|
833
|
+
QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
|
|
707
834
|
|
|
708
|
-
#
|
|
835
|
+
# behavior type
|
|
709
836
|
if column == cfg.behavioursFields["type"]:
|
|
710
837
|
self.behavior_type_doubleclicked(row)
|
|
711
838
|
|
|
839
|
+
# color
|
|
840
|
+
if column == cfg.behavioursFields[cfg.COLOR]:
|
|
841
|
+
self.color_doubleclicked(row)
|
|
842
|
+
|
|
712
843
|
# behavioral category
|
|
713
|
-
if column == cfg.behavioursFields[
|
|
844
|
+
if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
|
|
714
845
|
self.category_doubleclicked(row)
|
|
715
846
|
|
|
716
|
-
|
|
847
|
+
# modifiers
|
|
848
|
+
if column == cfg.behavioursFields[cfg.MODIFIERS]:
|
|
717
849
|
# check if behavior has coding map
|
|
718
850
|
if (
|
|
719
|
-
self.twBehaviors.item(row, cfg.behavioursFields[
|
|
720
|
-
and self.twBehaviors.item(row, cfg.behavioursFields[
|
|
851
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.CODING_MAP_sp]) is not None
|
|
852
|
+
and self.twBehaviors.item(row, cfg.behavioursFields[cfg.CODING_MAP_sp]).text()
|
|
721
853
|
):
|
|
722
854
|
QMessageBox.warning(self, cfg.programName, "Use the coding map to set/modify the areas")
|
|
723
855
|
else:
|
|
724
856
|
subjects_list = []
|
|
725
857
|
for subject_row in range(self.twSubjects.rowCount()):
|
|
726
858
|
key = self.twSubjects.item(subject_row, 0).text() if self.twSubjects.item(subject_row, 0) else ""
|
|
727
|
-
subjectName = (
|
|
728
|
-
self.twSubjects.item(subject_row, 1).text().strip()
|
|
729
|
-
if self.twSubjects.item(subject_row, 1)
|
|
730
|
-
else ""
|
|
731
|
-
)
|
|
859
|
+
subjectName = self.twSubjects.item(subject_row, 1).text().strip() if self.twSubjects.item(subject_row, 1) else ""
|
|
732
860
|
subjects_list.append((subjectName, key))
|
|
733
861
|
|
|
734
862
|
addModifierWindow = add_modifier.addModifierDialog(
|
|
735
|
-
self.twBehaviors.item(row, column).text(),
|
|
863
|
+
self.twBehaviors.item(row, column).text(),
|
|
864
|
+
subjects=subjects_list,
|
|
865
|
+
ask_at_stop_enabled=self.twBehaviors.item(row, cfg.behavioursFields["type"]).text() == cfg.STATE_EVENT,
|
|
736
866
|
)
|
|
737
867
|
addModifierWindow.setWindowTitle(f'Set modifiers for "{self.twBehaviors.item(row, 2).text()}" behavior')
|
|
868
|
+
|
|
738
869
|
if addModifierWindow.exec_():
|
|
739
|
-
self.twBehaviors.item(row, column).setText(addModifierWindow.
|
|
870
|
+
self.twBehaviors.item(row, column).setText(addModifierWindow.get_modifiers())
|
|
740
871
|
|
|
741
872
|
def behavior_type_doubleclicked(self, row):
|
|
742
873
|
"""
|
|
@@ -748,14 +879,36 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
748
879
|
else:
|
|
749
880
|
selected = 0
|
|
750
881
|
|
|
751
|
-
new_type, ok = QInputDialog.getItem(
|
|
752
|
-
self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False
|
|
753
|
-
)
|
|
882
|
+
new_type, ok = QInputDialog.getItem(self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False)
|
|
754
883
|
|
|
755
884
|
if ok and new_type:
|
|
756
885
|
self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText(new_type)
|
|
757
886
|
|
|
758
|
-
self.
|
|
887
|
+
self.behavior_type_changed(row)
|
|
888
|
+
|
|
889
|
+
def color_doubleclicked(self, row: int) -> None:
|
|
890
|
+
"""
|
|
891
|
+
select a color for behavior
|
|
892
|
+
Selecting black delete the color
|
|
893
|
+
"""
|
|
894
|
+
|
|
895
|
+
col_diag = QColorDialog()
|
|
896
|
+
col_diag.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
|
|
897
|
+
|
|
898
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
|
|
899
|
+
current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
|
|
900
|
+
if current_color.isValid():
|
|
901
|
+
print(f"{current_color=}")
|
|
902
|
+
col_diag.setCurrentColor(current_color)
|
|
903
|
+
|
|
904
|
+
if col_diag.exec():
|
|
905
|
+
color = col_diag.currentColor()
|
|
906
|
+
if color.name() == "#000000": # black -> delete color
|
|
907
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
|
|
908
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
|
|
909
|
+
elif color.isValid():
|
|
910
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
|
|
911
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
|
|
759
912
|
|
|
760
913
|
def category_doubleclicked(self, row):
|
|
761
914
|
"""
|
|
@@ -764,19 +917,17 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
764
917
|
|
|
765
918
|
categories = ["None"] + self.pj[cfg.BEHAVIORAL_CATEGORIES] if cfg.BEHAVIORAL_CATEGORIES in self.pj else ["None"]
|
|
766
919
|
|
|
767
|
-
if self.twBehaviors.item(row, cfg.behavioursFields[
|
|
768
|
-
selected = categories.index(self.twBehaviors.item(row, cfg.behavioursFields[
|
|
920
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text() in categories:
|
|
921
|
+
selected = categories.index(self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).text())
|
|
769
922
|
else:
|
|
770
923
|
selected = 0
|
|
771
924
|
|
|
772
|
-
category, ok = QInputDialog.getItem(
|
|
773
|
-
self, "Select a behavioral category", "Behavioral categories", categories, selected, False
|
|
774
|
-
)
|
|
925
|
+
category, ok = QInputDialog.getItem(self, "Select a behavioral category", "Behavioral categories", categories, selected, False)
|
|
775
926
|
|
|
776
927
|
if ok and category:
|
|
777
928
|
if category == "None":
|
|
778
929
|
category = ""
|
|
779
|
-
self.twBehaviors.item(row, cfg.behavioursFields[
|
|
930
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(category)
|
|
780
931
|
|
|
781
932
|
def check_variable_default_value(self, txt, varType):
|
|
782
933
|
"""
|
|
@@ -800,22 +951,15 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
800
951
|
|
|
801
952
|
if self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentText() == cfg.SET_OF_VALUES:
|
|
802
953
|
if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() == "NA":
|
|
803
|
-
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText(
|
|
804
|
-
"Double-click to add values"
|
|
805
|
-
)
|
|
954
|
+
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("Double-click to add values")
|
|
806
955
|
else:
|
|
807
956
|
# check if set of values defined
|
|
808
957
|
if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() not in [
|
|
809
958
|
"NA",
|
|
810
959
|
"Double-click to add values",
|
|
811
960
|
]:
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
== cfg.CANCEL
|
|
815
|
-
):
|
|
816
|
-
self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).setCurrentIndex(
|
|
817
|
-
cfg.SET_OF_VALUES_idx
|
|
818
|
-
)
|
|
961
|
+
if dialog.MessageDialog(cfg.programName, "Erase the set of values?", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
|
|
962
|
+
self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).setCurrentIndex(cfg.SET_OF_VALUES_idx)
|
|
819
963
|
return
|
|
820
964
|
else:
|
|
821
965
|
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
|
|
@@ -844,7 +988,6 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
844
988
|
|
|
845
989
|
existing_var = []
|
|
846
990
|
for r in range(self.twVariables.rowCount()):
|
|
847
|
-
|
|
848
991
|
if self.twVariables.item(r, 0).text().strip().upper() in existing_var:
|
|
849
992
|
return (
|
|
850
993
|
False,
|
|
@@ -882,7 +1025,6 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
882
1025
|
return True, "OK"
|
|
883
1026
|
|
|
884
1027
|
def cbtype_changed(self):
|
|
885
|
-
|
|
886
1028
|
self.leSetValues.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
|
|
887
1029
|
self.label_5.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
|
|
888
1030
|
|
|
@@ -892,10 +1034,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
892
1034
|
self.label_4.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
|
|
893
1035
|
|
|
894
1036
|
def cbtype_activated(self):
|
|
895
|
-
|
|
896
1037
|
if self.cbType.currentText() == cfg.TIMESTAMP:
|
|
897
1038
|
self.twVariables.item(self.selected_twvariables_row, 3).setText(
|
|
898
|
-
self.dte_default_date.dateTime().toString(
|
|
1039
|
+
self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
|
|
899
1040
|
)
|
|
900
1041
|
self.twVariables.item(self.selected_twvariables_row, 4).setText("")
|
|
901
1042
|
else:
|
|
@@ -975,7 +1116,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
975
1116
|
return
|
|
976
1117
|
|
|
977
1118
|
for row in range(self.twBehaviors.rowCount()):
|
|
978
|
-
if not self.twBehaviors.item(row, cfg.behavioursFields[
|
|
1119
|
+
if not self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
|
|
979
1120
|
QMessageBox.critical(
|
|
980
1121
|
None,
|
|
981
1122
|
cfg.programName,
|
|
@@ -1005,12 +1146,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1005
1146
|
)
|
|
1006
1147
|
|
|
1007
1148
|
for r in range(self.twBehaviors.rowCount()):
|
|
1008
|
-
|
|
1009
1149
|
if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
|
|
1010
|
-
|
|
1011
1150
|
if include_point_events == cfg.YES or (
|
|
1012
|
-
include_point_events == cfg.NO
|
|
1013
|
-
and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
|
|
1151
|
+
include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
|
|
1014
1152
|
):
|
|
1015
1153
|
allBehaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1016
1154
|
|
|
@@ -1065,9 +1203,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1065
1203
|
|
|
1066
1204
|
if c_name != r_name:
|
|
1067
1205
|
ex.checkboxes[f"{r_name}|{c_name}"] = QCheckBox()
|
|
1068
|
-
ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet(
|
|
1069
|
-
"text-align: center; margin-left:50%; margin-right:50%;"
|
|
1070
|
-
)
|
|
1206
|
+
ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet("text-align: center; margin-left:50%; margin-right:50%;")
|
|
1071
1207
|
|
|
1072
1208
|
if flag_left_bottom:
|
|
1073
1209
|
# hide if cell in left-bottom part of table
|
|
@@ -1095,18 +1231,15 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1095
1231
|
|
|
1096
1232
|
# update excluded field
|
|
1097
1233
|
for r in range(self.twBehaviors.rowCount()):
|
|
1098
|
-
if include_point_events == cfg.YES or (
|
|
1099
|
-
include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()
|
|
1100
|
-
):
|
|
1234
|
+
if include_point_events == cfg.YES or (include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()):
|
|
1101
1235
|
for e in excl:
|
|
1102
1236
|
if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
|
|
1103
1237
|
item = QTableWidgetItem(",".join(new_excl[e]))
|
|
1104
1238
|
item.setFlags(Qt.ItemIsEnabled)
|
|
1105
|
-
item.setBackground(
|
|
1239
|
+
item.setBackground(self.not_editable_column_color())
|
|
1106
1240
|
self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
|
|
1107
1241
|
|
|
1108
1242
|
def remove_all_behaviors(self):
|
|
1109
|
-
|
|
1110
1243
|
if not self.twBehaviors.rowCount():
|
|
1111
1244
|
QMessageBox.critical(
|
|
1112
1245
|
None,
|
|
@@ -1162,7 +1295,6 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1162
1295
|
self.lbObservationsState.setText("")
|
|
1163
1296
|
|
|
1164
1297
|
for r in range(self.twBehaviors.rowCount()):
|
|
1165
|
-
|
|
1166
1298
|
# check key
|
|
1167
1299
|
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX):
|
|
1168
1300
|
key = self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX).text()
|
|
@@ -1205,9 +1337,16 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1205
1337
|
for field in cfg.behavioursFields:
|
|
1206
1338
|
item = QTableWidgetItem(self.twBehaviors.item(row, cfg.behavioursFields[field]))
|
|
1207
1339
|
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
|
|
1208
|
-
if field in
|
|
1340
|
+
if field in (cfg.TYPE, "category", "excluded", "coding map", "modifiers"):
|
|
1341
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
1342
|
+
item.setBackground(self.not_editable_column_color())
|
|
1343
|
+
if field == cfg.COLOR:
|
|
1209
1344
|
item.setFlags(Qt.ItemIsEnabled)
|
|
1210
|
-
item
|
|
1345
|
+
if QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()).isValid():
|
|
1346
|
+
item.setBackground(QColor(self.twBehaviors.item(row, cfg.behavioursFields[field]).text()))
|
|
1347
|
+
else:
|
|
1348
|
+
item.setBackground(self.not_editable_column_color())
|
|
1349
|
+
|
|
1211
1350
|
self.twBehaviors.scrollToBottom()
|
|
1212
1351
|
|
|
1213
1352
|
def remove_behavior(self):
|
|
@@ -1227,27 +1366,22 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1227
1366
|
|
|
1228
1367
|
if not self.twBehaviors.selectedIndexes():
|
|
1229
1368
|
QMessageBox.warning(self, cfg.programName, "Select a behaviour to be removed")
|
|
1230
|
-
|
|
1231
|
-
if dialog.MessageDialog(cfg.programName, "Remove the selected behavior?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
1232
|
-
|
|
1233
|
-
# check if behavior already used in observations
|
|
1234
|
-
flag_break = False
|
|
1235
|
-
codeToDelete = self.twBehaviors.item(self.twBehaviors.selectedIndexes()[0].row(), 2).text()
|
|
1236
|
-
for obs_id in self.pj[cfg.OBSERVATIONS]:
|
|
1237
|
-
if codeToDelete in [
|
|
1238
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
1239
|
-
]:
|
|
1240
|
-
if (
|
|
1241
|
-
dialog.MessageDialog(
|
|
1242
|
-
cfg.programName, "The code to remove is used in observations!", [cfg.REMOVE, cfg.CANCEL]
|
|
1243
|
-
)
|
|
1244
|
-
== cfg.CANCEL
|
|
1245
|
-
):
|
|
1246
|
-
return
|
|
1247
|
-
break
|
|
1369
|
+
return
|
|
1248
1370
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1371
|
+
if dialog.MessageDialog(cfg.programName, "Remove the selected behavior?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
1372
|
+
# check if behavior already used in observations
|
|
1373
|
+
codeToDelete = self.twBehaviors.item(self.twBehaviors.selectedIndexes()[0].row(), 2).text()
|
|
1374
|
+
for obs_id in self.pj[cfg.OBSERVATIONS]:
|
|
1375
|
+
if codeToDelete in [event[cfg.EVENT_BEHAVIOR_FIELD_IDX] for event in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]]:
|
|
1376
|
+
if (
|
|
1377
|
+
dialog.MessageDialog(cfg.programName, "The code to remove is used in observations!", [cfg.REMOVE, cfg.CANCEL])
|
|
1378
|
+
== cfg.CANCEL
|
|
1379
|
+
):
|
|
1380
|
+
return
|
|
1381
|
+
break
|
|
1382
|
+
|
|
1383
|
+
self.twBehaviors.removeRow(self.twBehaviors.selectedIndexes()[0].row())
|
|
1384
|
+
self.twBehaviors_cellChanged(0, 0)
|
|
1251
1385
|
|
|
1252
1386
|
def add_behavior(self):
|
|
1253
1387
|
"""
|
|
@@ -1261,38 +1395,36 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1261
1395
|
if field_type == cfg.TYPE:
|
|
1262
1396
|
item.setText("Point event")
|
|
1263
1397
|
# no manual editing, gray back ground
|
|
1264
|
-
if field_type in
|
|
1398
|
+
if field_type in (cfg.TYPE, cfg.COLOR, "category", cfg.MODIFIERS, "modifiers", "excluded", "coding map"):
|
|
1265
1399
|
item.setFlags(Qt.ItemIsEnabled)
|
|
1266
|
-
item.setBackground(QColor(230, 230, 230))
|
|
1400
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
1401
|
+
item.setBackground(self.not_editable_column_color())
|
|
1267
1402
|
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
|
|
1268
1403
|
self.twBehaviors.scrollToBottom()
|
|
1269
1404
|
|
|
1270
|
-
def
|
|
1405
|
+
def behavior_type_changed(self, row: int) -> None:
|
|
1271
1406
|
"""
|
|
1272
1407
|
event type combobox changed
|
|
1273
1408
|
"""
|
|
1274
1409
|
|
|
1275
|
-
if
|
|
1410
|
+
if cfg.CODING_MAP_sp in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
1276
1411
|
# let user select a coding maop
|
|
1277
|
-
|
|
1412
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
1278
1413
|
self,
|
|
1279
|
-
"Select a coding map for "
|
|
1280
|
-
f"{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
|
|
1414
|
+
f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
|
|
1281
1415
|
"",
|
|
1282
1416
|
"BORIS map files (*.boris_map);;All files (*)",
|
|
1283
1417
|
)
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if fileName:
|
|
1418
|
+
if file_name:
|
|
1287
1419
|
try:
|
|
1288
|
-
new_map = json.loads(open(
|
|
1420
|
+
new_map = json.loads(open(file_name, "r").read())
|
|
1289
1421
|
except Exception:
|
|
1290
|
-
QMessageBox.critical(self, cfg.programName, "Error reding the
|
|
1422
|
+
QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
|
|
1291
1423
|
return
|
|
1292
1424
|
self.pj[cfg.CODING_MAP][new_map["name"]] = new_map
|
|
1293
1425
|
|
|
1294
1426
|
# add modifiers from coding map areas
|
|
1295
|
-
modifstr =
|
|
1427
|
+
modifstr = json.dumps(
|
|
1296
1428
|
{
|
|
1297
1429
|
"0": {
|
|
1298
1430
|
"name": new_map["name"],
|
|
@@ -1308,9 +1440,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1308
1440
|
else:
|
|
1309
1441
|
# if coding map already exists do not reset the behavior type if no filename selected
|
|
1310
1442
|
if not self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text():
|
|
1311
|
-
QMessageBox.critical(
|
|
1312
|
-
self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" '
|
|
1313
|
-
)
|
|
1443
|
+
QMessageBox.critical(self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" ')
|
|
1314
1444
|
self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText("Point event")
|
|
1315
1445
|
else:
|
|
1316
1446
|
self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText("")
|
|
@@ -1336,12 +1466,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1336
1466
|
QMessageBox.warning(self, cfg.programName, "Select a subject to remove")
|
|
1337
1467
|
else:
|
|
1338
1468
|
if dialog.MessageDialog(cfg.programName, "Remove the selected subject?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
1339
|
-
|
|
1340
1469
|
flagDel = False
|
|
1341
1470
|
if self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1):
|
|
1342
|
-
subjectToDelete = self.twSubjects.item(
|
|
1343
|
-
self.twSubjects.selectedIndexes()[0].row(), 1
|
|
1344
|
-
).text() # 1: subject name
|
|
1471
|
+
subjectToDelete = self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1).text() # 1: subject name
|
|
1345
1472
|
|
|
1346
1473
|
subjectsInObs = []
|
|
1347
1474
|
for obs in self.pj[cfg.OBSERVATIONS]:
|
|
@@ -1421,19 +1548,18 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1421
1548
|
|
|
1422
1549
|
self.twSubjects_cellChanged(0, 0)
|
|
1423
1550
|
|
|
1424
|
-
def twSubjects_cellChanged(self, row: int, column: int):
|
|
1551
|
+
def twSubjects_cellChanged(self, row: int, column: int) -> None:
|
|
1425
1552
|
"""
|
|
1426
1553
|
check if subject not unique
|
|
1427
1554
|
"""
|
|
1428
1555
|
|
|
1429
|
-
subjects
|
|
1556
|
+
subjects: list = []
|
|
1557
|
+
"""keys: list = []"""
|
|
1430
1558
|
self.lbSubjectsState.setText("")
|
|
1431
1559
|
|
|
1432
1560
|
for r in range(self.twSubjects.rowCount()):
|
|
1433
|
-
|
|
1434
1561
|
# check key
|
|
1435
1562
|
if self.twSubjects.item(r, 0):
|
|
1436
|
-
|
|
1437
1563
|
# check key length
|
|
1438
1564
|
if (
|
|
1439
1565
|
self.twSubjects.item(r, 0).text().upper() not in list(cfg.function_keys.values())
|
|
@@ -1448,11 +1574,14 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1448
1574
|
)
|
|
1449
1575
|
return
|
|
1450
1576
|
|
|
1577
|
+
# control of duplicated key removed 2024-01-29
|
|
1578
|
+
"""
|
|
1451
1579
|
if self.twSubjects.item(r, 0).text() in keys:
|
|
1452
1580
|
self.lbSubjectsState.setText(f'<font color="red">Key duplicated at row # {r + 1}</font>')
|
|
1453
1581
|
else:
|
|
1454
1582
|
if self.twSubjects.item(r, 0).text():
|
|
1455
1583
|
keys.append(self.twSubjects.item(r, 0).text())
|
|
1584
|
+
"""
|
|
1456
1585
|
|
|
1457
1586
|
# check subject
|
|
1458
1587
|
if self.twSubjects.item(r, 1):
|
|
@@ -1471,14 +1600,14 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1471
1600
|
logging.debug(f"selected row: {self.selected_twvariables_row}")
|
|
1472
1601
|
|
|
1473
1602
|
if self.selected_twvariables_row == -1:
|
|
1474
|
-
for widget in
|
|
1603
|
+
for widget in (
|
|
1475
1604
|
self.leLabel,
|
|
1476
1605
|
self.leDescription,
|
|
1477
1606
|
self.cbType,
|
|
1478
1607
|
self.lePredefined,
|
|
1479
1608
|
self.dte_default_date,
|
|
1480
1609
|
self.leSetValues,
|
|
1481
|
-
|
|
1610
|
+
):
|
|
1482
1611
|
widget.setEnabled(False)
|
|
1483
1612
|
self.leLabel.setText("")
|
|
1484
1613
|
self.leDescription.setText("")
|
|
@@ -1489,20 +1618,27 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1489
1618
|
return
|
|
1490
1619
|
|
|
1491
1620
|
# enable widget for indep var setting
|
|
1492
|
-
for widget in
|
|
1621
|
+
for widget in (
|
|
1493
1622
|
self.leLabel,
|
|
1494
1623
|
self.leDescription,
|
|
1495
1624
|
self.cbType,
|
|
1496
1625
|
self.lePredefined,
|
|
1497
1626
|
self.dte_default_date,
|
|
1498
1627
|
self.leSetValues,
|
|
1499
|
-
|
|
1628
|
+
):
|
|
1500
1629
|
widget.setEnabled(True)
|
|
1501
1630
|
|
|
1502
1631
|
self.leLabel.setText(self.twVariables.item(row, 0).text())
|
|
1503
1632
|
self.leDescription.setText(self.twVariables.item(row, 1).text())
|
|
1504
1633
|
self.lePredefined.setText(self.twVariables.item(row, 3).text())
|
|
1505
1634
|
self.leSetValues.setText(self.twVariables.item(row, 4).text())
|
|
1635
|
+
if self.twVariables.item(row, 2).text() == cfg.TIMESTAMP:
|
|
1636
|
+
if len(self.twVariables.item(row, 3).text()) == len("yyyy-MM-ddTHH:mm:ss.zzz"):
|
|
1637
|
+
datetime_format = "yyyy-MM-ddThh:mm:ss.zzz"
|
|
1638
|
+
if len(self.twVariables.item(row, 3).text()) == len("yyyy-MM-ddTHH:mm:ss"):
|
|
1639
|
+
datetime_format = "yyyy-MM-ddThh:mm:ss"
|
|
1640
|
+
|
|
1641
|
+
self.dte_default_date.setDateTime(QDateTime.fromString(self.twVariables.item(row, 3).text(), datetime_format))
|
|
1506
1642
|
|
|
1507
1643
|
self.cbType.clear()
|
|
1508
1644
|
self.cbType.addItems(cfg.AVAILABLE_INDEP_VAR_TYPES)
|
|
@@ -1512,12 +1648,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1512
1648
|
|
|
1513
1649
|
def pbCancel_clicked(self):
|
|
1514
1650
|
if self.flag_modified:
|
|
1515
|
-
if (
|
|
1516
|
-
dialog.MessageDialog(
|
|
1517
|
-
"BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]
|
|
1518
|
-
)
|
|
1519
|
-
== cfg.OK
|
|
1520
|
-
):
|
|
1651
|
+
if dialog.MessageDialog("BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]) == cfg.OK:
|
|
1521
1652
|
self.reject()
|
|
1522
1653
|
else:
|
|
1523
1654
|
self.reject()
|
|
@@ -1525,12 +1656,12 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1525
1656
|
def check_ethogram(self) -> dict:
|
|
1526
1657
|
"""
|
|
1527
1658
|
check ethogram for various parameter
|
|
1528
|
-
returns ethogram dict or {cfg.CANCEL: True
|
|
1659
|
+
returns ethogram dict or {cfg.CANCEL: True} in case of error
|
|
1529
1660
|
|
|
1530
1661
|
"""
|
|
1531
1662
|
# store behaviors
|
|
1532
|
-
missing_data = []
|
|
1533
|
-
checked_ethogram = {}
|
|
1663
|
+
missing_data: list = []
|
|
1664
|
+
checked_ethogram: dict = {}
|
|
1534
1665
|
|
|
1535
1666
|
# Ethogram
|
|
1536
1667
|
# coding maps in ethogram
|
|
@@ -1538,16 +1669,19 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1538
1669
|
# check for leading/trailing space in behaviors and modifiers
|
|
1539
1670
|
code_with_leading_trailing_spaces, modifiers_with_leading_trailing_spaces = [], []
|
|
1540
1671
|
for r in range(self.twBehaviors.rowCount()):
|
|
1541
|
-
|
|
1542
1672
|
if (
|
|
1543
|
-
self.twBehaviors.item(r, cfg.behavioursFields[
|
|
1544
|
-
!= self.twBehaviors.item(r, cfg.behavioursFields[
|
|
1673
|
+
self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
|
|
1674
|
+
!= self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text().strip()
|
|
1545
1675
|
):
|
|
1546
|
-
code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[
|
|
1676
|
+
code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1547
1677
|
|
|
1548
1678
|
if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text():
|
|
1549
1679
|
try:
|
|
1550
|
-
modifiers_dict =
|
|
1680
|
+
modifiers_dict = (
|
|
1681
|
+
json.loads(self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text())
|
|
1682
|
+
if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text()
|
|
1683
|
+
else {}
|
|
1684
|
+
)
|
|
1551
1685
|
for k in modifiers_dict:
|
|
1552
1686
|
for value in modifiers_dict[k]["values"]:
|
|
1553
1687
|
modif_code = value.split(" (")[0]
|
|
@@ -1563,7 +1697,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1563
1697
|
(
|
|
1564
1698
|
"<b>Warning!</b> Some leading and/or trailing spaces are present"
|
|
1565
1699
|
" in the following behaviors code(s):<br>"
|
|
1566
|
-
|
|
1700
|
+
"<b>"
|
|
1701
|
+
f"{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '█') for x in code_with_leading_trailing_spaces])}"
|
|
1702
|
+
"</b><br><br>"
|
|
1567
1703
|
"Do you want to remove the leading and trailing spaces (visualized as black boxes) from behaviors?<br><br>"
|
|
1568
1704
|
"""<font color="red"><b>Be careful with this option"""
|
|
1569
1705
|
""" if you have already done observations!</b></font>"""
|
|
@@ -1579,13 +1715,13 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1579
1715
|
cfg.programName,
|
|
1580
1716
|
(
|
|
1581
1717
|
"<b>Warning!</b> Some leading and/or trailing spaces are present"
|
|
1582
|
-
" in the following modifier(s):<br>"
|
|
1583
|
-
f"
|
|
1584
|
-
"Do you want to remove the leading and trailing spaces (visualized as black boxes) from modifiers?<br><br>"
|
|
1718
|
+
" in the following modifier(s):<br><b>"
|
|
1719
|
+
f"{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '█') for x in set(modifiers_with_leading_trailing_spaces)])}"
|
|
1720
|
+
"</b><br><br>Do you want to remove the leading and trailing spaces (visualized as black boxes) from modifiers?<br><br>"
|
|
1585
1721
|
"""<font color="red"><b>Be careful with this option"""
|
|
1586
1722
|
""" if you have already done observations!</b></font>"""
|
|
1587
1723
|
),
|
|
1588
|
-
|
|
1724
|
+
(cfg.YES, cfg.NO, cfg.CANCEL),
|
|
1589
1725
|
)
|
|
1590
1726
|
if remove_leading_trailing_spaces_in_modifiers == cfg.CANCEL:
|
|
1591
1727
|
return {cfg.CANCEL: True}
|
|
@@ -1595,9 +1731,8 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1595
1731
|
row = {}
|
|
1596
1732
|
for field in cfg.behavioursFields:
|
|
1597
1733
|
if self.twBehaviors.item(r, cfg.behavioursFields[field]):
|
|
1598
|
-
|
|
1599
1734
|
# check for | char in code
|
|
1600
|
-
if field ==
|
|
1735
|
+
if field == cfg.BEHAVIOR_CODE and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
|
|
1601
1736
|
QMessageBox.warning(
|
|
1602
1737
|
self,
|
|
1603
1738
|
cfg.programName,
|
|
@@ -1614,10 +1749,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1614
1749
|
row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
|
|
1615
1750
|
|
|
1616
1751
|
if field == "modifiers" and row["modifiers"]:
|
|
1617
|
-
|
|
1618
1752
|
if remove_leading_trailing_spaces_in_modifiers == cfg.YES:
|
|
1619
1753
|
try:
|
|
1620
|
-
modifiers_dict =
|
|
1754
|
+
modifiers_dict = json.loads(row["modifiers"]) if row["modifiers"] else {}
|
|
1621
1755
|
for k in modifiers_dict:
|
|
1622
1756
|
for idx, value in enumerate(modifiers_dict[k]["values"]):
|
|
1623
1757
|
modif_code = value.split(" (")[0]
|
|
@@ -1628,19 +1762,16 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1628
1762
|
|
|
1629
1763
|
row["modifiers"] = dict(modifiers_dict)
|
|
1630
1764
|
except Exception:
|
|
1631
|
-
|
|
1632
1765
|
logging.critical("Error removing leading/trailing spaces in modifiers")
|
|
1633
1766
|
|
|
1634
|
-
QMessageBox.critical(
|
|
1635
|
-
self, cfg.programName, "Error removing leading/trailing spaces in modifiers"
|
|
1636
|
-
)
|
|
1767
|
+
QMessageBox.critical(self, cfg.programName, "Error removing leading/trailing spaces in modifiers")
|
|
1637
1768
|
|
|
1638
1769
|
else:
|
|
1639
|
-
row["modifiers"] =
|
|
1770
|
+
row["modifiers"] = json.loads(row["modifiers"]) if row["modifiers"] else {}
|
|
1640
1771
|
else:
|
|
1641
1772
|
row[field] = ""
|
|
1642
1773
|
|
|
1643
|
-
if (row["type"]) and (row[
|
|
1774
|
+
if (row["type"]) and (row[cfg.BEHAVIOR_CODE]):
|
|
1644
1775
|
checked_ethogram[str(len(checked_ethogram))] = row
|
|
1645
1776
|
else:
|
|
1646
1777
|
missing_data.append(str(r + 1))
|
|
@@ -1662,27 +1793,40 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1662
1793
|
return {cfg.CANCEL: True}
|
|
1663
1794
|
|
|
1664
1795
|
# check if behavior belong to category that is not in categories list
|
|
1665
|
-
|
|
1796
|
+
missing_behavior_category: list = []
|
|
1666
1797
|
for idx in checked_ethogram:
|
|
1667
1798
|
if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
|
|
1668
1799
|
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
|
|
1669
1800
|
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
|
|
1670
|
-
|
|
1801
|
+
missing_behavior_category.append(
|
|
1671
1802
|
(checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
|
|
1672
1803
|
)
|
|
1673
|
-
if
|
|
1674
|
-
|
|
1804
|
+
if missing_behavior_category:
|
|
1675
1805
|
response = dialog.MessageDialog(
|
|
1676
1806
|
f"{cfg.programName} - Behavioral categories",
|
|
1677
1807
|
(
|
|
1678
|
-
"The behavioral
|
|
1679
|
-
f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in
|
|
1680
|
-
"are
|
|
1808
|
+
"The behavioral category/ies<br> "
|
|
1809
|
+
f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)<br>' for x in missing_behavior_category]))} "
|
|
1810
|
+
"are not defined in behavioral categories list.<br>"
|
|
1681
1811
|
),
|
|
1682
|
-
["Add behavioral category/ies",
|
|
1812
|
+
["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
|
|
1683
1813
|
)
|
|
1684
1814
|
if response == "Add behavioral category/ies":
|
|
1685
|
-
|
|
1815
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF not in self.pj:
|
|
1816
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
|
|
1817
|
+
for x1 in set(x[1] for x in missing_behavior_category):
|
|
1818
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1)
|
|
1819
|
+
|
|
1820
|
+
if self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]:
|
|
1821
|
+
index = str(max([int(k) for k in self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]]) + 1)
|
|
1822
|
+
else:
|
|
1823
|
+
index = "0"
|
|
1824
|
+
|
|
1825
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][index] = {
|
|
1826
|
+
"name": x1,
|
|
1827
|
+
cfg.COLOR: "",
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1686
1830
|
if response == cfg.CANCEL:
|
|
1687
1831
|
return {cfg.CANCEL: True}
|
|
1688
1832
|
|
|
@@ -1713,7 +1857,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1713
1857
|
self.pj[cfg.TIME_FORMAT] = cfg.HHMMSS
|
|
1714
1858
|
|
|
1715
1859
|
# store subjects
|
|
1716
|
-
self.subjects_conf = {}
|
|
1860
|
+
self.subjects_conf: dict = {}
|
|
1717
1861
|
|
|
1718
1862
|
# check for leading/trailing spaces in subjects names
|
|
1719
1863
|
subjects_name_with_leading_trailing_spaces = ""
|
|
@@ -1724,7 +1868,6 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1724
1868
|
|
|
1725
1869
|
remove_leading_trailing_spaces = cfg.NO
|
|
1726
1870
|
if subjects_name_with_leading_trailing_spaces:
|
|
1727
|
-
|
|
1728
1871
|
remove_leading_trailing_spaces = dialog.MessageDialog(
|
|
1729
1872
|
cfg.programName,
|
|
1730
1873
|
(
|
|
@@ -1740,10 +1883,9 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1740
1883
|
# check subjects
|
|
1741
1884
|
for row in range(self.twSubjects.rowCount()):
|
|
1742
1885
|
# check key
|
|
1886
|
+
key: str = ""
|
|
1743
1887
|
if self.twSubjects.item(row, 0):
|
|
1744
1888
|
key = self.twSubjects.item(row, 0).text()
|
|
1745
|
-
else:
|
|
1746
|
-
key = ""
|
|
1747
1889
|
|
|
1748
1890
|
# check subject name
|
|
1749
1891
|
if self.twSubjects.item(row, 1):
|
|
@@ -1754,9 +1896,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1754
1896
|
|
|
1755
1897
|
# check if subject name is empty
|
|
1756
1898
|
if subjectName == "":
|
|
1757
|
-
QMessageBox.warning(
|
|
1758
|
-
self, cfg.programName, f"The subject name can not be empty (check row #{row + 1})."
|
|
1759
|
-
)
|
|
1899
|
+
QMessageBox.warning(self, cfg.programName, f"The subject name can not be empty (check row #{row + 1}).")
|
|
1760
1900
|
return
|
|
1761
1901
|
|
|
1762
1902
|
if "|" in subjectName:
|
|
@@ -1767,13 +1907,11 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1767
1907
|
)
|
|
1768
1908
|
return
|
|
1769
1909
|
else:
|
|
1770
|
-
QMessageBox.warning(
|
|
1771
|
-
self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}"
|
|
1772
|
-
)
|
|
1910
|
+
QMessageBox.warning(self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}")
|
|
1773
1911
|
return
|
|
1774
1912
|
|
|
1775
1913
|
# description
|
|
1776
|
-
subjectDescription = ""
|
|
1914
|
+
subjectDescription: str = ""
|
|
1777
1915
|
if self.twSubjects.item(row, 2):
|
|
1778
1916
|
subjectDescription = self.twSubjects.item(row, 2).text().strip()
|
|
1779
1917
|
|
|
@@ -1783,6 +1921,25 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1783
1921
|
"description": subjectDescription,
|
|
1784
1922
|
}
|
|
1785
1923
|
|
|
1924
|
+
# check if coded subjects are defined in the subjects list
|
|
1925
|
+
subjects_list: list = [self.subjects_conf[x]["name"] for x in self.subjects_conf]
|
|
1926
|
+
coded_subjects = set(
|
|
1927
|
+
util.flatten_list([[y[1] for y in self.pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in self.pj[cfg.OBSERVATIONS]])
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
not_defined_subjects: list = []
|
|
1931
|
+
for subject in coded_subjects:
|
|
1932
|
+
if subject and subject not in subjects_list:
|
|
1933
|
+
not_defined_subjects.append(subject)
|
|
1934
|
+
|
|
1935
|
+
if not_defined_subjects:
|
|
1936
|
+
QMessageBox.warning(
|
|
1937
|
+
self,
|
|
1938
|
+
cfg.programName,
|
|
1939
|
+
f"The coded subject(s) <b>{', '.join(not_defined_subjects)}</b> is/are not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.",
|
|
1940
|
+
)
|
|
1941
|
+
return
|
|
1942
|
+
|
|
1786
1943
|
self.pj[cfg.SUBJECTS] = dict(self.subjects_conf)
|
|
1787
1944
|
|
|
1788
1945
|
# check ethogram
|
|
@@ -1820,7 +1977,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1820
1977
|
self.pj[cfg.INDEPENDENT_VARIABLES] = dict(self.indVar)
|
|
1821
1978
|
|
|
1822
1979
|
# converters
|
|
1823
|
-
converters = {}
|
|
1980
|
+
converters: dict = {}
|
|
1824
1981
|
for row in range(self.tw_converters.rowCount()):
|
|
1825
1982
|
converters[self.tw_converters.item(row, 0).text()] = {
|
|
1826
1983
|
"name": self.tw_converters.item(row, 0).text(),
|
|
@@ -1840,9 +1997,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1840
1997
|
for converter in sorted(self.converters.keys()):
|
|
1841
1998
|
self.tw_converters.setRowCount(self.tw_converters.rowCount() + 1)
|
|
1842
1999
|
self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 0, QTableWidgetItem(converter)) # id / name
|
|
1843
|
-
self.tw_converters.setItem(
|
|
1844
|
-
self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"])
|
|
1845
|
-
)
|
|
2000
|
+
self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"]))
|
|
1846
2001
|
self.tw_converters.setItem(
|
|
1847
2002
|
self.tw_converters.rowCount() - 1,
|
|
1848
2003
|
2,
|