boris-behav-obs 9.3.1__tar.gz → 9.3.3__tar.gz

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 (122) hide show
  1. {boris_behav_obs-9.3.1/boris_behav_obs.egg-info → boris_behav_obs-9.3.3}/PKG-INFO +1 -1
  2. boris_behav_obs-9.3.3/boris/analysis_plugins/_latency.py +59 -0
  3. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/behav_coding_map_creator.py +2 -6
  4. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/config.py +4 -1
  5. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/core.py +21 -60
  6. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/dialog.py +8 -3
  7. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/gui_utilities.py +30 -4
  8. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/media_file.py +12 -10
  9. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/modifier_coding_map_creator.py +6 -21
  10. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/observation.py +25 -20
  11. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/observation_operations.py +6 -6
  12. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/player_dock_widget.py +7 -5
  13. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plugins.py +4 -0
  14. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/project.py +69 -21
  15. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/project_import_export.py +3 -1
  16. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/utilities.py +35 -55
  17. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/version.py +2 -2
  18. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3/boris_behav_obs.egg-info}/PKG-INFO +1 -1
  19. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris_behav_obs.egg-info/SOURCES.txt +1 -1
  20. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/pyproject.toml +7 -2
  21. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_utilities.py +105 -70
  22. boris_behav_obs-9.3.1/boris/1.py +0 -45
  23. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/LICENSE.TXT +0 -0
  24. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/MANIFEST.in +0 -0
  25. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/README.TXT +0 -0
  26. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/README.md +0 -0
  27. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/__init__.py +0 -0
  28. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/__main__.py +0 -0
  29. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/about.py +0 -0
  30. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/add_modifier.py +0 -0
  31. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/add_modifier_ui.py +0 -0
  32. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/advanced_event_filtering.py +0 -0
  33. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/analysis_plugins/__init__.py +0 -0
  34. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/analysis_plugins/number_of_occurences.py +0 -0
  35. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  36. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/analysis_plugins/time_budget.py +0 -0
  37. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/behavior_binary_table.py +0 -0
  38. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/behaviors_coding_map.py +0 -0
  39. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/boris_cli.py +0 -0
  40. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/cmd_arguments.py +0 -0
  41. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/coding_pad.py +0 -0
  42. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/config_file.py +0 -0
  43. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/connections.py +0 -0
  44. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/converters.py +0 -0
  45. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/converters_ui.py +0 -0
  46. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/cooccurence.py +0 -0
  47. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/core_qrc.py +0 -0
  48. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/core_ui.py +0 -0
  49. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/db_functions.py +0 -0
  50. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/dev.py +0 -0
  51. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/duration_widget.py +0 -0
  52. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/edit_event.py +0 -0
  53. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/edit_event_ui.py +0 -0
  54. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/event_operations.py +0 -0
  55. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/events_cursor.py +0 -0
  56. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/events_snapshots.py +0 -0
  57. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/exclusion_matrix.py +0 -0
  58. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/export_events.py +0 -0
  59. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/export_observation.py +0 -0
  60. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/external_processes.py +0 -0
  61. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/geometric_measurement.py +0 -0
  62. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/image_overlay.py +0 -0
  63. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/import_observations.py +0 -0
  64. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/irr.py +0 -0
  65. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/latency.py +0 -0
  66. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/measurement_widget.py +0 -0
  67. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/menu_options.py +0 -0
  68. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/modifiers_coding_map.py +0 -0
  69. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/mpv-1.0.3.py +0 -0
  70. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/mpv.py +0 -0
  71. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/mpv2.py +0 -0
  72. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/observation_ui.py +0 -0
  73. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/observations_list.py +0 -0
  74. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/otx_parser.py +0 -0
  75. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/param_panel.py +0 -0
  76. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/param_panel_ui.py +0 -0
  77. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plot_data_module.py +0 -0
  78. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plot_events.py +0 -0
  79. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plot_events_rt.py +0 -0
  80. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plot_spectrogram_rt.py +0 -0
  81. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/plot_waveform_rt.py +0 -0
  82. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/__init__.py +0 -0
  83. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/const.py +0 -0
  84. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/dict.py +0 -0
  85. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/func.py +0 -0
  86. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/interval.py +0 -0
  87. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/portion/io.py +0 -0
  88. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/preferences.py +0 -0
  89. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/preferences_ui.py +0 -0
  90. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/project_functions.py +0 -0
  91. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/project_ui.py +0 -0
  92. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/qrc_boris.py +0 -0
  93. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/qrc_boris5.py +0 -0
  94. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/select_modifiers.py +0 -0
  95. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/select_observations.py +0 -0
  96. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/select_subj_behav.py +0 -0
  97. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/state_events.py +0 -0
  98. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/subjects_pad.py +0 -0
  99. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/synthetic_time_budget.py +0 -0
  100. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/time_budget_functions.py +0 -0
  101. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/time_budget_widget.py +0 -0
  102. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/transitions.py +0 -0
  103. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/video_equalizer.py +0 -0
  104. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/video_equalizer_ui.py +0 -0
  105. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/video_operations.py +0 -0
  106. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/view_df.py +0 -0
  107. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/view_df_ui.py +0 -0
  108. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris/write_event.py +0 -0
  109. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  110. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  111. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris_behav_obs.egg-info/requires.txt +0 -0
  112. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/boris_behav_obs.egg-info/top_level.txt +0 -0
  113. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/setup.cfg +0 -0
  114. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_db_functions.py +0 -0
  115. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_export_observation.py +0 -0
  116. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_irr.py +0 -0
  117. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_observation_gui.py +0 -0
  118. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_otx_parser.py +0 -0
  119. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_preferences_gui.py +0 -0
  120. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_project_functions.py +0 -0
  121. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_time_budget.py +0 -0
  122. {boris_behav_obs-9.3.1 → boris_behav_obs-9.3.3}/tests/test_utilities2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.3.1
3
+ Version: 9.3.3
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License-Expression: GPL-3.0-only
@@ -0,0 +1,59 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ number of occurences of behaviors
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ __version__ = "0.0.1"
10
+ __version_date__ = "2025-04-10"
11
+ __plugin_name__ = "Behavior latency"
12
+ __author__ = "Olivier Friard - University of Torino - Italy"
13
+
14
+
15
+ import itertools
16
+
17
+
18
+ def run(df: pd.DataFrame):
19
+ """
20
+ Latency of a behavior after another.
21
+ """
22
+
23
+ df["start_time"] = pd.to_datetime(df["Start (s)"])
24
+ df["end_time"] = pd.to_datetime(df["Stop (s)"])
25
+
26
+ latency_by_subject: dict = {}
27
+
28
+ for subject, group in df.groupby("subject"):
29
+ behaviors = group["behavior"].tolist()
30
+ # combinations = []
31
+ # Utiliser itertools pour créer des combinaisons 2 à 2 des comportements
32
+ for comb in itertools.combinations(behaviors, 2):
33
+ # combinations.append(comb)
34
+
35
+ last_A_end_time = None
36
+
37
+ # Liste pour stocker les latences de chaque sujet
38
+ subject_latency = []
39
+
40
+ for index, row in group.iterrows():
41
+ if row["behavior"] == comb[0]:
42
+ # Si on rencontre un comportement A, on réinitialise le temps de fin du comportement A
43
+ last_A_end_time = row["end_time"]
44
+ subject_latency.append(None) # Pas de latence pour A
45
+ elif row["behavior"] == comb[1] and last_A_end_time is not None:
46
+ # Si on rencontre un comportement B et qu'on a déjà vu un A avant
47
+ latency_time = row["start_time"] - last_A_end_time
48
+ subject_latency.append(latency_time)
49
+ else:
50
+ # Si on rencontre un B mais sans A avant
51
+ subject_latency.append(None)
52
+
53
+ # Ajout des latences calculées au DataFrame
54
+ df.loc[group.index, f"latency {comb[1]} after {comb[0]}"] = subject_latency
55
+
56
+ # Calcul de la latence totale ou moyenne par sujet
57
+ latency_by_subject[(subject, comb)] = df.groupby("subject")["latency"].agg(["sum", "mean"])
58
+
59
+ return str(latency_by_subject)
@@ -24,6 +24,7 @@ import binascii
24
24
  import io
25
25
  import json
26
26
  from pathlib import Path
27
+ import gui_utilities
27
28
 
28
29
  from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QLineF, QPoint, Qt, Signal
29
30
  from PySide6.QtGui import QBrush, QColor, QIcon, QMouseEvent, QPen, QPixmap, QPolygonF, QAction
@@ -790,7 +791,6 @@ class BehaviorsMapCreatorWindow(QMainWindow):
790
791
 
791
792
  if not self.fileName:
792
793
  return
793
- """if os.path.splitext(self.fileName)[1] != ".behav_coding_map":"""
794
794
  if Path(self.fileName).suffix != ".behav_coding_map":
795
795
  self.fileName += ".behav_coding_map"
796
796
  self.saveMap()
@@ -1106,10 +1106,6 @@ if __name__ == "__main__":
1106
1106
  app = QApplication(sys.argv)
1107
1107
  window = BehaviorsMapCreatorWindow(["North zone", "East zone", "South zone", "West zone"])
1108
1108
  window.bcm_list = []
1109
- window.resize(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1110
- screen_geometry = app.primaryScreen().geometry()
1111
- center_x = (screen_geometry.width() - window.width()) // 2
1112
- center_y = (screen_geometry.height() - window.height()) // 2
1113
- window.move(center_x, center_y)
1109
+ gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1114
1110
  window.show()
1115
1111
  sys.exit(app.exec())
@@ -155,6 +155,7 @@ CHECK_PROJECT_INTEGRITY = "check_project_integrity"
155
155
  YES = "Yes"
156
156
  NO = "No"
157
157
  CANCEL = "Cancel"
158
+ IGNORE = "Ignore"
158
159
  APPEND = "Append"
159
160
  CLOSE = "Close"
160
161
  REPLACE = "Replace"
@@ -454,7 +455,8 @@ POINT = "POINT"
454
455
  START = "START"
455
456
  STOP = "STOP"
456
457
 
457
- PLAYER1, PLAYER2 = "1", "2"
458
+ PLAYER1 = "1"
459
+ PLAYER2 = "2"
458
460
  ALL_PLAYERS = [str(x + 1) for x in range(N_PLAYER)]
459
461
 
460
462
  VISUALIZE_SPECTROGRAM = "visualize_spectrogram"
@@ -701,6 +703,7 @@ EMPTY_PROJECT = {
701
703
  ETHOGRAM: {},
702
704
  OBSERVATIONS: {},
703
705
  BEHAVIORAL_CATEGORIES: [],
706
+ BEHAVIORAL_CATEGORIES_CONF: {},
704
707
  INDEPENDENT_VARIABLES: {},
705
708
  CODING_MAP: {},
706
709
  BEHAVIORS_CODING_MAP: [],
@@ -114,7 +114,6 @@ from . import cmd_arguments
114
114
 
115
115
  from . import core_qrc
116
116
  from .core_ui import Ui_MainWindow
117
- import exifread
118
117
  from . import config as cfg
119
118
  from . import video_operations
120
119
 
@@ -135,8 +134,9 @@ __version__ = version.__version__
135
134
  __version_date__ = version.__version_date__
136
135
 
137
136
  # check minimal version of python
138
- if util.versiontuple(platform.python_version()) < util.versiontuple("3.8"):
139
- msg = f"BORIS requires Python 3.8+! You are using Python v. {platform.python_version()}\n"
137
+ MIN_PYTHON_VERSION = "3.12"
138
+ if util.versiontuple(platform.python_version()) < util.versiontuple(MIN_PYTHON_VERSION):
139
+ msg = f"BORIS requires Python {MIN_PYTHON_VERSION}+! You are using Python v. {platform.python_version()}\n"
140
140
  logging.critical(msg)
141
141
  sys.exit()
142
142
 
@@ -237,19 +237,6 @@ class TableModel(QAbstractTableModel):
237
237
  return self._data[row][event_idx]
238
238
 
239
239
 
240
- """
241
- class ButtonEventFilter(QObject):
242
- def eventFilter(self, obj, event):
243
- print("event filter")
244
- if isinstance(obj, QPushButton) and event.type() == QEvent.KeyPress:
245
- print("keypress")
246
- if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space):
247
- print("enter sapce")
248
- return False # Block the event
249
- return super().eventFilter(obj, event)
250
- """
251
-
252
-
253
240
  class MainWindow(QMainWindow, Ui_MainWindow):
254
241
  """
255
242
  Main BORIS window
@@ -301,7 +288,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
301
288
  ext_data_timer_list: list = []
302
289
  projectFileName: str = ""
303
290
  mediaTotalLength = None
304
- beep_every = 0
291
+ beep_every: float = 0.0
305
292
 
306
293
  plot_colors = cfg.BEHAVIORS_PLOT_COLORS
307
294
  behav_category_colors = cfg.CATEGORY_COLORS_LIST
@@ -315,19 +302,20 @@ class MainWindow(QMainWindow, Ui_MainWindow):
315
302
  fast = 10
316
303
 
317
304
  currentStates: dict = {}
318
- subject_name_index = {}
305
+ subject_name_index: dict = {}
319
306
  flag_slow = False
320
307
  play_rate: float = 1
321
308
  play_rate_step: float = 0.1
322
309
  currentSubject: str = "" # contains the current subject of observation
323
- coding_map_window_geometry = 0
324
310
 
325
311
  # FFmpeg
326
- memx, memy, mem_player = -1, -1, -1
312
+ memx = -1
313
+ memy = -1
314
+ mem_player = -1
327
315
 
328
316
  # path for ffmpeg/ffmpeg.exe program
329
- ffmpeg_bin = ""
330
- ffmpeg_cache_dir = ""
317
+ ffmpeg_bin: str = ""
318
+ ffmpeg_cache_dir: str = ""
331
319
 
332
320
  # dictionary for FPS storing
333
321
  fps = 0
@@ -390,9 +378,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
390
378
  super(MainWindow, self).__init__(parent)
391
379
  self.setupUi(self)
392
380
 
393
- # disable trigger with RETURN or SPACE keys
394
- """filter_obj = ButtonEventFilter()
395
- self.pb_live_obs.installEventFilter(filter_obj)"""
396
381
  self.pb_live_obs.setFocusPolicy(Qt.NoFocus)
397
382
 
398
383
  self.ffmpeg_bin = ffmpeg_bin
@@ -418,25 +403,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
418
403
  self.tb_export.setMenu(self.menu)
419
404
  """
420
405
 
421
- gui_utilities.set_icons(self, theme_mode=self.theme_mode())
406
+ gui_utilities.set_icons(self, theme_mode=gui_utilities.theme_mode())
422
407
 
423
408
  self.setWindowTitle(f"{cfg.programName} ({__version__})")
424
409
 
425
- self.w_obs_info.setVisible(False)
426
-
427
410
  self.lbLogoBoris.setPixmap(QPixmap(":/logo"))
428
-
429
411
  self.lbLogoBoris.setScaledContents(False)
430
412
  self.lbLogoBoris.setAlignment(Qt.AlignCenter)
431
413
 
432
- # self.lbLogoUnito.setPixmap(QPixmap(":/dbios_unito"))
433
- # self.lbLogoUnito.setScaledContents(False)
434
- # self.lbLogoUnito.setAlignment(Qt.AlignCenter)
435
-
436
414
  self.toolBar.setEnabled(True)
437
415
 
438
416
  # start with dock widget invisible
439
- for w in [self.dwEvents, self.dwEthogram, self.dwSubjects]:
417
+ for w in (self.w_obs_info, self.dwEvents, self.dwEthogram, self.dwSubjects):
440
418
  w.setVisible(False)
441
419
  w.keyPressEvent = self.keyPressEvent
442
420
 
@@ -475,15 +453,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
475
453
  self.lbTimeOffset.setMinimumWidth(160)
476
454
  self.statusbar.addPermanentWidget(self.lbTimeOffset)
477
455
 
478
- # play rate are now displayed in the main info widget
479
- """
480
- # SPEED
481
- self.lbSpeed = QLabel()
482
- self.lbSpeed.setFrameStyle(QFrame.StyledPanel)
483
- self.lbSpeed.setMinimumWidth(40)
484
- self.statusbar.addPermanentWidget(self.lbSpeed)
485
- """
486
-
487
456
  # set painter for tv_events to highlight current row
488
457
  delegate = self.CustomItemDelegate()
489
458
  self.tv_events.setItemDelegate(delegate)
@@ -499,14 +468,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
499
468
  plugins.load_plugins(self)
500
469
  plugins.add_plugins_to_menu(self)
501
470
 
502
- def theme_mode(self):
503
- """
504
- return the theme mode (dark or light) of the OS
505
- """
506
- palette = QApplication.instance().palette()
507
- color = palette.window().color()
508
- return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
509
-
510
471
  class CustomItemDelegate(QStyledItemDelegate):
511
472
  def paint(self, painter, option, index):
512
473
  # Custom drawing logic here (overriding paint)
@@ -1506,7 +1467,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1506
1467
  # one media
1507
1468
  if self.dw_player[player].player.playlist_count == 1:
1508
1469
  if new_time < self.dw_player[player].player.duration:
1509
- self.dw_player[player].player.seek(new_time, "absolute+exact")
1470
+ new_time_float = round(float(new_time), 3)
1471
+
1472
+ self.dw_player[player].player.seek(new_time_float, "absolute+exact")
1510
1473
 
1511
1474
  if player == 0 and not self.user_move_slider:
1512
1475
  self.video_slider.setValue(
@@ -3835,7 +3798,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3835
3798
  if self.geometric_measurements_mode:
3836
3799
  geometric_measurement.redraw_measurements(self)
3837
3800
 
3838
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3801
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
3839
3802
 
3840
3803
  def previous_frame(self) -> None:
3841
3804
  """
@@ -3859,7 +3822,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3859
3822
  if self.geometric_measurements_mode:
3860
3823
  geometric_measurement.redraw_measurements(self)
3861
3824
 
3862
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3825
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
3863
3826
 
3864
3827
  def run_event_outside(self):
3865
3828
  """
@@ -4463,7 +4426,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4463
4426
  for data_timer in self.ext_data_timer_list:
4464
4427
  data_timer.stop()
4465
4428
 
4466
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
4429
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
4467
4430
 
4468
4431
  if msg:
4469
4432
  self.lb_current_media_time.setText(msg)
@@ -5634,7 +5597,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5634
5597
  for data_timer in self.ext_data_timer_list:
5635
5598
  data_timer.start()
5636
5599
 
5637
- self.actionPlay.setIcon(QIcon(f":/pause_{self.theme_mode()}"))
5600
+ self.actionPlay.setIcon(QIcon(f":/pause_{gui_utilities.theme_mode()}"))
5638
5601
  self.actionPlay.setText("Pause")
5639
5602
 
5640
5603
  return True
@@ -5668,7 +5631,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5668
5631
  for idx in self.plot_data:
5669
5632
  self.timer_plot_data_out(self.plot_data[idx])
5670
5633
 
5671
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
5634
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
5672
5635
  self.actionPlay.setText("Play")
5673
5636
 
5674
5637
  def play_activated(self):
@@ -5823,8 +5786,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5823
5786
 
5824
5787
 
5825
5788
  def main():
5826
- # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
5827
-
5828
5789
  app = QApplication(sys.argv)
5829
5790
  app.setStyle("Fusion")
5830
5791
 
@@ -5917,7 +5878,7 @@ def main():
5917
5878
  results.show()
5918
5879
 
5919
5880
  window.show()
5920
- window.raise_()
5881
+ window.raise_() # for overlapping widget (?)
5921
5882
 
5922
5883
  if observation_to_open and "error" not in pj:
5923
5884
  r = observation_operations.load_observation(window, obs_id=observation_to_open, mode=cfg.OBS_START)
@@ -71,7 +71,12 @@ from . import utilities as util
71
71
 
72
72
  def MessageDialog(title: str, text: str, buttons: tuple) -> str:
73
73
  """
74
- generic message dialog
74
+ show a generic message dialog and returns the text of the clicked button
75
+
76
+ Args:
77
+ title (str): Title of the dialog box
78
+ text (str): text of the dialog box
79
+ buttons (tuple): text for buttons
75
80
 
76
81
  Return
77
82
  str: text of the clicked button
@@ -83,8 +88,8 @@ def MessageDialog(title: str, text: str, buttons: tuple) -> str:
83
88
  for button in buttons:
84
89
  message.addButton(button, QMessageBox.YesRole)
85
90
 
86
- # message.setWindowFlags(Qt.WindowStaysOnTopHint)
87
- message.exec_()
91
+ message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
92
+ message.exec()
88
93
  return message.clickedButton().text()
89
94
 
90
95
 
@@ -22,10 +22,19 @@ Copyright 2012-2025 Olivier Friard
22
22
  import pathlib as pl
23
23
  import logging
24
24
  from PySide6.QtCore import QSettings
25
- from PySide6.QtWidgets import QWidget
25
+ from PySide6.QtWidgets import QWidget, QApplication
26
26
  from PySide6.QtGui import QIcon
27
27
 
28
28
 
29
+ def theme_mode() -> str:
30
+ """
31
+ return the theme mode (dark or light) of the OS
32
+ """
33
+ palette = QApplication.instance().palette()
34
+ color = palette.window().color()
35
+ return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
36
+
37
+
29
38
  def save_geometry(widget: QWidget, widget_name: str):
30
39
  """
31
40
  save window geometry in ini file
@@ -44,6 +53,7 @@ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
44
53
  """
45
54
  restore window geometry in ini file
46
55
  """
56
+
47
57
  def default_resize(widget, default_width_height):
48
58
  if default_width_height != (0, 0):
49
59
  try:
@@ -51,15 +61,14 @@ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
51
61
  except Exception:
52
62
  logging.warning("Error during restoring default")
53
63
 
54
-
55
- logging.debug(f'restore geometry function for {widget_name}')
64
+ logging.debug(f"restore geometry function for {widget_name}")
56
65
  try:
57
66
  ini_file_path = pl.Path.home() / pl.Path(".boris")
58
67
  if ini_file_path.is_file():
59
68
  settings = QSettings(str(ini_file_path), QSettings.IniFormat)
60
69
  print(settings.value(f"{widget_name} geometry"))
61
70
  widget.restoreGeometry(settings.value(f"{widget_name} geometry"))
62
- logging.debug(f'geometry restored for {widget_name} {settings.value(f"{widget_name} geometry")}')
71
+ logging.debug(f"geometry restored for {widget_name} {settings.value(f'{widget_name} geometry')}")
63
72
  else:
64
73
  default_resize(widget, default_width_height)
65
74
  except Exception:
@@ -108,3 +117,20 @@ def set_icons(self, theme_mode: str) -> None:
108
117
  self.action_geometric_measurements.setIcon(QIcon(f":/measurement_{theme_mode}"))
109
118
  self.actionFind_in_current_obs.setIcon(QIcon(f":/find_{theme_mode}"))
110
119
  self.actionExplore_project.setIcon(QIcon(f":/explore_{theme_mode}"))
120
+
121
+
122
+ def resize_center(app, window, width: int, height: int) -> None:
123
+ """
124
+ resize and center window
125
+ """
126
+ window.resize(width, height)
127
+ screen_geometry = app.primaryScreen().geometry()
128
+ if window.height() > screen_geometry.height():
129
+ window.resize(window.width(), int(screen_geometry.height() * 0.8))
130
+ if window.width() > screen_geometry.width():
131
+ window.resize(screen_geometry.width(), window.height())
132
+ # center
133
+ center_x = (screen_geometry.width() - window.width()) // 2
134
+ center_y = (screen_geometry.height() - window.height()) // 2
135
+
136
+ window.move(center_x, center_y)
@@ -20,11 +20,13 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
+ from PySide6.QtWidgets import QFileDialog
24
+
23
25
  from . import config as cfg
24
26
  from . import utilities as util
25
27
  from . import dialog
26
28
  from . import project_functions
27
- from PySide6.QtWidgets import QFileDialog
29
+ from . import utilities as util
28
30
 
29
31
 
30
32
  def get_info(self) -> None:
@@ -38,17 +40,17 @@ def get_info(self) -> None:
38
40
  if "error" in r:
39
41
  ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
40
42
  else:
41
- ffmpeg_output = f"<br><b>{r['analysis_program'] } analysis</b><br>"
43
+ ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
42
44
 
43
45
  ffmpeg_output += (
44
46
  f"File path: <b>{media_full_path}</b><br><br>"
45
47
  f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
48
+ f"FPS: {r['fps']}<br>"
49
+ f"Resolution: {r['resolution']} pixels<br>"
46
50
  f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
47
51
  f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
48
- f"Resolution: {r['resolution']}<br>"
49
52
  f"Number of frames: {r['frames_number']}<br>"
50
53
  f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
51
- f"FPS: {r['fps']}<br>"
52
54
  f"Has video: {r['has_video']}<br>"
53
55
  f"Has audio: {r['has_audio']}<br>"
54
56
  f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
@@ -70,6 +72,12 @@ def get_info(self) -> None:
70
72
 
71
73
  mpv_output = (
72
74
  "<b>MPV information</b><br>"
75
+ f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
76
+ # "Position: {} %<br>"
77
+ f"FPS: {dw.player.container_fps}<br>"
78
+ # "Rate: {}<br>"
79
+ f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
80
+ # "Scale: {}<br>"
73
81
  f"Video format: {dw.player.video_format}<br>"
74
82
  # "State: {}<br>"
75
83
  # "Media Resource Location: {}<br>"
@@ -77,12 +85,6 @@ def get_info(self) -> None:
77
85
  # "Track: {}/{}<br>"
78
86
  f"Number of media in media list: {dw.player.playlist_count}<br>"
79
87
  f"Current time position: {dw.player.time_pos}<br>"
80
- f"Duration: {dw.player.duration}<br>"
81
- # "Position: {} %<br>"
82
- f"FPS: {dw.player.container_fps}<br>"
83
- # "Rate: {}<br>"
84
- f"Video size: {dw.player.width}x{dw.player.height}<br>"
85
- # "Scale: {}<br>"
86
88
  f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
87
89
  # "is seekable? {}<br>"
88
90
  # "has_vout? {}<br>"
@@ -26,8 +26,9 @@ This file is part of BORIS.
26
26
  import binascii
27
27
  import io
28
28
  import json
29
- import os
29
+ from pathlib import Path
30
30
  import re
31
+ import gui_utilities
31
32
 
32
33
  from PySide6.QtCore import (
33
34
  Qt,
@@ -727,7 +728,8 @@ class ModifiersMapCreatorWindow(QMainWindow):
727
728
  self.fileName = fn
728
729
 
729
730
  if self.fileName:
730
- if os.path.splitext(self.fileName)[1] != ".boris_map":
731
+ # if os.path.splitext(self.fileName)[1] != ".boris_map":
732
+ if Path(self.fileName).suffix != ".boris_map":
731
733
  self.fileName += ".boris_map"
732
734
  self.saveMap()
733
735
 
@@ -744,7 +746,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
744
746
  else:
745
747
  self.fileName = fn
746
748
 
747
- if self.fileName and os.path.splitext(self.fileName)[1] != ".boris_map":
749
+ if self.fileName and Path(self.fileName).suffix() != ".boris_map":
748
750
  self.fileName += ".boris_map"
749
751
 
750
752
  if self.fileName:
@@ -1006,25 +1008,8 @@ if __name__ == "__main__":
1006
1008
 
1007
1009
  app = QApplication(sys.argv)
1008
1010
  window = ModifiersMapCreatorWindow()
1009
- window.resize(800, 700)
1010
1011
 
1011
- print(f"{window.width()=}")
1012
- print(f"{window.height()=}")
1013
-
1014
- # Get the screen geometry (screen size and position)
1015
- screen_geometry = app.primaryScreen().geometry()
1016
-
1017
- print(f"{screen_geometry=}")
1018
-
1019
- # Calculate the center of the screen
1020
- center_x = (screen_geometry.width() - window.width()) // 2
1021
- center_y = (screen_geometry.height() - window.height()) // 2
1022
-
1023
- print(f"{center_x=}")
1024
- print(f"{center_y=}")
1025
-
1026
- # Move the widget to the center of the screen
1027
- window.move(center_x, center_y)
1012
+ gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1028
1013
 
1029
1014
  window.show()
1030
1015
  sys.exit(app.exec())
@@ -1008,27 +1008,30 @@ class Observation(QDialog, Ui_Form):
1008
1008
 
1009
1009
  # check if observation id not empty
1010
1010
  if not self.leObservationId.text():
1011
- self.qm = QMessageBox()
1012
- self.qm.setIcon(QMessageBox.Critical)
1013
- self.qm.setText("The <b>observation id</b> is mandatory and must be unique.")
1014
- self.qm.exec_()
1011
+ QMessageBox.critical(
1012
+ self,
1013
+ cfg.programName,
1014
+ "The <b>observation id</b> is mandatory and must be unique.",
1015
+ )
1015
1016
  return False
1016
1017
 
1017
1018
  # check if observation_type
1018
1019
  if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
1019
- self.qm = QMessageBox()
1020
- self.qm.setIcon(QMessageBox.Critical)
1021
- self.qm.setText("Choose an observation type.")
1022
- self.qm.exec_()
1020
+ QMessageBox.critical(
1021
+ self,
1022
+ cfg.programName,
1023
+ "Choose an observation type.",
1024
+ )
1023
1025
  return False
1024
1026
 
1025
1027
  # check if offset is correct
1026
1028
  if self.cb_time_offset.isChecked():
1027
1029
  if self.obs_time_offset.get_time() is None:
1028
- self.qm = QMessageBox()
1029
- self.qm.setIcon(QMessageBox.Critical)
1030
- self.qm.setText("Check the time offset value.")
1031
- self.qm.exec_()
1030
+ QMessageBox.critical(
1031
+ self,
1032
+ cfg.programName,
1033
+ "Check the time offset value.",
1034
+ )
1032
1035
  return False
1033
1036
 
1034
1037
  if self.rb_media_files.isChecked(): # observation based on media file(s)
@@ -1051,18 +1054,20 @@ class Observation(QDialog, Ui_Form):
1051
1054
 
1052
1055
  # check if player #1 is used
1053
1056
  if not players_list or min(players_list) > 1:
1054
- self.qm = QMessageBox()
1055
- self.qm.setIcon(QMessageBox.Critical)
1056
- self.qm.setText("A media file must be loaded in player #1")
1057
- self.qm.exec_()
1057
+ QMessageBox.critical(
1058
+ self,
1059
+ cfg.programName,
1060
+ "A media file must be loaded in player #1",
1061
+ )
1058
1062
  return False
1059
1063
 
1060
1064
  # check if players are used in crescent order
1061
1065
  if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
1062
- self.qm = QMessageBox()
1063
- self.qm.setIcon(QMessageBox.Critical)
1064
- self.qm.setText("Some player are not used. Please reorganize your media files")
1065
- self.qm.exec_()
1066
+ QMessageBox.critical(
1067
+ self,
1068
+ cfg.programName,
1069
+ "Some player are not used. Please reorganize your media files",
1070
+ )
1066
1071
  return False
1067
1072
 
1068
1073
  # check if more media in player #1 and media in other players
@@ -888,7 +888,7 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
888
888
  self.pj[cfg.OBSERVATIONS][obsId][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS]
889
889
  )
890
890
 
891
- rv = observationWindow.exec_()
891
+ rv = observationWindow.exec()
892
892
 
893
893
  # save geometry
894
894
  gui_utilities.save_geometry(observationWindow, "new observation")
@@ -2012,7 +2012,7 @@ def initialize_new_media_observation(self) -> bool:
2012
2012
  self.mpv_eof_reached_signal.connect(self.mpv_eof_reached)
2013
2013
  self.video_click_signal.connect(self.player_clicked)
2014
2014
 
2015
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
2015
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
2016
2016
 
2017
2017
  self.display_statusbar_info(self.observationId)
2018
2018
 
@@ -2406,15 +2406,15 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2406
2406
  """
2407
2407
 
2408
2408
  cumul_media_durations: list = [dec(0)]
2409
- for media_file in observation[cfg.FILE]["1"]:
2409
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2410
2410
  media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2411
- cumul_media_durations.append(cumul_media_durations[-1] + media_duration)
2411
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2412
2412
 
2413
2413
  cumul_media_durations.remove(dec(0))
2414
2414
 
2415
2415
  # test if timestamp is at end of last media
2416
2416
  if timestamp == cumul_media_durations[-1]:
2417
- player_idx = len(observation[cfg.FILE]["1"]) - 1
2417
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2418
2418
  else:
2419
2419
  player_idx = -1
2420
2420
  for idx, value in enumerate(cumul_media_durations):
@@ -2424,7 +2424,7 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2424
2424
  break
2425
2425
 
2426
2426
  if player_idx != -1:
2427
- video_file_name = observation[cfg.FILE]["1"][player_idx]
2427
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx]
2428
2428
  else:
2429
2429
  video_file_name = None
2430
2430