boris-behav-obs 8.27.9__py2.py3-none-any.whl → 9.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. boris/about.py +7 -5
  2. boris/add_modifier.py +35 -35
  3. boris/add_modifier_ui.py +229 -129
  4. boris/advanced_event_filtering.py +3 -3
  5. boris/analysis_plugins/__init__.py +0 -0
  6. boris/analysis_plugins/number_of_occurences.py +60 -0
  7. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +72 -0
  8. boris/analysis_plugins/time_budget.py +95 -0
  9. boris/behav_coding_map_creator.py +103 -108
  10. boris/behavior_binary_table.py +1 -1
  11. boris/behaviors_coding_map.py +8 -8
  12. boris/coding_pad.py +6 -6
  13. boris/config.py +6 -0
  14. boris/config_file.py +1 -1
  15. boris/connections.py +4 -2
  16. boris/converters.py +2 -3
  17. boris/converters_ui.py +187 -110
  18. boris/cooccurence.py +2 -2
  19. boris/core.py +340 -94
  20. boris/core_qrc.py +16088 -13246
  21. boris/core_ui.py +922 -812
  22. boris/db_functions.py +3 -1
  23. boris/dialog.py +14 -13
  24. boris/duration_widget.py +5 -5
  25. boris/edit_event.py +1 -1
  26. boris/edit_event_ui.py +162 -88
  27. boris/event_operations.py +4 -25
  28. boris/events_cursor.py +17 -9
  29. boris/events_snapshots.py +5 -5
  30. boris/exclusion_matrix.py +1 -1
  31. boris/export_events.py +38 -28
  32. boris/export_observation.py +1 -1
  33. boris/external_processes.py +3 -5
  34. boris/geometric_measurement.py +49 -26
  35. boris/gui_utilities.py +31 -30
  36. boris/import_observations.py +2 -4
  37. boris/irr.py +1 -1
  38. boris/latency.py +1 -1
  39. boris/map_creator.py +77 -89
  40. boris/measurement_widget.py +4 -4
  41. boris/media_file.py +2 -4
  42. boris/menu_options.py +1 -3
  43. boris/modifiers_coding_map.py +4 -4
  44. boris/mpv2.py +0 -2
  45. boris/observation.py +124 -29
  46. boris/observation_operations.py +18 -40
  47. boris/observation_ui.py +566 -374
  48. boris/observations_list.py +6 -6
  49. boris/param_panel.py +2 -2
  50. boris/param_panel_ui.py +246 -141
  51. boris/player_dock_widget.py +16 -21
  52. boris/plot_data_module.py +6 -6
  53. boris/plot_events_rt.py +7 -8
  54. boris/plot_spectrogram_rt.py +7 -8
  55. boris/plot_waveform_rt.py +6 -7
  56. boris/plugins.py +79 -0
  57. boris/preferences.py +127 -17
  58. boris/preferences_ui.py +464 -240
  59. boris/project.py +69 -72
  60. boris/project_functions.py +233 -31
  61. boris/project_import_export.py +59 -67
  62. boris/project_ui.py +672 -440
  63. boris/qrc_boris.py +6 -3
  64. boris/qrc_boris5.py +6 -3
  65. boris/select_modifiers.py +2 -2
  66. boris/select_observations.py +2 -2
  67. boris/select_subj_behav.py +3 -3
  68. boris/state_events.py +1 -1
  69. boris/subjects_pad.py +5 -5
  70. boris/synthetic_time_budget.py +2 -2
  71. boris/time_budget_functions.py +15 -0
  72. boris/time_budget_widget.py +4 -4
  73. boris/transitions.py +34 -25
  74. boris/utilities.py +95 -2
  75. boris/version.py +2 -2
  76. boris/video_equalizer.py +4 -4
  77. boris/video_equalizer_ui.py +199 -130
  78. boris/video_operations.py +1 -1
  79. boris/view_df.py +106 -0
  80. boris/view_df_ui.py +75 -0
  81. boris/write_event.py +9 -1
  82. {boris_behav_obs-8.27.9.dist-info → boris_behav_obs-9.0.1.dist-info}/METADATA +5 -5
  83. boris_behav_obs-9.0.1.dist-info/RECORD +103 -0
  84. {boris_behav_obs-8.27.9.dist-info → boris_behav_obs-9.0.1.dist-info}/WHEEL +1 -1
  85. boris/qdarkstyle/__init__.py +0 -479
  86. boris/qdarkstyle/__main__.py +0 -66
  87. boris/qdarkstyle/colorsystem.py +0 -38
  88. boris/qdarkstyle/dark/__init__.py +0 -1
  89. boris/qdarkstyle/dark/darkstyle_rc.py +0 -11379
  90. boris/qdarkstyle/dark/palette.py +0 -38
  91. boris/qdarkstyle/example/__init__.py +0 -4
  92. boris/qdarkstyle/example/__main__.py +0 -386
  93. boris/qdarkstyle/example/ui/__init__.py +0 -4
  94. boris/qdarkstyle/light/__init__.py +0 -1
  95. boris/qdarkstyle/light/lightstyle_rc.py +0 -11305
  96. boris/qdarkstyle/light/palette.py +0 -37
  97. boris/qdarkstyle/palette.py +0 -102
  98. boris/qdarkstyle/utils/__init__.py +0 -73
  99. boris/qdarkstyle/utils/__main__.py +0 -96
  100. boris/qdarkstyle/utils/images.py +0 -449
  101. boris/qdarkstyle/utils/scss.py +0 -318
  102. boris/vlc_local.py +0 -83
  103. boris_behav_obs-8.27.9.dist-info/RECORD +0 -114
  104. {boris_behav_obs-8.27.9.dist-info → boris_behav_obs-9.0.1.dist-info}/LICENSE.TXT +0 -0
  105. {boris_behav_obs-8.27.9.dist-info → boris_behav_obs-9.0.1.dist-info}/entry_points.txt +0 -0
  106. {boris_behav_obs-8.27.9.dist-info → boris_behav_obs-9.0.1.dist-info}/top_level.txt +0 -0
boris/core.py CHANGED
@@ -26,7 +26,7 @@ import sys
26
26
  os.environ["PATH"] = os.path.dirname(__file__) + os.sep + "misc" + os.pathsep + os.environ["PATH"]
27
27
 
28
28
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".")))
29
- import qdarkstyle
29
+
30
30
 
31
31
  import datetime
32
32
 
@@ -56,37 +56,27 @@ import matplotlib
56
56
  import zipfile
57
57
  import shutil
58
58
 
59
- matplotlib.use("Qt5Agg")
60
-
61
- from PyQt5.QtCore import (
62
- Qt,
63
- QPoint,
64
- pyqtSignal,
65
- QEvent,
66
- QDateTime,
67
- QTime,
68
- QUrl,
69
- QAbstractTableModel,
70
- QT_VERSION_STR,
71
- PYQT_VERSION_STR,
72
- )
73
- from PyQt5.QtGui import QIcon, QPixmap, QFont, QKeyEvent, QDesktopServices, QColor, QPainter, QPolygon
74
- from PyQt5.QtMultimedia import QSound
75
- from PyQt5.QtWidgets import (
76
- QLabel,
77
- QMessageBox,
78
- QMainWindow,
79
- QListWidgetItem,
59
+ matplotlib.use("QtAgg")
60
+
61
+ import PySide6
62
+ from PySide6.QtCore import Qt, QPoint, Signal, QEvent, QDateTime, QUrl, QAbstractTableModel, qVersion, QElapsedTimer
63
+ from PySide6.QtGui import QIcon, QPixmap, QFont, QKeyEvent, QDesktopServices, QColor, QPainter, QPolygon, QAction
64
+ from PySide6.QtMultimedia import QSoundEffect
65
+ from PySide6.QtWidgets import (
66
+ QAbstractItemView,
67
+ QApplication,
68
+ QDockWidget,
80
69
  QFileDialog,
81
- QInputDialog,
82
- QTableWidgetItem,
83
70
  QFrame,
84
- QDockWidget,
85
- QApplication,
86
- QAction,
87
- QAbstractItemView,
88
- QSplashScreen,
89
71
  QHeaderView,
72
+ QInputDialog,
73
+ QLabel,
74
+ QListWidgetItem,
75
+ QMainWindow,
76
+ QMessageBox,
77
+ QSplashScreen,
78
+ QStyledItemDelegate,
79
+ QTableWidgetItem,
90
80
  )
91
81
  from PIL.ImageQt import Image
92
82
 
@@ -103,6 +93,7 @@ from . import plot_events
103
93
  from . import plot_spectrogram_rt
104
94
  from . import plot_waveform_rt
105
95
  from . import plot_events_rt
96
+ from . import plugins
106
97
  from . import project_functions
107
98
 
108
99
  from . import select_observations
@@ -168,7 +159,8 @@ logging.info(f"BORIS version {__version__} release date: {__version_date__}")
168
159
  logging.info(f"Operating system: {platform.uname().system} {platform.uname().release} {platform.uname().version}")
169
160
  logging.info(f"CPU: {platform.uname().machine} {platform.uname().processor}")
170
161
  logging.info(f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})")
171
- logging.info(f"Qt {QT_VERSION_STR} - PyQt {PYQT_VERSION_STR}")
162
+ logging.info(f"Qt {qVersion()} - PySide {PySide6.__version__}")
163
+
172
164
 
173
165
  (r, memory) = util.mem_info()
174
166
  if not r:
@@ -180,6 +172,18 @@ if not r:
180
172
  )
181
173
 
182
174
 
175
+ def excepthook(exception_type, exception_value, traceback_object):
176
+ """
177
+ global error management
178
+ """
179
+ logging.debug("excepthook")
180
+
181
+ dialog.global_error_message(exception_type, exception_value, traceback_object)
182
+
183
+
184
+ sys.excepthook = excepthook
185
+
186
+
183
187
  class TableModel(QAbstractTableModel):
184
188
  """
185
189
  class for populating table view with events
@@ -236,9 +240,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
236
240
 
237
241
  state_behaviors_codes: tuple = tuple()
238
242
 
239
- time_observer_signal = pyqtSignal(float)
240
- mpv_eof_reached_signal = pyqtSignal(float)
241
- video_click_signal = pyqtSignal(int, str)
243
+ time_observer_signal = Signal(float)
244
+ mpv_eof_reached_signal = Signal(float)
245
+ video_click_signal = Signal(int, str)
242
246
 
243
247
  processes: list = [] # list of QProcess processes
244
248
  overlays: dict = {} # dict for storing video overlays
@@ -362,8 +366,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
362
366
  super(MainWindow, self).__init__(parent)
363
367
  self.setupUi(self)
364
368
 
365
- sys.excepthook = self.excepthook
366
-
367
369
  self.ffmpeg_bin = ffmpeg_bin
368
370
  # set icons
369
371
  self.setWindowIcon(QIcon(":/small_logo"))
@@ -387,7 +389,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
387
389
  self.tb_export.setMenu(self.menu)
388
390
  """
389
391
 
390
- gui_utilities.set_icons(self)
392
+ gui_utilities.set_icons(self, theme_mode=self.theme_mode())
391
393
 
392
394
  self.setWindowTitle(f"{cfg.programName} ({__version__})")
393
395
 
@@ -453,8 +455,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
453
455
  self.statusbar.addPermanentWidget(self.lbSpeed)
454
456
  """
455
457
 
456
- # set painter for twEvents to highlight current row
457
- # self.twEvents.setItemDelegate(events_cursor.StyledItemDelegateTriangle(self.events_current_row))
458
+ # set painter for tv_events to highlight current row
459
+ delegate = self.CustomItemDelegate()
460
+ self.tv_events.setItemDelegate(delegate)
461
+
462
+ # PySide6
458
463
  self.tv_events.setItemDelegate(events_cursor.StyledItemDelegateTriangle(self.events_current_row))
459
464
 
460
465
  connections.connections(self)
@@ -462,11 +467,21 @@ class MainWindow(QMainWindow, Ui_MainWindow):
462
467
  config_file.read(self)
463
468
  menu_options.update_menu(self)
464
469
 
465
- def excepthook(self, exception_type, exception_value, traceback_object):
470
+ plugins.load_plugins(self)
471
+ plugins.add_plugins_to_menu(self)
472
+
473
+ def theme_mode(self):
466
474
  """
467
- global error management
475
+ return the theme mode (dark or light) of the OS
468
476
  """
469
- dialog.global_error_message(exception_type, exception_value, traceback_object)
477
+ palette = QApplication.instance().palette()
478
+ color = palette.window().color()
479
+ return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
480
+
481
+ class CustomItemDelegate(QStyledItemDelegate):
482
+ def paint(self, painter, option, index):
483
+ # Custom drawing logic here (overriding paint)
484
+ super().paint(painter, option, index)
470
485
 
471
486
  def block_dockwidgets(self):
472
487
  """
@@ -567,7 +582,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
567
582
  if (
568
583
  dialog.MessageDialog(
569
584
  cfg.programName,
570
- ("Removing the path of external data files is irreversible.<br>" "Are you sure to continue?"),
585
+ ("Removing the path of external data files is irreversible.<br>Are you sure to continue?"),
571
586
  [cfg.YES, cfg.NO],
572
587
  )
573
588
  == cfg.NO
@@ -1638,9 +1653,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1638
1653
 
1639
1654
  self.save_project_activated()
1640
1655
  else:
1641
- logging.debug(
1642
- (f"project not autosaved: " f"observation id: {self.observationId} " f"project file name: {self.projectFileName}")
1643
- )
1656
+ logging.debug((f"project not autosaved: observation id: {self.observationId} project file name: {self.projectFileName}"))
1644
1657
 
1645
1658
  def update_subject(self, subject: str) -> None:
1646
1659
  """
@@ -1677,11 +1690,19 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1677
1690
  break
1678
1691
  return currentMedia, round(frameCurrentMedia)
1679
1692
 
1693
+ '''
1680
1694
  def extract_exif_DateTimeOriginal(self, file_path: str) -> int:
1681
1695
  """
1682
- extract the exif extract_exif_DateTimeOriginal tag
1696
+ extract the EXIF DateTimeOriginal tag
1683
1697
  return epoch time
1684
1698
  if the tag is not available return -1
1699
+
1700
+ Args:
1701
+ file_path (str): path of the media file
1702
+
1703
+ Returns:
1704
+ int: timestamp
1705
+
1685
1706
  """
1686
1707
  try:
1687
1708
  with open(file_path, "rb") as f_in:
@@ -1704,6 +1725,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1704
1725
 
1705
1726
  except Exception:
1706
1727
  return -1
1728
+ '''
1707
1729
 
1708
1730
  def extract_frame(self, dw):
1709
1731
  """
@@ -1721,6 +1743,19 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1721
1743
  )
1722
1744
 
1723
1745
  if self.playerType == cfg.IMAGES:
1746
+ # print(f"{self.images_list=}")
1747
+ # print(f"{self.image_idx=}")
1748
+
1749
+ if self.image_idx >= len(self.images_list):
1750
+ QMessageBox.critical(
1751
+ None,
1752
+ cfg.programName,
1753
+ ("The picture directory was changed since the creation of observation."),
1754
+ QMessageBox.Ok | QMessageBox.Default,
1755
+ QMessageBox.NoButton,
1756
+ )
1757
+ return
1758
+
1724
1759
  pixmap = QPixmap(self.images_list[self.image_idx])
1725
1760
  self.current_image_size = (pixmap.size().width(), pixmap.size().height())
1726
1761
 
@@ -1728,7 +1763,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1728
1763
 
1729
1764
  # extract EXIF tag
1730
1765
  if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
1731
- date_time_original = self.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
1766
+ date_time_original = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
1732
1767
  if date_time_original != -1:
1733
1768
  msg += f"<br>EXIF Date/Time Original: <b>{datetime.datetime.fromtimestamp(date_time_original):%Y-%m-%d %H:%M:%S}</b>"
1734
1769
  else:
@@ -1853,11 +1888,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1853
1888
 
1854
1889
  return painter
1855
1890
 
1856
- output_dir = QFileDialog().getExistingDirectory(
1891
+ output_dir = QFileDialog.getExistingDirectory(
1857
1892
  self,
1858
1893
  "Select a directory to save the frames",
1859
1894
  os.path.expanduser("~"),
1860
- options=QFileDialog().ShowDirsOnly,
1895
+ options=QFileDialog.ShowDirsOnly,
1861
1896
  )
1862
1897
  if not output_dir:
1863
1898
  return
@@ -2628,7 +2663,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2628
2663
  start_time = f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL][0]:.3f}"
2629
2664
  stop_time = f"{self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL][1]:.3f}"
2630
2665
 
2631
- self.lb_obs_time_interval.setText(("Observation time interval: " f"{start_time} - {stop_time}"))
2666
+ self.lb_obs_time_interval.setText((f"Observation time interval: {start_time} - {stop_time}"))
2632
2667
  else:
2633
2668
  self.lb_obs_time_interval.clear()
2634
2669
  else:
@@ -2720,11 +2755,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2720
2755
  plot_directory = ""
2721
2756
  file_format = "png"
2722
2757
  if len(selected_observations) > 1:
2723
- plot_directory = QFileDialog().getExistingDirectory(
2758
+ plot_directory = QFileDialog.getExistingDirectory(
2724
2759
  self,
2725
2760
  "Choose a directory to save the plots",
2726
2761
  os.path.expanduser("~"),
2727
- options=QFileDialog(self).ShowDirsOnly,
2762
+ options=QFileDialog.ShowDirsOnly,
2728
2763
  )
2729
2764
 
2730
2765
  if not plot_directory:
@@ -2864,11 +2899,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2864
2899
  plot_directory = ""
2865
2900
  output_format = ""
2866
2901
  if len(selected_observations) > 1:
2867
- plot_directory = QFileDialog().getExistingDirectory(
2902
+ plot_directory = QFileDialog.getExistingDirectory(
2868
2903
  self,
2869
2904
  "Choose a directory to save the plots",
2870
2905
  os.path.expanduser("~"),
2871
- options=QFileDialog(self).ShowDirsOnly,
2906
+ options=QFileDialog.ShowDirsOnly,
2872
2907
  )
2873
2908
  if not plot_directory:
2874
2909
  return
@@ -2899,7 +2934,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2899
2934
 
2900
2935
  def load_project(self, project_path: str, project_changed, pj: dict):
2901
2936
  """
2902
- load project from pj dict
2937
+ load project into widgets from pj dict
2903
2938
 
2904
2939
  Args:
2905
2940
  project_path (str): path of project file
@@ -2910,10 +2945,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2910
2945
  None
2911
2946
  """
2912
2947
  self.pj = dict(pj)
2913
- memProjectChanged = project_changed
2914
2948
  self.clear_interface()
2915
- self.projectChanged = True
2916
- self.projectChanged = memProjectChanged
2949
+ self.projectChanged = project_changed
2917
2950
  self.load_behaviors_in_twEthogram([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
2918
2951
  self.load_subjects_in_twSubjects([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
2919
2952
  self.projectFileName = str(pl.Path(project_path).absolute())
@@ -2961,13 +2994,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2961
2994
  return
2962
2995
 
2963
2996
  if action.text() == "Open project":
2964
- fn = QFileDialog().getOpenFileName(
2997
+ file_name, _ = QFileDialog.getOpenFileName(
2965
2998
  self,
2966
2999
  "Open project",
2967
3000
  "",
2968
- ("Project files (*.boris *.boris.gz);;" "All files (*)"),
3001
+ ("Project files (*.boris *.boris.gz);;All files (*)"),
2969
3002
  )
2970
- file_name = fn[0] if type(fn) is tuple else fn
2971
3003
 
2972
3004
  else: # recent project
2973
3005
  file_name = action.text()
@@ -3070,13 +3102,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3070
3102
  if response == cfg.CANCEL:
3071
3103
  return
3072
3104
 
3073
- fn = QFileDialog().getOpenFileName(
3105
+ file_name, _ = QFileDialog.getOpenFileName(
3074
3106
  self,
3075
3107
  "Import project from Noldus The Observer",
3076
3108
  "",
3077
3109
  "Noldus Observer files (*.otx *.otb *.odx);;All files (*)",
3078
3110
  )
3079
- file_name = fn[0] if type(fn) is tuple else fn
3080
3111
 
3081
3112
  if not file_name:
3082
3113
  return
@@ -3145,6 +3176,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3145
3176
 
3146
3177
  self.w_obs_info.setVisible(False)
3147
3178
 
3179
+ def not_editable_column_color(self):
3180
+ """
3181
+ return a color for the not editable column
3182
+ """
3183
+ window_color = QApplication.instance().palette().window().color()
3184
+ return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
3185
+
3148
3186
  def edit_project(self, mode: str):
3149
3187
  """
3150
3188
  project management
@@ -3264,13 +3302,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3264
3302
  cfg.MODIFIERS,
3265
3303
  ):
3266
3304
  item.setFlags(Qt.ItemIsEnabled)
3267
- item.setBackground(QColor(230, 230, 230))
3305
+ # item.setBackground(QColor(230, 230, 230))
3306
+ item.setBackground(self.not_editable_column_color())
3307
+
3268
3308
  if field == cfg.COLOR:
3269
3309
  item.setFlags(Qt.ItemIsEnabled)
3270
3310
  if QColor(newProjectWindow.pj[cfg.ETHOGRAM][i].get(field, "")).isValid():
3271
3311
  item.setBackground(QColor(newProjectWindow.pj[cfg.ETHOGRAM][i][field]))
3272
3312
  else:
3273
- item.setBackground(QColor(230, 230, 230))
3313
+ # item.setBackground(QColor(230, 230, 230))
3314
+ item.setBackground(self.not_editable_column_color())
3274
3315
 
3275
3316
  newProjectWindow.twBehaviors.setItem(
3276
3317
  newProjectWindow.twBehaviors.rowCount() - 1,
@@ -3465,7 +3506,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3465
3506
  self,
3466
3507
  "Save project as",
3467
3508
  os.path.dirname(self.projectFileName),
3468
- ("Project files (*.boris);;" "Compressed project files (*.boris.gz);;" "All files (*)"),
3509
+ ("Project files (*.boris);;Compressed project files (*.boris.gz);;All files (*)"),
3469
3510
  )
3470
3511
 
3471
3512
  if not project_new_file_name:
@@ -3529,7 +3570,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3529
3570
  self,
3530
3571
  "Save project",
3531
3572
  txt,
3532
- ("Project files (*.boris);;" "Compressed project files (*.boris.gz);;" "All files (*)"),
3573
+ ("Project files (*.boris);;Compressed project files (*.boris.gz);;All files (*)"),
3533
3574
  )
3534
3575
 
3535
3576
  if not self.projectFileName:
@@ -3670,7 +3711,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3670
3711
 
3671
3712
  self.pb_live_obs.setText("Stop live observation")
3672
3713
 
3673
- self.liveStartTime = QTime()
3714
+ self.liveStartTime = QElapsedTimer()
3674
3715
  # set to now
3675
3716
  self.liveStartTime.start()
3676
3717
  # start timer
@@ -3745,11 +3786,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3745
3786
  )
3746
3787
  return
3747
3788
 
3748
- export_dir = QFileDialog().getExistingDirectory(
3789
+ export_dir = QFileDialog.getExistingDirectory(
3749
3790
  self,
3750
3791
  "Choose a directory to save subtitles",
3751
3792
  os.path.expanduser("~"),
3752
- options=QFileDialog(self).ShowDirsOnly,
3793
+ options=QFileDialog.ShowDirsOnly,
3753
3794
  )
3754
3795
  if not export_dir:
3755
3796
  return
@@ -3786,7 +3827,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3786
3827
  if self.geometric_measurements_mode:
3787
3828
  geometric_measurement.redraw_measurements(self)
3788
3829
 
3789
- self.actionPlay.setIcon(QIcon(":/play"))
3830
+ self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3790
3831
 
3791
3832
  def previous_frame(self) -> None:
3792
3833
  """
@@ -3810,7 +3851,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3810
3851
  if self.geometric_measurements_mode:
3811
3852
  geometric_measurement.redraw_measurements(self)
3812
3853
 
3813
- self.actionPlay.setIcon(QIcon(":/play"))
3854
+ self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3814
3855
 
3815
3856
  def run_event_outside(self):
3816
3857
  """
@@ -3923,7 +3964,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3923
3964
  QMessageBox.critical(
3924
3965
  self,
3925
3966
  cfg.programName,
3926
- ("The current observation is opened in VIEW mode.\n" "It is not allowed to log events in this mode."),
3967
+ ("The current observation is opened in VIEW mode.\nIt is not allowed to log events in this mode."),
3927
3968
  )
3928
3969
  return
3929
3970
 
@@ -3949,7 +3990,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3949
3990
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
3950
3991
  event[cfg.FRAME_INDEX] = self.get_frame_index()
3951
3992
 
3952
- write_event.write_event(self, event, self.getLaps())
3993
+ time_, cumulative_time = self.get_obs_time()
3994
+
3995
+ """
3996
+ print(time_)
3997
+ print(cumulative_time)
3998
+ """
3999
+
4000
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
4001
+ write_event.write_event(self, event, time_)
4002
+ else:
4003
+ write_event.write_event(self, event, cumulative_time)
4004
+ # write_event.write_event(self, event, self.getLaps())
3953
4005
 
3954
4006
  def get_frame_index(self, player_idx: int = 0) -> Union[int, str]:
3955
4007
  """
@@ -4001,7 +4053,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4001
4053
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
4002
4054
  event[cfg.FRAME_INDEX] = self.get_frame_index()
4003
4055
 
4004
- write_event.write_event(self.event, self.getLaps())
4056
+ time_, cumulative_time = self.get_obs_time()
4057
+
4058
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
4059
+ write_event.write_event(self, event, time_)
4060
+ else:
4061
+ write_event.write_event(self, event, cumulative_time)
4062
+
4063
+ # write_event.write_event(self.event, self.getLaps())
4005
4064
 
4006
4065
  def keypress_signal_from_behaviors_coding_map(self, event):
4007
4066
  """
@@ -4088,7 +4147,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4088
4147
 
4089
4148
  # print(f"{self.events_current_row=}")
4090
4149
 
4091
- # self.twEvents.setItemDelegate(events_cursor.StyledItemDelegateTriangle(self.events_current_row))
4092
4150
  self.tv_events.setItemDelegate(events_cursor.StyledItemDelegateTriangle(self.events_current_row))
4093
4151
 
4094
4152
  # print(f"{self.twEvents.item(self.events_current_row, 0)=}")
@@ -4380,7 +4438,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4380
4438
  # if many media files
4381
4439
  if self.dw_player[0].player.playlist_count > 1:
4382
4440
  msg += (
4383
- f"<br>Total: <b>{util.convertTime(self.timeFormat,cumulative_time_pos)} / "
4441
+ f"<br>Total: <b>{util.convertTime(self.timeFormat, cumulative_time_pos)} / "
4384
4442
  f"{util.convertTime(self.timeFormat, all_media_duration)}</b>"
4385
4443
  )
4386
4444
 
@@ -4391,7 +4449,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4391
4449
  for data_timer in self.ext_data_timer_list:
4392
4450
  data_timer.stop()
4393
4451
 
4394
- self.actionPlay.setIcon(QIcon(":/play"))
4452
+ self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
4395
4453
 
4396
4454
  if msg:
4397
4455
  self.lb_current_media_time.setText(msg)
@@ -4657,9 +4715,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4657
4715
  if "Live observation finished" in self.pb_live_obs.text():
4658
4716
  return dec("NaN")
4659
4717
  if self.liveObservationStarted:
4660
- now = QTime()
4661
- now.start() # current time
4662
- memLaps = dec(str(round(self.liveStartTime.msecsTo(now) / 1000, 3)))
4718
+ memLaps = dec(str(round(self.liveStartTime.elapsed() / 1000, 3)))
4663
4719
  return memLaps
4664
4720
  else:
4665
4721
  return dec("0")
@@ -4672,9 +4728,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4672
4728
  time_ = dec("NaN")
4673
4729
  if (
4674
4730
  self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False)
4675
- and self.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) != -1
4731
+ and util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) != -1
4676
4732
  ):
4677
- time_ = self.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) - self.image_time_ref
4733
+ time_ = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) - self.image_time_ref
4678
4734
 
4679
4735
  elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
4680
4736
  time_ = (self.image_idx + 1) * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
@@ -4682,7 +4738,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4682
4738
  return dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN)
4683
4739
 
4684
4740
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
4685
- if self.playerType in [cfg.VIEWER_LIVE, cfg.VIEWER_MEDIA]:
4741
+ if self.playerType in (cfg.VIEWER_LIVE, cfg.VIEWER_MEDIA):
4686
4742
  return dec(0)
4687
4743
 
4688
4744
  if self.playerType == cfg.MEDIA:
@@ -4693,6 +4749,60 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4693
4749
 
4694
4750
  return dec(str(round(mem_laps / 1000, 3)))
4695
4751
 
4752
+ def get_obs_time(self, n_player: int = 0) -> Tuple[dec, dec | None]:
4753
+ """
4754
+ returns time in current media and cumulative time from begining of observation
4755
+ do not add time offset
4756
+
4757
+ Args:
4758
+ n_player (int): player
4759
+ Returns:
4760
+ decimal: cumulative time in seconds
4761
+
4762
+ """
4763
+
4764
+ if not self.observationId:
4765
+ return dec("0")
4766
+
4767
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
4768
+ if "Live observation finished" in self.pb_live_obs.text():
4769
+ return dec("NaN"), None
4770
+ if self.liveObservationStarted:
4771
+ return dec(str(round(self.liveStartTime.elapsed() / 1000, 3))), None
4772
+ else:
4773
+ return dec("0"), None
4774
+
4775
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
4776
+ if self.playerType == cfg.VIEWER_IMAGES:
4777
+ return dec("NaN"), None
4778
+
4779
+ if self.playerType == cfg.IMAGES:
4780
+ time_ = dec("NaN")
4781
+ if (
4782
+ self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False)
4783
+ and util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) != -1
4784
+ ):
4785
+ time_ = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx]) - self.image_time_ref
4786
+
4787
+ elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
4788
+ time_ = (self.image_idx + 1) * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
4789
+
4790
+ return dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN), None
4791
+
4792
+ if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
4793
+ if self.playerType in (cfg.VIEWER_LIVE, cfg.VIEWER_MEDIA):
4794
+ return dec(0), dec(0)
4795
+
4796
+ if self.playerType == cfg.MEDIA:
4797
+ # cumulative time
4798
+ mem_laps = sum(self.dw_player[n_player].media_durations[0 : self.dw_player[n_player].player.playlist_pos]) + (
4799
+ 0 if self.dw_player[n_player].player.time_pos is None else self.dw_player[n_player].player.time_pos * 1000
4800
+ )
4801
+
4802
+ return dec(0) if self.dw_player[n_player].player.time_pos is None else dec(
4803
+ str(round(self.dw_player[n_player].player.time_pos, 3))
4804
+ ), dec(str(round(mem_laps / 1000, 3)))
4805
+
4696
4806
  def full_event(self, behavior_idx: str) -> dict:
4697
4807
  """
4698
4808
  get event as dict
@@ -4746,7 +4856,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4746
4856
  sound_type (str): type of sound
4747
4857
  """
4748
4858
 
4749
- QSound.play(f":/{sound_type}")
4859
+ QSoundEffect.play(f":/{sound_type}")
4750
4860
 
4751
4861
  def is_playing(self) -> bool:
4752
4862
  """
@@ -4789,7 +4899,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4789
4899
  def keyPressEvent(self, event) -> None:
4790
4900
  """
4791
4901
  http://qt-project.org/doc/qt-5.0/qtcore/qt.html#Key-enum
4792
- https://github.com/pyqt/python-qt5/blob/master/PyQt5/qml/builtins.qmltypes
4902
+ https://github.com/pyqt/python-qt5/blob/master/PySide6/qml/builtins.qmltypes
4793
4903
 
4794
4904
  ESC: 16777216
4795
4905
  """
@@ -4835,7 +4945,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4835
4945
  QMessageBox.critical(
4836
4946
  self,
4837
4947
  cfg.programName,
4838
- ("The current observation is opened in VIEW mode.\n" "It is not allowed to log events in this mode."),
4948
+ ("The current observation is opened in VIEW mode.\nIt is not allowed to log events in this mode."),
4839
4949
  )
4840
4950
  return
4841
4951
 
@@ -5000,7 +5110,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5000
5110
  if self.playerType == cfg.MEDIA:
5001
5111
  event[cfg.FRAME_INDEX] = self.get_frame_index()
5002
5112
 
5003
- write_event.write_event(self, event, memLaps)
5113
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
5114
+ time_, _ = self.get_obs_time()
5115
+ write_event.write_event(self, event, time_)
5116
+ else:
5117
+ write_event.write_event(self, event, memLaps)
5118
+
5004
5119
  return
5005
5120
 
5006
5121
  # count key occurence in subjects
@@ -5087,7 +5202,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5087
5202
  if self.playerType == cfg.MEDIA:
5088
5203
  event[cfg.FRAME_INDEX] = self.get_frame_index()
5089
5204
 
5090
- write_event.write_event(self, event, memLaps)
5205
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
5206
+ time_, _ = self.get_obs_time()
5207
+ write_event.write_event(self, event, time_)
5208
+ else:
5209
+ write_event.write_event(self, event, memLaps)
5210
+
5211
+ # write_event.write_event(self, event, memLaps)
5091
5212
 
5092
5213
  elif subject_idx is not None:
5093
5214
  self.update_subject(self.pj[cfg.SUBJECTS][subject_idx][cfg.SUBJECT_NAME])
@@ -5473,7 +5594,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5473
5594
  for data_timer in self.ext_data_timer_list:
5474
5595
  data_timer.start()
5475
5596
 
5476
- self.actionPlay.setIcon(QIcon(":/pause"))
5597
+ self.actionPlay.setIcon(QIcon(f":/pause_{self.theme_mode()}"))
5477
5598
  self.actionPlay.setText("Pause")
5478
5599
 
5479
5600
  return True
@@ -5507,7 +5628,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5507
5628
  for idx in self.plot_data:
5508
5629
  self.timer_plot_data_out(self.plot_data[idx])
5509
5630
 
5510
- self.actionPlay.setIcon(QIcon(":/play"))
5631
+ self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
5511
5632
  self.actionPlay.setText("Play")
5512
5633
 
5513
5634
  def play_activated(self):
@@ -5599,6 +5720,131 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5599
5720
  self.image_idx = 0
5600
5721
  self.extract_frame(self.dw_player[0])
5601
5722
 
5723
+ def obs_param(self):
5724
+ _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
5725
+
5726
+ if not selected_observations:
5727
+ return [], {}
5728
+
5729
+ # check if coded behaviors are defined in ethogram
5730
+ if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
5731
+ return [], {}
5732
+
5733
+ # check if state events are paired
5734
+ not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
5735
+ if not_ok or not selected_observations:
5736
+ return [], {}
5737
+
5738
+ max_media_duration_all_obs, total_media_duration_all_obs = observation_operations.media_duration(
5739
+ self.pj[cfg.OBSERVATIONS], selected_observations
5740
+ )
5741
+
5742
+ logging.debug(
5743
+ f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
5744
+ )
5745
+
5746
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
5747
+
5748
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
5749
+ self,
5750
+ selected_observations,
5751
+ start_coding=start_coding,
5752
+ end_coding=end_coding,
5753
+ maxTime=max_media_duration_all_obs,
5754
+ by_category=False,
5755
+ n_observations=len(selected_observations),
5756
+ show_exclude_non_coded_modifiers=True,
5757
+ )
5758
+ if parameters == {}:
5759
+ return [], {}
5760
+
5761
+ if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
5762
+ QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
5763
+ return [], {}
5764
+
5765
+ logging.debug(f"{parameters=}")
5766
+ return selected_observations, parameters
5767
+
5768
+ def run_plugin(self):
5769
+ """
5770
+ run plugin
5771
+ """
5772
+ if not self.project:
5773
+ QMessageBox.warning(
5774
+ self,
5775
+ cfg.programName,
5776
+ "No observations found. Open a project first",
5777
+ QMessageBox.Ok | QMessageBox.Default,
5778
+ QMessageBox.NoButton,
5779
+ )
5780
+ return
5781
+
5782
+ import importlib
5783
+
5784
+ print(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
5785
+
5786
+ plugin_name = self.sender().text()
5787
+ if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
5788
+ QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
5789
+ return
5790
+
5791
+ plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
5792
+ print(f"{plugin_path=}")
5793
+ if not pl.Path(plugin_path).is_file():
5794
+ QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
5795
+ return
5796
+
5797
+ logging.debug(f"run plugin from {plugin_path}")
5798
+
5799
+ module_name = pl.Path(plugin_path).stem
5800
+
5801
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
5802
+ plugin_module = importlib.util.module_from_spec(spec)
5803
+ print(f"{plugin_module=}")
5804
+ spec.loader.exec_module(plugin_module)
5805
+
5806
+ print(
5807
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
5808
+ )
5809
+
5810
+ """
5811
+ plugins_dir = pl.Path(__file__).parent / "analysis_plugins"
5812
+
5813
+ print(f"{plugins_dir=}")
5814
+
5815
+ module_path = f"{plugins_dir.name}.{plugin}"
5816
+
5817
+ print(f"{module_path=}")
5818
+
5819
+ try:
5820
+ plugin_module = importlib.import_module(module_path)
5821
+ print(f"{plugin} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}")
5822
+ except Exception:
5823
+ QMessageBox.critical(self, cfg.programName, f"Error loding the plugin {plugin}")
5824
+ return
5825
+
5826
+ print(f"{plugin_module=}")
5827
+ """
5828
+
5829
+ selected_observations, parameters = self.obs_param()
5830
+ if not selected_observations:
5831
+ return
5832
+
5833
+ df = project_functions.project2dataframe(self.pj, selected_observations)
5834
+
5835
+ print(f"{df.head()=}")
5836
+
5837
+ df_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5838
+
5839
+ from . import view_df
5840
+
5841
+ self.view_dataframe = view_df.View_df(
5842
+ self.sender().text(), f"{plugin_module.__version__} ({plugin_module.__version_date__})", df_results
5843
+ )
5844
+ self.view_dataframe.show()
5845
+
5846
+ # print(f"{results=}")
5847
+
5602
5848
 
5603
5849
  def main():
5604
5850
  # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
@@ -5637,8 +5883,8 @@ def main():
5637
5883
 
5638
5884
  window = MainWindow(ffmpeg_bin)
5639
5885
 
5640
- if window.config_param.get(cfg.DARK_MODE, cfg.DARK_MODE_DEFAULT_VALUE):
5641
- app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyqt5"))
5886
+ # if window.config_param.get(cfg.DARK_MODE, cfg.DARK_MODE_DEFAULT_VALUE):
5887
+ # app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="PySide6"))
5642
5888
 
5643
5889
  # open project/start observation on command line
5644
5890
 
@@ -5686,7 +5932,7 @@ def main():
5686
5932
  if not options.nosplashscreen and (sys.platform != "darwin"):
5687
5933
  splash.finish(window)
5688
5934
 
5689
- return_code = app.exec_()
5935
+ return_code = app.exec()
5690
5936
 
5691
5937
  del window
5692
5938