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.
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
boris/boris_cli.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ BORIS CLI
3
+
4
+ Behavioral Observation Research Interactive Software Command Line Interface
5
+
6
+ Copyright 2012-2025 Olivier Friard
7
+
8
+ This program 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 2 of the License, or
11
+ (at your option) any later version.
12
+
13
+ This program 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, write to the Free Software
20
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21
+ MA 02110-1301, USA.
22
+ """
23
+
24
+ import argparse
25
+ import sys
26
+ import re
27
+ import pathlib
28
+ import utilities
29
+ import project_functions
30
+ from config import *
31
+ import db_functions
32
+ import export_observation
33
+ import irr
34
+ import plot_events
35
+ import version
36
+
37
+ __version__ = version.__version__
38
+ __version_date__ = version.__version_date__
39
+
40
+
41
+ def cleanhtml(raw_html):
42
+ raw_html = raw_html.replace("<br>", "\n")
43
+ cleanr = re.compile("<.*?>")
44
+ cleantext = re.sub(cleanr, "", raw_html)
45
+ return cleantext
46
+
47
+
48
+ def all_observations(pj):
49
+ return [idx for idx in sorted(pj[OBSERVATIONS])]
50
+
51
+
52
+ commands_list = ["check_state_events", "export_events", "irr", "subtitles", "check_project_integrity", "plot_events"]
53
+ commands_usage = {
54
+ "check_state_events": (
55
+ "usage:\nboris_cli -p PROJECT_FILE -o OBSERVATION_ID --command check_state_events\n"
56
+ "where\n"
57
+ "PROJECT_FILE is the path of the BORIS project\n"
58
+ "OBSERVATION_ID is the id of observation(s) (if ommitted all observations are checked)"
59
+ ),
60
+ "export_events": (
61
+ "usage:\nboris_cli -p PROJECT_FILE -o OBSERVATION_ID --command export_events [OUTPUT_FORMAT]\n"
62
+ "where:\n"
63
+ "PROJECT_FILE is the path of the BORIS project\n"
64
+ "OBSERVATION_ID is the id of observation(s) (if ommitted all observations are exported)\n"
65
+ "OUTPUT_FORMAT can be tsv (default), csv, xls, xlsx, ods, html"
66
+ ),
67
+ "irr": (
68
+ 'usage:\nboris_cli -p PROJECT_FILE -o "OBSERVATION_ID1" "OBSERVATION_ID2" --command irr [INTERVAL] [INCLUDE_MODIFIERS]\n'
69
+ "where:\n"
70
+ "PROJECT_FILE is the path of the BORIS project\n"
71
+ "INTERVAL in seconds (default is 1)\n"
72
+ "INCLUDE_MODIFIERS must be true or false (default is true)"
73
+ ),
74
+ "subtitles": (
75
+ 'usage:\nboris_cli -p PROJECT_FILE -o "OBSERVATION_ID" --command subtitles [OUTPUT_DIRECTORY]\n'
76
+ "where:\n"
77
+ "OUTPUT_DIRECTORY is the directory where subtitles files will be saved"
78
+ ),
79
+ "check_project_integrity": "usage:\nboris_cli -p PROJECT_FILE --command check_project_integrity",
80
+ "plot_events": (
81
+ "usage:\nboris_cli - p PROJECT_FILE -o OBSERVATION_ID --command plot_events "
82
+ "[OUTPUT_DIRECTORY] [INCLUDE_MODIFIERS] [EXCLUDE_BEHAVIORS] [PLOT_FORMAT]\n"
83
+ "where\n"
84
+ "OUTPUT_DIRECTORY is the directory where the plots will be saved\n"
85
+ "INCLUDE_MODIFIERS must be true or false (default is true)\n"
86
+ "EXCLUDE_BEHAVIORS: True: behaviors without events are not plotted (default is true)\n"
87
+ "PLOT_FORMAT can be png, svg, pdf, ps"
88
+ ),
89
+ }
90
+
91
+ parser = argparse.ArgumentParser(description="BORIS CLI")
92
+ parser.add_argument("-v", "--version", action="store_true", dest="version", help="BORIS version")
93
+ parser.add_argument("-p", "--project", action="store", dest="project_file", help="Project file path")
94
+ parser.add_argument("-o", "--observation", nargs="*", action="store", default=[], dest="observation_id", help="Observation id")
95
+ parser.add_argument("-i", "--info", action="store_true", dest="project_info", help="Project information")
96
+ parser.add_argument("-c", "--command", nargs="*", action="store", dest="command", help="Command to execute")
97
+
98
+ args = parser.parse_args()
99
+
100
+ pj, observations_id_list = {}, {}
101
+
102
+ if args.version:
103
+ print("version {}".format(__version__))
104
+ sys.exit()
105
+
106
+ if args.command:
107
+ if args.command[0].upper() == "LIST":
108
+ for command in commands_list:
109
+ print(command)
110
+ print("=" * len(command))
111
+ if command in commands_usage:
112
+ print(commands_usage[command])
113
+ print()
114
+ print()
115
+ sys.exit()
116
+
117
+ if args.project_file:
118
+ if not args.command:
119
+ print("Project path: {}".format(args.project_file))
120
+
121
+ project_path, project_changed, pj, msg = project_functions.open_project_json(args.project_file)
122
+ if "error" in pj:
123
+ print(pj["error"])
124
+ sys.exit()
125
+ if msg:
126
+ print(msg)
127
+
128
+ if args.observation_id:
129
+ observations_id_list = args.observation_id
130
+ """
131
+ if not args.command:
132
+ print("\nObservations:")
133
+ for observation_id in observations_id_list:
134
+ if observation_id in pj[OBSERVATIONS]:
135
+ print("Id: {}".format(observation_id))
136
+ else:
137
+ print("{}: NOT FOUND in project".format(observation_id))
138
+ print()
139
+ """
140
+
141
+ if args.project_info:
142
+ if not args.command:
143
+ if pj:
144
+ print("Project name: {}".format(pj[PROJECT_NAME]))
145
+ print("Project date: {}".format(pj[PROJECT_DATE]))
146
+ print("Project description: {}".format(pj[PROJECT_DESCRIPTION]))
147
+ print()
148
+
149
+ if not observations_id_list:
150
+ print("Ethogram\n========")
151
+ print("Number of behaviors in ethogram: {}".format(len(pj[ETHOGRAM])))
152
+ for idx in utilities.sorted_keys(pj[ETHOGRAM]):
153
+ print(
154
+ "Code: {}\tDescription: {}\tType: {}".format(
155
+ pj[ETHOGRAM][idx][BEHAVIOR_CODE], pj[ETHOGRAM][idx]["description"], pj[ETHOGRAM][idx][TYPE]
156
+ )
157
+ )
158
+ """print("Behaviors: {}".format(",".join([pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])])))"""
159
+ print()
160
+
161
+ print("Subjects\n========")
162
+ print("Number of subjects: {}".format(len(pj[SUBJECTS])))
163
+ for idx in utilities.sorted_keys(pj[SUBJECTS]):
164
+ print("Name: {}\tDescription: {}".format(pj[SUBJECTS][idx]["name"], pj[SUBJECTS][idx]["description"]))
165
+ print()
166
+
167
+ print("Observations\n============")
168
+ print("Number of observations: {}".format(len(pj[OBSERVATIONS])))
169
+ print("List of observations:")
170
+ for observation_id in sorted(pj[OBSERVATIONS].keys()):
171
+ print("Id: {}\tDate: {}".format(observation_id, pj[OBSERVATIONS][observation_id]["date"]))
172
+
173
+ else:
174
+ for observation_id in observations_id_list:
175
+ print("Observation id: {}".format(observation_id))
176
+ if pj[OBSERVATIONS][observation_id][EVENTS]:
177
+ for event in pj[OBSERVATIONS][observation_id][EVENTS]:
178
+ print("\t".join([str(x) for x in event]))
179
+ else:
180
+ print("No events recorded")
181
+ print()
182
+ else:
183
+ print("No project")
184
+ sys.exit()
185
+
186
+ if args.command:
187
+ print("Command: {}\n".format(" ".join(args.command)))
188
+
189
+ if not pj:
190
+ print("No project")
191
+ sys.exit()
192
+
193
+ if "check_state_events" in args.command:
194
+ if not observations_id_list:
195
+ print("No observation selected. Command applied on all observations found in project\n")
196
+ observations_id_list = all_observations(pj)
197
+
198
+ for observation_id in observations_id_list:
199
+ ret, msg = project_functions.check_state_events_obs(observation_id, pj[ETHOGRAM], pj[OBSERVATIONS][observation_id], HHMMSS)
200
+ print("{}: {}".format(observation_id, cleanhtml(msg)))
201
+ sys.exit()
202
+
203
+ if "export_events" in args.command:
204
+ if not observations_id_list:
205
+ print("No observation selected. Command applied on all observations found in project\n")
206
+ observations_id_list = [idx for idx in pj[OBSERVATIONS]]
207
+
208
+ behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
209
+ subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
210
+
211
+ output_format = "tsv"
212
+ if len(args.command) > 1:
213
+ output_format = args.command[1]
214
+
215
+ for observation_id in observations_id_list:
216
+ ok, msg = export_observation.export_events(
217
+ {"selected subjects": subjects, "selected behaviors": behaviors},
218
+ observation_id,
219
+ pj[OBSERVATIONS][observation_id],
220
+ pj[ETHOGRAM],
221
+ utilities.safeFileName(observation_id + "." + output_format),
222
+ output_format,
223
+ )
224
+ if not ok:
225
+ print(msg)
226
+
227
+ sys.exit()
228
+
229
+ if "irr" in args.command:
230
+ if len(observations_id_list) != 2:
231
+ print("select 2 observations")
232
+ sys.exit()
233
+
234
+ behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
235
+ subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
236
+
237
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(pj, subjects, observations_id_list, behaviors)
238
+
239
+ if not ok:
240
+ print(cleanhtml(msg))
241
+ sys.exit()
242
+
243
+ cursor = db_connector.cursor()
244
+
245
+ interval = 1
246
+ if len(args.command) > 1:
247
+ interval = utilities.float2decimal(args.command[1])
248
+
249
+ include_modifiers = True
250
+ if len(args.command) > 2:
251
+ include_modifiers = "TRUE" in args.command[2].upper()
252
+
253
+ K, out = irr.cohen_kappa(cursor, observations_id_list[0], observations_id_list[1], interval, subjects, include_modifiers)
254
+
255
+ print(("Cohen's Kappa - Index of Inter-Rater Reliability\n\nInterval time: {interval:.3f} s\n").format(interval=interval))
256
+
257
+ print(out)
258
+ sys.exit()
259
+
260
+ if "subtitles" in args.command:
261
+ if not observations_id_list:
262
+ print("No observation selected. Command applied on all observations found in project\n")
263
+ observations_id_list = all_observations(pj)
264
+
265
+ behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
266
+ subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
267
+
268
+ export_dir = "."
269
+ if len(args.command) > 1:
270
+ export_dir = args.command[1]
271
+ if not pathlib.Path(export_dir).is_dir():
272
+ print("{} is not a valid directory".format(export_dir))
273
+ sys.exit()
274
+
275
+ ok, msg = project_functions.create_subtitles(
276
+ pj,
277
+ observations_id_list,
278
+ {"selected subjects": subjects, "selected behaviors": behaviors, "include modifiers": True},
279
+ export_dir,
280
+ )
281
+ if not ok:
282
+ print(cleanhtml(msg))
283
+ sys.exit()
284
+
285
+ if "check_project_integrity" in args.command[0]:
286
+ msg = project_functions.check_project_integrity(pj, HHMMSS, args.project_file)
287
+ if msg:
288
+ print(cleanhtml(msg))
289
+ else:
290
+ print("No issuses found in project")
291
+ sys.exit()
292
+
293
+ if "plot_events" in args.command[0]:
294
+ if not observations_id_list:
295
+ print("No observation selected. Command applied on all observations found in project\n")
296
+ observations_id_list = all_observations(pj)
297
+
298
+ behaviors = [pj[ETHOGRAM][k]["code"] for k in utilities.sorted_keys(pj[ETHOGRAM])]
299
+ subjects = [pj[SUBJECTS][k]["name"] for k in utilities.sorted_keys(pj[SUBJECTS])] + [NO_FOCAL_SUBJECT]
300
+
301
+ export_dir = "."
302
+ if len(args.command) > 1:
303
+ export_dir = args.command[1]
304
+ if not pathlib.Path(export_dir).is_dir():
305
+ print("{} is not a valid directory".format(export_dir))
306
+ sys.exit()
307
+
308
+ include_modifiers = True
309
+ if len(args.command) > 2:
310
+ include_modifiers = "TRUE" in args.command[2].upper()
311
+
312
+ exclude_behaviors = True
313
+ if len(args.command) > 3:
314
+ exclude_behaviors = False if "FALSE" in args.command[3].upper() else True
315
+
316
+ plot_format = "png"
317
+ if len(args.command) > 4:
318
+ plot_format = args.command[4].lower()
319
+
320
+ plot_events.create_events_plot(
321
+ pj,
322
+ observations_id_list,
323
+ {
324
+ "selected subjects": subjects,
325
+ "selected behaviors": behaviors,
326
+ "include modifiers": include_modifiers,
327
+ "exclude behaviors": exclude_behaviors,
328
+ "time": TIME_FULL_OBS,
329
+ "start time": 0,
330
+ "end time": 0,
331
+ },
332
+ plot_colors=BEHAVIORS_PLOT_COLORS,
333
+ plot_directory=export_dir,
334
+ file_format=plot_format,
335
+ )
336
+ sys.exit()
337
+
338
+ print("Command {} not found!".format(args.command[0]))
339
+
340
+ print()
boris/cmd_arguments.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+
7
+ This program is free software; you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation; either version 2 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program; if not, write to the Free Software
19
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ MA 02110-1301, USA.
21
+
22
+ """
23
+
24
+ from optparse import OptionParser
25
+
26
+
27
+ def parse_arguments():
28
+ # check if argument
29
+ usage = 'usage: %prog [options] [-p PROJECT_PATH] [-o "OBSERVATION ID"]'
30
+ parser = OptionParser(usage=usage)
31
+
32
+ parser.add_option("-d", "--debug", action="store_true", default=False, dest="debug", help="Use debugging mode")
33
+ parser.add_option("-q", "--quit", action="store_true", default=False, dest="quit", help="Quit after launch")
34
+ parser.add_option("-v", "--version", action="store_true", default=False, dest="version", help="Print version")
35
+ parser.add_option("-n", "--nosplashscreen", action="store_true", default=False, help="No splash screen")
36
+ parser.add_option("-p", "--project", action="store", default="", dest="project", help="Project file")
37
+ parser.add_option("-o", "--observation", action="store", default="", dest="observation", help="Observation id")
38
+ parser.add_option("-i", "--ipc", action="store_true", default="", dest="ipc", help="MPV IPC mode")
39
+
40
+ parser.add_option(
41
+ "-f",
42
+ "--no-first-launch-dialog",
43
+ action="store_true",
44
+ default=False,
45
+ dest="no_first_launch_dialog",
46
+ help="No first launch dialog (for new version automatic check)",
47
+ )
48
+
49
+ return parser.parse_args()
boris/coding_pad.py ADDED
@@ -0,0 +1,280 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+ """
21
+
22
+ from PySide6.QtCore import Qt, Signal, QEvent, QRect
23
+ from PySide6.QtGui import QFont
24
+ from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGridLayout, QComboBox, QMessageBox
25
+
26
+ from . import config as cfg
27
+ from . import utilities as util
28
+
29
+
30
+ class Button(QWidget):
31
+ def __init__(self, parent=None):
32
+ super(Button, self).__init__(parent)
33
+ self.pushButton = QPushButton()
34
+ self.pushButton.setFocusPolicy(Qt.NoFocus)
35
+ layout = QHBoxLayout()
36
+ layout.addWidget(self.pushButton)
37
+ self.setLayout(layout)
38
+
39
+
40
+ class CodingPad(QWidget):
41
+ clickSignal = Signal(str)
42
+ sendEventSignal = Signal(QEvent)
43
+ close_signal = Signal(QRect, dict)
44
+
45
+ def __init__(self, pj: dict, filtered_behaviors, parent=None):
46
+ super().__init__(parent)
47
+ self.pj: dict = pj
48
+ self.filtered_behaviors = filtered_behaviors
49
+
50
+ self.behavioral_category_colors_list: list = []
51
+ self.behavior_colors_list: list = []
52
+
53
+ self.behavioral_category_colors: dict = {}
54
+ self.behavior_colors: dict = {}
55
+
56
+ self.preferences: dict = {"button font size": 20, "button color": cfg.BEHAVIOR_CATEGORY}
57
+
58
+ self.button_css: str = "min-width: 50px; min-height:50px; font-weight: bold; max-height:5000px; max-width: 5000px;"
59
+
60
+ self.setWindowTitle("Coding pad")
61
+
62
+ self.grid = QGridLayout(self)
63
+
64
+ self.installEventFilter(self)
65
+
66
+ def config(self):
67
+ """
68
+ Configure the coding pad
69
+ """
70
+ if self.cb_config.currentIndex() == 1: # increase text size
71
+ self.preferences["button font size"] += 4
72
+ if self.cb_config.currentIndex() == 2: # decrease text size
73
+ self.preferences["button font size"] -= 4
74
+ if self.cb_config.currentIndex() == 3:
75
+ self.preferences["button color"] = cfg.BEHAVIOR_CATEGORY
76
+ if self.cb_config.currentIndex() == 4:
77
+ self.preferences["button color"] = "behavior"
78
+ if self.cb_config.currentIndex() == 5:
79
+ self.preferences["button color"] = "no color"
80
+
81
+ self.cb_config.setCurrentIndex(0)
82
+ self.button_configuration()
83
+
84
+ def compose(self):
85
+ """
86
+ Add buttons to coding pad
87
+ """
88
+ for i in reversed(range(self.grid.count())):
89
+ if self.grid.itemAt(i).widget() is not None:
90
+ self.grid.itemAt(i).widget().setParent(None)
91
+
92
+ # combobox for coding pad configuration
93
+ vlayout = QHBoxLayout()
94
+ self.cb_config = QComboBox()
95
+ self.cb_config.setFocusPolicy(Qt.NoFocus)
96
+ self.cb_config.addItems(
97
+ [
98
+ "Choose an option to configure",
99
+ "Increase button text size",
100
+ "Decrease button text size",
101
+ "Color button by behavioral category",
102
+ "Color button by behavior",
103
+ "No color",
104
+ ]
105
+ )
106
+ self.cb_config.currentIndexChanged.connect(self.config)
107
+ vlayout.addWidget(self.cb_config)
108
+ self.grid.addLayout(vlayout, 0, 1, 1, 1)
109
+
110
+ self.all_behaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]
111
+
112
+ # behavioral category colors
113
+ self.unique_behavioral_categories = sorted(
114
+ set([self.pj[cfg.ETHOGRAM][x].get(cfg.BEHAVIOR_CATEGORY, "") for x in self.pj[cfg.ETHOGRAM]])
115
+ )
116
+ for idx, category in enumerate(self.unique_behavioral_categories):
117
+ self.behavioral_category_colors[category] = self.behavioral_category_colors_list[
118
+ idx % len(self.behavioral_category_colors_list)
119
+ ]
120
+
121
+ # sorted list of unique behavior categories
122
+ behaviorsList = [
123
+ [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY], self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]]
124
+ for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])
125
+ if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][x] and self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] in self.filtered_behaviors
126
+ ]
127
+
128
+ # square grid dimension
129
+ dim = int(len(behaviorsList) ** 0.5 + 0.999)
130
+
131
+ c = 0
132
+ for i in range(1, dim + 1):
133
+ for j in range(1, dim + 1):
134
+ if c >= len(behaviorsList):
135
+ break
136
+ self.addWidget(behaviorsList[c][1], i, j)
137
+ c += 1
138
+
139
+ self.button_configuration()
140
+
141
+ def addWidget(self, behavior_code: str, i: int, j: int) -> None:
142
+ self.grid.addWidget(Button(), i, j)
143
+ index = self.grid.count() - 1
144
+ widget = self.grid.itemAt(index).widget()
145
+
146
+ if widget is not None:
147
+ widget.pushButton.setText(behavior_code)
148
+ widget.pushButton.clicked.connect(lambda: self.click(behavior_code))
149
+
150
+ def button_configuration(self):
151
+ """
152
+ configure the font and color of buttons
153
+ """
154
+
155
+ state_behaviors_list = util.state_behavior_codes(self.pj[cfg.ETHOGRAM])
156
+
157
+ for index in range(self.grid.count()):
158
+ if self.grid.itemAt(index).widget() is None:
159
+ continue
160
+ behavior_code = self.grid.itemAt(index).widget().pushButton.text()
161
+
162
+ if self.preferences["button color"] == cfg.BEHAVIOR_CATEGORY:
163
+ behav_cat = [
164
+ self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY]
165
+ for x in self.pj[cfg.ETHOGRAM]
166
+ if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == behavior_code
167
+ ][0]
168
+ if cfg.BEHAVIORAL_CATEGORIES_CONF in self.pj:
169
+ behav_cat_color = util.behav_category_user_color(self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF], behav_cat)
170
+ else:
171
+ behav_cat_color = None
172
+ if behav_cat_color is None:
173
+ color = self.behavioral_category_colors[behav_cat]
174
+ else:
175
+ color = behav_cat_color
176
+
177
+ if self.preferences["button color"] == "behavior":
178
+ # behavioral categories are not defined
179
+ behavior_position = int(
180
+ [x for x in util.sorted_keys(self.pj[cfg.ETHOGRAM]) if self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] == behavior_code][0]
181
+ )
182
+
183
+ # behavior button color
184
+ behav_color = util.behavior_user_color(self.pj[cfg.ETHOGRAM], behavior_code)
185
+
186
+ if behav_color is not None:
187
+ color = behav_color
188
+ else:
189
+ color = self.behavior_colors_list[behavior_position % len(self.behavior_colors_list)].replace("tab:", "")
190
+
191
+ if self.preferences["button color"] == "no color":
192
+ color = ""
193
+
194
+ # set checkable if state behavior
195
+ self.grid.itemAt(index).widget().pushButton.setCheckable(behavior_code in state_behaviors_list)
196
+ self.grid.itemAt(index).widget().pushButton.setStyleSheet(self.button_css + (f"background-color: {color};" if color else ""))
197
+ font = QFont("Arial", self.preferences["button font size"])
198
+ self.grid.itemAt(index).widget().pushButton.setFont(font)
199
+
200
+ def resizeEvent(self, event):
201
+ """
202
+ Resize event
203
+ button are redesigned with new font size
204
+ """
205
+ self.button_configuration()
206
+
207
+ def click(self, behavior_code: str) -> None:
208
+ """
209
+ Button clicked
210
+ """
211
+ self.clickSignal.emit(behavior_code)
212
+
213
+ def eventFilter(self, receiver, event) -> bool:
214
+ """
215
+ send event (if keypress) to main window
216
+ """
217
+ if event.type() == QEvent.KeyPress:
218
+ self.sendEventSignal.emit(event)
219
+ return True
220
+ else:
221
+ return False
222
+
223
+ def closeEvent(self, event) -> None:
224
+ """
225
+ send event for widget geometry memory
226
+ """
227
+ self.close_signal.emit(self.geometry(), self.preferences)
228
+
229
+
230
+ def show_coding_pad(self):
231
+ """
232
+ show coding pad window
233
+ """
234
+ if self.playerType in cfg.VIEWERS:
235
+ QMessageBox.warning(self, cfg.programName, "The coding pad is not available in <b>VIEW</b> mode")
236
+ return
237
+
238
+ if hasattr(self, "codingpad"):
239
+ self.codingpad.filtered_behaviors = [self.twEthogram.item(i, 1).text() for i in range(self.twEthogram.rowCount())]
240
+ if not self.codingpad.filtered_behaviors:
241
+ QMessageBox.warning(self, cfg.programName, "No behaviors to show!")
242
+ return
243
+ self.codingpad.show()
244
+
245
+ # update colors
246
+ self.codingpad.behavior_colors_list = self.plot_colors
247
+ self.codingpad.behavioral_category_colors_list = self.behav_category_colors
248
+
249
+ else: # coding pad does not exist
250
+ filtered_behaviors = [self.twEthogram.item(i, 1).text() for i in range(self.twEthogram.rowCount())]
251
+ if not filtered_behaviors:
252
+ QMessageBox.warning(self, cfg.programName, "No behaviors to show!")
253
+ return
254
+ self.codingpad = CodingPad(self.pj, filtered_behaviors)
255
+
256
+ self.codingpad.behavior_colors_list = self.plot_colors
257
+ self.codingpad.behavioral_category_colors_list = self.behav_category_colors
258
+
259
+ self.codingpad.compose()
260
+
261
+ self.codingpad.setWindowFlags(Qt.WindowStaysOnTopHint)
262
+ self.codingpad.sendEventSignal.connect(self.signal_from_widget)
263
+
264
+ self.codingpad.clickSignal.connect(self.click_signal_from_coding_pad)
265
+ self.codingpad.close_signal.connect(self.close_signal_from_coding_pad)
266
+ self.codingpad.show()
267
+
268
+ if self.config_param.get(cfg.CODING_PAD_GEOMETRY, None):
269
+ self.codingpad.setGeometry(
270
+ self.config_param[cfg.CODING_PAD_GEOMETRY].x(),
271
+ self.config_param[cfg.CODING_PAD_GEOMETRY].y(),
272
+ self.config_param[cfg.CODING_PAD_GEOMETRY].width(),
273
+ self.config_param[cfg.CODING_PAD_GEOMETRY].height(),
274
+ )
275
+ else:
276
+ self.codingpad.setGeometry(100, 100, 660, 500)
277
+
278
+ if self.config_param.get(cfg.CODING_PAD_CONFIG, {}):
279
+ self.codingpad.preferences = self.config_param[cfg.CODING_PAD_CONFIG]
280
+ self.codingpad.button_configuration()