boris-behav-obs 9.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- 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
boris/project.py
ADDED
|
@@ -0,0 +1,2007 @@
|
|
|
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 json
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
from PySide6.QtCore import Qt, QDateTime
|
|
28
|
+
from PySide6.QtGui import QColor
|
|
29
|
+
from PySide6.QtWidgets import (
|
|
30
|
+
QAbstractItemView,
|
|
31
|
+
QApplication,
|
|
32
|
+
QCheckBox,
|
|
33
|
+
QColorDialog,
|
|
34
|
+
QDialog,
|
|
35
|
+
QFileDialog,
|
|
36
|
+
QHBoxLayout,
|
|
37
|
+
QHeaderView,
|
|
38
|
+
QInputDialog,
|
|
39
|
+
QLabel,
|
|
40
|
+
QLineEdit,
|
|
41
|
+
QMenu,
|
|
42
|
+
QMessageBox,
|
|
43
|
+
QPushButton,
|
|
44
|
+
QSizePolicy,
|
|
45
|
+
QSpacerItem,
|
|
46
|
+
QTableWidget,
|
|
47
|
+
QTableWidgetItem,
|
|
48
|
+
QVBoxLayout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from . import add_modifier
|
|
52
|
+
from . import config as cfg
|
|
53
|
+
from . import utilities as util
|
|
54
|
+
from . import converters, dialog, exclusion_matrix, project_import_export
|
|
55
|
+
from .project_ui import Ui_dlgProject
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BehavioralCategories(QDialog):
|
|
59
|
+
"""
|
|
60
|
+
Class for managing the behavioral categories
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, pj):
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self.pj = pj
|
|
67
|
+
self.setWindowTitle("Behavioral categories")
|
|
68
|
+
|
|
69
|
+
self.renamed = None
|
|
70
|
+
self.removed = None
|
|
71
|
+
|
|
72
|
+
self.vbox = QVBoxLayout(self)
|
|
73
|
+
|
|
74
|
+
self.label = QLabel()
|
|
75
|
+
self.label.setText("Behavioral categories")
|
|
76
|
+
self.vbox.addWidget(self.label)
|
|
77
|
+
|
|
78
|
+
# self.lw = QListWidget()
|
|
79
|
+
self.lw = QTableWidget()
|
|
80
|
+
self.lw.cellDoubleClicked[int, int].connect(self.lw_double_clicked)
|
|
81
|
+
|
|
82
|
+
# add categories
|
|
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)
|
|
121
|
+
|
|
122
|
+
self.vbox.addWidget(self.lw)
|
|
123
|
+
|
|
124
|
+
self.hbox0 = QHBoxLayout()
|
|
125
|
+
self.pbAddCategory = QPushButton("Add category", clicked=self.add_behavioral_category)
|
|
126
|
+
self.pbRemoveCategory = QPushButton("Remove category", clicked=self.remove_behavioral_category)
|
|
127
|
+
self.pb_rename_category = QPushButton("Rename category", clicked=self.pb_rename_category_clicked)
|
|
128
|
+
|
|
129
|
+
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
130
|
+
self.hbox0.addItem(spacerItem)
|
|
131
|
+
self.hbox0.addWidget(self.pb_rename_category)
|
|
132
|
+
self.hbox0.addWidget(self.pbRemoveCategory)
|
|
133
|
+
self.hbox0.addWidget(self.pbAddCategory)
|
|
134
|
+
self.vbox.addLayout(self.hbox0)
|
|
135
|
+
|
|
136
|
+
hbox1 = QHBoxLayout()
|
|
137
|
+
self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
|
|
138
|
+
self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.accept)
|
|
139
|
+
|
|
140
|
+
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
141
|
+
hbox1.addItem(spacerItem)
|
|
142
|
+
hbox1.addWidget(self.pbCancel)
|
|
143
|
+
hbox1.addWidget(self.pbOK)
|
|
144
|
+
self.vbox.addLayout(hbox1)
|
|
145
|
+
|
|
146
|
+
self.setLayout(self.vbox)
|
|
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
|
+
|
|
220
|
+
def add_behavioral_category(self):
|
|
221
|
+
"""
|
|
222
|
+
add a behavioral category
|
|
223
|
+
"""
|
|
224
|
+
category, ok = QInputDialog.getText(self, "New behavioral category", "Category name:")
|
|
225
|
+
if ok:
|
|
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)
|
|
233
|
+
|
|
234
|
+
def remove_behavioral_category(self):
|
|
235
|
+
"""
|
|
236
|
+
remove the selected behavioral category
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
for selected_item in self.lw.selectedItems():
|
|
240
|
+
# check if behavioral category is in use
|
|
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 = []
|
|
253
|
+
for idx in self.pj[cfg.ETHOGRAM]:
|
|
254
|
+
if (
|
|
255
|
+
cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
|
|
256
|
+
and self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] == category_to_remove
|
|
257
|
+
):
|
|
258
|
+
behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
|
|
259
|
+
flag_remove = False
|
|
260
|
+
if behaviors_in_category:
|
|
261
|
+
flag_remove = (
|
|
262
|
+
dialog.MessageDialog(
|
|
263
|
+
cfg.programName,
|
|
264
|
+
(
|
|
265
|
+
f"Some behavior belong to the <b>{category_to_remove}</b> to remove:<br>"
|
|
266
|
+
f"{'<br>'.join(behaviors_in_category)}<br>"
|
|
267
|
+
"<br>Some features may not be available anymore.<br>"
|
|
268
|
+
),
|
|
269
|
+
("Remove category", cfg.CANCEL),
|
|
270
|
+
)
|
|
271
|
+
== "Remove category"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
else:
|
|
275
|
+
flag_remove = True
|
|
276
|
+
|
|
277
|
+
if flag_remove:
|
|
278
|
+
self.lw.removeRow(self.lw.row(selected_item))
|
|
279
|
+
self.removed = category_to_remove
|
|
280
|
+
|
|
281
|
+
self.accept()
|
|
282
|
+
|
|
283
|
+
def pb_rename_category_clicked(self, row: int):
|
|
284
|
+
"""
|
|
285
|
+
rename the selected behavioral category
|
|
286
|
+
"""
|
|
287
|
+
for selected_item in self.lw.selectedItems():
|
|
288
|
+
# check if behavioral category is in use
|
|
289
|
+
category_to_rename = self.lw.item(self.lw.row(selected_item), 0).text().strip()
|
|
290
|
+
behaviors_in_category = []
|
|
291
|
+
for idx in self.pj[cfg.ETHOGRAM]:
|
|
292
|
+
if (
|
|
293
|
+
cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
|
|
294
|
+
and self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY] == category_to_rename
|
|
295
|
+
):
|
|
296
|
+
behaviors_in_category.append(self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE])
|
|
297
|
+
|
|
298
|
+
flag_rename = False
|
|
299
|
+
if behaviors_in_category:
|
|
300
|
+
flag_rename = (
|
|
301
|
+
dialog.MessageDialog(
|
|
302
|
+
cfg.programName,
|
|
303
|
+
(f"Some behavior belong to the <b>{category_to_rename}</b> to rename:<br>{'<br>'.join(behaviors_in_category)}<br>"),
|
|
304
|
+
["Rename category", cfg.CANCEL],
|
|
305
|
+
)
|
|
306
|
+
== "Rename category"
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
flag_rename = True
|
|
310
|
+
|
|
311
|
+
if flag_rename:
|
|
312
|
+
new_category_name, ok = QInputDialog.getText(
|
|
313
|
+
self, "Rename behavioral category", "New category name:", QLineEdit.Normal, category_to_rename
|
|
314
|
+
)
|
|
315
|
+
if ok:
|
|
316
|
+
self.lw.item(self.lw.indexFromItem(selected_item).row(), 0).setText(new_category_name)
|
|
317
|
+
# check behaviors belonging to the renamed category
|
|
318
|
+
self.renamed = [category_to_rename, new_category_name]
|
|
319
|
+
# self.accept()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class projectDialog(QDialog, Ui_dlgProject):
|
|
323
|
+
def __init__(self, parent=None):
|
|
324
|
+
super().__init__()
|
|
325
|
+
|
|
326
|
+
self.setupUi(self)
|
|
327
|
+
|
|
328
|
+
self.lbObservationsState.setText("")
|
|
329
|
+
self.lbSubjectsState.setText("")
|
|
330
|
+
|
|
331
|
+
# ethogram tab
|
|
332
|
+
behavior_button_items = [
|
|
333
|
+
"new|Add new behavior",
|
|
334
|
+
"clone|Clone behavior",
|
|
335
|
+
"remove|Remove behavior",
|
|
336
|
+
"remove all|Remove all behaviors",
|
|
337
|
+
"lower|Convert keys to lower case",
|
|
338
|
+
]
|
|
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)
|
|
343
|
+
|
|
344
|
+
import_button_items = [
|
|
345
|
+
"boris|from a BORIS project",
|
|
346
|
+
"spreadsheet|from a spreadsheet file (XLSX/ODS)",
|
|
347
|
+
"jwatcher|from a JWatcher project",
|
|
348
|
+
"text|from a text file (CSV or TSV)",
|
|
349
|
+
"clipboard|from the clipboard",
|
|
350
|
+
"repository|from the BORIS repository",
|
|
351
|
+
]
|
|
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)
|
|
356
|
+
|
|
357
|
+
self.pbBehaviorsCategories.clicked.connect(self.pbBehaviorsCategories_clicked)
|
|
358
|
+
|
|
359
|
+
self.pb_exclusion_matrix.clicked.connect(self.exclusion_matrix)
|
|
360
|
+
|
|
361
|
+
self.pbExportEthogram.clicked.connect(lambda: project_import_export.export_ethogram(self))
|
|
362
|
+
|
|
363
|
+
self.twBehaviors.cellChanged[int, int].connect(self.twBehaviors_cellChanged)
|
|
364
|
+
self.twBehaviors.cellDoubleClicked[int, int].connect(self.twBehaviors_cellDoubleClicked)
|
|
365
|
+
|
|
366
|
+
# left align table header
|
|
367
|
+
for i in range(self.twBehaviors.columnCount()):
|
|
368
|
+
self.twBehaviors.horizontalHeaderItem(i).setTextAlignment(Qt.AlignLeft)
|
|
369
|
+
|
|
370
|
+
# subjects
|
|
371
|
+
subjects_button_items = [
|
|
372
|
+
"new|Add a new subject",
|
|
373
|
+
# "clone|Clone subject",
|
|
374
|
+
"remove|Remove subject",
|
|
375
|
+
"remove all|Remove all subjects",
|
|
376
|
+
"lower|Convert keys to lower case",
|
|
377
|
+
]
|
|
378
|
+
|
|
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)
|
|
383
|
+
|
|
384
|
+
subjects_import_button_items = [
|
|
385
|
+
"boris|from a BORIS project",
|
|
386
|
+
"spreadsheet|from a spreadsheet file (XLSX/ODS)",
|
|
387
|
+
"text|from a text file (CSV or TSV)",
|
|
388
|
+
"clipboard|from the clipboard",
|
|
389
|
+
]
|
|
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)
|
|
394
|
+
|
|
395
|
+
self.pb_export_subjects.clicked.connect(lambda: project_import_export.export_subjects(self))
|
|
396
|
+
|
|
397
|
+
self.twSubjects.cellChanged[int, int].connect(self.twSubjects_cellChanged)
|
|
398
|
+
|
|
399
|
+
# independent variables tab
|
|
400
|
+
self.pbAddVariable.clicked.connect(self.pbAddVariable_clicked)
|
|
401
|
+
self.pbRemoveVariable.clicked.connect(self.pbRemoveVariable_clicked)
|
|
402
|
+
|
|
403
|
+
self.leLabel.textChanged.connect(self.leLabel_changed)
|
|
404
|
+
self.leDescription.textChanged.connect(self.leDescription_changed)
|
|
405
|
+
self.lePredefined.textChanged.connect(self.lePredefined_changed)
|
|
406
|
+
self.leSetValues.textChanged.connect(self.leSetValues_changed)
|
|
407
|
+
self.dte_default_date.dateTimeChanged.connect(self.dte_default_date_changed)
|
|
408
|
+
|
|
409
|
+
self.twVariables.cellClicked[int, int].connect(self.twVariables_cellClicked)
|
|
410
|
+
|
|
411
|
+
self.cbType.currentIndexChanged.connect(self.cbtype_changed)
|
|
412
|
+
self.cbType.activated.connect(self.cbtype_activated)
|
|
413
|
+
|
|
414
|
+
self.pbImportVarFromProject.clicked.connect(self.pbImportVarFromProject_clicked)
|
|
415
|
+
|
|
416
|
+
self.pbOK.clicked.connect(self.pbOK_clicked)
|
|
417
|
+
self.pbCancel.clicked.connect(self.pbCancel_clicked)
|
|
418
|
+
|
|
419
|
+
self.selected_twvariables_row = -1
|
|
420
|
+
|
|
421
|
+
self.pbAddBehaviorsCodingMap.clicked.connect(self.add_behaviors_coding_map)
|
|
422
|
+
self.pbRemoveBehaviorsCodingMap.clicked.connect(self.remove_behaviors_coding_map)
|
|
423
|
+
|
|
424
|
+
# converters tab
|
|
425
|
+
self.pb_add_converter.clicked.connect(lambda: converters.add_converter(self))
|
|
426
|
+
self.pb_modify_converter.clicked.connect(lambda: converters.modify_converter(self))
|
|
427
|
+
self.pb_save_converter.clicked.connect(lambda: converters.save_converter(self))
|
|
428
|
+
self.pb_cancel_converter.clicked.connect(lambda: converters.cancel_converter(self))
|
|
429
|
+
self.pb_delete_converter.clicked.connect(lambda: converters.delete_converter(self))
|
|
430
|
+
|
|
431
|
+
self.pb_load_from_file.clicked.connect(lambda: converters.load_converters_from_file_repo(self, mode="file"))
|
|
432
|
+
self.pb_load_from_repo.clicked.connect(lambda: converters.load_converters_from_file_repo(self, mode="repo"))
|
|
433
|
+
|
|
434
|
+
self.pb_code_help.clicked.connect(lambda: converters.pb_code_help_clicked(self))
|
|
435
|
+
|
|
436
|
+
self.row_in_modification = -1
|
|
437
|
+
self.flag_modified = False
|
|
438
|
+
|
|
439
|
+
for w in (
|
|
440
|
+
self.le_converter_name,
|
|
441
|
+
self.le_converter_description,
|
|
442
|
+
self.pteCode,
|
|
443
|
+
self.pb_save_converter,
|
|
444
|
+
self.pb_cancel_converter,
|
|
445
|
+
):
|
|
446
|
+
w.setEnabled(False)
|
|
447
|
+
|
|
448
|
+
# disable widget for indep var setting
|
|
449
|
+
for widget in (
|
|
450
|
+
self.leLabel,
|
|
451
|
+
self.le_converter_description,
|
|
452
|
+
self.cbType,
|
|
453
|
+
self.lePredefined,
|
|
454
|
+
self.dte_default_date,
|
|
455
|
+
self.leSetValues,
|
|
456
|
+
):
|
|
457
|
+
widget.setEnabled(False)
|
|
458
|
+
|
|
459
|
+
self.twBehaviors.horizontalHeader().sortIndicatorChanged.connect(self.sort_twBehaviors)
|
|
460
|
+
self.twSubjects.horizontalHeader().sortIndicatorChanged.connect(self.sort_twSubjects)
|
|
461
|
+
self.twVariables.horizontalHeader().sortIndicatorChanged.connect(self.sort_twVariables)
|
|
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
|
+
|
|
474
|
+
def add_button_menu(self, data, menu_obj):
|
|
475
|
+
"""
|
|
476
|
+
add menu option from dictionary
|
|
477
|
+
"""
|
|
478
|
+
if isinstance(data, dict):
|
|
479
|
+
for k, v in data.items():
|
|
480
|
+
sub_menu = QMenu(k, menu_obj)
|
|
481
|
+
menu_obj.addMenu(sub_menu)
|
|
482
|
+
self.add_button_menu(v, sub_menu)
|
|
483
|
+
elif isinstance(data, list):
|
|
484
|
+
for element in data:
|
|
485
|
+
self.add_button_menu(element, menu_obj)
|
|
486
|
+
else:
|
|
487
|
+
action = menu_obj.addAction(data.split("|")[1])
|
|
488
|
+
# tips are used to discriminate the menu option
|
|
489
|
+
action.setStatusTip(data.split("|")[0])
|
|
490
|
+
action.setIconVisibleInMenu(False)
|
|
491
|
+
|
|
492
|
+
def behavior(self, action: str):
|
|
493
|
+
"""
|
|
494
|
+
behavior menu
|
|
495
|
+
"""
|
|
496
|
+
if action == "new":
|
|
497
|
+
self.add_behavior()
|
|
498
|
+
if action == "clone":
|
|
499
|
+
self.clone_behavior()
|
|
500
|
+
if action == "remove":
|
|
501
|
+
self.remove_behavior()
|
|
502
|
+
if action == "remove all":
|
|
503
|
+
self.remove_all_behaviors()
|
|
504
|
+
if action == "lower":
|
|
505
|
+
self.convert_behaviors_keys_to_lower_case()
|
|
506
|
+
|
|
507
|
+
def import_ethogram(self, action: str):
|
|
508
|
+
"""
|
|
509
|
+
import behaviors
|
|
510
|
+
"""
|
|
511
|
+
if action == "boris":
|
|
512
|
+
project_import_export.import_behaviors_from_project(self)
|
|
513
|
+
if action == "jwatcher":
|
|
514
|
+
project_import_export.import_behaviors_from_JWatcher(self)
|
|
515
|
+
if action == "text":
|
|
516
|
+
project_import_export.import_behaviors_from_text_file(self)
|
|
517
|
+
if action == "spreadsheet":
|
|
518
|
+
project_import_export.import_behaviors_from_spreadsheet(self)
|
|
519
|
+
if action == "clipboard":
|
|
520
|
+
project_import_export.import_behaviors_from_clipboard(self)
|
|
521
|
+
if action == "repository":
|
|
522
|
+
project_import_export.import_behaviors_from_repository(self)
|
|
523
|
+
|
|
524
|
+
def import_subjects(self, action: str):
|
|
525
|
+
"""
|
|
526
|
+
import subjects
|
|
527
|
+
"""
|
|
528
|
+
if action == "boris":
|
|
529
|
+
project_import_export.import_subjects_from_project(self)
|
|
530
|
+
if action == "text":
|
|
531
|
+
project_import_export.import_subjects_from_text_file(self)
|
|
532
|
+
if action == "spreadsheet":
|
|
533
|
+
project_import_export.import_subjects_from_spreadsheet(self)
|
|
534
|
+
if action == "clipboard":
|
|
535
|
+
project_import_export.import_subjects_from_clipboard(self)
|
|
536
|
+
|
|
537
|
+
def subjects(self, action: str):
|
|
538
|
+
"""
|
|
539
|
+
subjects menu
|
|
540
|
+
"""
|
|
541
|
+
if action == "new":
|
|
542
|
+
self.add_subject()
|
|
543
|
+
# if action == "clone":
|
|
544
|
+
# self.clone_behavior()
|
|
545
|
+
if action == "remove":
|
|
546
|
+
self.remove_subject()
|
|
547
|
+
if action == "remove all":
|
|
548
|
+
self.remove_all_subjects()
|
|
549
|
+
if action == "lower":
|
|
550
|
+
self.convert_subjects_keys_to_lower_case()
|
|
551
|
+
|
|
552
|
+
def sort_twBehaviors(self, index, order):
|
|
553
|
+
"""
|
|
554
|
+
order ethogram table
|
|
555
|
+
"""
|
|
556
|
+
self.twBehaviors.sortItems(index, order)
|
|
557
|
+
|
|
558
|
+
def sort_twSubjects(self, index, order):
|
|
559
|
+
"""
|
|
560
|
+
order subjects table
|
|
561
|
+
"""
|
|
562
|
+
self.twSubjects.sortItems(index, order)
|
|
563
|
+
|
|
564
|
+
def sort_twVariables(self, index, order):
|
|
565
|
+
"""
|
|
566
|
+
order variables table
|
|
567
|
+
"""
|
|
568
|
+
self.twVariables.sortItems(index, order)
|
|
569
|
+
|
|
570
|
+
def convert_behaviors_keys_to_lower_case(self):
|
|
571
|
+
"""
|
|
572
|
+
convert behaviors key to lower case to help to migrate to v. 7
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
if not self.twBehaviors.rowCount():
|
|
576
|
+
QMessageBox.critical(
|
|
577
|
+
None,
|
|
578
|
+
cfg.programName,
|
|
579
|
+
"The ethogram is empty",
|
|
580
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
581
|
+
QMessageBox.NoButton,
|
|
582
|
+
)
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
# check if some keys will be duplicated after conversion
|
|
586
|
+
try:
|
|
587
|
+
all_keys = [self.twBehaviors.item(row, cfg.behavioursFields["key"]).text() for row in range(self.twBehaviors.rowCount())]
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
if all_keys == [x.lower() for x in all_keys]:
|
|
591
|
+
QMessageBox.information(self, cfg.programName, "All keys are already lower case")
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
|
|
598
|
+
if (
|
|
599
|
+
dialog.MessageDialog(
|
|
600
|
+
cfg.programName,
|
|
601
|
+
"Some keys will be duplicated after conversion. Proceed?",
|
|
602
|
+
[cfg.YES, cfg.CANCEL],
|
|
603
|
+
)
|
|
604
|
+
== cfg.CANCEL
|
|
605
|
+
):
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
for row in range(self.twBehaviors.rowCount()):
|
|
609
|
+
if self.twBehaviors.item(row, cfg.behavioursFields["key"]).text():
|
|
610
|
+
self.twBehaviors.item(row, cfg.behavioursFields["key"]).setText(
|
|
611
|
+
self.twBehaviors.item(row, cfg.behavioursFields["key"]).text().lower()
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# convert modifier shortcuts
|
|
615
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text():
|
|
616
|
+
modifiers_dict = (
|
|
617
|
+
json.loads(self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text())
|
|
618
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).text()
|
|
619
|
+
else {}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
for modifier_set in modifiers_dict:
|
|
623
|
+
try:
|
|
624
|
+
for idx2, value in enumerate(modifiers_dict[modifier_set]["values"]):
|
|
625
|
+
if re.findall(r"\((\w+)\)", value):
|
|
626
|
+
modifiers_dict[modifier_set]["values"][idx2] = (
|
|
627
|
+
value.split("(")[0] + "(" + re.findall(r"\((\w+)\)", value)[0].lower() + ")" + value.split(")")[-1]
|
|
628
|
+
)
|
|
629
|
+
except Exception:
|
|
630
|
+
logging.warning("error during conversion of modifier short cut to lower case")
|
|
631
|
+
|
|
632
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.MODIFIERS]).setText(json.dumps(modifiers_dict))
|
|
633
|
+
|
|
634
|
+
def convert_subjects_keys_to_lower_case(self):
|
|
635
|
+
"""
|
|
636
|
+
convert subjects key to lower case to help to migrate to v. 7
|
|
637
|
+
"""
|
|
638
|
+
# check if some keys will be duplicated after conversion
|
|
639
|
+
try:
|
|
640
|
+
all_keys = [self.twSubjects.item(row, cfg.subjectsFields.index("key")).text() for row in range(self.twSubjects.rowCount())]
|
|
641
|
+
except Exception:
|
|
642
|
+
pass
|
|
643
|
+
if all_keys == [x.lower() for x in all_keys]:
|
|
644
|
+
QMessageBox.information(self, cfg.programName, "All keys are already lower case")
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
if dialog.MessageDialog(cfg.programName, "Confirm the conversion of key to lower case.", [cfg.YES, cfg.CANCEL]) == cfg.CANCEL:
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
if len([x.lower() for x in all_keys]) != len(set([x.lower() for x in all_keys])):
|
|
651
|
+
if (
|
|
652
|
+
dialog.MessageDialog(
|
|
653
|
+
cfg.programName,
|
|
654
|
+
"Some keys will be duplicated after conversion. Proceed?",
|
|
655
|
+
[cfg.YES, cfg.CANCEL],
|
|
656
|
+
)
|
|
657
|
+
== cfg.CANCEL
|
|
658
|
+
):
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
for row in range(self.twSubjects.rowCount()):
|
|
662
|
+
if self.twSubjects.item(row, cfg.subjectsFields.index("key")).text():
|
|
663
|
+
self.twSubjects.item(row, cfg.subjectsFields.index("key")).setText(
|
|
664
|
+
self.twSubjects.item(row, cfg.subjectsFields.index("key")).text().lower()
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
def add_behaviors_coding_map(self):
|
|
668
|
+
"""
|
|
669
|
+
Add a behaviors coding map from file
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
673
|
+
self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
674
|
+
)
|
|
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
|
|
682
|
+
|
|
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.")
|
|
685
|
+
|
|
686
|
+
if cfg.BEHAVIORS_CODING_MAP not in self.pj:
|
|
687
|
+
self.pj[cfg.BEHAVIORS_CODING_MAP] = []
|
|
688
|
+
|
|
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)
|
|
694
|
+
|
|
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
|
+
)
|
|
703
|
+
|
|
704
|
+
self.pj[cfg.BEHAVIORS_CODING_MAP].append(dict(bcm))
|
|
705
|
+
|
|
706
|
+
self.twBehavCodingMap.setRowCount(self.twBehavCodingMap.rowCount() + 1)
|
|
707
|
+
|
|
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))
|
|
711
|
+
|
|
712
|
+
def remove_behaviors_coding_map(self):
|
|
713
|
+
"""
|
|
714
|
+
remove the first selected behaviors coding map
|
|
715
|
+
"""
|
|
716
|
+
if not self.twBehavCodingMap.selectedIndexes():
|
|
717
|
+
QMessageBox.warning(self, cfg.programName, "Select a behaviors coding map")
|
|
718
|
+
else:
|
|
719
|
+
if dialog.MessageDialog(cfg.programName, "Remove the selected behaviors coding map?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
720
|
+
del self.pj[cfg.BEHAVIORS_CODING_MAP][self.twBehavCodingMap.selectedIndexes()[0].row()]
|
|
721
|
+
self.twBehavCodingMap.removeRow(self.twBehavCodingMap.selectedIndexes()[0].row())
|
|
722
|
+
|
|
723
|
+
def leLabel_changed(self):
|
|
724
|
+
"""
|
|
725
|
+
independent variable label changed
|
|
726
|
+
"""
|
|
727
|
+
if self.selected_twvariables_row != -1:
|
|
728
|
+
self.twVariables.item(self.selected_twvariables_row, 0).setText(self.leLabel.text())
|
|
729
|
+
|
|
730
|
+
def leDescription_changed(self):
|
|
731
|
+
"""
|
|
732
|
+
independent variable description changed
|
|
733
|
+
"""
|
|
734
|
+
if self.selected_twvariables_row != -1:
|
|
735
|
+
self.twVariables.item(self.selected_twvariables_row, 1).setText(self.leDescription.text())
|
|
736
|
+
|
|
737
|
+
def lePredefined_changed(self):
|
|
738
|
+
"""
|
|
739
|
+
independent variable predefined value changed
|
|
740
|
+
"""
|
|
741
|
+
if self.selected_twvariables_row != -1:
|
|
742
|
+
self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
|
|
743
|
+
if not self.lePredefined.hasFocus():
|
|
744
|
+
r, msg = self.check_indep_var_config()
|
|
745
|
+
if not r:
|
|
746
|
+
QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
|
|
747
|
+
|
|
748
|
+
def leSetValues_changed(self):
|
|
749
|
+
"""
|
|
750
|
+
independent variable available values changed
|
|
751
|
+
"""
|
|
752
|
+
if self.selected_twvariables_row != -1:
|
|
753
|
+
self.twVariables.item(self.selected_twvariables_row, 4).setText(self.leSetValues.text())
|
|
754
|
+
|
|
755
|
+
def dte_default_date_changed(self):
|
|
756
|
+
"""
|
|
757
|
+
independent variable default timestamp changed
|
|
758
|
+
"""
|
|
759
|
+
if self.selected_twvariables_row != -1:
|
|
760
|
+
self.twVariables.item(self.selected_twvariables_row, 3).setText(
|
|
761
|
+
self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
def pbBehaviorsCategories_clicked(self):
|
|
765
|
+
"""
|
|
766
|
+
behavioral categories manager
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
bc = BehavioralCategories(self.pj)
|
|
770
|
+
|
|
771
|
+
if bc.exec_():
|
|
772
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
|
|
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
|
+
}
|
|
780
|
+
|
|
781
|
+
# sort
|
|
782
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES] = sorted(self.pj[cfg.BEHAVIORAL_CATEGORIES])
|
|
783
|
+
|
|
784
|
+
# check if behavior belong to removed category
|
|
785
|
+
if bc.removed:
|
|
786
|
+
for row in range(self.twBehaviors.rowCount()):
|
|
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:
|
|
789
|
+
if (
|
|
790
|
+
dialog.MessageDialog(
|
|
791
|
+
cfg.programName,
|
|
792
|
+
(
|
|
793
|
+
f"The <b>{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()}</b> behavior belongs "
|
|
794
|
+
"to a behavioral category "
|
|
795
|
+
f"<b>{self.twBehaviors.item(row, cfg.behavioursFields['category']).text()}</b> "
|
|
796
|
+
"that is no more in the behavioral categories list.<br><br>"
|
|
797
|
+
"Remove the behavior from category?"
|
|
798
|
+
),
|
|
799
|
+
[cfg.YES, cfg.CANCEL],
|
|
800
|
+
)
|
|
801
|
+
== cfg.YES
|
|
802
|
+
):
|
|
803
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText("")
|
|
804
|
+
if bc.renamed:
|
|
805
|
+
for row in range(self.twBehaviors.rowCount()):
|
|
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])
|
|
809
|
+
|
|
810
|
+
def twBehaviors_cellDoubleClicked(self, row: int, column: int) -> None:
|
|
811
|
+
"""
|
|
812
|
+
manage double-click on ethogram table:
|
|
813
|
+
* color
|
|
814
|
+
* behavioral category
|
|
815
|
+
* modifiers
|
|
816
|
+
* exclusion
|
|
817
|
+
* modifiers coding map
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
row (int): row double-clicked
|
|
821
|
+
column (int): column double-clicked
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
# excluded column
|
|
825
|
+
if column == cfg.behavioursFields[cfg.EXCLUDED]:
|
|
826
|
+
self.exclusion_matrix()
|
|
827
|
+
|
|
828
|
+
# coding map
|
|
829
|
+
if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
|
|
830
|
+
if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
831
|
+
self.behavior_type_changed(row)
|
|
832
|
+
else:
|
|
833
|
+
QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
|
|
834
|
+
|
|
835
|
+
# behavior type
|
|
836
|
+
if column == cfg.behavioursFields["type"]:
|
|
837
|
+
self.behavior_type_doubleclicked(row)
|
|
838
|
+
|
|
839
|
+
# color
|
|
840
|
+
if column == cfg.behavioursFields[cfg.COLOR]:
|
|
841
|
+
self.color_doubleclicked(row)
|
|
842
|
+
|
|
843
|
+
# behavioral category
|
|
844
|
+
if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
|
|
845
|
+
self.category_doubleclicked(row)
|
|
846
|
+
|
|
847
|
+
# modifiers
|
|
848
|
+
if column == cfg.behavioursFields[cfg.MODIFIERS]:
|
|
849
|
+
# check if behavior has coding map
|
|
850
|
+
if (
|
|
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()
|
|
853
|
+
):
|
|
854
|
+
QMessageBox.warning(self, cfg.programName, "Use the coding map to set/modify the areas")
|
|
855
|
+
else:
|
|
856
|
+
subjects_list = []
|
|
857
|
+
for subject_row in range(self.twSubjects.rowCount()):
|
|
858
|
+
key = self.twSubjects.item(subject_row, 0).text() if self.twSubjects.item(subject_row, 0) else ""
|
|
859
|
+
subjectName = self.twSubjects.item(subject_row, 1).text().strip() if self.twSubjects.item(subject_row, 1) else ""
|
|
860
|
+
subjects_list.append((subjectName, key))
|
|
861
|
+
|
|
862
|
+
addModifierWindow = add_modifier.addModifierDialog(
|
|
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,
|
|
866
|
+
)
|
|
867
|
+
addModifierWindow.setWindowTitle(f'Set modifiers for "{self.twBehaviors.item(row, 2).text()}" behavior')
|
|
868
|
+
|
|
869
|
+
if addModifierWindow.exec_():
|
|
870
|
+
self.twBehaviors.item(row, column).setText(addModifierWindow.get_modifiers())
|
|
871
|
+
|
|
872
|
+
def behavior_type_doubleclicked(self, row):
|
|
873
|
+
"""
|
|
874
|
+
select type for behavior
|
|
875
|
+
"""
|
|
876
|
+
|
|
877
|
+
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text() in cfg.BEHAVIOR_TYPES:
|
|
878
|
+
selected = cfg.BEHAVIOR_TYPES.index(self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text())
|
|
879
|
+
else:
|
|
880
|
+
selected = 0
|
|
881
|
+
|
|
882
|
+
new_type, ok = QInputDialog.getItem(self, "Select a behavior type", "Types of behavior", cfg.BEHAVIOR_TYPES, selected, False)
|
|
883
|
+
|
|
884
|
+
if ok and new_type:
|
|
885
|
+
self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText(new_type)
|
|
886
|
+
|
|
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())
|
|
912
|
+
|
|
913
|
+
def category_doubleclicked(self, row):
|
|
914
|
+
"""
|
|
915
|
+
select category for behavior
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
categories = ["None"] + self.pj[cfg.BEHAVIORAL_CATEGORIES] if cfg.BEHAVIORAL_CATEGORIES in self.pj else ["None"]
|
|
919
|
+
|
|
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())
|
|
922
|
+
else:
|
|
923
|
+
selected = 0
|
|
924
|
+
|
|
925
|
+
category, ok = QInputDialog.getItem(self, "Select a behavioral category", "Behavioral categories", categories, selected, False)
|
|
926
|
+
|
|
927
|
+
if ok and category:
|
|
928
|
+
if category == "None":
|
|
929
|
+
category = ""
|
|
930
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]).setText(category)
|
|
931
|
+
|
|
932
|
+
def check_variable_default_value(self, txt, varType):
|
|
933
|
+
"""
|
|
934
|
+
check if variable default value is compatible with variable type
|
|
935
|
+
"""
|
|
936
|
+
# check for numeric type
|
|
937
|
+
if varType == cfg.NUMERIC:
|
|
938
|
+
try:
|
|
939
|
+
if txt:
|
|
940
|
+
float(txt)
|
|
941
|
+
return True
|
|
942
|
+
except Exception:
|
|
943
|
+
return False
|
|
944
|
+
|
|
945
|
+
return True
|
|
946
|
+
|
|
947
|
+
def variableTypeChanged(self, row):
|
|
948
|
+
"""
|
|
949
|
+
variable type combobox changed
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
if self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentText() == cfg.SET_OF_VALUES:
|
|
953
|
+
if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() == "NA":
|
|
954
|
+
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("Double-click to add values")
|
|
955
|
+
else:
|
|
956
|
+
# check if set of values defined
|
|
957
|
+
if self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).text() not in [
|
|
958
|
+
"NA",
|
|
959
|
+
"Double-click to add values",
|
|
960
|
+
]:
|
|
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)
|
|
963
|
+
return
|
|
964
|
+
else:
|
|
965
|
+
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
|
|
966
|
+
else:
|
|
967
|
+
self.twVariables.item(row, cfg.tw_indVarFields.index("possible values")).setText("NA")
|
|
968
|
+
|
|
969
|
+
# check compatibility between variable type and default value
|
|
970
|
+
if not self.check_variable_default_value(
|
|
971
|
+
self.twVariables.item(row, cfg.tw_indVarFields.index("default value")).text(),
|
|
972
|
+
self.twVariables.cellWidget(row, cfg.tw_indVarFields.index("type")).currentIndex(),
|
|
973
|
+
):
|
|
974
|
+
QMessageBox.warning(
|
|
975
|
+
self,
|
|
976
|
+
cfg.programName + " - Independent variables error",
|
|
977
|
+
(
|
|
978
|
+
f"The default value ({self.twVariables.item(row, cfg.tw_indVarFields.index('default value')).text()}) "
|
|
979
|
+
f"of variable <b>{self.twVariables.item(row, cfg.tw_indVarFields.index('label')).text()}</b> "
|
|
980
|
+
"is not compatible with variable type"
|
|
981
|
+
),
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
def check_indep_var_config(self) -> tuple:
|
|
985
|
+
"""
|
|
986
|
+
check if default type is compatible with var type
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
existing_var = []
|
|
990
|
+
for r in range(self.twVariables.rowCount()):
|
|
991
|
+
if self.twVariables.item(r, 0).text().strip().upper() in existing_var:
|
|
992
|
+
return (
|
|
993
|
+
False,
|
|
994
|
+
f"Row: {r + 1} - The variable label <b>{self.twVariables.item(r, 0).text()}</b> is already in use.",
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# check if same lables
|
|
998
|
+
existing_var.append(self.twVariables.item(r, 0).text().strip().upper())
|
|
999
|
+
|
|
1000
|
+
# check default value
|
|
1001
|
+
if self.twVariables.item(r, 2).text() != cfg.TIMESTAMP and not self.check_variable_default_value(
|
|
1002
|
+
self.twVariables.item(r, 3).text(), self.twVariables.item(r, 2).text()
|
|
1003
|
+
):
|
|
1004
|
+
return False, (
|
|
1005
|
+
f"Row: {r + 1} - "
|
|
1006
|
+
f"The default value ({self.twVariables.item(r, 3).text()}) is not compatible "
|
|
1007
|
+
f"with the variable type ({self.twVariables.item(r, 2).text()})"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
# check if default value in set of values
|
|
1011
|
+
if self.twVariables.item(r, 2).text() == cfg.SET_OF_VALUES and self.twVariables.item(r, 4).text() == "":
|
|
1012
|
+
return False, "No values were defined in set"
|
|
1013
|
+
|
|
1014
|
+
if (
|
|
1015
|
+
self.twVariables.item(r, 2).text() == cfg.SET_OF_VALUES
|
|
1016
|
+
and self.twVariables.item(r, 4).text()
|
|
1017
|
+
and self.twVariables.item(r, 3).text()
|
|
1018
|
+
and self.twVariables.item(r, 3).text() not in self.twVariables.item(r, 4).text().split(",")
|
|
1019
|
+
):
|
|
1020
|
+
return (
|
|
1021
|
+
False,
|
|
1022
|
+
f"The default value ({self.twVariables.item(r, 3).text()}) is not contained in set of values",
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
return True, "OK"
|
|
1026
|
+
|
|
1027
|
+
def cbtype_changed(self):
|
|
1028
|
+
self.leSetValues.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
|
|
1029
|
+
self.label_5.setVisible(self.cbType.currentText() == cfg.SET_OF_VALUES)
|
|
1030
|
+
|
|
1031
|
+
self.dte_default_date.setVisible(self.cbType.currentText() == cfg.TIMESTAMP)
|
|
1032
|
+
self.label_9.setVisible(self.cbType.currentText() == cfg.TIMESTAMP)
|
|
1033
|
+
self.lePredefined.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
|
|
1034
|
+
self.label_4.setVisible(self.cbType.currentText() != cfg.TIMESTAMP)
|
|
1035
|
+
|
|
1036
|
+
def cbtype_activated(self):
|
|
1037
|
+
if self.cbType.currentText() == cfg.TIMESTAMP:
|
|
1038
|
+
self.twVariables.item(self.selected_twvariables_row, 3).setText(
|
|
1039
|
+
self.dte_default_date.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
|
|
1040
|
+
)
|
|
1041
|
+
self.twVariables.item(self.selected_twvariables_row, 4).setText("")
|
|
1042
|
+
else:
|
|
1043
|
+
self.twVariables.item(self.selected_twvariables_row, 3).setText(self.lePredefined.text())
|
|
1044
|
+
self.twVariables.item(self.selected_twvariables_row, 4).setText("")
|
|
1045
|
+
|
|
1046
|
+
# remove spaces after and before comma
|
|
1047
|
+
if self.cbType.currentText() == cfg.SET_OF_VALUES:
|
|
1048
|
+
self.twVariables.item(self.selected_twvariables_row, 4).setText(
|
|
1049
|
+
",".join([x.strip() for x in self.leSetValues.text().split(",")])
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
self.twVariables.item(self.selected_twvariables_row, 2).setText(self.cbType.currentText())
|
|
1053
|
+
|
|
1054
|
+
r, msg = self.check_indep_var_config()
|
|
1055
|
+
|
|
1056
|
+
if not r:
|
|
1057
|
+
QMessageBox.warning(self, f"{cfg.programName} - Independent variables error", msg)
|
|
1058
|
+
|
|
1059
|
+
def pbAddVariable_clicked(self):
|
|
1060
|
+
"""
|
|
1061
|
+
add an independent variable
|
|
1062
|
+
"""
|
|
1063
|
+
|
|
1064
|
+
logging.debug("add an independent variable")
|
|
1065
|
+
self.twVariables.setRowCount(self.twVariables.rowCount() + 1)
|
|
1066
|
+
self.selected_twvariables_row = self.twVariables.rowCount() - 1
|
|
1067
|
+
|
|
1068
|
+
for idx, field in enumerate(cfg.tw_indVarFields):
|
|
1069
|
+
if field == "type":
|
|
1070
|
+
item = QTableWidgetItem("numeric")
|
|
1071
|
+
else:
|
|
1072
|
+
item = QTableWidgetItem("")
|
|
1073
|
+
self.twVariables.setItem(self.twVariables.rowCount() - 1, idx, item)
|
|
1074
|
+
|
|
1075
|
+
self.twVariables.setCurrentCell(self.twVariables.rowCount() - 1, 0)
|
|
1076
|
+
|
|
1077
|
+
self.twVariables_cellClicked(self.twVariables.rowCount() - 1, 0)
|
|
1078
|
+
|
|
1079
|
+
def pbRemoveVariable_clicked(self):
|
|
1080
|
+
"""
|
|
1081
|
+
remove the selected independent variable
|
|
1082
|
+
"""
|
|
1083
|
+
logging.debug("remove selected independent variable")
|
|
1084
|
+
|
|
1085
|
+
if not self.twVariables.selectedIndexes():
|
|
1086
|
+
QMessageBox.warning(self, cfg.programName, "Select a variable to remove")
|
|
1087
|
+
else:
|
|
1088
|
+
if dialog.MessageDialog(cfg.programName, "Remove the selected variable?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
1089
|
+
self.twVariables.removeRow(self.twVariables.selectedIndexes()[0].row())
|
|
1090
|
+
|
|
1091
|
+
if self.twVariables.selectedIndexes():
|
|
1092
|
+
self.twVariables_cellClicked(self.twVariables.selectedIndexes()[0].row(), 0)
|
|
1093
|
+
else:
|
|
1094
|
+
self.twVariables_cellClicked(-1, 0)
|
|
1095
|
+
|
|
1096
|
+
def pbImportVarFromProject_clicked(self):
|
|
1097
|
+
"""
|
|
1098
|
+
import independent variables from another project
|
|
1099
|
+
"""
|
|
1100
|
+
|
|
1101
|
+
project_import_export.import_indep_variables_from_project(self)
|
|
1102
|
+
|
|
1103
|
+
def exclusion_matrix(self):
|
|
1104
|
+
"""
|
|
1105
|
+
activate exclusion matrix window
|
|
1106
|
+
"""
|
|
1107
|
+
|
|
1108
|
+
if not self.twBehaviors.rowCount():
|
|
1109
|
+
QMessageBox.critical(
|
|
1110
|
+
None,
|
|
1111
|
+
cfg.programName,
|
|
1112
|
+
"The ethogram is empty",
|
|
1113
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1114
|
+
QMessageBox.NoButton,
|
|
1115
|
+
)
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
for row in range(self.twBehaviors.rowCount()):
|
|
1119
|
+
if not self.twBehaviors.item(row, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
|
|
1120
|
+
QMessageBox.critical(
|
|
1121
|
+
None,
|
|
1122
|
+
cfg.programName,
|
|
1123
|
+
f"A behavior code is empty at row {row + 1}",
|
|
1124
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1125
|
+
QMessageBox.NoButton,
|
|
1126
|
+
)
|
|
1127
|
+
return
|
|
1128
|
+
|
|
1129
|
+
ex = exclusion_matrix.ExclusionMatrix()
|
|
1130
|
+
|
|
1131
|
+
state_behaviors, point_behaviors, allBehaviors, excl, new_excl = [], [], [], {}, {}
|
|
1132
|
+
|
|
1133
|
+
# list of point events
|
|
1134
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1135
|
+
if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
|
|
1136
|
+
if "Point" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text():
|
|
1137
|
+
point_behaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1138
|
+
|
|
1139
|
+
# check if point are present and if user want to include them in exclusion matrix
|
|
1140
|
+
include_point_events = cfg.NO
|
|
1141
|
+
if point_behaviors:
|
|
1142
|
+
include_point_events = dialog.MessageDialog(
|
|
1143
|
+
cfg.programName,
|
|
1144
|
+
"Do you want to include the point events in the exclusion matrix?",
|
|
1145
|
+
[cfg.YES, cfg.NO],
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1149
|
+
if self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]):
|
|
1150
|
+
if include_point_events == cfg.YES or (
|
|
1151
|
+
include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text()
|
|
1152
|
+
):
|
|
1153
|
+
allBehaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1154
|
+
|
|
1155
|
+
excl[self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()] = (
|
|
1156
|
+
self.twBehaviors.item(r, cfg.behavioursFields["excluded"]).text().split(",")
|
|
1157
|
+
)
|
|
1158
|
+
new_excl[self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()] = []
|
|
1159
|
+
|
|
1160
|
+
if "State" in self.twBehaviors.item(r, cfg.behavioursFields[cfg.TYPE]).text():
|
|
1161
|
+
state_behaviors.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1162
|
+
|
|
1163
|
+
logging.debug(f"point behaviors: {point_behaviors}")
|
|
1164
|
+
logging.debug(f"state behaviors: {state_behaviors}")
|
|
1165
|
+
|
|
1166
|
+
if not state_behaviors:
|
|
1167
|
+
QMessageBox.critical(
|
|
1168
|
+
None,
|
|
1169
|
+
cfg.programName,
|
|
1170
|
+
"No state events were defined in ethogram",
|
|
1171
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1172
|
+
QMessageBox.NoButton,
|
|
1173
|
+
)
|
|
1174
|
+
return
|
|
1175
|
+
|
|
1176
|
+
logging.debug(f"exclusion matrix {excl}")
|
|
1177
|
+
|
|
1178
|
+
# first row contain state events
|
|
1179
|
+
ex.twExclusions.setColumnCount(len(state_behaviors))
|
|
1180
|
+
ex.twExclusions.setHorizontalHeaderLabels(state_behaviors)
|
|
1181
|
+
ex.twExclusions.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
|
|
1182
|
+
|
|
1183
|
+
# first column contains all events: point + state
|
|
1184
|
+
ex.twExclusions.setRowCount(len(point_behaviors + state_behaviors))
|
|
1185
|
+
for idx, header in enumerate(point_behaviors + state_behaviors):
|
|
1186
|
+
item = QTableWidgetItem(header)
|
|
1187
|
+
if header in point_behaviors:
|
|
1188
|
+
item.setBackground(QColor(0, 200, 200))
|
|
1189
|
+
ex.twExclusions.setVerticalHeaderItem(idx, item)
|
|
1190
|
+
ex.twExclusions.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
|
|
1191
|
+
|
|
1192
|
+
ex.allBehaviors = allBehaviors
|
|
1193
|
+
ex.stateBehaviors = state_behaviors
|
|
1194
|
+
ex.point_behaviors = point_behaviors
|
|
1195
|
+
|
|
1196
|
+
ex.checkboxes = {}
|
|
1197
|
+
|
|
1198
|
+
for c, c_name in enumerate(state_behaviors):
|
|
1199
|
+
flag_left_bottom = False
|
|
1200
|
+
for r, r_name in enumerate(point_behaviors + state_behaviors):
|
|
1201
|
+
if c_name == r_name:
|
|
1202
|
+
flag_left_bottom = True
|
|
1203
|
+
|
|
1204
|
+
if c_name != r_name:
|
|
1205
|
+
ex.checkboxes[f"{r_name}|{c_name}"] = QCheckBox()
|
|
1206
|
+
ex.checkboxes[f"{r_name}|{c_name}"].setStyleSheet("text-align: center; margin-left:50%; margin-right:50%;")
|
|
1207
|
+
|
|
1208
|
+
if flag_left_bottom:
|
|
1209
|
+
# hide if cell in left-bottom part of table
|
|
1210
|
+
ex.checkboxes[f"{r_name}|{c_name}"].setEnabled(False)
|
|
1211
|
+
|
|
1212
|
+
# connect function when a CB is clicked
|
|
1213
|
+
ex.checkboxes[f"{r_name}|{c_name}"].clicked.connect(ex.cb_clicked)
|
|
1214
|
+
if c_name in excl[r_name]:
|
|
1215
|
+
ex.checkboxes[f"{r_name}|{c_name}"].setChecked(True)
|
|
1216
|
+
ex.twExclusions.setCellWidget(r, c, ex.checkboxes[f"{r_name}|{c_name}"])
|
|
1217
|
+
|
|
1218
|
+
ex.twExclusions.resizeColumnsToContents()
|
|
1219
|
+
# check corresponding checkbox
|
|
1220
|
+
ex.cb_clicked()
|
|
1221
|
+
|
|
1222
|
+
if ex.exec_():
|
|
1223
|
+
for c, c_name in enumerate(state_behaviors):
|
|
1224
|
+
for r, r_name in enumerate(point_behaviors + state_behaviors):
|
|
1225
|
+
if c_name != r_name:
|
|
1226
|
+
if ex.twExclusions.cellWidget(r, c).isChecked():
|
|
1227
|
+
if c_name not in new_excl[r_name]:
|
|
1228
|
+
new_excl[r_name].append(c_name)
|
|
1229
|
+
|
|
1230
|
+
logging.debug(f"new exclusion matrix {new_excl}")
|
|
1231
|
+
|
|
1232
|
+
# update excluded field
|
|
1233
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1234
|
+
if include_point_events == cfg.YES or (include_point_events == cfg.NO and "State" in self.twBehaviors.item(r, 0).text()):
|
|
1235
|
+
for e in excl:
|
|
1236
|
+
if e == self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text():
|
|
1237
|
+
item = QTableWidgetItem(",".join(new_excl[e]))
|
|
1238
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
1239
|
+
item.setBackground(self.not_editable_column_color())
|
|
1240
|
+
self.twBehaviors.setItem(r, cfg.behavioursFields["excluded"], item)
|
|
1241
|
+
|
|
1242
|
+
def remove_all_behaviors(self):
|
|
1243
|
+
if not self.twBehaviors.rowCount():
|
|
1244
|
+
QMessageBox.critical(
|
|
1245
|
+
None,
|
|
1246
|
+
cfg.programName,
|
|
1247
|
+
"The ethogram is empty",
|
|
1248
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1249
|
+
QMessageBox.NoButton,
|
|
1250
|
+
)
|
|
1251
|
+
return
|
|
1252
|
+
|
|
1253
|
+
if dialog.MessageDialog(cfg.programName, "Remove all behaviors?", [cfg.YES, cfg.CANCEL]) != cfg.YES:
|
|
1254
|
+
return
|
|
1255
|
+
|
|
1256
|
+
# delete ethogram rows without behavior code
|
|
1257
|
+
for r in range(self.twBehaviors.rowCount() - 1, -1, -1):
|
|
1258
|
+
if not self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
|
|
1259
|
+
self.twBehaviors.removeRow(r)
|
|
1260
|
+
|
|
1261
|
+
# extract all codes to delete
|
|
1262
|
+
codesToDelete = []
|
|
1263
|
+
row_mem = {}
|
|
1264
|
+
for r in range(self.twBehaviors.rowCount() - 1, -1, -1):
|
|
1265
|
+
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
|
|
1266
|
+
codesToDelete.append(self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text())
|
|
1267
|
+
row_mem[self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text()] = r
|
|
1268
|
+
|
|
1269
|
+
# extract all codes used in observations
|
|
1270
|
+
codesInObs = []
|
|
1271
|
+
for obs in self.pj[cfg.OBSERVATIONS]:
|
|
1272
|
+
events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
|
|
1273
|
+
for event in events:
|
|
1274
|
+
codesInObs.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
1275
|
+
|
|
1276
|
+
for codeToDelete in codesToDelete:
|
|
1277
|
+
# if code to delete used in obs ask confirmation
|
|
1278
|
+
if codeToDelete in codesInObs:
|
|
1279
|
+
response = dialog.MessageDialog(
|
|
1280
|
+
cfg.programName,
|
|
1281
|
+
f"The code <b>{codeToDelete}</b> is used in observations!",
|
|
1282
|
+
["Remove", cfg.CANCEL],
|
|
1283
|
+
)
|
|
1284
|
+
if response == "Remove":
|
|
1285
|
+
self.twBehaviors.removeRow(row_mem[codeToDelete])
|
|
1286
|
+
else: # remove without asking
|
|
1287
|
+
self.twBehaviors.removeRow(row_mem[codeToDelete])
|
|
1288
|
+
|
|
1289
|
+
def twBehaviors_cellChanged(self, row, column):
|
|
1290
|
+
"""
|
|
1291
|
+
check ethogram
|
|
1292
|
+
"""
|
|
1293
|
+
|
|
1294
|
+
keys, codes = [], []
|
|
1295
|
+
self.lbObservationsState.setText("")
|
|
1296
|
+
|
|
1297
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1298
|
+
# check key
|
|
1299
|
+
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX):
|
|
1300
|
+
key = self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_KEY_FIELD_IDX).text()
|
|
1301
|
+
# check key length
|
|
1302
|
+
if key.upper() not in list(cfg.function_keys.values()) and len(key) > 1:
|
|
1303
|
+
self.lbObservationsState.setText('<font color="red">Key length > 1</font>')
|
|
1304
|
+
return
|
|
1305
|
+
|
|
1306
|
+
keys.append(key)
|
|
1307
|
+
|
|
1308
|
+
# check code
|
|
1309
|
+
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX):
|
|
1310
|
+
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text() in codes:
|
|
1311
|
+
self.lbObservationsState.setText(f'<font color="red">Code duplicated at line {r + 1} </font>')
|
|
1312
|
+
else:
|
|
1313
|
+
if self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text():
|
|
1314
|
+
codes.append(self.twBehaviors.item(r, cfg.PROJECT_BEHAVIORS_CODE_FIELD_IDX).text())
|
|
1315
|
+
|
|
1316
|
+
def clone_behavior(self):
|
|
1317
|
+
"""
|
|
1318
|
+
clone the selected behavior
|
|
1319
|
+
"""
|
|
1320
|
+
|
|
1321
|
+
if not self.twBehaviors.rowCount():
|
|
1322
|
+
QMessageBox.critical(
|
|
1323
|
+
None,
|
|
1324
|
+
cfg.programName,
|
|
1325
|
+
"The ethogram is empty",
|
|
1326
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1327
|
+
QMessageBox.NoButton,
|
|
1328
|
+
)
|
|
1329
|
+
return
|
|
1330
|
+
|
|
1331
|
+
if not self.twBehaviors.selectedIndexes():
|
|
1332
|
+
QMessageBox.about(self, cfg.programName, "First select a behavior")
|
|
1333
|
+
else:
|
|
1334
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
1335
|
+
|
|
1336
|
+
row = self.twBehaviors.selectedIndexes()[0].row()
|
|
1337
|
+
for field in cfg.behavioursFields:
|
|
1338
|
+
item = QTableWidgetItem(self.twBehaviors.item(row, cfg.behavioursFields[field]))
|
|
1339
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
|
|
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:
|
|
1344
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
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
|
+
|
|
1350
|
+
self.twBehaviors.scrollToBottom()
|
|
1351
|
+
|
|
1352
|
+
def remove_behavior(self):
|
|
1353
|
+
"""
|
|
1354
|
+
remove behavior
|
|
1355
|
+
"""
|
|
1356
|
+
|
|
1357
|
+
if not self.twBehaviors.rowCount():
|
|
1358
|
+
QMessageBox.critical(
|
|
1359
|
+
None,
|
|
1360
|
+
cfg.programName,
|
|
1361
|
+
"The ethogram is empty",
|
|
1362
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1363
|
+
QMessageBox.NoButton,
|
|
1364
|
+
)
|
|
1365
|
+
return
|
|
1366
|
+
|
|
1367
|
+
if not self.twBehaviors.selectedIndexes():
|
|
1368
|
+
QMessageBox.warning(self, cfg.programName, "Select a behaviour to be removed")
|
|
1369
|
+
return
|
|
1370
|
+
|
|
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)
|
|
1385
|
+
|
|
1386
|
+
def add_behavior(self):
|
|
1387
|
+
"""
|
|
1388
|
+
add new behavior to ethogram
|
|
1389
|
+
"""
|
|
1390
|
+
|
|
1391
|
+
# Add behavior to table
|
|
1392
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
1393
|
+
for field_type in cfg.behavioursFields:
|
|
1394
|
+
item = QTableWidgetItem()
|
|
1395
|
+
if field_type == cfg.TYPE:
|
|
1396
|
+
item.setText("Point event")
|
|
1397
|
+
# no manual editing, gray back ground
|
|
1398
|
+
if field_type in (cfg.TYPE, cfg.COLOR, "category", cfg.MODIFIERS, "modifiers", "excluded", "coding map"):
|
|
1399
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
1400
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
1401
|
+
item.setBackground(self.not_editable_column_color())
|
|
1402
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
|
|
1403
|
+
self.twBehaviors.scrollToBottom()
|
|
1404
|
+
|
|
1405
|
+
def behavior_type_changed(self, row: int) -> None:
|
|
1406
|
+
"""
|
|
1407
|
+
event type combobox changed
|
|
1408
|
+
"""
|
|
1409
|
+
|
|
1410
|
+
if cfg.CODING_MAP_sp in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
1411
|
+
# let user select a coding maop
|
|
1412
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
1413
|
+
self,
|
|
1414
|
+
f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
|
|
1415
|
+
"",
|
|
1416
|
+
"BORIS map files (*.boris_map);;All files (*)",
|
|
1417
|
+
)
|
|
1418
|
+
if file_name:
|
|
1419
|
+
try:
|
|
1420
|
+
new_map = json.loads(open(file_name, "r").read())
|
|
1421
|
+
except Exception:
|
|
1422
|
+
QMessageBox.critical(self, cfg.programName, "Error reding the coding map")
|
|
1423
|
+
return
|
|
1424
|
+
self.pj[cfg.CODING_MAP][new_map["name"]] = new_map
|
|
1425
|
+
|
|
1426
|
+
# add modifiers from coding map areas
|
|
1427
|
+
modifstr = json.dumps(
|
|
1428
|
+
{
|
|
1429
|
+
"0": {
|
|
1430
|
+
"name": new_map["name"],
|
|
1431
|
+
"type": cfg.MULTI_SELECTION,
|
|
1432
|
+
"values": list(sorted(new_map["areas"].keys())),
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
self.twBehaviors.item(row, cfg.behavioursFields["modifiers"]).setText(modifstr)
|
|
1438
|
+
self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText(new_map["name"])
|
|
1439
|
+
|
|
1440
|
+
else:
|
|
1441
|
+
# if coding map already exists do not reset the behavior type if no filename selected
|
|
1442
|
+
if not self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).text():
|
|
1443
|
+
QMessageBox.critical(self, cfg.programName, 'No coding map was selected.\nEvent type will be reset to "Point event" ')
|
|
1444
|
+
self.twBehaviors.item(row, cfg.behavioursFields["type"]).setText("Point event")
|
|
1445
|
+
else:
|
|
1446
|
+
self.twBehaviors.item(row, cfg.behavioursFields["coding map"]).setText("")
|
|
1447
|
+
|
|
1448
|
+
def add_subject(self):
|
|
1449
|
+
"""
|
|
1450
|
+
add a subject
|
|
1451
|
+
"""
|
|
1452
|
+
|
|
1453
|
+
self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
|
|
1454
|
+
for col in range(len(cfg.subjectsFields)):
|
|
1455
|
+
item = QTableWidgetItem("")
|
|
1456
|
+
self.twSubjects.setItem(self.twSubjects.rowCount() - 1, col, item)
|
|
1457
|
+
self.twSubjects.scrollToBottom()
|
|
1458
|
+
|
|
1459
|
+
def remove_subject(self):
|
|
1460
|
+
"""
|
|
1461
|
+
remove selected subject from subjects list
|
|
1462
|
+
control before if subject used in observations
|
|
1463
|
+
"""
|
|
1464
|
+
|
|
1465
|
+
if not self.twSubjects.selectedIndexes():
|
|
1466
|
+
QMessageBox.warning(self, cfg.programName, "Select a subject to remove")
|
|
1467
|
+
else:
|
|
1468
|
+
if dialog.MessageDialog(cfg.programName, "Remove the selected subject?", [cfg.YES, cfg.CANCEL]) == cfg.YES:
|
|
1469
|
+
flagDel = False
|
|
1470
|
+
if self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1):
|
|
1471
|
+
subjectToDelete = self.twSubjects.item(self.twSubjects.selectedIndexes()[0].row(), 1).text() # 1: subject name
|
|
1472
|
+
|
|
1473
|
+
subjectsInObs = []
|
|
1474
|
+
for obs in self.pj[cfg.OBSERVATIONS]:
|
|
1475
|
+
events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
|
|
1476
|
+
for event in events:
|
|
1477
|
+
subjectsInObs.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
|
|
1478
|
+
if subjectToDelete in subjectsInObs:
|
|
1479
|
+
if (
|
|
1480
|
+
dialog.MessageDialog(
|
|
1481
|
+
cfg.programName,
|
|
1482
|
+
"The subject to remove is used in observations!",
|
|
1483
|
+
[cfg.REMOVE, cfg.CANCEL],
|
|
1484
|
+
)
|
|
1485
|
+
== cfg.REMOVE
|
|
1486
|
+
):
|
|
1487
|
+
flagDel = True
|
|
1488
|
+
else:
|
|
1489
|
+
# code not used
|
|
1490
|
+
flagDel = True
|
|
1491
|
+
else:
|
|
1492
|
+
flagDel = True
|
|
1493
|
+
|
|
1494
|
+
if flagDel:
|
|
1495
|
+
self.twSubjects.removeRow(self.twSubjects.selectedIndexes()[0].row())
|
|
1496
|
+
|
|
1497
|
+
self.twSubjects_cellChanged(0, 0)
|
|
1498
|
+
|
|
1499
|
+
def remove_all_subjects(self):
|
|
1500
|
+
"""
|
|
1501
|
+
remove all subjects.
|
|
1502
|
+
Verify if they are used in observations
|
|
1503
|
+
"""
|
|
1504
|
+
|
|
1505
|
+
if not self.twSubjects.rowCount():
|
|
1506
|
+
return
|
|
1507
|
+
|
|
1508
|
+
if dialog.MessageDialog(cfg.programName, "Remove all subjects?", [cfg.YES, cfg.CANCEL]) != cfg.YES:
|
|
1509
|
+
return
|
|
1510
|
+
|
|
1511
|
+
# delete ethogram rows without behavior code
|
|
1512
|
+
for r in range(self.twSubjects.rowCount() - 1, -1, -1):
|
|
1513
|
+
if not self.twSubjects.item(r, 1).text(): # no name
|
|
1514
|
+
self.twSubjects.removeRow(r)
|
|
1515
|
+
|
|
1516
|
+
# extract all subjects names to delete
|
|
1517
|
+
namesToDelete: list = []
|
|
1518
|
+
row_mem: dict = {}
|
|
1519
|
+
for r in range(self.twSubjects.rowCount() - 1, -1, -1):
|
|
1520
|
+
if self.twSubjects.item(r, 1).text():
|
|
1521
|
+
namesToDelete.append(self.twSubjects.item(r, 1).text())
|
|
1522
|
+
row_mem[self.twSubjects.item(r, 1).text()] = r
|
|
1523
|
+
|
|
1524
|
+
# extract all subjects name used in observations
|
|
1525
|
+
namesInObs: list = []
|
|
1526
|
+
for obs in self.pj[cfg.OBSERVATIONS]:
|
|
1527
|
+
events = self.pj[cfg.OBSERVATIONS][obs][cfg.EVENTS]
|
|
1528
|
+
for event in events:
|
|
1529
|
+
namesInObs.append(event[cfg.EVENT_SUBJECT_FIELD_IDX])
|
|
1530
|
+
|
|
1531
|
+
flag_force: bool = False
|
|
1532
|
+
for nameToDelete in namesToDelete:
|
|
1533
|
+
# if name to delete used in obs ask confirmation
|
|
1534
|
+
if nameToDelete in namesInObs and not flag_force:
|
|
1535
|
+
response = dialog.MessageDialog(
|
|
1536
|
+
cfg.programName,
|
|
1537
|
+
f"The subject <b>{nameToDelete}</b> is used in observations!",
|
|
1538
|
+
["Force removing of all subjects", cfg.REMOVE, cfg.CANCEL],
|
|
1539
|
+
)
|
|
1540
|
+
if response == "Force removing of all subjects":
|
|
1541
|
+
flag_force = True
|
|
1542
|
+
self.twSubjects.removeRow(row_mem[nameToDelete])
|
|
1543
|
+
|
|
1544
|
+
if response == cfg.REMOVE:
|
|
1545
|
+
self.twSubjects.removeRow(row_mem[nameToDelete])
|
|
1546
|
+
else: # remove without asking
|
|
1547
|
+
self.twSubjects.removeRow(row_mem[nameToDelete])
|
|
1548
|
+
|
|
1549
|
+
self.twSubjects_cellChanged(0, 0)
|
|
1550
|
+
|
|
1551
|
+
def twSubjects_cellChanged(self, row: int, column: int) -> None:
|
|
1552
|
+
"""
|
|
1553
|
+
check if subject not unique
|
|
1554
|
+
"""
|
|
1555
|
+
|
|
1556
|
+
subjects: list = []
|
|
1557
|
+
"""keys: list = []"""
|
|
1558
|
+
self.lbSubjectsState.setText("")
|
|
1559
|
+
|
|
1560
|
+
for r in range(self.twSubjects.rowCount()):
|
|
1561
|
+
# check key
|
|
1562
|
+
if self.twSubjects.item(r, 0):
|
|
1563
|
+
# check key length
|
|
1564
|
+
if (
|
|
1565
|
+
self.twSubjects.item(r, 0).text().upper() not in list(cfg.function_keys.values())
|
|
1566
|
+
and len(self.twSubjects.item(r, 0).text()) > 1
|
|
1567
|
+
):
|
|
1568
|
+
self.lbSubjectsState.setText(
|
|
1569
|
+
(
|
|
1570
|
+
f'<font color="red">Error on key {self.twSubjects.item(r, 0).text()} for subject!</font>'
|
|
1571
|
+
"The key is too long (keys must be of one character"
|
|
1572
|
+
" except for function keys _F1, F2..._)"
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
return
|
|
1576
|
+
|
|
1577
|
+
# control of duplicated key removed 2024-01-29
|
|
1578
|
+
"""
|
|
1579
|
+
if self.twSubjects.item(r, 0).text() in keys:
|
|
1580
|
+
self.lbSubjectsState.setText(f'<font color="red">Key duplicated at row # {r + 1}</font>')
|
|
1581
|
+
else:
|
|
1582
|
+
if self.twSubjects.item(r, 0).text():
|
|
1583
|
+
keys.append(self.twSubjects.item(r, 0).text())
|
|
1584
|
+
"""
|
|
1585
|
+
|
|
1586
|
+
# check subject
|
|
1587
|
+
if self.twSubjects.item(r, 1):
|
|
1588
|
+
if self.twSubjects.item(r, 1).text() in subjects:
|
|
1589
|
+
self.lbSubjectsState.setText(f'<font color="red">Subject duplicated at row # {r + 1}</font>')
|
|
1590
|
+
else:
|
|
1591
|
+
if self.twSubjects.item(r, 1).text():
|
|
1592
|
+
subjects.append(self.twSubjects.item(r, 1).text())
|
|
1593
|
+
|
|
1594
|
+
def twVariables_cellClicked(self, row, column):
|
|
1595
|
+
"""
|
|
1596
|
+
check if variable default values are compatible with variable type
|
|
1597
|
+
"""
|
|
1598
|
+
|
|
1599
|
+
self.selected_twvariables_row = row
|
|
1600
|
+
logging.debug(f"selected row: {self.selected_twvariables_row}")
|
|
1601
|
+
|
|
1602
|
+
if self.selected_twvariables_row == -1:
|
|
1603
|
+
for widget in (
|
|
1604
|
+
self.leLabel,
|
|
1605
|
+
self.leDescription,
|
|
1606
|
+
self.cbType,
|
|
1607
|
+
self.lePredefined,
|
|
1608
|
+
self.dte_default_date,
|
|
1609
|
+
self.leSetValues,
|
|
1610
|
+
):
|
|
1611
|
+
widget.setEnabled(False)
|
|
1612
|
+
self.leLabel.setText("")
|
|
1613
|
+
self.leDescription.setText("")
|
|
1614
|
+
self.lePredefined.setText("")
|
|
1615
|
+
self.leSetValues.setText("")
|
|
1616
|
+
|
|
1617
|
+
self.cbType.clear()
|
|
1618
|
+
return
|
|
1619
|
+
|
|
1620
|
+
# enable widget for indep var setting
|
|
1621
|
+
for widget in (
|
|
1622
|
+
self.leLabel,
|
|
1623
|
+
self.leDescription,
|
|
1624
|
+
self.cbType,
|
|
1625
|
+
self.lePredefined,
|
|
1626
|
+
self.dte_default_date,
|
|
1627
|
+
self.leSetValues,
|
|
1628
|
+
):
|
|
1629
|
+
widget.setEnabled(True)
|
|
1630
|
+
|
|
1631
|
+
self.leLabel.setText(self.twVariables.item(row, 0).text())
|
|
1632
|
+
self.leDescription.setText(self.twVariables.item(row, 1).text())
|
|
1633
|
+
self.lePredefined.setText(self.twVariables.item(row, 3).text())
|
|
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))
|
|
1642
|
+
|
|
1643
|
+
self.cbType.clear()
|
|
1644
|
+
self.cbType.addItems(cfg.AVAILABLE_INDEP_VAR_TYPES)
|
|
1645
|
+
self.cbType.setCurrentIndex(cfg.NUMERIC_idx)
|
|
1646
|
+
|
|
1647
|
+
self.cbType.setCurrentIndex(cfg.AVAILABLE_INDEP_VAR_TYPES.index(self.twVariables.item(row, 2).text()))
|
|
1648
|
+
|
|
1649
|
+
def pbCancel_clicked(self):
|
|
1650
|
+
if self.flag_modified:
|
|
1651
|
+
if dialog.MessageDialog("BORIS", "The converters were modified. Are you sure to cancel?", [cfg.CANCEL, cfg.OK]) == cfg.OK:
|
|
1652
|
+
self.reject()
|
|
1653
|
+
else:
|
|
1654
|
+
self.reject()
|
|
1655
|
+
|
|
1656
|
+
def check_ethogram(self) -> dict:
|
|
1657
|
+
"""
|
|
1658
|
+
check ethogram for various parameter
|
|
1659
|
+
returns ethogram dict or {cfg.CANCEL: True} in case of error
|
|
1660
|
+
|
|
1661
|
+
"""
|
|
1662
|
+
# store behaviors
|
|
1663
|
+
missing_data: list = []
|
|
1664
|
+
checked_ethogram: dict = {}
|
|
1665
|
+
|
|
1666
|
+
# Ethogram
|
|
1667
|
+
# coding maps in ethogram
|
|
1668
|
+
|
|
1669
|
+
# check for leading/trailing space in behaviors and modifiers
|
|
1670
|
+
code_with_leading_trailing_spaces, modifiers_with_leading_trailing_spaces = [], []
|
|
1671
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1672
|
+
if (
|
|
1673
|
+
self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text()
|
|
1674
|
+
!= self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text().strip()
|
|
1675
|
+
):
|
|
1676
|
+
code_with_leading_trailing_spaces.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.BEHAVIOR_CODE]).text())
|
|
1677
|
+
|
|
1678
|
+
if self.twBehaviors.item(r, cfg.behavioursFields["modifiers"]).text():
|
|
1679
|
+
try:
|
|
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
|
+
)
|
|
1685
|
+
for k in modifiers_dict:
|
|
1686
|
+
for value in modifiers_dict[k]["values"]:
|
|
1687
|
+
modif_code = value.split(" (")[0]
|
|
1688
|
+
if modif_code.strip() != modif_code:
|
|
1689
|
+
modifiers_with_leading_trailing_spaces.append(modif_code)
|
|
1690
|
+
except Exception:
|
|
1691
|
+
logging.critical("error checking leading/trailing spaces in modifiers")
|
|
1692
|
+
|
|
1693
|
+
remove_leading_trailing_spaces = cfg.NO
|
|
1694
|
+
if code_with_leading_trailing_spaces:
|
|
1695
|
+
remove_leading_trailing_spaces = dialog.MessageDialog(
|
|
1696
|
+
cfg.programName,
|
|
1697
|
+
(
|
|
1698
|
+
"<b>Warning!</b> Some leading and/or trailing spaces are present"
|
|
1699
|
+
" in the following behaviors code(s):<br>"
|
|
1700
|
+
"<b>"
|
|
1701
|
+
f"{'<br>'.join([util.replace_leading_trailing_chars(x, ' ', '█') for x in code_with_leading_trailing_spaces])}"
|
|
1702
|
+
"</b><br><br>"
|
|
1703
|
+
"Do you want to remove the leading and trailing spaces (visualized as black boxes) from behaviors?<br><br>"
|
|
1704
|
+
"""<font color="red"><b>Be careful with this option"""
|
|
1705
|
+
""" if you have already done observations!</b></font>"""
|
|
1706
|
+
),
|
|
1707
|
+
[cfg.YES, cfg.NO, cfg.CANCEL],
|
|
1708
|
+
)
|
|
1709
|
+
if remove_leading_trailing_spaces == cfg.CANCEL:
|
|
1710
|
+
return {cfg.CANCEL: True}
|
|
1711
|
+
|
|
1712
|
+
remove_leading_trailing_spaces_in_modifiers = cfg.NO
|
|
1713
|
+
if modifiers_with_leading_trailing_spaces:
|
|
1714
|
+
remove_leading_trailing_spaces_in_modifiers = dialog.MessageDialog(
|
|
1715
|
+
cfg.programName,
|
|
1716
|
+
(
|
|
1717
|
+
"<b>Warning!</b> Some leading and/or trailing spaces are present"
|
|
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>"
|
|
1721
|
+
"""<font color="red"><b>Be careful with this option"""
|
|
1722
|
+
""" if you have already done observations!</b></font>"""
|
|
1723
|
+
),
|
|
1724
|
+
(cfg.YES, cfg.NO, cfg.CANCEL),
|
|
1725
|
+
)
|
|
1726
|
+
if remove_leading_trailing_spaces_in_modifiers == cfg.CANCEL:
|
|
1727
|
+
return {cfg.CANCEL: True}
|
|
1728
|
+
|
|
1729
|
+
codingMapsList = []
|
|
1730
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
1731
|
+
row = {}
|
|
1732
|
+
for field in cfg.behavioursFields:
|
|
1733
|
+
if self.twBehaviors.item(r, cfg.behavioursFields[field]):
|
|
1734
|
+
# check for | char in code
|
|
1735
|
+
if field == cfg.BEHAVIOR_CODE and "|" in self.twBehaviors.item(r, cfg.behavioursFields[field]).text():
|
|
1736
|
+
QMessageBox.warning(
|
|
1737
|
+
self,
|
|
1738
|
+
cfg.programName,
|
|
1739
|
+
(
|
|
1740
|
+
"The pipe (|) character is not allowed in code "
|
|
1741
|
+
f"<b>{self.twBehaviors.item(r, cfg.behavioursFields[field]).text()}</b> !"
|
|
1742
|
+
),
|
|
1743
|
+
)
|
|
1744
|
+
return {cfg.CANCEL: True}
|
|
1745
|
+
|
|
1746
|
+
if remove_leading_trailing_spaces == cfg.YES:
|
|
1747
|
+
row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text().strip()
|
|
1748
|
+
else:
|
|
1749
|
+
row[field] = self.twBehaviors.item(r, cfg.behavioursFields[field]).text()
|
|
1750
|
+
|
|
1751
|
+
if field == "modifiers" and row["modifiers"]:
|
|
1752
|
+
if remove_leading_trailing_spaces_in_modifiers == cfg.YES:
|
|
1753
|
+
try:
|
|
1754
|
+
modifiers_dict = json.loads(row["modifiers"]) if row["modifiers"] else {}
|
|
1755
|
+
for k in modifiers_dict:
|
|
1756
|
+
for idx, value in enumerate(modifiers_dict[k]["values"]):
|
|
1757
|
+
modif_code = value.split(" (")[0]
|
|
1758
|
+
|
|
1759
|
+
modifiers_dict[k]["values"][idx] = modifiers_dict[k]["values"][idx].replace(
|
|
1760
|
+
modif_code, modif_code.strip()
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
row["modifiers"] = dict(modifiers_dict)
|
|
1764
|
+
except Exception:
|
|
1765
|
+
logging.critical("Error removing leading/trailing spaces in modifiers")
|
|
1766
|
+
|
|
1767
|
+
QMessageBox.critical(self, cfg.programName, "Error removing leading/trailing spaces in modifiers")
|
|
1768
|
+
|
|
1769
|
+
else:
|
|
1770
|
+
row["modifiers"] = json.loads(row["modifiers"]) if row["modifiers"] else {}
|
|
1771
|
+
else:
|
|
1772
|
+
row[field] = ""
|
|
1773
|
+
|
|
1774
|
+
if (row["type"]) and (row[cfg.BEHAVIOR_CODE]):
|
|
1775
|
+
checked_ethogram[str(len(checked_ethogram))] = row
|
|
1776
|
+
else:
|
|
1777
|
+
missing_data.append(str(r + 1))
|
|
1778
|
+
|
|
1779
|
+
if self.twBehaviors.item(r, cfg.behavioursFields["coding map"]).text():
|
|
1780
|
+
codingMapsList.append(self.twBehaviors.item(r, cfg.behavioursFields["coding map"]).text())
|
|
1781
|
+
|
|
1782
|
+
# remove coding map from project if not in ethogram
|
|
1783
|
+
cmToDelete = []
|
|
1784
|
+
for cm in self.pj[cfg.CODING_MAP]:
|
|
1785
|
+
if cm not in codingMapsList:
|
|
1786
|
+
cmToDelete.append(cm)
|
|
1787
|
+
|
|
1788
|
+
for cm in cmToDelete:
|
|
1789
|
+
del self.pj[cfg.CODING_MAP][cm]
|
|
1790
|
+
|
|
1791
|
+
if missing_data:
|
|
1792
|
+
QMessageBox.warning(self, cfg.programName, f"Missing data in ethogram at row {','.join(missing_data)} !")
|
|
1793
|
+
return {cfg.CANCEL: True}
|
|
1794
|
+
|
|
1795
|
+
# check if behavior belong to category that is not in categories list
|
|
1796
|
+
missing_behavior_category: list = []
|
|
1797
|
+
for idx in checked_ethogram:
|
|
1798
|
+
if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
|
|
1799
|
+
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
|
|
1800
|
+
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
|
|
1801
|
+
missing_behavior_category.append(
|
|
1802
|
+
(checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
|
|
1803
|
+
)
|
|
1804
|
+
if missing_behavior_category:
|
|
1805
|
+
response = dialog.MessageDialog(
|
|
1806
|
+
f"{cfg.programName} - Behavioral categories",
|
|
1807
|
+
(
|
|
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>"
|
|
1811
|
+
),
|
|
1812
|
+
["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
|
|
1813
|
+
)
|
|
1814
|
+
if response == "Add behavioral category/ies":
|
|
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
|
+
|
|
1830
|
+
if response == cfg.CANCEL:
|
|
1831
|
+
return {cfg.CANCEL: True}
|
|
1832
|
+
|
|
1833
|
+
# delete coding maps loaded in pj and not cited in ethogram
|
|
1834
|
+
return checked_ethogram
|
|
1835
|
+
|
|
1836
|
+
def pbOK_clicked(self):
|
|
1837
|
+
"""
|
|
1838
|
+
verify project configuration
|
|
1839
|
+
"""
|
|
1840
|
+
|
|
1841
|
+
if self.lbObservationsState.text():
|
|
1842
|
+
QMessageBox.warning(self, cfg.programName, self.lbObservationsState.text())
|
|
1843
|
+
return
|
|
1844
|
+
|
|
1845
|
+
if self.lbSubjectsState.text():
|
|
1846
|
+
QMessageBox.warning(self, cfg.programName, self.lbSubjectsState.text())
|
|
1847
|
+
return
|
|
1848
|
+
|
|
1849
|
+
self.pj[cfg.PROJECT_NAME] = self.leProjectName.text().strip()
|
|
1850
|
+
self.pj[cfg.PROJECT_DATE] = self.dteDate.dateTime().toString(Qt.ISODate)
|
|
1851
|
+
self.pj[cfg.PROJECT_DESCRIPTION] = self.teDescription.toPlainText()
|
|
1852
|
+
|
|
1853
|
+
# time format
|
|
1854
|
+
if self.rbSeconds.isChecked():
|
|
1855
|
+
self.pj[cfg.TIME_FORMAT] = cfg.S
|
|
1856
|
+
if self.rbHMS.isChecked():
|
|
1857
|
+
self.pj[cfg.TIME_FORMAT] = cfg.HHMMSS
|
|
1858
|
+
|
|
1859
|
+
# store subjects
|
|
1860
|
+
self.subjects_conf: dict = {}
|
|
1861
|
+
|
|
1862
|
+
# check for leading/trailing spaces in subjects names
|
|
1863
|
+
subjects_name_with_leading_trailing_spaces = ""
|
|
1864
|
+
for row in range(self.twSubjects.rowCount()):
|
|
1865
|
+
if self.twSubjects.item(row, 1):
|
|
1866
|
+
if self.twSubjects.item(row, 1).text() != self.twSubjects.item(row, 1).text().strip():
|
|
1867
|
+
subjects_name_with_leading_trailing_spaces += f'"{self.twSubjects.item(row, 1).text()}" '
|
|
1868
|
+
|
|
1869
|
+
remove_leading_trailing_spaces = cfg.NO
|
|
1870
|
+
if subjects_name_with_leading_trailing_spaces:
|
|
1871
|
+
remove_leading_trailing_spaces = dialog.MessageDialog(
|
|
1872
|
+
cfg.programName,
|
|
1873
|
+
(
|
|
1874
|
+
"Attention! Some leading and/or trailing spaces are present in the following <b>subject name(s)</b>:<br>"
|
|
1875
|
+
f"<b>{subjects_name_with_leading_trailing_spaces}</b><br><br>"
|
|
1876
|
+
"Do you want to remove the leading and trailing spaces?<br><br>"
|
|
1877
|
+
'<font color="red"><b>Be careful with this option'
|
|
1878
|
+
" if you have already done observations!</b></font>"
|
|
1879
|
+
),
|
|
1880
|
+
[cfg.YES, cfg.NO],
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
# check subjects
|
|
1884
|
+
for row in range(self.twSubjects.rowCount()):
|
|
1885
|
+
# check key
|
|
1886
|
+
key: str = ""
|
|
1887
|
+
if self.twSubjects.item(row, 0):
|
|
1888
|
+
key = self.twSubjects.item(row, 0).text()
|
|
1889
|
+
|
|
1890
|
+
# check subject name
|
|
1891
|
+
if self.twSubjects.item(row, 1):
|
|
1892
|
+
if remove_leading_trailing_spaces == cfg.YES:
|
|
1893
|
+
subjectName = self.twSubjects.item(row, 1).text().strip()
|
|
1894
|
+
else:
|
|
1895
|
+
subjectName = self.twSubjects.item(row, 1).text()
|
|
1896
|
+
|
|
1897
|
+
# check if subject name is empty
|
|
1898
|
+
if subjectName == "":
|
|
1899
|
+
QMessageBox.warning(self, cfg.programName, f"The subject name can not be empty (check row #{row + 1}).")
|
|
1900
|
+
return
|
|
1901
|
+
|
|
1902
|
+
if "|" in subjectName:
|
|
1903
|
+
QMessageBox.warning(
|
|
1904
|
+
self,
|
|
1905
|
+
cfg.programName,
|
|
1906
|
+
f"The pipe (|) character is not allowed in subject name <b>{subjectName}</b>",
|
|
1907
|
+
)
|
|
1908
|
+
return
|
|
1909
|
+
else:
|
|
1910
|
+
QMessageBox.warning(self, cfg.programName, f"Missing subject name in subjects configuration at row #{row + 1}")
|
|
1911
|
+
return
|
|
1912
|
+
|
|
1913
|
+
# description
|
|
1914
|
+
subjectDescription: str = ""
|
|
1915
|
+
if self.twSubjects.item(row, 2):
|
|
1916
|
+
subjectDescription = self.twSubjects.item(row, 2).text().strip()
|
|
1917
|
+
|
|
1918
|
+
self.subjects_conf[str(len(self.subjects_conf))] = {
|
|
1919
|
+
"key": key,
|
|
1920
|
+
"name": subjectName,
|
|
1921
|
+
"description": subjectDescription,
|
|
1922
|
+
}
|
|
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
|
+
|
|
1943
|
+
self.pj[cfg.SUBJECTS] = dict(self.subjects_conf)
|
|
1944
|
+
|
|
1945
|
+
# check ethogram
|
|
1946
|
+
r = dict(self.check_ethogram())
|
|
1947
|
+
if cfg.CANCEL in r:
|
|
1948
|
+
return
|
|
1949
|
+
self.pj[cfg.ETHOGRAM] = dict(r)
|
|
1950
|
+
|
|
1951
|
+
# independent variables
|
|
1952
|
+
r, msg = self.check_indep_var_config()
|
|
1953
|
+
if not r:
|
|
1954
|
+
QMessageBox.warning(self, cfg.programName + " - Independent variables error", msg)
|
|
1955
|
+
return
|
|
1956
|
+
|
|
1957
|
+
self.indVar = {}
|
|
1958
|
+
for r in range(self.twVariables.rowCount()):
|
|
1959
|
+
row = {}
|
|
1960
|
+
for idx, field in enumerate(cfg.tw_indVarFields):
|
|
1961
|
+
if self.twVariables.item(r, idx):
|
|
1962
|
+
# check if label is empty
|
|
1963
|
+
if field == "label" and self.twVariables.item(r, idx).text() == "":
|
|
1964
|
+
QMessageBox.warning(
|
|
1965
|
+
self,
|
|
1966
|
+
cfg.programName,
|
|
1967
|
+
f"The label of an indipendent variable can not be empty (check row #{r + 1}).",
|
|
1968
|
+
)
|
|
1969
|
+
return
|
|
1970
|
+
|
|
1971
|
+
row[field] = self.twVariables.item(r, idx).text().strip()
|
|
1972
|
+
else:
|
|
1973
|
+
row[field] = ""
|
|
1974
|
+
|
|
1975
|
+
self.indVar[str(len(self.indVar))] = row
|
|
1976
|
+
|
|
1977
|
+
self.pj[cfg.INDEPENDENT_VARIABLES] = dict(self.indVar)
|
|
1978
|
+
|
|
1979
|
+
# converters
|
|
1980
|
+
converters: dict = {}
|
|
1981
|
+
for row in range(self.tw_converters.rowCount()):
|
|
1982
|
+
converters[self.tw_converters.item(row, 0).text()] = {
|
|
1983
|
+
"name": self.tw_converters.item(row, 0).text(),
|
|
1984
|
+
"description": self.tw_converters.item(row, 1).text(),
|
|
1985
|
+
"code": self.tw_converters.item(row, 2).text().replace("@", "\n"),
|
|
1986
|
+
}
|
|
1987
|
+
self.pj[cfg.CONVERTERS] = dict(converters)
|
|
1988
|
+
|
|
1989
|
+
self.accept()
|
|
1990
|
+
|
|
1991
|
+
def load_converters_in_table(self):
|
|
1992
|
+
"""
|
|
1993
|
+
load converters in table
|
|
1994
|
+
"""
|
|
1995
|
+
self.tw_converters.setRowCount(0)
|
|
1996
|
+
|
|
1997
|
+
for converter in sorted(self.converters.keys()):
|
|
1998
|
+
self.tw_converters.setRowCount(self.tw_converters.rowCount() + 1)
|
|
1999
|
+
self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 0, QTableWidgetItem(converter)) # id / name
|
|
2000
|
+
self.tw_converters.setItem(self.tw_converters.rowCount() - 1, 1, QTableWidgetItem(self.converters[converter]["description"]))
|
|
2001
|
+
self.tw_converters.setItem(
|
|
2002
|
+
self.tw_converters.rowCount() - 1,
|
|
2003
|
+
2,
|
|
2004
|
+
QTableWidgetItem(self.converters[converter]["code"].replace("\n", "@")),
|
|
2005
|
+
)
|
|
2006
|
+
|
|
2007
|
+
[self.tw_converters.resizeColumnToContents(idx) for idx in [0, 1]]
|