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.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1096 @@
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 logging
24
+ import urllib
25
+ import json
26
+ import pathlib as pl
27
+ import pandas as pd
28
+ import tablib
29
+ import pickle
30
+
31
+ from PySide6.QtCore import Qt
32
+ from PySide6.QtGui import QFont
33
+ from PySide6.QtWidgets import QApplication, QFileDialog, QListWidgetItem, QMessageBox, QTableWidgetItem
34
+
35
+
36
+ from . import config as cfg
37
+ from . import dialog, param_panel, project_functions, export_observation
38
+ from . import utilities as util
39
+
40
+
41
+ def export_project_as_pickle_object(pj: dict) -> None:
42
+ """
43
+ export the project dictionary as a pickle file
44
+ """
45
+ file_name, _ = QFileDialog().getSaveFileName(None, "Export project as pickle file", "", "All files (*)")
46
+ if not file_name:
47
+ return
48
+ try:
49
+ with open(file_name, "wb") as f_out:
50
+ pickle.dump(pj, f_out)
51
+ except Exception:
52
+ QMessageBox.critical(
53
+ None,
54
+ cfg.programName,
55
+ "Error during file saving.",
56
+ QMessageBox.Ok | QMessageBox.Default,
57
+ QMessageBox.NoButton,
58
+ )
59
+
60
+
61
+ def export_ethogram(self) -> None:
62
+ """
63
+ export ethogram in various format
64
+ """
65
+ extended_file_formats: list = [
66
+ "BORIS project file (*.boris)",
67
+ "Tab Separated Values (*.tsv)",
68
+ "Comma Separated Values (*.csv)",
69
+ "Open Document Spreadsheet ODS (*.ods)",
70
+ "Microsoft Excel Spreadsheet XLSX (*.xlsx)",
71
+ "Legacy Microsoft Excel Spreadsheet XLS (*.xls)",
72
+ "HTML (*.html)",
73
+ ]
74
+ file_formats: list = ["boris", cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
75
+
76
+ filediag_func = QFileDialog().getSaveFileName
77
+
78
+ file_name, filter_ = filediag_func(self, "Export ethogram", "", ";;".join(extended_file_formats))
79
+ if not file_name:
80
+ return
81
+
82
+ output_format: str = file_formats[extended_file_formats.index(filter_)]
83
+ if pl.Path(file_name).suffix != "." + output_format:
84
+ file_name = str(pl.Path(file_name)) + "." + output_format
85
+
86
+ if output_format == "boris":
87
+ r = self.check_ethogram()
88
+ if cfg.CANCEL in r:
89
+ return
90
+ pj = dict(cfg.EMPTY_PROJECT)
91
+ pj[cfg.ETHOGRAM] = dict(r)
92
+
93
+ # behavioral categories
94
+ pj[cfg.BEHAVIORAL_CATEGORIES] = list(self.pj[cfg.BEHAVIORAL_CATEGORIES])
95
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict(self.pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
96
+
97
+ # project file indentation
98
+ file_indentation = self.config_param.get(cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
99
+ try:
100
+ with open(file_name, "w") as f_out:
101
+ f_out.write(json.dumps(pj, indent=file_indentation))
102
+ except Exception:
103
+ QMessageBox.critical(
104
+ None,
105
+ cfg.programName,
106
+ "Error during file saving.",
107
+ QMessageBox.Ok | QMessageBox.Default,
108
+ QMessageBox.NoButton,
109
+ )
110
+
111
+ else:
112
+ ethogram_data = tablib.Dataset()
113
+ ethogram_data.title = "Ethogram"
114
+ if self.leProjectName.text():
115
+ ethogram_data.title = f"Ethogram of {self.leProjectName.text()} project"
116
+
117
+ ethogram_data.headers = [
118
+ "Behavior code",
119
+ "Behavior type",
120
+ "Description",
121
+ "Key",
122
+ "Color",
123
+ "Behavioral category",
124
+ "Excluded behaviors",
125
+ "Modifiers",
126
+ "Modifiers (JSON)",
127
+ ]
128
+
129
+ for r in range(self.twBehaviors.rowCount()):
130
+ row: list = []
131
+ for field in ("code", cfg.TYPE, "description", "key", cfg.COLOR, "category", "excluded"):
132
+ row.append(self.twBehaviors.item(r, cfg.behavioursFields[field]).text())
133
+
134
+ # modifiers
135
+ if self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text():
136
+ # modifiers a string
137
+ modifiers_dict = json.loads(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
138
+ modifiers_list = []
139
+ for key in modifiers_dict:
140
+ values = ",".join(modifiers_dict[key]["values"])
141
+ modifiers_list.append(f"{modifiers_dict[key]['name']}:{values}")
142
+ row.append(";".join(modifiers_list))
143
+ # modifiers as JSON
144
+ row.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
145
+ else:
146
+ # modifiers a string
147
+ row.append("")
148
+ # modifiers as JSON
149
+ row.append("")
150
+
151
+ ethogram_data.append(row)
152
+
153
+ ok, msg = export_observation.dataset_write(ethogram_data, file_name, output_format)
154
+ if not ok:
155
+ QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
156
+
157
+
158
+ def export_subjects(self) -> None:
159
+ """
160
+ export the subjetcs list in various format
161
+ """
162
+ extended_file_formats: list = [
163
+ cfg.TSV,
164
+ cfg.CSV,
165
+ cfg.ODS,
166
+ cfg.XLSX,
167
+ cfg.XLS,
168
+ cfg.HTML,
169
+ ]
170
+ file_formats: list = [cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
171
+
172
+ filediag_func = QFileDialog().getSaveFileName
173
+
174
+ file_name, filter_ = filediag_func(self, "Export the subjects list", "", ";;".join(extended_file_formats))
175
+ if not file_name:
176
+ return
177
+
178
+ outputFormat = file_formats[extended_file_formats.index(filter_)]
179
+ if pl.Path(file_name).suffix != "." + outputFormat:
180
+ file_name = str(pl.Path(file_name)) + "." + outputFormat
181
+
182
+ subjects_data = tablib.Dataset()
183
+ subjects_data.title = "Subjects"
184
+ if self.leProjectName.text():
185
+ subjects_data.title = f"Subjects defined in the {self.leProjectName.text()} project"
186
+
187
+ subjects_data.headers: list = [
188
+ "Key",
189
+ "Subject name",
190
+ "Description",
191
+ ]
192
+
193
+ for r in range(self.twSubjects.rowCount()):
194
+ row: list = []
195
+ for idx, _ in enumerate(("Key", "Subject name", "Description")):
196
+ row.append(self.twSubjects.item(r, idx).text())
197
+
198
+ subjects_data.append(row)
199
+
200
+ ok, msg = export_observation.dataset_write(subjects_data, file_name, outputFormat)
201
+ if not ok:
202
+ QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
203
+
204
+
205
+ def select_behaviors(
206
+ title: str = "Record value from external data file",
207
+ text: str = "Behaviors",
208
+ behavioral_categories: list = [],
209
+ ethogram: dict = {},
210
+ behavior_type=[cfg.STATE_EVENT, cfg.POINT_EVENT],
211
+ ) -> list:
212
+ """
213
+ allow user to select behaviors to import
214
+
215
+ Args:
216
+ title (str): title of dialog box
217
+ text (str): text of dialog box
218
+ behavioral_categories (list): behavioral categories
219
+ ethogram (dict): ethogram
220
+
221
+ """
222
+
223
+ paramPanelWindow = param_panel.Param_panel()
224
+ paramPanelWindow.resize(800, 600)
225
+ paramPanelWindow.setWindowTitle(title)
226
+ paramPanelWindow.lbBehaviors.setText(text)
227
+ for w in (
228
+ paramPanelWindow.lwSubjects,
229
+ paramPanelWindow.pbSelectAllSubjects,
230
+ paramPanelWindow.pbUnselectAllSubjects,
231
+ paramPanelWindow.pbReverseSubjectsSelection,
232
+ paramPanelWindow.lbSubjects,
233
+ paramPanelWindow.cbIncludeModifiers,
234
+ paramPanelWindow.cbExcludeBehaviors,
235
+ paramPanelWindow.frm_time,
236
+ paramPanelWindow.frm_time_bin_size,
237
+ ):
238
+ w.setVisible(False)
239
+
240
+ if behavioral_categories:
241
+ categories = behavioral_categories
242
+ # check if behavior not included in a category
243
+ if "" in [ethogram[idx][cfg.BEHAVIOR_CATEGORY] for idx in ethogram if cfg.BEHAVIOR_CATEGORY in ethogram[idx]]:
244
+ categories += [""]
245
+ else:
246
+ categories = ["###no category###"]
247
+
248
+ for category in categories:
249
+ if category != "###no category###":
250
+ if category == "":
251
+ paramPanelWindow.item = QListWidgetItem("No category")
252
+ paramPanelWindow.item.setData(34, "No category")
253
+ else:
254
+ paramPanelWindow.item = QListWidgetItem(category)
255
+ paramPanelWindow.item.setData(34, category)
256
+
257
+ font = QFont()
258
+ font.setBold(True)
259
+ paramPanelWindow.item.setFont(font)
260
+ paramPanelWindow.item.setData(33, "category")
261
+ paramPanelWindow.item.setData(35, False)
262
+
263
+ paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
264
+
265
+ # check if behavior type must be shown
266
+ for behavior in [ethogram[x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(ethogram)]:
267
+ if (categories == ["###no category###"]) or (
268
+ behavior
269
+ in [
270
+ ethogram[x][cfg.BEHAVIOR_CODE]
271
+ for x in ethogram
272
+ if cfg.BEHAVIOR_CATEGORY in ethogram[x] and ethogram[x][cfg.BEHAVIOR_CATEGORY] == category
273
+ ]
274
+ ):
275
+ paramPanelWindow.item = QListWidgetItem(behavior)
276
+ paramPanelWindow.item.setCheckState(Qt.Unchecked)
277
+
278
+ if category != "###no category###":
279
+ paramPanelWindow.item.setData(33, "behavior")
280
+ if category == "":
281
+ paramPanelWindow.item.setData(34, "No category")
282
+ else:
283
+ paramPanelWindow.item.setData(34, category)
284
+
285
+ paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
286
+
287
+ if paramPanelWindow.exec_():
288
+ return paramPanelWindow.selectedBehaviors
289
+
290
+ return []
291
+
292
+
293
+ def check_text_file_type(rows: list):
294
+ """
295
+ check text file
296
+ returns separator and number of fields (if unique)
297
+ """
298
+ for separator in "\t,;":
299
+ cs: list = []
300
+ for row in rows:
301
+ cs.append(row.count(separator))
302
+ if len(set(cs)) == 1:
303
+ return separator, cs[0] + 1
304
+ return None, None
305
+
306
+
307
+ def import_ethogram_from_dict(self, project: dict):
308
+ """
309
+ Import behaviors from a BORIS project dictionary
310
+ """
311
+ # import behavioral_categories
312
+ self.pj[cfg.BEHAVIORAL_CATEGORIES] = list(project.get(cfg.BEHAVIORAL_CATEGORIES, []))
313
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = list(project.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
314
+
315
+ # configuration of behaviours
316
+ if not (cfg.ETHOGRAM in project and project[cfg.ETHOGRAM]):
317
+ QMessageBox.warning(self, cfg.programName, "No behaviors configuration found in project")
318
+ return
319
+
320
+ if self.twBehaviors.rowCount():
321
+ response = dialog.MessageDialog(
322
+ cfg.programName,
323
+ ("Some behaviors are already configured. Do you want to append behaviors or replace them?"),
324
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
325
+ )
326
+ if response == cfg.REPLACE:
327
+ self.twBehaviors.setRowCount(0)
328
+ self.twBehaviors_cellChanged(0, 0)
329
+ if response == cfg.CANCEL:
330
+ return
331
+
332
+ behaviors_to_import = select_behaviors(
333
+ title="Select the behaviors to import",
334
+ text="Behaviors",
335
+ behavioral_categories=list(project.get(cfg.BEHAVIORAL_CATEGORIES, [])),
336
+ ethogram=dict(project[cfg.ETHOGRAM]),
337
+ behavior_type=[cfg.STATE_EVENT, cfg.POINT_EVENT],
338
+ )
339
+
340
+ for i in util.sorted_keys(project[cfg.ETHOGRAM]):
341
+ if project[cfg.ETHOGRAM][i][cfg.BEHAVIOR_CODE] not in behaviors_to_import:
342
+ continue
343
+
344
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
345
+
346
+ for field in project[cfg.ETHOGRAM][i]:
347
+ item = QTableWidgetItem()
348
+
349
+ if field == cfg.TYPE:
350
+ item.setText(project[cfg.ETHOGRAM][i][field])
351
+ item.setFlags(Qt.ItemIsEnabled)
352
+ # item.setBackground(QColor(230, 230, 230))
353
+ item.setBackground(self.not_editable_column_color())
354
+
355
+ else:
356
+ if field == cfg.MODIFIERS:
357
+ if isinstance(project[cfg.ETHOGRAM][i][field], str):
358
+ modif_set_dict = {}
359
+ if project[cfg.ETHOGRAM][i][field]:
360
+ modif_set_list = project[cfg.ETHOGRAM][i][field].split("|")
361
+ for modif_set in modif_set_list:
362
+ modif_set_dict[str(len(modif_set_dict))] = {
363
+ "name": "",
364
+ "type": cfg.SINGLE_SELECTION,
365
+ "values": modif_set.split(","),
366
+ }
367
+ project[cfg.ETHOGRAM][i][field] = dict(modif_set_dict)
368
+ else:
369
+ item.setText(json.dumps(project[cfg.ETHOGRAM][i][field]))
370
+ else:
371
+ item.setText(project[cfg.ETHOGRAM][i][field])
372
+
373
+ if field not in cfg.ETHOGRAM_EDITABLE_FIELDS:
374
+ item.setFlags(Qt.ItemIsEnabled)
375
+ # item.setBackground(QColor(230, 230, 230))
376
+ item.setBackground(self.not_editable_column_color())
377
+
378
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
379
+
380
+ self.twBehaviors.resizeColumnsToContents()
381
+
382
+
383
+ def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
384
+ """
385
+ Load pandas dataframe into the twBehaviors table widget
386
+
387
+ Returns:
388
+ int: 0 if no error else error code
389
+ """
390
+
391
+ expected_labels: list = [
392
+ "Behavior code",
393
+ "Behavior type",
394
+ "Description",
395
+ "Key",
396
+ "Behavioral category",
397
+ "Excluded behaviors",
398
+ ]
399
+
400
+ ethogram_header: dict = {
401
+ "code": "Behavior code",
402
+ "description": "Description",
403
+ "key": "Key",
404
+ "color": "Color",
405
+ "category": "Behavioral category",
406
+ "excluded": "Excluded behaviors",
407
+ "modifiers": "modifiers (JSON)",
408
+ }
409
+
410
+ # change all column names to uppercase
411
+ df.columns = df.columns.str.upper()
412
+
413
+ for column in expected_labels:
414
+ if column.upper() not in list(df.columns):
415
+ QMessageBox.warning(
416
+ None,
417
+ cfg.programName,
418
+ (
419
+ f"The {column} column was not found in the file header.<br>"
420
+ "For information the current file header contains the following labels:<br>"
421
+ f"{'<br>'.join(['<b>' + util.replace_leading_trailing_chars(x, ' ', '&#9608;') + '</b>' for x in df.columns])}<br>"
422
+ "<br>"
423
+ "The first row of the spreadsheet must contain the following labels:<br>"
424
+ f"{'<br>'.join(['<b>' + x + '</b>' for x in expected_labels])}<br>"
425
+ "<br>The order is not mandatory."
426
+ ),
427
+ QMessageBox.Ok | QMessageBox.Default,
428
+ QMessageBox.NoButton,
429
+ )
430
+ return 1
431
+
432
+ for _, row in df.iterrows():
433
+ behavior = {"coding map": ""}
434
+ for x in ethogram_header:
435
+ if ethogram_header[x].upper() in row:
436
+ behavior[x] = row[ethogram_header[x].upper()] if str(row[ethogram_header[x].upper()]) != "nan" else ""
437
+ else:
438
+ behavior[x] = ""
439
+
440
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
441
+
442
+ for field_type in cfg.behavioursFields:
443
+ if field_type == cfg.TYPE:
444
+ item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
445
+ # add type combobox
446
+ if cfg.POINT in row["Behavior type".upper()].upper():
447
+ item = QTableWidgetItem(cfg.POINT_EVENT)
448
+ elif cfg.STATE in row["Behavior type".upper()].upper():
449
+ item = QTableWidgetItem(cfg.STATE_EVENT)
450
+ else:
451
+ QMessageBox.critical(
452
+ None,
453
+ cfg.programName,
454
+ f"{row['Behavior code']} has no behavior type (POINT or STATE)",
455
+ QMessageBox.Ok | QMessageBox.Default,
456
+ QMessageBox.NoButton,
457
+ )
458
+ return 2
459
+
460
+ else:
461
+ item = QTableWidgetItem(str(behavior[field_type]))
462
+
463
+ if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS:
464
+ item.setFlags(Qt.ItemIsEnabled)
465
+ # item.setBackground(QColor(230, 230, 230))
466
+ item.setBackground(self.not_editable_column_color())
467
+
468
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
469
+
470
+ return 0
471
+
472
+
473
+ def import_behaviors_from_project(self):
474
+ """
475
+ import ethogram from a BORIS project file
476
+ """
477
+ file_name, _ = QFileDialog.getOpenFileName(
478
+ self, "Import behaviors from BORIS project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
479
+ )
480
+ if not file_name:
481
+ return
482
+ _, _, project, _ = project_functions.open_project_json(file_name)
483
+
484
+ import_ethogram_from_dict(self, project)
485
+
486
+
487
+ def import_behaviors_from_text_file(self):
488
+ """
489
+ Import ethogram from text file (CSV or TSV)
490
+ """
491
+
492
+ if self.twBehaviors.rowCount():
493
+ response = dialog.MessageDialog(
494
+ cfg.programName,
495
+ "There are behaviors already configured. Do you want to append behaviors or replace them?",
496
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
497
+ )
498
+ if response == cfg.CANCEL:
499
+ return
500
+
501
+ file_name, _ = QFileDialog.getOpenFileName(
502
+ self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
503
+ )
504
+
505
+ if not file_name:
506
+ return
507
+
508
+ if pl.Path(file_name).suffix.upper() == ".CSV":
509
+ delimiter = ","
510
+ elif pl.Path(file_name).suffix.upper() == ".TSV":
511
+ delimiter = "\t"
512
+ else:
513
+ QMessageBox.warning(
514
+ None,
515
+ cfg.programName,
516
+ ("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
517
+ QMessageBox.Ok | QMessageBox.Default,
518
+ QMessageBox.NoButton,
519
+ )
520
+ return
521
+
522
+ try:
523
+ df = pd.read_csv(file_name, delimiter=delimiter)
524
+ except Exception:
525
+ QMessageBox.warning(
526
+ None,
527
+ cfg.programName,
528
+ ("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
529
+ QMessageBox.Ok | QMessageBox.Default,
530
+ QMessageBox.NoButton,
531
+ )
532
+ return
533
+
534
+ if self.twBehaviors.rowCount() and response == cfg.REPLACE:
535
+ self.twBehaviors.setRowCount(0)
536
+
537
+ load_dataframe_into_behaviors_tablewidget(self, df)
538
+
539
+
540
+ def import_behaviors_from_spreadsheet(self):
541
+ """
542
+ Import behaviors from a spreadsheet file (XLSX)
543
+ """
544
+
545
+ if self.twBehaviors.rowCount():
546
+ response = dialog.MessageDialog(
547
+ cfg.programName,
548
+ "There are behaviors already configured. Do you want to append behaviors or replace them?",
549
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
550
+ )
551
+ if response == cfg.CANCEL:
552
+ return
553
+
554
+ file_name, _ = QFileDialog.getOpenFileName(
555
+ self, "Import behaviors from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
556
+ )
557
+
558
+ if not file_name:
559
+ return
560
+
561
+ if pl.Path(file_name).suffix.upper() == ".XLSX":
562
+ engine = "openpyxl"
563
+ elif pl.Path(file_name).suffix.upper() == ".ODS":
564
+ engine = "odf"
565
+ else:
566
+ QMessageBox.warning(
567
+ None,
568
+ cfg.programName,
569
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
570
+ QMessageBox.Ok | QMessageBox.Default,
571
+ QMessageBox.NoButton,
572
+ )
573
+ return
574
+
575
+ try:
576
+ df = pd.read_excel(file_name, sheet_name=0, engine=engine)
577
+ except Exception:
578
+ QMessageBox.warning(
579
+ None,
580
+ cfg.programName,
581
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
582
+ QMessageBox.Ok | QMessageBox.Default,
583
+ QMessageBox.NoButton,
584
+ )
585
+ return
586
+
587
+ if self.twBehaviors.rowCount() and response == cfg.REPLACE:
588
+ self.twBehaviors.setRowCount(0)
589
+
590
+ load_dataframe_into_behaviors_tablewidget(self, df)
591
+
592
+
593
+ def import_behaviors_from_clipboard(self):
594
+ """
595
+ import ethogram from clipboard
596
+ """
597
+
598
+ cb = QApplication.clipboard()
599
+ cb_text = cb.text()
600
+ if not cb_text:
601
+ QMessageBox.warning(
602
+ None,
603
+ cfg.programName,
604
+ "The clipboard is empty",
605
+ QMessageBox.Ok | QMessageBox.Default,
606
+ QMessageBox.NoButton,
607
+ )
608
+ return
609
+
610
+ if self.twBehaviors.rowCount():
611
+ response = dialog.MessageDialog(
612
+ cfg.programName,
613
+ "Some behaviors are already configured. Do you want to append behaviors or replace them?",
614
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
615
+ )
616
+ if response == cfg.CANCEL:
617
+ return
618
+
619
+ if response == cfg.REPLACE:
620
+ self.twBehaviors.setRowCount(0)
621
+
622
+ cb_text_splitted = cb_text.split("\n")
623
+ while "" in cb_text_splitted:
624
+ cb_text_splitted.remove("")
625
+
626
+ if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1:
627
+ QMessageBox.warning(
628
+ None,
629
+ cfg.programName,
630
+ (
631
+ "The clipboard content does not have a constant number of fields.<br>"
632
+ "From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)"
633
+ ),
634
+ QMessageBox.Ok | QMessageBox.Default,
635
+ QMessageBox.NoButton,
636
+ )
637
+ return
638
+
639
+ for row in cb_text_splitted:
640
+ if set(row.split("\t")) != set([""]):
641
+ behavior = {"type": cfg.DEFAULT_BEHAVIOR_TYPE}
642
+ for idx, field in enumerate(row.split("\t")):
643
+ if idx == 0:
644
+ behavior["type"] = (
645
+ cfg.STATE_EVENT if cfg.STATE in field.upper() else (cfg.POINT_EVENT if cfg.POINT in field.upper() else "")
646
+ )
647
+ if idx == 1:
648
+ behavior["key"] = field.strip() if len(field.strip()) == 1 else ""
649
+ if idx == 2:
650
+ behavior["code"] = field.strip()
651
+ if idx == 3:
652
+ behavior["description"] = field.strip()
653
+ if idx == 4:
654
+ behavior["category"] = field.strip()
655
+
656
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
657
+
658
+ for field_type in cfg.behavioursFields:
659
+ if field_type == cfg.TYPE:
660
+ item = QTableWidgetItem(behavior.get("type", cfg.DEFAULT_BEHAVIOR_TYPE))
661
+ else:
662
+ item = QTableWidgetItem(behavior.get(field_type, ""))
663
+
664
+ if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS: # [TYPE, "excluded", "coding map", "modifiers", "category"]:
665
+ item.setFlags(Qt.ItemIsEnabled)
666
+ # item.setBackground(QColor(230, 230, 230))
667
+ item.setBackground(self.not_editable_column_color())
668
+
669
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
670
+
671
+
672
+ def import_behaviors_from_JWatcher(self):
673
+ """
674
+ import behaviors configuration from JWatcher (GDF file)
675
+ """
676
+
677
+ if self.twBehaviors.rowCount():
678
+ response = dialog.MessageDialog(
679
+ cfg.programName,
680
+ "There are behaviors already configured. Do you want to append behaviors or replace them?",
681
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
682
+ )
683
+ if response == cfg.CANCEL:
684
+ return
685
+
686
+ fileName, _ = QFileDialog().getOpenFileName(self, "Import behaviors from JWatcher", "", "Global Definition File (*.gdf);;All files (*)")
687
+
688
+ if not fileName:
689
+ return
690
+ if self.twBehaviors.rowCount() and response == cfg.REPLACE:
691
+ self.twBehaviors.setRowCount(0)
692
+
693
+ with open(fileName, "r") as f:
694
+ rows = f.readlines()
695
+
696
+ for idx, row in enumerate(rows):
697
+ if row and row[0] == "#":
698
+ continue
699
+
700
+ if "Behavior.name." in row and "=" in row:
701
+ key, code = row.split("=")
702
+ key = key.replace("Behavior.name.", "")
703
+ # read description
704
+ if idx < len(rows) and "Behavior.description." in rows[idx + 1]:
705
+ description = rows[idx + 1].split("=")[-1]
706
+
707
+ behavior = {
708
+ "key": key,
709
+ "code": code,
710
+ "description": description,
711
+ "modifiers": "",
712
+ "excluded": "",
713
+ "coding map": "",
714
+ "category": "",
715
+ }
716
+
717
+ self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
718
+
719
+ for field_type in cfg.behavioursFields:
720
+ if field_type == cfg.TYPE:
721
+ item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
722
+ else:
723
+ item = QTableWidgetItem(behavior[field_type])
724
+
725
+ if field_type in [cfg.TYPE, "excluded", "category", "coding map", "modifiers"]:
726
+ item.setFlags(Qt.ItemIsEnabled)
727
+ # item.setBackground(QColor(230, 230, 230))
728
+ item.setBackground(self.not_editable_column_color())
729
+
730
+ self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
731
+
732
+
733
+ def import_behaviors_from_repository(self):
734
+ """
735
+ import behaviors from the BORIS ethogram repository
736
+ """
737
+
738
+ try:
739
+ ethogram_list = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/ethogram_list.json").read().strip().decode("utf-8")
740
+ except Exception:
741
+ QMessageBox.critical(self, cfg.programName, "An error occured during retrieving the ethogram list from BORIS repository")
742
+ return
743
+
744
+ try:
745
+ ethogram_list_list = json.loads(ethogram_list)
746
+ except Exception:
747
+ QMessageBox.critical(self, cfg.programName, "An error occured during loading ethogram list from BORIS repository")
748
+ return
749
+
750
+ choice_dialog = dialog.ChooseObservationsToImport(
751
+ "Choose the ethogram to import:", sorted([f"{x['species']} by {x['author']}" for x in ethogram_list_list])
752
+ )
753
+ while True:
754
+ if not choice_dialog.exec_():
755
+ return
756
+
757
+ if len(choice_dialog.get_selected_observations()) == 0:
758
+ QMessageBox.critical(self, cfg.programName, "Choose one ethogram")
759
+ continue
760
+
761
+ if len(choice_dialog.get_selected_observations()) > 1:
762
+ QMessageBox.critical(self, cfg.programName, "Choose only one ethogram")
763
+ continue
764
+
765
+ break
766
+
767
+ for x in ethogram_list_list:
768
+ if f"{x['species']} by {x['author']}" == choice_dialog.get_selected_observations()[0]:
769
+ file_name = x["file name"]
770
+ break
771
+
772
+ try:
773
+ boris_project_str = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/{file_name}").read().strip().decode("utf-8")
774
+ except Exception:
775
+ QMessageBox.critical(self, cfg.programName, f"An error occured during retrieving {file_name} from BORIS repository")
776
+ return
777
+ boris_project = json.loads(boris_project_str)
778
+
779
+ import_ethogram_from_dict(self, boris_project)
780
+
781
+
782
+ def load_dataframe_into_subjects_tablewidget(self, df: pd.DataFrame) -> int:
783
+ """
784
+ Load pandas dataframe into the twSubjects table widget
785
+
786
+ Returns:
787
+ int: 0 if no error else error code
788
+
789
+ """
790
+
791
+ expected_labels: list = ["Key", "Subject name", "Description"]
792
+
793
+ # change all column names to uppercase
794
+ df.columns = df.columns.str.upper()
795
+
796
+ for column in expected_labels:
797
+ if column.upper() not in list(df.columns):
798
+ QMessageBox.warning(
799
+ None,
800
+ cfg.programName,
801
+ (
802
+ f"The column {column} was not found in the file header.<br>"
803
+ "The first row of spreadsheet must contain the following labels:<br>"
804
+ "Subject name, Description, Key<br>"
805
+ "The order is not mandatory."
806
+ ),
807
+ QMessageBox.Ok | QMessageBox.Default,
808
+ QMessageBox.NoButton,
809
+ )
810
+ return 1
811
+
812
+ for _, row in df.iterrows():
813
+ self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
814
+
815
+ for idx, field in enumerate(expected_labels):
816
+ self.twSubjects.setItem(
817
+ self.twSubjects.rowCount() - 1,
818
+ idx,
819
+ QTableWidgetItem(str(row[field.upper()]) if str(row[field.upper()]) != "nan" else ""),
820
+ )
821
+
822
+ return 0
823
+
824
+
825
+ def import_subjects_from_clipboard(self):
826
+ """
827
+ import subjects from clipboard
828
+ """
829
+ cb = QApplication.clipboard()
830
+ cb_text = cb.text()
831
+ if not cb_text:
832
+ QMessageBox.warning(
833
+ None,
834
+ cfg.programName,
835
+ "The clipboard is empty",
836
+ QMessageBox.Ok | QMessageBox.Default,
837
+ QMessageBox.NoButton,
838
+ )
839
+ return
840
+
841
+ if self.twSubjects.rowCount():
842
+ response = dialog.MessageDialog(
843
+ cfg.programName,
844
+ "Some subjects are already configured. Do you want to append subjects or replace them?",
845
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
846
+ )
847
+ if response == cfg.CANCEL:
848
+ return
849
+
850
+ if response == cfg.REPLACE:
851
+ self.twSubjects.setRowCount(0)
852
+
853
+ cb_text_splitted = cb_text.split("\n")
854
+
855
+ if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1:
856
+ QMessageBox.warning(
857
+ None,
858
+ cfg.programName,
859
+ (
860
+ "The clipboard content does not have a constant number of fields.<br>"
861
+ "From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)"
862
+ ),
863
+ QMessageBox.Ok | QMessageBox.Default,
864
+ QMessageBox.NoButton,
865
+ )
866
+ return
867
+
868
+ for row in cb_text_splitted:
869
+ if set(row.split("\t")) != set([""]):
870
+ subject = {}
871
+ for idx, field in enumerate(row.split("\t")):
872
+ if idx == 0:
873
+ subject["key"] = field.strip() if len(field.strip()) == 1 else ""
874
+ if idx == 1:
875
+ subject[cfg.SUBJECT_NAME] = field.strip()
876
+ if idx == 2:
877
+ subject["description"] = field.strip()
878
+
879
+ self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
880
+
881
+ for idx, field_name in enumerate(cfg.subjectsFields):
882
+ item = QTableWidgetItem(subject.get(field_name, ""))
883
+ self.twSubjects.setItem(self.twSubjects.rowCount() - 1, idx, item)
884
+
885
+
886
+ def import_subjects_from_project(self):
887
+ """
888
+ import subjects from a BORIS project
889
+ """
890
+
891
+ file_name, _ = QFileDialog().getOpenFileName(
892
+ self, "Import subjects from project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
893
+ )
894
+ if not file_name:
895
+ return
896
+
897
+ _, _, project, _ = project_functions.open_project_json(file_name)
898
+
899
+ if "error" in project:
900
+ logging.debug(project["error"])
901
+ QMessageBox.critical(self, cfg.programName, project["error"])
902
+ return
903
+
904
+ # configuration of subjects
905
+ if not (cfg.SUBJECTS in project and project[cfg.SUBJECTS]):
906
+ QMessageBox.warning(self, cfg.programName, "No subjects configuration found in project")
907
+ return
908
+
909
+ if self.twSubjects.rowCount():
910
+ response = dialog.MessageDialog(
911
+ cfg.programName,
912
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
913
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
914
+ )
915
+
916
+ if response == cfg.REPLACE:
917
+ self.twSubjects.setRowCount(0)
918
+
919
+ if response == cfg.CANCEL:
920
+ return
921
+
922
+ for idx in util.sorted_keys(project[cfg.SUBJECTS]):
923
+ self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
924
+
925
+ for idx2, sbjField in enumerate(cfg.subjectsFields):
926
+ if sbjField in project[cfg.SUBJECTS][idx]:
927
+ self.twSubjects.setItem(
928
+ self.twSubjects.rowCount() - 1,
929
+ idx2,
930
+ QTableWidgetItem(project[cfg.SUBJECTS][idx][sbjField]),
931
+ )
932
+ else:
933
+ self.twSubjects.setItem(self.twSubjects.rowCount() - 1, idx2, QTableWidgetItem(""))
934
+
935
+ self.twSubjects.resizeColumnsToContents()
936
+
937
+
938
+ def import_subjects_from_text_file(self):
939
+ """
940
+ import subjects from a text file (CSV or TSV)
941
+ """
942
+
943
+ if self.twSubjects.rowCount():
944
+ response = dialog.MessageDialog(
945
+ cfg.programName,
946
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
947
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
948
+ )
949
+
950
+ if response == cfg.CANCEL:
951
+ return
952
+
953
+ file_name, _ = QFileDialog().getOpenFileName(
954
+ self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
955
+ )
956
+ if not file_name:
957
+ return
958
+
959
+ if self.twSubjects.rowCount() and response == cfg.REPLACE:
960
+ self.twSubjects.setRowCount(0)
961
+
962
+ if pl.Path(file_name).suffix.upper() == ".CSV":
963
+ delimiter = ","
964
+ elif pl.Path(file_name).suffix.upper() == ".TSV":
965
+ delimiter = "\t"
966
+ else:
967
+ QMessageBox.warning(
968
+ None,
969
+ cfg.programName,
970
+ ("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
971
+ QMessageBox.Ok | QMessageBox.Default,
972
+ QMessageBox.NoButton,
973
+ )
974
+ return
975
+
976
+ try:
977
+ df = pd.read_csv(file_name, delimiter=delimiter)
978
+ except Exception:
979
+ QMessageBox.warning(
980
+ None,
981
+ cfg.programName,
982
+ ("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
983
+ QMessageBox.Ok | QMessageBox.Default,
984
+ QMessageBox.NoButton,
985
+ )
986
+ return
987
+
988
+ load_dataframe_into_subjects_tablewidget(self, df)
989
+
990
+
991
+ def import_subjects_from_spreadsheet(self):
992
+ """
993
+ import subjects from a spreadsheet file (XLSX or ODS)
994
+ """
995
+
996
+ if self.twSubjects.rowCount():
997
+ response = dialog.MessageDialog(
998
+ cfg.programName,
999
+ ("There are subjects already configured. Do you want to append subjects or replace them?"),
1000
+ [cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
1001
+ )
1002
+
1003
+ if response == cfg.CANCEL:
1004
+ return
1005
+
1006
+ file_name, _ = QFileDialog().getOpenFileName(
1007
+ self, "Import subjects from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
1008
+ )
1009
+ if not file_name:
1010
+ return
1011
+
1012
+ if self.twSubjects.rowCount() and response == cfg.REPLACE:
1013
+ self.twSubjects.setRowCount(0)
1014
+
1015
+ if pl.Path(file_name).suffix.upper() == ".XLSX":
1016
+ engine = "openpyxl"
1017
+ elif pl.Path(file_name).suffix.upper() == ".ODS":
1018
+ engine = "odf"
1019
+ else:
1020
+ QMessageBox.warning(
1021
+ None,
1022
+ cfg.programName,
1023
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
1024
+ QMessageBox.Ok | QMessageBox.Default,
1025
+ QMessageBox.NoButton,
1026
+ )
1027
+ return
1028
+
1029
+ try:
1030
+ df = pd.read_excel(file_name, sheet_name=0, engine=engine)
1031
+ except Exception:
1032
+ QMessageBox.warning(
1033
+ None,
1034
+ cfg.programName,
1035
+ ("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
1036
+ QMessageBox.Ok | QMessageBox.Default,
1037
+ QMessageBox.NoButton,
1038
+ )
1039
+ return
1040
+
1041
+ load_dataframe_into_subjects_tablewidget(self, df)
1042
+
1043
+
1044
+ def import_indep_variables_from_project(self):
1045
+ """
1046
+ import independent variables from another project
1047
+ """
1048
+
1049
+ file_name, _ = QFileDialog().getOpenFileName(
1050
+ self,
1051
+ "Import independent variables from project file",
1052
+ "",
1053
+ ("Project files (*.boris *.boris.gz);;All files (*)"),
1054
+ )
1055
+ if not file_name:
1056
+ return
1057
+
1058
+ _, _, project, _ = project_functions.open_project_json(file_name)
1059
+
1060
+ if "error" in project:
1061
+ logging.debug(project["error"])
1062
+ QMessageBox.critical(self, cfg.programName, project["error"])
1063
+ return
1064
+
1065
+ # independent variables
1066
+ if not (cfg.INDEPENDENT_VARIABLES in project and project[cfg.INDEPENDENT_VARIABLES]):
1067
+ QMessageBox.warning(self, cfg.programName, "No independent variables found in project")
1068
+ return
1069
+
1070
+ # check if variables are already present
1071
+ existing_var = []
1072
+
1073
+ for r in range(self.twVariables.rowCount()):
1074
+ existing_var.append(self.twVariables.item(r, 0).text().strip().upper())
1075
+
1076
+ for i in util.sorted_keys(project[cfg.INDEPENDENT_VARIABLES]):
1077
+ self.twVariables.setRowCount(self.twVariables.rowCount() + 1)
1078
+ flag_renamed = False
1079
+ for idx, field in enumerate(cfg.tw_indVarFields):
1080
+ item = QTableWidgetItem()
1081
+ if field in project[cfg.INDEPENDENT_VARIABLES][i]:
1082
+ if field == "label":
1083
+ txt = project[cfg.INDEPENDENT_VARIABLES][i]["label"].strip()
1084
+ while txt.upper() in existing_var:
1085
+ txt += "_2"
1086
+ flag_renamed = True
1087
+ else:
1088
+ txt = project[cfg.INDEPENDENT_VARIABLES][i][field].strip()
1089
+ item.setText(txt)
1090
+ else:
1091
+ item.setText("")
1092
+ self.twVariables.setItem(self.twVariables.rowCount() - 1, idx, item)
1093
+
1094
+ self.twVariables.resizeColumnsToContents()
1095
+ if flag_renamed:
1096
+ QMessageBox.information(self, cfg.programName, "Some variables already present were renamed")