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
boris/portion/io.py ADDED
@@ -0,0 +1,181 @@
1
+ import re
2
+
3
+ from .const import Bound, inf
4
+ from .interval import Interval
5
+
6
+
7
+ def from_string(
8
+ string,
9
+ conv,
10
+ *,
11
+ bound=r".+?",
12
+ disj=r" ?\| ?",
13
+ sep=r", ?",
14
+ left_open=r"\(",
15
+ left_closed=r"\[",
16
+ right_open=r"\)",
17
+ right_closed=r"\]",
18
+ pinf=r"\+inf",
19
+ ninf=r"-inf",
20
+ ):
21
+ """
22
+ Parse given string and create an Interval instance.
23
+ A converter function has to be provided to convert a bound (as string) to a value.
24
+
25
+ :param string: string to parse.
26
+ :param conv: function that converts a bound (as string) to an object.
27
+ :param bound: pattern that matches a value.
28
+ :param disj: pattern that matches the disjunctive operator (default matches '|').
29
+ :param sep: pattern that matches a bounds separator (default matches ',').
30
+ :param left_open: pattern that matches a left open boundary (default matches '(').
31
+ :param left_closed: pattern that matches a left closed boundary (default matches '[').
32
+ :param right_open: pattern that matches a right open boundary (default matches ')').
33
+ :param right_closed: pattern that matches a right closed boundary (default matches ']').
34
+ :param pinf: pattern that matches a positive infinity (default matches '+inf').
35
+ :param ninf: pattern that matches a negative infinity (default matches '-inf').
36
+ :return: an Interval instance.
37
+ """
38
+
39
+ re_left_boundary = r"(?P<left>{}|{})".format(left_open, left_closed)
40
+ re_right_boundary = r"(?P<right>{}|{})".format(right_open, right_closed)
41
+ re_bounds = r"(?P<lower>{bound})({sep}(?P<upper>{bound}))?".format(bound=bound, sep=sep)
42
+ re_interval = r"{}(|{}){}".format(re_left_boundary, re_bounds, re_right_boundary)
43
+ re_intervals = r"{}(?P<disj>{})?".format(re_interval, disj)
44
+
45
+ intervals = []
46
+ has_more = True
47
+
48
+ def _convert(bound):
49
+ if re.match(pinf, bound):
50
+ return inf
51
+ elif re.match(ninf, bound):
52
+ return -inf
53
+ else:
54
+ return conv(bound)
55
+
56
+ while has_more:
57
+ match = re.match(re_intervals, string)
58
+ if match is None:
59
+ has_more = False
60
+ else:
61
+ group = match.groupdict()
62
+
63
+ left = Bound.CLOSED if re.match(left_closed + "$", group["left"]) else Bound.OPEN
64
+ right = Bound.CLOSED if re.match(right_closed + "$", group["right"]) else Bound.OPEN
65
+
66
+ lower = group.get("lower", None)
67
+ upper = group.get("upper", None)
68
+ lower = _convert(lower) if lower is not None else inf
69
+ upper = _convert(upper) if upper is not None else lower
70
+
71
+ intervals.append(Interval.from_atomic(left, lower, upper, right))
72
+ string = string[match.end() :]
73
+
74
+ return Interval(*intervals)
75
+
76
+
77
+ def to_string(
78
+ interval, conv=repr, *, disj=" | ", sep=",", left_open="(", left_closed="[", right_open=")", right_closed="]", pinf="+inf", ninf="-inf"
79
+ ):
80
+ """
81
+ Export given interval to string.
82
+
83
+ :param interval: an interval.
84
+ :param conv: function that is used to represent a bound (default is `repr`).
85
+ :param disj: string representing disjunctive operator (default is ' | ').
86
+ :param sep: string representing bound separator (default is ',').
87
+ :param left_open: string representing left open boundary (default is '(').
88
+ :param left_closed: string representing left closed boundary (default is '[').
89
+ :param right_open: string representing right open boundary (default is ')').
90
+ :param right_closed: string representing right closed boundary (default is ']').
91
+ :param pinf: string representing a positive infinity (default is '+inf').
92
+ :param ninf: string representing a negative infinity (default is '-inf').
93
+ :return: a string representation for given interval.
94
+ """
95
+ if interval.empty:
96
+ return "{}{}".format(left_open, right_open)
97
+
98
+ def _convert(bound):
99
+ if bound == inf:
100
+ return pinf
101
+ elif bound == -inf:
102
+ return ninf
103
+ else:
104
+ return conv(bound)
105
+
106
+ exported_intervals = []
107
+ for item in interval:
108
+ left = left_open if item.left == Bound.OPEN else left_closed
109
+ right = right_open if item.right == Bound.OPEN else right_closed
110
+
111
+ lower = _convert(item.lower)
112
+ upper = _convert(item.upper)
113
+
114
+ if item.lower == item.upper:
115
+ exported_intervals.append("{}{}{}".format(left, lower, right))
116
+ else:
117
+ exported_intervals.append("{}{}{}{}{}".format(left, lower, sep, upper, right))
118
+
119
+ return disj.join(exported_intervals)
120
+
121
+
122
+ def from_data(data, conv=None, *, pinf=float("inf"), ninf=float("-inf")):
123
+ """
124
+ Import an interval from a piece of data.
125
+
126
+ :param data: a list of 4-uples (left, lower, upper, right).
127
+ :param conv: function that converts "lower" and "upper" to bounds, default to identity.
128
+ :param pinf: value used to represent positive infinity.
129
+ :param ninf: value used to represent negative infinity.
130
+ :return: an Interval instance.
131
+ """
132
+ intervals = []
133
+ conv = (lambda v: v) if conv is None else conv
134
+
135
+ def _convert(bound):
136
+ if bound == pinf:
137
+ return inf
138
+ elif bound == ninf:
139
+ return -inf
140
+ else:
141
+ return conv(bound)
142
+
143
+ for item in data:
144
+ left, lower, upper, right = item
145
+ intervals.append(
146
+ Interval.from_atomic(
147
+ Bound(left),
148
+ _convert(lower),
149
+ _convert(upper),
150
+ Bound(right),
151
+ )
152
+ )
153
+ return Interval(*intervals)
154
+
155
+
156
+ def to_data(interval, conv=None, *, pinf=float("inf"), ninf=float("-inf")):
157
+ """
158
+ Export given interval to a list of 4-uples (left, lower,
159
+ upper, right).
160
+
161
+ :param interval: an interval.
162
+ :param conv: function that convert bounds to "lower" and "upper", default to identity.
163
+ :param pinf: value used to encode positive infinity.
164
+ :param ninf: value used to encode negative infinity.
165
+ :return: a list of 4-uples (left, lower, upper, right)
166
+ """
167
+ conv = (lambda v: v) if conv is None else conv
168
+
169
+ data = []
170
+
171
+ def _convert(bound):
172
+ if bound == inf:
173
+ return pinf
174
+ elif bound == -inf:
175
+ return ninf
176
+ else:
177
+ return conv(bound)
178
+
179
+ for item in interval:
180
+ data.append((item.left.value, _convert(item.lower), _convert(item.upper), item.right.value))
181
+ return data
boris/preferences.py ADDED
@@ -0,0 +1,510 @@
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 os
25
+ from pathlib import Path
26
+ import sys
27
+ from . import dialog
28
+ from . import gui_utilities
29
+ from . import menu_options
30
+ from . import config as cfg
31
+ from . import config_file
32
+ from . import plugins
33
+
34
+ from .preferences_ui import Ui_prefDialog
35
+
36
+ from PySide6.QtWidgets import QDialog, QFileDialog, QListWidgetItem, QMessageBox
37
+ from PySide6.QtCore import Qt
38
+ from PySide6.QtGui import QFont
39
+
40
+
41
+ class Preferences(QDialog, Ui_prefDialog):
42
+ def __init__(self, parent=None):
43
+ super().__init__()
44
+ self.setupUi(self)
45
+
46
+ # plugins
47
+ self.pb_browse_plugins_dir.clicked.connect(self.browse_plugins_dir)
48
+
49
+ self.pbBrowseFFmpegCacheDir.clicked.connect(self.browseFFmpegCacheDir)
50
+
51
+ self.pb_reset_behav_colors.clicked.connect(self.reset_behav_colors)
52
+ self.pb_reset_category_colors.clicked.connect(self.reset_category_colors)
53
+
54
+ self.pb_refresh.clicked.connect(self.refresh_preferences)
55
+ self.pbOK.clicked.connect(self.accept)
56
+ self.pbCancel.clicked.connect(self.reject)
57
+
58
+ self.flag_refresh = False
59
+
60
+ # Create a monospace QFont
61
+ monospace_font = QFont("Courier New") # or "Monospace", "Consolas", "Liberation Mono", etc.
62
+ monospace_font.setStyleHint(QFont.Monospace)
63
+ monospace_font.setPointSize(12)
64
+ self.pte_plugin_code.setFont(monospace_font)
65
+
66
+ def browse_plugins_dir(self):
67
+ """
68
+ get the personal plugins directory
69
+ """
70
+ directory = QFileDialog.getExistingDirectory(None, "Select the plugins directory", self.le_personal_plugins_dir.text())
71
+ if not directory:
72
+ return
73
+
74
+ self.le_personal_plugins_dir.setText(directory)
75
+ self.lw_personal_plugins.clear()
76
+ for file_ in Path(directory).glob("*.py"):
77
+ if file_.name.startswith("_"):
78
+ continue
79
+ plugin_name = plugins.get_plugin_name(file_)
80
+ if plugin_name is None:
81
+ continue
82
+ # check if personal plugin name is in BORIS plugins (case sensitive)
83
+ if plugin_name in [self.lv_all_plugins.item(i).text() for i in range(self.lv_all_plugins.count())]:
84
+ continue
85
+ item = QListWidgetItem(plugin_name)
86
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
87
+ item.setCheckState(Qt.Checked)
88
+ item.setData(100, str(file_))
89
+ self.lw_personal_plugins.addItem(item)
90
+
91
+ if self.lw_personal_plugins.count() == 0:
92
+ QMessageBox.warning(self, cfg.programName, f"No plugin found in {directory}")
93
+
94
+ def refresh_preferences(self):
95
+ """
96
+ allow user to delete the config file (.boris)
97
+ """
98
+ if (
99
+ dialog.MessageDialog(
100
+ "BORIS",
101
+ ("Refresh will re-initialize all your preferences and close BORIS"),
102
+ [cfg.CANCEL, "Refresh preferences"],
103
+ )
104
+ == "Refresh preferences"
105
+ ):
106
+ self.flag_refresh = True
107
+ self.accept()
108
+
109
+ def browseFFmpegCacheDir(self):
110
+ """
111
+ allow user select a cache dir for ffmpeg images
112
+ """
113
+ FFmpegCacheDir = QFileDialog.getExistingDirectory(
114
+ self,
115
+ "Select a directory",
116
+ os.path.expanduser("~"),
117
+ options=QFileDialog.ShowDirsOnly,
118
+ )
119
+ if FFmpegCacheDir:
120
+ self.leFFmpegCacheDir.setText(FFmpegCacheDir)
121
+
122
+ def reset_behav_colors(self):
123
+ """
124
+ reset behavior colors to default
125
+ """
126
+ self.te_behav_colors.setPlainText("\n".join(cfg.BEHAVIORS_PLOT_COLORS))
127
+
128
+ logging.debug("reset behaviors colors to default")
129
+
130
+ def reset_category_colors(self):
131
+ """
132
+ reset category colors to default
133
+ """
134
+ self.te_category_colors.setPlainText("\n".join(cfg.CATEGORY_COLORS_LIST))
135
+
136
+ logging.debug("reset category colors to default")
137
+
138
+
139
+ def preferences(self):
140
+ """
141
+ show preferences window
142
+ """
143
+
144
+ def show_plugin_info(item):
145
+ """
146
+ display information about the clicked plugin
147
+ """
148
+
149
+ if item.text() not in self.config_param[cfg.ANALYSIS_PLUGINS]:
150
+ return
151
+
152
+ plugin_path = item.data(100)
153
+
154
+ # Python plugins
155
+ if Path(plugin_path).suffix == ".py":
156
+ import importlib
157
+
158
+ module_name = Path(plugin_path).stem
159
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
160
+ plugin_module = importlib.util.module_from_spec(spec)
161
+ spec.loader.exec_module(plugin_module)
162
+ attributes_list = dir(plugin_module)
163
+
164
+ out: list = []
165
+ out.append((plugin_module.__plugin_name__ + "\n") if "__plugin_name__" in attributes_list else "No plugin name provided")
166
+ out.append(plugin_module.__author__ if "__author__" in attributes_list else "No author provided")
167
+ version_str: str = ""
168
+ if "__version__" in attributes_list:
169
+ version_str += str(plugin_module.__version__)
170
+ if "__version_date__" in attributes_list:
171
+ version_str += " " if version_str else ""
172
+ version_str += f"({plugin_module.__version_date__})"
173
+
174
+ out.append(f"Version: {version_str}\n" if version_str else "No version provided")
175
+
176
+ # out.append(plugin_module.run.__doc__.strip())
177
+ # description
178
+ if "__description__" in attributes_list:
179
+ out.append("Description:\n")
180
+ out.append(plugin_module.__description__ if "__description__" in attributes_list else "No description provided")
181
+
182
+ preferencesWindow.pte_plugin_description.setPlainText("\n".join(out))
183
+
184
+ # R plugins
185
+ if Path(plugin_path).suffix == ".R":
186
+ plugin_description = plugins.get_r_plugin_description(plugin_path)
187
+ if plugin_description is not None:
188
+ preferencesWindow.pte_plugin_description.setPlainText("\n".join(plugin_description.split("\\n")))
189
+ else:
190
+ preferencesWindow.pte_plugin_description.setPlainText("No description provided")
191
+
192
+ # display plugin code
193
+ try:
194
+ with open(plugin_path, "r") as f_in:
195
+ plugin_code = f_in.read()
196
+ except Exception:
197
+ plugin_code = "Not available"
198
+
199
+ preferencesWindow.pte_plugin_code.setPlainText(plugin_code)
200
+
201
+ preferencesWindow = Preferences()
202
+ preferencesWindow.tabWidget.setCurrentIndex(0)
203
+
204
+ if self.timeFormat == cfg.S:
205
+ preferencesWindow.cbTimeFormat.setCurrentIndex(0)
206
+
207
+ if self.timeFormat == cfg.HHMMSS:
208
+ preferencesWindow.cbTimeFormat.setCurrentIndex(1)
209
+
210
+ preferencesWindow.sbffSpeed.setValue(self.fast)
211
+ preferencesWindow.cb_adapt_fast_jump.setChecked(self.config_param.get(cfg.ADAPT_FAST_JUMP, False))
212
+ preferencesWindow.sbRepositionTimeOffset.setValue(self.repositioningTimeOffset)
213
+ preferencesWindow.sbSpeedStep.setValue(self.play_rate_step)
214
+ # automatic backup
215
+ preferencesWindow.sbAutomaticBackup.setValue(self.automaticBackup)
216
+ # separator for behavioural strings
217
+ preferencesWindow.leSeparator.setText(self.behav_seq_separator)
218
+ # close same event indep of modifiers
219
+ preferencesWindow.cbCloseSameEvent.setChecked(self.close_the_same_current_event)
220
+ # confirm sound
221
+ preferencesWindow.cbConfirmSound.setChecked(self.confirmSound)
222
+ # beep every
223
+ preferencesWindow.sbBeepEvery.setValue(self.beep_every)
224
+ # frame step size
225
+ # preferencesWindow.sb_frame_step_size.setValue(self.config_param.get(cfg.FRAME_STEP_SIZE, cfg.FRAME_STEP_SIZE_DEFAULT_VALUE))
226
+
227
+ # alert no focal subject
228
+ preferencesWindow.cbAlertNoFocalSubject.setChecked(self.alertNoFocalSubject)
229
+ # tracking cursor above event
230
+ preferencesWindow.cbTrackingCursorAboveEvent.setChecked(self.trackingCursorAboveEvent)
231
+ # check for new version
232
+ preferencesWindow.cbCheckForNewVersion.setChecked(self.checkForNewVersion)
233
+ # display subtitles
234
+ preferencesWindow.cb_display_subtitles.setChecked(self.config_param.get(cfg.DISPLAY_SUBTITLES, False))
235
+ # pause before add event
236
+ preferencesWindow.cb_pause_before_addevent.setChecked(self.pause_before_addevent)
237
+ # MPV hwdec
238
+ preferencesWindow.cb_hwdec.clear()
239
+ preferencesWindow.cb_hwdec.addItems(cfg.MPV_HWDEC_OPTIONS)
240
+ try:
241
+ preferencesWindow.cb_hwdec.setCurrentIndex(
242
+ cfg.MPV_HWDEC_OPTIONS.index(self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE))
243
+ )
244
+ except Exception:
245
+ preferencesWindow.cb_hwdec.setCurrentIndex(cfg.MPV_HWDEC_OPTIONS.index(cfg.MPV_HWDEC_DEFAULT_VALUE))
246
+ # check integrity
247
+ preferencesWindow.cb_check_integrity_at_opening.setChecked(self.config_param.get(cfg.CHECK_PROJECT_INTEGRITY, True))
248
+
249
+ # BORIS plugins
250
+ preferencesWindow.lv_all_plugins.itemClicked.connect(show_plugin_info)
251
+
252
+ preferencesWindow.lv_all_plugins.clear()
253
+
254
+ for file_ in (Path(__file__).parent / "analysis_plugins").glob("*.py"):
255
+ if file_.name.startswith("_"):
256
+ continue
257
+ plugin_name = plugins.get_plugin_name(file_)
258
+ if plugin_name is not None:
259
+ item = QListWidgetItem(plugin_name)
260
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
261
+ if plugin_name in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
262
+ item.setCheckState(Qt.Unchecked)
263
+ else:
264
+ item.setCheckState(Qt.Checked)
265
+ item.setData(100, str(file_))
266
+ preferencesWindow.lv_all_plugins.addItem(item)
267
+
268
+ # personal plugins
269
+ preferencesWindow.le_personal_plugins_dir.setText(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""))
270
+ preferencesWindow.lw_personal_plugins.itemClicked.connect(show_plugin_info)
271
+
272
+ preferencesWindow.lw_personal_plugins.clear()
273
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
274
+ # Python plugins
275
+ for file_ in Path(self.config_param[cfg.PERSONAL_PLUGINS_DIR]).glob("*.py"):
276
+ if file_.name.startswith("_"):
277
+ continue
278
+ plugin_name = plugins.get_plugin_name(file_)
279
+ if plugin_name is None:
280
+ continue
281
+ # check if personal plugin name is in BORIS plugins (case sensitive)
282
+ if plugin_name in [preferencesWindow.lv_all_plugins.item(i).text() for i in range(preferencesWindow.lv_all_plugins.count())]:
283
+ continue
284
+ item = QListWidgetItem(plugin_name)
285
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
286
+ if plugin_name in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
287
+ item.setCheckState(Qt.Unchecked)
288
+ else:
289
+ item.setCheckState(Qt.Checked)
290
+ item.setData(100, str(file_))
291
+ preferencesWindow.lw_personal_plugins.addItem(item)
292
+
293
+ # R plugins
294
+ for file_ in Path(self.config_param[cfg.PERSONAL_PLUGINS_DIR]).glob("*.R"):
295
+ plugin_name = plugins.get_r_plugin_name(file_)
296
+ if plugin_name is None:
297
+ continue
298
+ # check if personal plugin name is in BORIS plugins (case sensitive)
299
+ if plugin_name in [preferencesWindow.lv_all_plugins.item(i).text() for i in range(preferencesWindow.lv_all_plugins.count())]:
300
+ continue
301
+ item = QListWidgetItem(plugin_name)
302
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
303
+ if plugin_name in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
304
+ item.setCheckState(Qt.Unchecked)
305
+ else:
306
+ item.setCheckState(Qt.Checked)
307
+ item.setData(100, str(file_))
308
+ preferencesWindow.lw_personal_plugins.addItem(item)
309
+
310
+ # PROJET FILE INDENTATION
311
+ preferencesWindow.combo_project_file_indentation.clear()
312
+ preferencesWindow.combo_project_file_indentation.addItems(cfg.PROJECT_FILE_INDENTATION_COMBO_OPTIONS)
313
+ try:
314
+ preferencesWindow.combo_project_file_indentation.setCurrentIndex(
315
+ cfg.PROJECT_FILE_INDENTATION_OPTIONS.index(
316
+ self.config_param.get(
317
+ cfg.PROJECT_FILE_INDENTATION,
318
+ cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
319
+ )
320
+ )
321
+ )
322
+ except Exception:
323
+ preferencesWindow.combo_project_file_indentation.setCurrentText(
324
+ cfg.PROJECT_FILE_INDENTATION_COMBO_OPTIONS[
325
+ cfg.PROJECT_FILE_INDENTATION_OPTIONS.index(cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
326
+ ]
327
+ )
328
+
329
+ # FFmpeg for frame by frame mode
330
+ preferencesWindow.lbFFmpegPath.setText(f"FFmpeg path: {self.ffmpeg_bin}")
331
+ preferencesWindow.leFFmpegCacheDir.setText(self.ffmpeg_cache_dir)
332
+
333
+ # spectrogram
334
+ preferencesWindow.cbSpectrogramColorMap.clear()
335
+ preferencesWindow.cbSpectrogramColorMap.addItems(cfg.SPECTROGRAM_COLOR_MAPS)
336
+ try:
337
+ preferencesWindow.cbSpectrogramColorMap.setCurrentIndex(cfg.SPECTROGRAM_COLOR_MAPS.index(self.spectrogram_color_map))
338
+ except Exception:
339
+ preferencesWindow.cbSpectrogramColorMap.setCurrentIndex(cfg.SPECTROGRAM_COLOR_MAPS.index(cfg.SPECTROGRAM_DEFAULT_COLOR_MAP))
340
+ # time interval
341
+ try:
342
+ preferencesWindow.sb_time_interval.setValue(self.spectrogram_time_interval)
343
+ except Exception:
344
+ preferencesWindow.sb_time_interval.setValue(cfg.SPECTROGRAM_DEFAULT_TIME_INTERVAL)
345
+ # window type
346
+ preferencesWindow.cb_window_type.setCurrentText(self.config_param.get(cfg.SPECTROGRAM_WINDOW_TYPE, cfg.SPECTROGRAM_DEFAULT_WINDOW_TYPE))
347
+ # NFFT
348
+ preferencesWindow.cb_NFFT.setCurrentText(self.config_param.get(cfg.SPECTROGRAM_NFFT, cfg.SPECTROGRAM_DEFAULT_NFFT))
349
+ # noverlap
350
+ preferencesWindow.sb_noverlap.setValue(self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP))
351
+ # vmin
352
+ preferencesWindow.sb_vmin.setValue(self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN))
353
+ # vmax
354
+ preferencesWindow.sb_vmax.setValue(self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX))
355
+
356
+ # behavior colors
357
+ if not self.plot_colors:
358
+ self.plot_colors = cfg.BEHAVIORS_PLOT_COLORS
359
+ preferencesWindow.te_behav_colors.setPlainText("\n".join(self.plot_colors))
360
+
361
+ # category colors
362
+ if not self.behav_category_colors:
363
+ self.behav_category_colors = cfg.CATEGORY_COLORS_LIST
364
+ preferencesWindow.te_category_colors.setPlainText("\n".join(self.behav_category_colors))
365
+
366
+ # interface
367
+ preferencesWindow.sb_toolbar_icon_size.setValue(self.config_param.get(cfg.TOOLBAR_ICON_SIZE, cfg.DEFAULT_TOOLBAR_ICON_SIZE_VALUE))
368
+
369
+ gui_utilities.restore_geometry(preferencesWindow, "preferences", (700, 500))
370
+
371
+ while True:
372
+ if preferencesWindow.exec():
373
+ if preferencesWindow.sb_vmin.value() >= preferencesWindow.sb_vmax.value():
374
+ QMessageBox.warning(self, cfg.programName, "Spectrogram parameters: the vmin value must be lower than the vmax value.")
375
+ continue
376
+
377
+ if preferencesWindow.sb_noverlap.value() >= int(preferencesWindow.cb_NFFT.currentText()):
378
+ QMessageBox.warning(self, cfg.programName, "Spectrogram parameters: the noverlap value must be lower than the NFFT value.")
379
+ continue
380
+
381
+ gui_utilities.save_geometry(preferencesWindow, "preferences")
382
+
383
+ if preferencesWindow.flag_refresh:
384
+ # refresh preferences remove the config file
385
+
386
+ logging.debug("flag refresh ")
387
+
388
+ self.config_param["refresh_preferences"] = True
389
+ self.close()
390
+ # check if refresh canceled for not saved project
391
+ if "refresh_preferences" in self.config_param:
392
+ if (Path.home() / ".boris").exists():
393
+ os.remove(Path.home() / ".boris")
394
+ sys.exit()
395
+
396
+ if preferencesWindow.cbTimeFormat.currentIndex() == 0:
397
+ self.timeFormat = cfg.S
398
+
399
+ if preferencesWindow.cbTimeFormat.currentIndex() == 1:
400
+ self.timeFormat = cfg.HHMMSS
401
+
402
+ self.fast = preferencesWindow.sbffSpeed.value()
403
+
404
+ self.config_param[cfg.ADAPT_FAST_JUMP] = preferencesWindow.cb_adapt_fast_jump.isChecked()
405
+
406
+ self.repositioningTimeOffset = preferencesWindow.sbRepositionTimeOffset.value()
407
+
408
+ self.play_rate_step = preferencesWindow.sbSpeedStep.value()
409
+
410
+ self.automaticBackup = preferencesWindow.sbAutomaticBackup.value()
411
+ if self.automaticBackup:
412
+ self.automaticBackupTimer.start(self.automaticBackup * 60000)
413
+ else:
414
+ self.automaticBackupTimer.stop()
415
+
416
+ self.behav_seq_separator = preferencesWindow.leSeparator.text()
417
+
418
+ self.close_the_same_current_event = preferencesWindow.cbCloseSameEvent.isChecked()
419
+
420
+ self.confirmSound = preferencesWindow.cbConfirmSound.isChecked()
421
+
422
+ self.beep_every = preferencesWindow.sbBeepEvery.value()
423
+
424
+ # frame step size
425
+ # self.config_param[cfg.FRAME_STEP_SIZE] = preferencesWindow.sb_frame_step_size.value()
426
+
427
+ self.alertNoFocalSubject = preferencesWindow.cbAlertNoFocalSubject.isChecked()
428
+
429
+ self.trackingCursorAboveEvent = preferencesWindow.cbTrackingCursorAboveEvent.isChecked()
430
+
431
+ self.checkForNewVersion = preferencesWindow.cbCheckForNewVersion.isChecked()
432
+
433
+ self.config_param[cfg.DISPLAY_SUBTITLES] = preferencesWindow.cb_display_subtitles.isChecked()
434
+
435
+ self.pause_before_addevent = preferencesWindow.cb_pause_before_addevent.isChecked()
436
+
437
+ # MPV hwdec
438
+ self.config_param[cfg.MPV_HWDEC] = cfg.MPV_HWDEC_OPTIONS[preferencesWindow.cb_hwdec.currentIndex()]
439
+
440
+ # check project integrity
441
+ self.config_param[cfg.CHECK_PROJECT_INTEGRITY] = preferencesWindow.cb_check_integrity_at_opening.isChecked()
442
+
443
+ # update BORIS analysis plugins
444
+ self.config_param[cfg.ANALYSIS_PLUGINS] = {}
445
+ self.config_param[cfg.EXCLUDED_PLUGINS] = set()
446
+ for i in range(preferencesWindow.lv_all_plugins.count()):
447
+ if preferencesWindow.lv_all_plugins.item(i).checkState() == Qt.Checked:
448
+ self.config_param[cfg.ANALYSIS_PLUGINS][preferencesWindow.lv_all_plugins.item(i).text()] = (
449
+ preferencesWindow.lv_all_plugins.item(i).data(100)
450
+ )
451
+ else:
452
+ self.config_param[cfg.EXCLUDED_PLUGINS].add(preferencesWindow.lv_all_plugins.item(i).text())
453
+
454
+ # update personal plugins
455
+ self.config_param[cfg.PERSONAL_PLUGINS_DIR] = preferencesWindow.le_personal_plugins_dir.text()
456
+ for i in range(preferencesWindow.lw_personal_plugins.count()):
457
+ if preferencesWindow.lw_personal_plugins.item(i).checkState() == Qt.Checked:
458
+ self.config_param[cfg.ANALYSIS_PLUGINS][preferencesWindow.lw_personal_plugins.item(i).text()] = (
459
+ preferencesWindow.lw_personal_plugins.item(i).data(100)
460
+ )
461
+ else:
462
+ self.config_param[cfg.EXCLUDED_PLUGINS].add(preferencesWindow.lw_personal_plugins.item(i).text())
463
+
464
+ plugins.load_plugins(self)
465
+ plugins.add_plugins_to_menu(self)
466
+
467
+ # project file indentation
468
+ self.config_param[cfg.PROJECT_FILE_INDENTATION] = cfg.PROJECT_FILE_INDENTATION_OPTIONS[
469
+ preferencesWindow.combo_project_file_indentation.currentIndex()
470
+ ]
471
+
472
+ if self.observationId:
473
+ self.load_tw_events(self.observationId)
474
+ self.display_statusbar_info(self.observationId)
475
+
476
+ self.ffmpeg_cache_dir = preferencesWindow.leFFmpegCacheDir.text()
477
+
478
+ # spectrogram
479
+ self.spectrogram_color_map = preferencesWindow.cbSpectrogramColorMap.currentText()
480
+ self.spectrogram_time_interval = preferencesWindow.sb_time_interval.value()
481
+ # window type
482
+ self.config_param[cfg.SPECTROGRAM_WINDOW_TYPE] = preferencesWindow.cb_window_type.currentText()
483
+ # NFFT
484
+ self.config_param[cfg.SPECTROGRAM_NFFT] = preferencesWindow.cb_NFFT.currentText()
485
+ # noverlap
486
+ self.config_param[cfg.SPECTROGRAM_NOVERLAP] = preferencesWindow.sb_noverlap.value()
487
+ # vmin
488
+ self.config_param[cfg.SPECTROGRAM_VMIN] = preferencesWindow.sb_vmin.value()
489
+ # vmax
490
+ self.config_param[cfg.SPECTROGRAM_VMAX] = preferencesWindow.sb_vmax.value()
491
+
492
+ # behav colors
493
+ self.plot_colors = preferencesWindow.te_behav_colors.toPlainText().split()
494
+ # category colors
495
+ self.behav_category_colors = preferencesWindow.te_category_colors.toPlainText().split()
496
+
497
+ # interface
498
+ self.config_param[cfg.TOOLBAR_ICON_SIZE] = preferencesWindow.sb_toolbar_icon_size.value()
499
+
500
+ menu_options.update_menu(self)
501
+
502
+ config_file.save(self)
503
+
504
+ break
505
+
506
+ else:
507
+ break
508
+
509
+ # activate main window
510
+ self.activateWindow()