boris-behav-obs 8.27.10__py2.py3-none-any.whl → 9.0.2__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 (105) 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 +341 -94
  20. boris/core_qrc.py +16088 -13246
  21. boris/core_ui.py +922 -812
  22. boris/dialog.py +14 -13
  23. boris/duration_widget.py +5 -5
  24. boris/edit_event.py +1 -1
  25. boris/edit_event_ui.py +162 -88
  26. boris/event_operations.py +4 -25
  27. boris/events_cursor.py +17 -9
  28. boris/events_snapshots.py +5 -5
  29. boris/exclusion_matrix.py +1 -1
  30. boris/export_events.py +38 -28
  31. boris/export_observation.py +1 -1
  32. boris/external_processes.py +3 -5
  33. boris/geometric_measurement.py +49 -26
  34. boris/gui_utilities.py +31 -30
  35. boris/import_observations.py +2 -4
  36. boris/irr.py +1 -1
  37. boris/latency.py +1 -1
  38. boris/map_creator.py +77 -89
  39. boris/measurement_widget.py +4 -4
  40. boris/media_file.py +2 -4
  41. boris/menu_options.py +1 -3
  42. boris/modifiers_coding_map.py +4 -4
  43. boris/mpv2.py +0 -2
  44. boris/observation.py +124 -29
  45. boris/observation_operations.py +18 -40
  46. boris/observation_ui.py +566 -374
  47. boris/observations_list.py +6 -6
  48. boris/param_panel.py +2 -2
  49. boris/param_panel_ui.py +246 -141
  50. boris/player_dock_widget.py +16 -21
  51. boris/plot_data_module.py +6 -6
  52. boris/plot_events_rt.py +7 -8
  53. boris/plot_spectrogram_rt.py +7 -8
  54. boris/plot_waveform_rt.py +6 -7
  55. boris/plugins.py +79 -0
  56. boris/preferences.py +127 -17
  57. boris/preferences_ui.py +464 -240
  58. boris/project.py +69 -72
  59. boris/project_functions.py +233 -31
  60. boris/project_import_export.py +59 -67
  61. boris/project_ui.py +672 -440
  62. boris/qrc_boris.py +6 -3
  63. boris/qrc_boris5.py +6 -3
  64. boris/select_modifiers.py +2 -2
  65. boris/select_observations.py +2 -2
  66. boris/select_subj_behav.py +3 -3
  67. boris/state_events.py +1 -1
  68. boris/subjects_pad.py +5 -5
  69. boris/synthetic_time_budget.py +2 -2
  70. boris/time_budget_functions.py +15 -0
  71. boris/time_budget_widget.py +4 -4
  72. boris/transitions.py +34 -25
  73. boris/utilities.py +96 -3
  74. boris/version.py +2 -2
  75. boris/video_equalizer.py +4 -4
  76. boris/video_equalizer_ui.py +199 -130
  77. boris/video_operations.py +1 -1
  78. boris/view_df.py +106 -0
  79. boris/view_df_ui.py +75 -0
  80. boris/write_event.py +9 -1
  81. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.2.dist-info}/METADATA +5 -5
  82. boris_behav_obs-9.0.2.dist-info/RECORD +103 -0
  83. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.2.dist-info}/WHEEL +1 -1
  84. boris/qdarkstyle/__init__.py +0 -479
  85. boris/qdarkstyle/__main__.py +0 -66
  86. boris/qdarkstyle/colorsystem.py +0 -38
  87. boris/qdarkstyle/dark/__init__.py +0 -1
  88. boris/qdarkstyle/dark/darkstyle_rc.py +0 -11379
  89. boris/qdarkstyle/dark/palette.py +0 -38
  90. boris/qdarkstyle/example/__init__.py +0 -4
  91. boris/qdarkstyle/example/__main__.py +0 -386
  92. boris/qdarkstyle/example/ui/__init__.py +0 -4
  93. boris/qdarkstyle/light/__init__.py +0 -1
  94. boris/qdarkstyle/light/lightstyle_rc.py +0 -11305
  95. boris/qdarkstyle/light/palette.py +0 -37
  96. boris/qdarkstyle/palette.py +0 -102
  97. boris/qdarkstyle/utils/__init__.py +0 -73
  98. boris/qdarkstyle/utils/__main__.py +0 -96
  99. boris/qdarkstyle/utils/images.py +0 -449
  100. boris/qdarkstyle/utils/scss.py +0 -318
  101. boris/vlc_local.py +0 -83
  102. boris_behav_obs-8.27.10.dist-info/RECORD +0 -114
  103. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.2.dist-info}/LICENSE.TXT +0 -0
  104. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.2.dist-info}/entry_points.txt +0 -0
  105. {boris_behav_obs-8.27.10.dist-info → boris_behav_obs-9.0.2.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
@@ -4707,6 +4817,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4707
4817
 
4708
4818
  event = dict(self.pj[cfg.ETHOGRAM][behavior_idx])
4709
4819
  # check if coding map for modifiers
4820
+
4710
4821
  if util.has_coding_map(self.pj[cfg.ETHOGRAM], behavior_idx):
4711
4822
  # pause if media and media playing
4712
4823
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
@@ -4746,7 +4857,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4746
4857
  sound_type (str): type of sound
4747
4858
  """
4748
4859
 
4749
- QSound.play(f":/{sound_type}")
4860
+ QSoundEffect.play(f":/{sound_type}")
4750
4861
 
4751
4862
  def is_playing(self) -> bool:
4752
4863
  """
@@ -4789,7 +4900,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4789
4900
  def keyPressEvent(self, event) -> None:
4790
4901
  """
4791
4902
  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
4903
+ https://github.com/pyqt/python-qt5/blob/master/PySide6/qml/builtins.qmltypes
4793
4904
 
4794
4905
  ESC: 16777216
4795
4906
  """
@@ -4835,7 +4946,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4835
4946
  QMessageBox.critical(
4836
4947
  self,
4837
4948
  cfg.programName,
4838
- ("The current observation is opened in VIEW mode.\n" "It is not allowed to log events in this mode."),
4949
+ ("The current observation is opened in VIEW mode.\nIt is not allowed to log events in this mode."),
4839
4950
  )
4840
4951
  return
4841
4952
 
@@ -5000,7 +5111,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5000
5111
  if self.playerType == cfg.MEDIA:
5001
5112
  event[cfg.FRAME_INDEX] = self.get_frame_index()
5002
5113
 
5003
- write_event.write_event(self, event, memLaps)
5114
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
5115
+ time_, _ = self.get_obs_time()
5116
+ write_event.write_event(self, event, time_)
5117
+ else:
5118
+ write_event.write_event(self, event, memLaps)
5119
+
5004
5120
  return
5005
5121
 
5006
5122
  # count key occurence in subjects
@@ -5087,7 +5203,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5087
5203
  if self.playerType == cfg.MEDIA:
5088
5204
  event[cfg.FRAME_INDEX] = self.get_frame_index()
5089
5205
 
5090
- write_event.write_event(self, event, memLaps)
5206
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
5207
+ time_, _ = self.get_obs_time()
5208
+ write_event.write_event(self, event, time_)
5209
+ else:
5210
+ write_event.write_event(self, event, memLaps)
5211
+
5212
+ # write_event.write_event(self, event, memLaps)
5091
5213
 
5092
5214
  elif subject_idx is not None:
5093
5215
  self.update_subject(self.pj[cfg.SUBJECTS][subject_idx][cfg.SUBJECT_NAME])
@@ -5473,7 +5595,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5473
5595
  for data_timer in self.ext_data_timer_list:
5474
5596
  data_timer.start()
5475
5597
 
5476
- self.actionPlay.setIcon(QIcon(":/pause"))
5598
+ self.actionPlay.setIcon(QIcon(f":/pause_{self.theme_mode()}"))
5477
5599
  self.actionPlay.setText("Pause")
5478
5600
 
5479
5601
  return True
@@ -5507,7 +5629,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5507
5629
  for idx in self.plot_data:
5508
5630
  self.timer_plot_data_out(self.plot_data[idx])
5509
5631
 
5510
- self.actionPlay.setIcon(QIcon(":/play"))
5632
+ self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
5511
5633
  self.actionPlay.setText("Play")
5512
5634
 
5513
5635
  def play_activated(self):
@@ -5599,6 +5721,131 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5599
5721
  self.image_idx = 0
5600
5722
  self.extract_frame(self.dw_player[0])
5601
5723
 
5724
+ def obs_param(self):
5725
+ _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
5726
+
5727
+ if not selected_observations:
5728
+ return [], {}
5729
+
5730
+ # check if coded behaviors are defined in ethogram
5731
+ if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
5732
+ return [], {}
5733
+
5734
+ # check if state events are paired
5735
+ not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
5736
+ if not_ok or not selected_observations:
5737
+ return [], {}
5738
+
5739
+ max_media_duration_all_obs, total_media_duration_all_obs = observation_operations.media_duration(
5740
+ self.pj[cfg.OBSERVATIONS], selected_observations
5741
+ )
5742
+
5743
+ logging.debug(
5744
+ f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}"
5745
+ )
5746
+
5747
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
5748
+
5749
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
5750
+ self,
5751
+ selected_observations,
5752
+ start_coding=start_coding,
5753
+ end_coding=end_coding,
5754
+ maxTime=max_media_duration_all_obs,
5755
+ by_category=False,
5756
+ n_observations=len(selected_observations),
5757
+ show_exclude_non_coded_modifiers=True,
5758
+ )
5759
+ if parameters == {}:
5760
+ return [], {}
5761
+
5762
+ if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
5763
+ QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
5764
+ return [], {}
5765
+
5766
+ logging.debug(f"{parameters=}")
5767
+ return selected_observations, parameters
5768
+
5769
+ def run_plugin(self):
5770
+ """
5771
+ run plugin
5772
+ """
5773
+ if not self.project:
5774
+ QMessageBox.warning(
5775
+ self,
5776
+ cfg.programName,
5777
+ "No observations found. Open a project first",
5778
+ QMessageBox.Ok | QMessageBox.Default,
5779
+ QMessageBox.NoButton,
5780
+ )
5781
+ return
5782
+
5783
+ import importlib
5784
+
5785
+ print(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
5786
+
5787
+ plugin_name = self.sender().text()
5788
+ if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
5789
+ QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
5790
+ return
5791
+
5792
+ plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
5793
+ print(f"{plugin_path=}")
5794
+ if not pl.Path(plugin_path).is_file():
5795
+ QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
5796
+ return
5797
+
5798
+ logging.debug(f"run plugin from {plugin_path}")
5799
+
5800
+ module_name = pl.Path(plugin_path).stem
5801
+
5802
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
5803
+ plugin_module = importlib.util.module_from_spec(spec)
5804
+ print(f"{plugin_module=}")
5805
+ spec.loader.exec_module(plugin_module)
5806
+
5807
+ print(
5808
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
5809
+ )
5810
+
5811
+ """
5812
+ plugins_dir = pl.Path(__file__).parent / "analysis_plugins"
5813
+
5814
+ print(f"{plugins_dir=}")
5815
+
5816
+ module_path = f"{plugins_dir.name}.{plugin}"
5817
+
5818
+ print(f"{module_path=}")
5819
+
5820
+ try:
5821
+ plugin_module = importlib.import_module(module_path)
5822
+ print(f"{plugin} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}")
5823
+ except Exception:
5824
+ QMessageBox.critical(self, cfg.programName, f"Error loding the plugin {plugin}")
5825
+ return
5826
+
5827
+ print(f"{plugin_module=}")
5828
+ """
5829
+
5830
+ selected_observations, parameters = self.obs_param()
5831
+ if not selected_observations:
5832
+ return
5833
+
5834
+ df = project_functions.project2dataframe(self.pj, selected_observations)
5835
+
5836
+ print(f"{df.head()=}")
5837
+
5838
+ df_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5839
+
5840
+ from . import view_df
5841
+
5842
+ self.view_dataframe = view_df.View_df(
5843
+ self.sender().text(), f"{plugin_module.__version__} ({plugin_module.__version_date__})", df_results
5844
+ )
5845
+ self.view_dataframe.show()
5846
+
5847
+ # print(f"{results=}")
5848
+
5602
5849
 
5603
5850
  def main():
5604
5851
  # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
@@ -5637,8 +5884,8 @@ def main():
5637
5884
 
5638
5885
  window = MainWindow(ffmpeg_bin)
5639
5886
 
5640
- if window.config_param.get(cfg.DARK_MODE, cfg.DARK_MODE_DEFAULT_VALUE):
5641
- app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyqt5"))
5887
+ # if window.config_param.get(cfg.DARK_MODE, cfg.DARK_MODE_DEFAULT_VALUE):
5888
+ # app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="PySide6"))
5642
5889
 
5643
5890
  # open project/start observation on command line
5644
5891
 
@@ -5686,7 +5933,7 @@ def main():
5686
5933
  if not options.nosplashscreen and (sys.platform != "darwin"):
5687
5934
  splash.finish(window)
5688
5935
 
5689
- return_code = app.exec_()
5936
+ return_code = app.exec()
5690
5937
 
5691
5938
  del window
5692
5939