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,286 @@
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
+ from decimal import Decimal as dec
25
+ from typing import Optional
26
+
27
+ from PySide6.QtCore import Qt
28
+ from PySide6.QtGui import QFont
29
+ from PySide6.QtWidgets import QCheckBox, QListWidgetItem, QMessageBox
30
+
31
+ from . import config as cfg
32
+ from . import gui_utilities, param_panel, project_functions
33
+ from . import utilities as util
34
+
35
+
36
+ def choose_obs_subj_behav_category(
37
+ self,
38
+ selected_observations: list,
39
+ start_coding: Optional[dec] = dec("NaN"), # Union[..., None]
40
+ end_coding: Optional[dec] = dec("NaN"),
41
+ start_interval: Optional[dec] = dec("NaN"),
42
+ end_interval: Optional[dec] = dec("NaN"),
43
+ maxTime: Optional[dec] = None,
44
+ show_include_modifiers: bool = True,
45
+ show_exclude_non_coded_behaviors: bool = True,
46
+ by_category: bool = False,
47
+ n_observations: int = 1,
48
+ show_time_bin_size: bool = False,
49
+ window_title: str = "Select subjects and behaviors",
50
+ show_exclude_non_coded_modifiers: bool = False,
51
+ ) -> dict:
52
+ """
53
+ show window for:
54
+ - selection of subjects
55
+ - selection of behaviors (based on selected subjects)
56
+ - selection of time interval
57
+ - inclusion/exclusion of modifiers
58
+ - inclusion/exclusion of behaviors without events (show_exclude_non_coded_behaviors == True)
59
+ - selection of time bin size (show_time_bin_size == True)
60
+
61
+ Args:
62
+ selected_observations (list): List of selected observations
63
+ ...
64
+ show_exclude_non_coded_modifiers (bool): display the Exclude non coded modifiers checkbox
65
+
66
+ Returns:
67
+ dict: {"selected subjects": selectedSubjects,
68
+ "selected behaviors": selectedBehaviors,
69
+ "include modifiers": True/False,
70
+ "exclude behaviors": True/False,
71
+ "time": TIME_FULL_OBS / TIME_EVENTS / TIME_ARBITRARY_INTERVAL / TIME_OBS_INTERVAL
72
+ "start time": startTime,
73
+ "end time": endTime
74
+ }
75
+
76
+ """
77
+
78
+ paramPanelWindow = param_panel.Param_panel()
79
+ paramPanelWindow.resize(400, 400)
80
+ paramPanelWindow.setWindowTitle(window_title)
81
+ paramPanelWindow.selectedObservations = selected_observations
82
+ paramPanelWindow.pj = self.pj
83
+ paramPanelWindow.extract_observed_behaviors = self.extract_observed_behaviors
84
+
85
+ paramPanelWindow.cbIncludeModifiers.setVisible(show_include_modifiers)
86
+ paramPanelWindow.cb_exclude_non_coded_modifiers.setVisible(show_exclude_non_coded_modifiers)
87
+ paramPanelWindow.cbExcludeBehaviors.setVisible(show_exclude_non_coded_behaviors)
88
+ # show_time_bin_size:
89
+ paramPanelWindow.frm_time_bin_size.setVisible(show_time_bin_size)
90
+
91
+ if by_category:
92
+ paramPanelWindow.cbIncludeModifiers.setVisible(False)
93
+ paramPanelWindow.cbExcludeBehaviors.setVisible(False)
94
+ paramPanelWindow.cb_exclude_non_coded_modifiers.setVisible(False)
95
+
96
+ # set state of cb_exclude_non_coded_modifiers
97
+ paramPanelWindow.cb_exclude_non_coded_modifiers.setEnabled(paramPanelWindow.cbIncludeModifiers.isChecked())
98
+
99
+ paramPanelWindow.media_duration = maxTime
100
+ paramPanelWindow.start_coding = start_coding
101
+ paramPanelWindow.end_coding = end_coding
102
+ paramPanelWindow.start_interval = start_interval
103
+ paramPanelWindow.end_interval = end_interval
104
+
105
+ if self.timeFormat == cfg.S:
106
+ paramPanelWindow.start_time.rb_seconds.setChecked(True)
107
+ paramPanelWindow.end_time.rb_seconds.setChecked(True)
108
+ if self.timeFormat == cfg.HHMMSS:
109
+ paramPanelWindow.start_time.rb_time.setChecked(True)
110
+ paramPanelWindow.end_time.rb_time.setChecked(True)
111
+
112
+ if n_observations > 1:
113
+ paramPanelWindow.frm_time_interval.setVisible(False)
114
+ else:
115
+ if (start_coding is None) or (start_coding.is_nan()):
116
+ paramPanelWindow.rb_observed_events.setEnabled(False)
117
+ paramPanelWindow.frm_time_interval.setVisible(False)
118
+ paramPanelWindow.rb_user_defined.setVisible(False)
119
+ paramPanelWindow.rb_obs_interval.setVisible(False)
120
+ paramPanelWindow.rb_media_duration.setVisible(False)
121
+ else:
122
+ paramPanelWindow.frm_time_interval.setEnabled(False)
123
+ paramPanelWindow.start_time.set_time(start_coding)
124
+ paramPanelWindow.end_time.set_time(end_coding)
125
+
126
+ # check observation time interval
127
+ if start_interval is None or start_interval.is_nan() or end_interval is None or end_interval.is_nan():
128
+ paramPanelWindow.rb_obs_interval.setEnabled(False)
129
+
130
+ if selected_observations:
131
+ observedSubjects = project_functions.extract_observed_subjects(self.pj, selected_observations)
132
+ else:
133
+ # load all subjects and "No focal subject"
134
+ observedSubjects = [self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]] + [""]
135
+ selectedSubjects = []
136
+
137
+ # add 'No focal subject'
138
+ if "" in observedSubjects:
139
+ selectedSubjects.append(cfg.NO_FOCAL_SUBJECT)
140
+ paramPanelWindow.item = QListWidgetItem(paramPanelWindow.lwSubjects)
141
+ paramPanelWindow.ch = QCheckBox()
142
+ paramPanelWindow.ch.setText(cfg.NO_FOCAL_SUBJECT)
143
+ paramPanelWindow.ch.stateChanged.connect(paramPanelWindow.cb_changed)
144
+ paramPanelWindow.ch.setChecked(True)
145
+ paramPanelWindow.lwSubjects.setItemWidget(paramPanelWindow.item, paramPanelWindow.ch)
146
+
147
+ all_subjects = [self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in util.sorted_keys(self.pj[cfg.SUBJECTS])]
148
+
149
+ for subject in all_subjects:
150
+ paramPanelWindow.item = QListWidgetItem(paramPanelWindow.lwSubjects)
151
+ paramPanelWindow.ch = QCheckBox()
152
+ paramPanelWindow.ch.setText(subject)
153
+ paramPanelWindow.ch.stateChanged.connect(paramPanelWindow.cb_changed)
154
+ if subject in observedSubjects:
155
+ selectedSubjects.append(subject)
156
+ paramPanelWindow.ch.setChecked(True)
157
+
158
+ paramPanelWindow.lwSubjects.setItemWidget(paramPanelWindow.item, paramPanelWindow.ch)
159
+
160
+ logging.debug(f"selected subjects: {selectedSubjects}")
161
+
162
+ if selected_observations:
163
+ observedBehaviors = self.extract_observed_behaviors(selected_observations, selectedSubjects) # not sorted
164
+ else:
165
+ # load all behaviors
166
+ observedBehaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]]
167
+
168
+ logging.debug(f"observed behaviors: {observedBehaviors}")
169
+
170
+ if cfg.BEHAVIORAL_CATEGORIES in self.pj:
171
+ categories = self.pj[cfg.BEHAVIORAL_CATEGORIES][:]
172
+ # check if behavior not included in a category
173
+ try:
174
+ if "" in [
175
+ self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CATEGORY]
176
+ for idx in self.pj[cfg.ETHOGRAM]
177
+ if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][idx]
178
+ ]:
179
+ categories += [""]
180
+ except Exception:
181
+ categories = ["###no category###"]
182
+
183
+ else:
184
+ categories = ["###no category###"]
185
+
186
+ for category in categories:
187
+ if category != "###no category###":
188
+ if category == "":
189
+ paramPanelWindow.item = QListWidgetItem("No category")
190
+ paramPanelWindow.item.setData(34, "No category")
191
+ else:
192
+ paramPanelWindow.item = QListWidgetItem(category)
193
+ paramPanelWindow.item.setData(34, category)
194
+
195
+ font = QFont()
196
+ font.setBold(True)
197
+ paramPanelWindow.item.setFont(font)
198
+ paramPanelWindow.item.setData(33, "category")
199
+ paramPanelWindow.item.setData(35, False)
200
+
201
+ paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
202
+
203
+ for behavior in [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]:
204
+ if (categories == ["###no category###"]) or (
205
+ behavior
206
+ in [
207
+ self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE]
208
+ for x in self.pj[cfg.ETHOGRAM]
209
+ if cfg.BEHAVIOR_CATEGORY in self.pj[cfg.ETHOGRAM][x] and self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CATEGORY] == category
210
+ ]
211
+ ):
212
+ paramPanelWindow.item = QListWidgetItem(behavior)
213
+ if behavior in observedBehaviors:
214
+ paramPanelWindow.item.setCheckState(Qt.Checked)
215
+ else:
216
+ paramPanelWindow.item.setCheckState(Qt.Unchecked)
217
+
218
+ if category != "###no category###":
219
+ paramPanelWindow.item.setData(33, "behavior")
220
+ if category == "":
221
+ paramPanelWindow.item.setData(34, "No category")
222
+ else:
223
+ paramPanelWindow.item.setData(34, category)
224
+
225
+ paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
226
+
227
+ gui_utilities.restore_geometry(paramPanelWindow, "param panel", (600, 500))
228
+
229
+ if not paramPanelWindow.exec_():
230
+ return {}
231
+
232
+ gui_utilities.save_geometry(paramPanelWindow, "param panel")
233
+
234
+ selectedSubjects = paramPanelWindow.selectedSubjects
235
+ selectedBehaviors = paramPanelWindow.selectedBehaviors
236
+
237
+ logging.debug(f"selected subjects: {selectedSubjects}")
238
+ logging.debug(f"selected behaviors: {selectedBehaviors}")
239
+
240
+ if paramPanelWindow.rb_user_defined.isChecked():
241
+ startTime = paramPanelWindow.start_time.get_time()
242
+ endTime = paramPanelWindow.end_time.get_time()
243
+
244
+ if startTime > endTime:
245
+ QMessageBox.warning(
246
+ None,
247
+ cfg.programName,
248
+ "The start time is after the end time",
249
+ QMessageBox.Ok | QMessageBox.Default,
250
+ QMessageBox.NoButton,
251
+ )
252
+ return {cfg.SELECTED_SUBJECTS: [], cfg.SELECTED_BEHAVIORS: []}
253
+
254
+ elif paramPanelWindow.rb_obs_interval.isChecked() and not ((start_interval is None) or start_interval.is_nan()):
255
+ startTime = paramPanelWindow.start_time.get_time()
256
+ endTime = paramPanelWindow.end_time.get_time()
257
+
258
+ else:
259
+ startTime = None
260
+ endTime = None
261
+
262
+ # if startTime is None:
263
+ # startTime = dec("NaN")
264
+ # if endTime is None:
265
+ # endTime = dec("NaN")
266
+
267
+ if paramPanelWindow.rb_media_duration.isChecked():
268
+ time_param = cfg.TIME_FULL_OBS
269
+ if paramPanelWindow.rb_observed_events.isChecked():
270
+ time_param = cfg.TIME_EVENTS
271
+ if paramPanelWindow.rb_obs_interval.isChecked():
272
+ time_param = cfg.TIME_OBS_INTERVAL
273
+ if paramPanelWindow.rb_user_defined.isChecked():
274
+ time_param = cfg.TIME_ARBITRARY_INTERVAL
275
+
276
+ return {
277
+ cfg.SELECTED_SUBJECTS: selectedSubjects,
278
+ cfg.SELECTED_BEHAVIORS: selectedBehaviors,
279
+ cfg.INCLUDE_MODIFIERS: paramPanelWindow.cbIncludeModifiers.isChecked(),
280
+ cfg.EXCLUDE_BEHAVIORS: paramPanelWindow.cbExcludeBehaviors.isChecked(),
281
+ cfg.EXCLUDE_NON_CODED_MODIFIERS: paramPanelWindow.cb_exclude_non_coded_modifiers.isChecked(),
282
+ "time": time_param,
283
+ cfg.START_TIME: startTime,
284
+ cfg.END_TIME: endTime,
285
+ cfg.TIME_BIN_SIZE: paramPanelWindow.sb_time_bin_size.value(),
286
+ }
boris/state_events.py ADDED
@@ -0,0 +1,197 @@
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
+ Module containing functions for state events
22
+
23
+ """
24
+
25
+ import time
26
+ from decimal import Decimal as dec
27
+
28
+ from PySide6.QtWidgets import QMessageBox, QAbstractItemView
29
+
30
+ from . import config as cfg
31
+ from . import dialog, project_functions, select_observations
32
+
33
+
34
+ def check_state_events(self, mode: str = "all") -> None:
35
+ """
36
+ check state events for each subject
37
+ use check_state_events_obs function in project_functions.py
38
+
39
+ Args:
40
+ mode (str): current: check current observation / all: ask user to select observations
41
+ """
42
+
43
+ tot_out = ""
44
+ if mode == "current":
45
+ if self.observationId:
46
+ _, msg = project_functions.check_state_events_obs(
47
+ self.observationId,
48
+ self.pj[cfg.ETHOGRAM],
49
+ self.pj[cfg.OBSERVATIONS][self.observationId],
50
+ self.timeFormat,
51
+ )
52
+ tot_out = f"Observation: <strong>{self.observationId}</strong><br>{msg}<br><br>"
53
+
54
+ if mode == "all":
55
+ if not self.pj[cfg.OBSERVATIONS]:
56
+ QMessageBox.warning(
57
+ self,
58
+ cfg.programName,
59
+ "The project does not contain any observation",
60
+ QMessageBox.Ok | QMessageBox.Default,
61
+ QMessageBox.NoButton,
62
+ )
63
+ return
64
+
65
+ # ask user observations to analyze
66
+ _, selectedObservations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
67
+ if not selectedObservations:
68
+ return
69
+
70
+ for obsId in sorted(selectedObservations):
71
+ r, msg = project_functions.check_state_events_obs(
72
+ obsId, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obsId], self.timeFormat
73
+ )
74
+
75
+ tot_out += f"<strong>{obsId}</strong><br>{msg}<br>"
76
+
77
+ results = dialog.Results_dialog()
78
+ results.setWindowTitle("Check state events")
79
+ results.ptText.clear()
80
+ results.ptText.setReadOnly(True)
81
+ results.ptText.appendHtml(tot_out)
82
+ results.exec_()
83
+
84
+
85
+ def fix_unpaired_events(self, silent_mode: bool = False):
86
+ """
87
+ fix unpaired state events
88
+ """
89
+
90
+ if self.observationId:
91
+ r, msg = project_functions.check_state_events_obs(
92
+ self.observationId, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][self.observationId]
93
+ )
94
+ if not silent_mode and "not PAIRED" not in msg:
95
+ QMessageBox.information(
96
+ None,
97
+ cfg.programName,
98
+ "All state events are already paired",
99
+ QMessageBox.Ok | QMessageBox.Default,
100
+ QMessageBox.NoButton,
101
+ )
102
+ return
103
+
104
+ w = dialog.Ask_time(0)
105
+ w.setWindowTitle("Fix UNPAIRED state events")
106
+ w.label.setText("Fix UNPAIRED events at time:")
107
+
108
+ if not w.exec_():
109
+ return
110
+
111
+ fix_at_time = w.time_widget.get_time()
112
+ if fix_at_time.is_nan():
113
+ QMessageBox.warning(
114
+ self,
115
+ cfg.programName,
116
+ ("Select a time format"),
117
+ )
118
+ return
119
+
120
+ events_to_add = project_functions.fix_unpaired_state_events(
121
+ self.pj[cfg.ETHOGRAM],
122
+ self.pj[cfg.OBSERVATIONS][self.observationId],
123
+ fix_at_time - dec("0.001"),
124
+ )
125
+
126
+ if events_to_add:
127
+ # determine the new frame index
128
+ if (self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA) and self.playerType == cfg.MEDIA:
129
+ mem_time = self.getLaps()
130
+ for event in events_to_add:
131
+ if not self.seek_mediaplayer(event[0]):
132
+ time.sleep(0.1)
133
+ frame_idx = self.get_frame_index()
134
+ event[cfg.PJ_OBS_FIELDS[cfg.MEDIA][cfg.FRAME_INDEX]] = frame_idx
135
+ self.seek_mediaplayer(mem_time)
136
+
137
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
138
+ self.project_changed()
139
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort(key=lambda x: x[:3])
140
+ self.load_tw_events(self.observationId)
141
+
142
+ index = self.tv_events.model().index(
143
+ [
144
+ event_idx
145
+ for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
146
+ if event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]] == fix_at_time
147
+ ][0],
148
+ 0,
149
+ )
150
+ self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
151
+
152
+ # selected observations
153
+ else:
154
+ _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
155
+ if not selected_observations:
156
+ return
157
+
158
+ # check if state events are paired
159
+ out: str = ""
160
+ for obs_id in selected_observations:
161
+ r, msg = project_functions.check_state_events_obs(obs_id, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id])
162
+ if "NOT PAIRED" in msg.upper():
163
+ # determine max time of events
164
+ fix_at_time = max(x[0] for x in self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS])
165
+ # list of events to add to fix unpaired events
166
+ events_to_add = project_functions.fix_unpaired_state_events(
167
+ self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id], fix_at_time
168
+ )
169
+ if events_to_add:
170
+ events_backup = self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][:]
171
+ self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].extend(events_to_add)
172
+
173
+ # check if modified obs if fixed
174
+ r, msg = project_functions.check_state_events_obs(obs_id, self.pj[cfg.ETHOGRAM], self.pj[cfg.OBSERVATIONS][obs_id])
175
+ if "NOT PAIRED" in msg.upper():
176
+ out += f"The observation <b>{obs_id}</b> can not be automatically fixed.<br><br>"
177
+ self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS] = events_backup
178
+ else:
179
+ out += f"<b>{obs_id}</b><br>"
180
+ self.project_changed()
181
+ if out:
182
+ out = "The following observations were modified to fix the unpaired state events:<br><br>" + out
183
+ self.results = dialog.Results_dialog()
184
+ self.results.setWindowTitle(cfg.programName + " - Fixed observations")
185
+ self.results.ptText.setReadOnly(True)
186
+ self.results.ptText.appendHtml(out)
187
+ self.results.pbSave.setVisible(False)
188
+ self.results.pbCancel.setVisible(True)
189
+ self.results.exec_()
190
+ else:
191
+ QMessageBox.information(
192
+ None,
193
+ cfg.programName,
194
+ "All state events are already paired",
195
+ QMessageBox.Ok | QMessageBox.Default,
196
+ QMessageBox.NoButton,
197
+ )
boris/subjects_pad.py ADDED
@@ -0,0 +1,106 @@
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 Signal, QRect, QEvent, Qt
23
+ from PySide6.QtWidgets import QGridLayout, QPushButton, QHBoxLayout, QWidget
24
+
25
+ from . import config as cfg
26
+ from . import utilities as util
27
+
28
+
29
+ class SubjectsPad(QWidget):
30
+ clickSignal = Signal(str)
31
+ sendEventSignal = Signal(QEvent)
32
+ close_signal = Signal(QRect)
33
+
34
+ def __init__(self, pj, filtered_subjects, parent=None):
35
+ super(SubjectsPad, self).__init__(parent)
36
+ self.pj = pj
37
+ self.filtered_subjects = filtered_subjects
38
+
39
+ self.setWindowTitle("Subjects pad")
40
+ self.grid = QGridLayout(self)
41
+ self.installEventFilter(self)
42
+ self.compose()
43
+
44
+ def compose(self):
45
+ for i in reversed(range(self.grid.count())):
46
+ self.grid.itemAt(i).widget().setParent(None)
47
+
48
+ subjects_list = [
49
+ ["", self.pj[cfg.SUBJECTS][x]["name"]]
50
+ for x in util.sorted_keys(self.pj[cfg.SUBJECTS])
51
+ if self.pj[cfg.SUBJECTS][x]["name"] in self.filtered_subjects
52
+ ]
53
+ dim = int(len(subjects_list) ** 0.5 + 0.999)
54
+
55
+ c = 0
56
+ for i in range(1, dim + 1):
57
+ for j in range(1, dim + 1):
58
+ if c >= len(subjects_list):
59
+ break
60
+ self.addWidget(subjects_list[c][1], i, j)
61
+ c += 1
62
+
63
+ def addWidget(self, subject, i, j):
64
+ self.grid.addWidget(Button(), i, j)
65
+ index = self.grid.count() - 1
66
+ widget = self.grid.itemAt(index).widget()
67
+
68
+ if widget is not None:
69
+ widget.pushButton.setText(subject)
70
+ color = "cyan"
71
+ widget.pushButton.setStyleSheet(
72
+ (
73
+ "background-color: {}; border-radius: 0px; min-width: 50px; max-width: 200px;"
74
+ " min-height:50px; max-height:200px; font-weight: bold;"
75
+ ).format(color)
76
+ )
77
+ widget.pushButton.clicked.connect(lambda: self.click(subject))
78
+
79
+ def click(self, subject):
80
+ self.clickSignal.emit(subject)
81
+
82
+ def eventFilter(self, receiver, event):
83
+ """
84
+ send event (if keypress) to main window
85
+ """
86
+ if event.type() == QEvent.KeyPress:
87
+ self.sendEventSignal.emit(event)
88
+ return True
89
+ else:
90
+ return False
91
+
92
+ def closeEvent(self, event):
93
+ """
94
+ send event for widget geometry memory
95
+ """
96
+ self.close_signal.emit(self.geometry())
97
+
98
+
99
+ class Button(QWidget):
100
+ def __init__(self, parent=None):
101
+ super(Button, self).__init__(parent)
102
+ self.pushButton = QPushButton()
103
+ self.pushButton.setFocusPolicy(Qt.NoFocus)
104
+ layout = QHBoxLayout()
105
+ layout.addWidget(self.pushButton)
106
+ self.setLayout(layout)