boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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 (126) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +266 -144
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_export_to_feral.py +225 -0
  9. boris/analysis_plugins/_latency.py +59 -0
  10. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  11. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  13. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  14. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  15. boris/analysis_plugins/number_of_occurences.py +22 -0
  16. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  17. boris/analysis_plugins/time_budget.py +61 -0
  18. boris/behav_coding_map_creator.py +235 -236
  19. boris/behavior_binary_table.py +33 -50
  20. boris/behaviors_coding_map.py +17 -18
  21. boris/boris_cli.py +6 -25
  22. boris/cmd_arguments.py +12 -1
  23. boris/coding_pad.py +19 -36
  24. boris/config.py +109 -50
  25. boris/config_file.py +58 -67
  26. boris/connections.py +105 -58
  27. boris/converters.py +13 -37
  28. boris/converters_ui.py +187 -110
  29. boris/cooccurence.py +250 -0
  30. boris/core.py +2174 -1303
  31. boris/core_qrc.py +15892 -10829
  32. boris/core_ui.py +941 -806
  33. boris/db_functions.py +17 -42
  34. boris/dev.py +27 -7
  35. boris/dialog.py +461 -242
  36. boris/duration_widget.py +9 -14
  37. boris/edit_event.py +61 -31
  38. boris/edit_event_ui.py +208 -97
  39. boris/event_operations.py +405 -281
  40. boris/events_cursor.py +25 -17
  41. boris/events_snapshots.py +36 -82
  42. boris/exclusion_matrix.py +4 -9
  43. boris/export_events.py +180 -203
  44. boris/export_observation.py +60 -73
  45. boris/external_processes.py +123 -98
  46. boris/geometric_measurement.py +427 -218
  47. boris/gui_utilities.py +91 -14
  48. boris/image_overlay.py +4 -4
  49. boris/import_observations.py +190 -98
  50. boris/ipc_mpv.py +325 -0
  51. boris/irr.py +20 -57
  52. boris/latency.py +31 -24
  53. boris/measurement_widget.py +14 -18
  54. boris/media_file.py +17 -19
  55. boris/menu_options.py +16 -6
  56. boris/modifier_coding_map_creator.py +1013 -0
  57. boris/modifiers_coding_map.py +7 -9
  58. boris/mpv2.py +128 -35
  59. boris/observation.py +501 -211
  60. boris/observation_operations.py +1037 -393
  61. boris/observation_ui.py +573 -363
  62. boris/observations_list.py +51 -58
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +45 -59
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +91 -56
  67. boris/plot_data_module.py +20 -53
  68. boris/plot_events.py +56 -153
  69. boris/plot_events_rt.py +16 -30
  70. boris/plot_spectrogram_rt.py +83 -56
  71. boris/plot_waveform_rt.py +27 -49
  72. boris/plugins.py +468 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +307 -123
  80. boris/preferences_ui.py +686 -227
  81. boris/project.py +294 -271
  82. boris/project_functions.py +626 -537
  83. boris/project_import_export.py +204 -213
  84. boris/project_ui.py +673 -441
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +62 -90
  88. boris/select_observations.py +19 -197
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +51 -33
  91. boris/subjects_pad.py +7 -9
  92. boris/synthetic_time_budget.py +42 -26
  93. boris/time_budget_functions.py +169 -169
  94. boris/time_budget_widget.py +77 -89
  95. boris/transitions.py +41 -41
  96. boris/utilities.py +594 -226
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +86 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +240 -136
  104. boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
  106. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -37
  112. boris/core.ui +0 -1571
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -982
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1074
  120. boris/vlc_local.py +0 -90
  121. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  122. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  123. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  124. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  125. {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
  126. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
boris/observation.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -20,15 +20,14 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- import glob
24
23
  import logging
25
24
  import os
26
25
  import pandas as pd
27
26
  import pathlib as pl
28
27
 
29
- from PyQt5.QtCore import Qt
30
- from PyQt5.QtGui import QColor
31
- from PyQt5.QtWidgets import (
28
+ from PySide6.QtCore import Qt
29
+ from PySide6.QtGui import QColor
30
+ from PySide6.QtWidgets import (
32
31
  QDialog,
33
32
  QVBoxLayout,
34
33
  QHBoxLayout,
@@ -43,10 +42,11 @@ from PyQt5.QtWidgets import (
43
42
  QApplication,
44
43
  QMenu,
45
44
  QListWidgetItem,
45
+ QHeaderView,
46
46
  )
47
47
 
48
48
  from . import config as cfg
49
- from . import dialog, duration_widget, plot_data_module, project_functions
49
+ from . import dialog, plot_data_module, project_functions
50
50
  from . import utilities as util
51
51
  from . import gui_utilities
52
52
  from .observation_ui import Ui_Form
@@ -76,7 +76,10 @@ class AssignConverter(QDialog):
76
76
  self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
77
77
 
78
78
  if column_idx in col_conv:
79
- self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
79
+ if col_conv[column_idx] in (["None"] + sorted(converters.keys())):
80
+ self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
81
+ else:
82
+ self.cbb[-1].setCurrentIndex(0)
80
83
  else:
81
84
  self.cbb[-1].setCurrentIndex(0)
82
85
  hbox.addWidget(self.cbb[-1])
@@ -97,7 +100,7 @@ class AssignConverter(QDialog):
97
100
 
98
101
 
99
102
  class Observation(QDialog, Ui_Form):
100
- def __init__(self, tmp_dir, project_path="", converters={}, time_format=cfg.S, parent=None):
103
+ def __init__(self, tmp_dir: str, project_path: str = "", converters: dict = {}, time_format: str = cfg.S, parent=None):
101
104
  """
102
105
  Args:
103
106
  tmp_dir (str): path of temporary directory
@@ -111,22 +114,31 @@ class Observation(QDialog, Ui_Form):
111
114
  self.project_path = project_path
112
115
  self.converters = converters
113
116
  self.time_format = time_format
114
- self.observation_time_interval = [0, 0]
117
+ self.observation_time_interval: tuple = [0, 0]
115
118
  self.mem_dir = ""
116
119
  self.test = None
117
120
 
118
121
  self.setupUi(self)
119
122
 
120
123
  # insert duration widget for time offset
121
- self.obs_time_offset = duration_widget.Duration_widget(0)
124
+ # self.obs_time_offset = duration_widget.Duration_widget(0)
125
+ self.obs_time_offset = dialog.get_time_widget(0)
122
126
  self.horizontalLayout_6.insertWidget(1, self.obs_time_offset)
127
+ self.obs_time_offset.setEnabled(False)
128
+
129
+ # time offset
130
+ self.cb_time_offset.stateChanged.connect(self.cb_time_offset_changed)
131
+ # date offset
132
+ """self.cb_date_offset.stateChanged.connect(self.cb_date_offset_changed)"""
123
133
 
124
134
  # observation type
125
135
  self.rb_media_files.toggled.connect(self.obs_type_changed)
126
136
  self.rb_live.toggled.connect(self.obs_type_changed)
127
137
  self.rb_images.toggled.connect(self.obs_type_changed)
128
138
 
129
- menu_items = [
139
+ # button menu for media
140
+
141
+ add_media_menu_items = [
130
142
  "media abs path|with absolute path",
131
143
  "media rel path|with relative path",
132
144
  {
@@ -136,23 +148,68 @@ class Observation(QDialog, Ui_Form):
136
148
  ]
137
149
  },
138
150
  ]
139
- menu = QMenu()
140
- menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
141
- self.add_button_menu(menu_items, menu)
142
- self.pbAddVideo.setMenu(menu)
151
+
152
+ self.media_menu = QMenu()
153
+ # Add actions to the menu
154
+ """
155
+ self.action1 = QAction("with absolute path")
156
+ self.action2 = QAction("with relative path")
157
+ self.action3 = QAction("directory with absolute path")
158
+ self.action4 = QAction("directory with relative path")
159
+
160
+ self.menu.addAction(self.action1)
161
+ self.menu.addAction(self.action2)
162
+ self.menu.addAction(self.action3)
163
+ self.menu.addAction(self.action4)
164
+
165
+ # Connect actions to functions
166
+ self.action1.triggered.connect(lambda: self.add_media(mode="media abs path|with absolute path"))
167
+ self.action2.triggered.connect(lambda: self.add_media(mode="media rel path|with relative path"))
168
+ self.action3.triggered.connect(lambda: self.add_media(mode="dir abs path|with absolute path"))
169
+ self.action4.triggered.connect(lambda: self.add_media(mode="dir rel path|wih relative path"))
170
+ """
171
+
172
+ self.media_menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
173
+ self.add_button_menu(add_media_menu_items, self.media_menu)
174
+ self.pbAddVideo.setMenu(self.media_menu)
143
175
 
144
176
  self.pbRemoveVideo.clicked.connect(self.remove_media)
145
177
 
146
- # add data file
178
+ # button menu for data file
147
179
  data_menu_items = [
148
180
  "data abs path|with absolute path",
149
181
  "data rel path|with relative path",
150
182
  ]
151
183
 
152
- menu_data = QMenu()
153
- menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
154
- self.add_button_menu(data_menu_items, menu_data)
155
- self.pb_add_data_file.setMenu(menu_data)
184
+ self.menu_data = QMenu()
185
+
186
+ # Add actions to the menu
187
+ """
188
+ self.data_action1 = QAction("with absolute path")
189
+ self.data_action2 = QAction("with relative path")
190
+ self.menu_data.addAction(self.data_action1)
191
+ self.menu_data.addAction(self.data_action2)
192
+
193
+ # Connect actions to functions
194
+ self.data_action1.triggered.connect(lambda: self.add_data_file(mode="data abs path|with absolute path"))
195
+ self.data_action2.triggered.connect(lambda: self.add_data_file(mode="data rel path|with relative path"))
196
+ """
197
+
198
+ self.menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
199
+ self.add_button_menu(data_menu_items, self.menu_data)
200
+ self.pb_add_data_file.setMenu(self.menu_data)
201
+
202
+ # button menu for images
203
+ images_menu_items = [
204
+ "images abs path|with absolute path",
205
+ "images rel path|with relative path",
206
+ ]
207
+
208
+ self.menu_images = QMenu()
209
+
210
+ self.menu_images.triggered.connect(lambda x: self.add_images_directory(mode=x.statusTip()))
211
+ self.add_button_menu(images_menu_items, self.menu_images)
212
+ self.pb_add_directory.setMenu(self.menu_images)
156
213
 
157
214
  self.pb_remove_data_file.clicked.connect(self.remove_data_file)
158
215
  self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
@@ -163,6 +220,7 @@ class Observation(QDialog, Ui_Form):
163
220
 
164
221
  self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
165
222
  self.cb_visualize_waveform.clicked.connect(self.extract_wav)
223
+
166
224
  self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
167
225
 
168
226
  self.pbSave.clicked.connect(self.pbSave_clicked)
@@ -170,21 +228,27 @@ class Observation(QDialog, Ui_Form):
170
228
  self.pbCancel.clicked.connect(self.pbCancel_clicked)
171
229
 
172
230
  self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
231
+ self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
173
232
 
174
- self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio = {}, {}, {}, {}
233
+ self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
175
234
 
176
- self.cbVisualizeSpectrogram.setEnabled(False)
177
- self.cb_visualize_waveform.setEnabled(False)
178
- self.cb_observation_time_interval.setEnabled(True)
235
+ self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
236
+
237
+ for w in (
238
+ self.cbVisualizeSpectrogram,
239
+ self.cb_visualize_waveform,
240
+ self.cb_observation_time_interval,
241
+ self.cb_media_creation_date_as_offset,
242
+ self.cbCloseCurrentBehaviorsBetweenVideo,
243
+ ):
244
+ w.setEnabled(False)
179
245
 
180
- # disabled due to problem when video goes back
181
- self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
182
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
246
+ self.cb_observation_time_interval.setEnabled(True)
183
247
 
184
248
  self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
185
249
 
186
250
  # images
187
- self.pb_add_directory.clicked.connect(self.add_images_directory)
251
+ # self.pb_add_directory.clicked.connect(self.add_images_directory)
188
252
  self.pb_remove_directory.clicked.connect(self.remove_images_directory)
189
253
 
190
254
  self.tabWidget.setCurrentIndex(0)
@@ -192,6 +256,60 @@ class Observation(QDialog, Ui_Form):
192
256
  # geometry
193
257
  gui_utilities.restore_geometry(self, "new observation", (800, 650))
194
258
 
259
+ # def cb_date_offset_changed(self):
260
+ # """
261
+ # activate/desactivate time value
262
+ # """
263
+ # self.de_date_offset.setEnabled(self.cb_date_offset.isChecked())
264
+
265
+ def check_media_creation_date(self):
266
+ """
267
+ check if all media files contain creation date time
268
+ search in metadata then in filename
269
+ """
270
+
271
+ creation_date_not_found: list = []
272
+ flag_filename_used = False
273
+
274
+ self.media_creation_time = {}
275
+
276
+ if self.cb_media_creation_date_as_offset.isChecked():
277
+ for row in range(self.twVideo1.rowCount()):
278
+ if self.twVideo1.item(row, 2).text(): # media file path
279
+ date_time_original = util.extract_video_creation_date(
280
+ project_functions.full_path(self.twVideo1.item(row, 2).text(), self.project_path)
281
+ )
282
+ if date_time_original is None:
283
+ date_time_file_name = util.extract_date_time_from_file_name(self.twVideo1.item(row, 2).text())
284
+ if date_time_file_name is None:
285
+ creation_date_not_found.append(self.twVideo1.item(row, 2).text())
286
+ else:
287
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_file_name
288
+ flag_filename_used = True
289
+ else:
290
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_original
291
+
292
+ if creation_date_not_found:
293
+ QMessageBox.warning(
294
+ self, cfg.programName, "The creation date time was not found for all media file(s).\nThe option was disabled."
295
+ )
296
+ self.cb_media_creation_date_as_offset.setChecked(False)
297
+ self.media_creation_time = {}
298
+ return 1
299
+
300
+ elif flag_filename_used:
301
+ QMessageBox.information(
302
+ self, cfg.programName, "The creation date time was not found in metadata. The media file name(s) was/were used"
303
+ )
304
+
305
+ return 0
306
+
307
+ def cb_time_offset_changed(self):
308
+ """
309
+ activate/desactivate date value
310
+ """
311
+ self.obs_time_offset.setEnabled(self.cb_time_offset.isChecked())
312
+
195
313
  def use_media_file_name_as_obsid(self) -> None:
196
314
  """
197
315
  set observation id with the media file name value (without path)
@@ -200,7 +318,7 @@ class Observation(QDialog, Ui_Form):
200
318
  QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
201
319
  return
202
320
 
203
- first_media_file = ""
321
+ first_media_file: str = ""
204
322
  for row in range(self.twVideo1.rowCount()):
205
323
  if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
206
324
  first_media_file = self.twVideo1.item(row, 2).text()
@@ -235,11 +353,39 @@ class Observation(QDialog, Ui_Form):
235
353
  # hide 'limit observation to time interval' for images
236
354
  self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
237
355
 
238
- def add_images_directory(self):
356
+ def add_images_directory(self, mode: str):
239
357
  """
240
358
  add path to images directory
241
359
  """
242
- dir_path = QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))
360
+
361
+ if mode.split("|")[0] not in (
362
+ "images abs path",
363
+ "images rel path",
364
+ ):
365
+ QMessageBox.critical(
366
+ self,
367
+ cfg.programName,
368
+ (f"Wrong mode to add a pictures directory {mode}"),
369
+ )
370
+ return
371
+
372
+ # check if project saved
373
+ if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
374
+ QMessageBox.critical(
375
+ self,
376
+ cfg.programName,
377
+ ("It is not possible to add a pictures directory with a relative path if the project is not already saved"),
378
+ )
379
+ return
380
+
381
+ fd = QFileDialog()
382
+ fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
383
+
384
+ dir_path = fd.getExistingDirectory(None, "Select directory")
385
+
386
+ if not dir_path:
387
+ return
388
+
243
389
  result = util.dir_images_number(dir_path)
244
390
  if not result.get("number of images", 0):
245
391
  response = dialog.MessageDialog(
@@ -250,7 +396,25 @@ class Observation(QDialog, Ui_Form):
250
396
  if response == "Cancel":
251
397
  return
252
398
 
253
- self.lw_images_directory.addItem(QListWidgetItem(dir_path))
399
+ # store directory for next usage
400
+ self.mem_dir = str(pl.Path(dir_path))
401
+
402
+ if " rel " in mode:
403
+ try:
404
+ pl.Path(dir_path).parent.relative_to(pl.Path(self.project_path).parent)
405
+ except ValueError:
406
+ QMessageBox.critical(
407
+ self,
408
+ cfg.programName,
409
+ f"The directory <b>{pl.Path(dir_path).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
410
+ )
411
+ return
412
+
413
+ if " rel " in mode:
414
+ # convert to relative path (relative to BORIS project file)
415
+ self.lw_images_directory.addItem(QListWidgetItem(str(pl.Path(dir_path).relative_to(pl.Path(self.project_path).parent))))
416
+ else:
417
+ self.lw_images_directory.addItem(QListWidgetItem(dir_path))
254
418
  self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
255
419
 
256
420
  def remove_images_directory(self):
@@ -290,10 +454,14 @@ class Observation(QDialog, Ui_Form):
290
454
  """
291
455
 
292
456
  if self.cb_observation_time_interval.isChecked():
293
- time_interval_dialog = dialog.Ask_time(self.time_format)
457
+ time_interval_dialog = dialog.Ask_time(0)
458
+ if self.time_format == cfg.S:
459
+ time_interval_dialog.time_widget.rb_seconds.setChecked(True)
460
+ if self.time_format == cfg.HHMMSS:
461
+ time_interval_dialog.time_widget.rb_time.setChecked(True)
294
462
  time_interval_dialog.time_widget.set_time(0)
295
463
  time_interval_dialog.setWindowTitle("Start observation at")
296
- time_interval_dialog.label.setText("Start observation at")
464
+ time_interval_dialog.label.setText("<b>Start</b> observation at")
297
465
  start_time, stop_time = 0, 0
298
466
  if time_interval_dialog.exec_():
299
467
  start_time = time_interval_dialog.time_widget.get_time()
@@ -302,7 +470,7 @@ class Observation(QDialog, Ui_Form):
302
470
  return
303
471
  time_interval_dialog.time_widget.set_time(0)
304
472
  time_interval_dialog.setWindowTitle("Stop observation at")
305
- time_interval_dialog.label.setText("Stop observation at")
473
+ time_interval_dialog.label.setText("<b>Stop</b> observation at")
306
474
  if time_interval_dialog.exec_():
307
475
  stop_time = time_interval_dialog.time_widget.get_time()
308
476
  else:
@@ -316,7 +484,10 @@ class Observation(QDialog, Ui_Form):
316
484
  return
317
485
  self.observation_time_interval = [start_time, stop_time]
318
486
  self.cb_observation_time_interval.setText(
319
- f"Limit observation to a time interval: {start_time} - {stop_time}"
487
+ (
488
+ "Limit observation to a time interval: "
489
+ f"{util.smart_time_format(start_time, self.time_format)} - {util.smart_time_format(stop_time, self.time_format)}"
490
+ )
320
491
  )
321
492
  else:
322
493
  self.observation_time_interval = [0, 0]
@@ -338,9 +509,7 @@ class Observation(QDialog, Ui_Form):
338
509
 
339
510
  if w.exec_():
340
511
  d = {}
341
- for col_idx, cb in zip(
342
- self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb
343
- ):
512
+ for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
344
513
  if cb.currentText() != "None":
345
514
  d[col_idx] = cb.currentText()
346
515
  self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
@@ -361,7 +530,6 @@ class Observation(QDialog, Ui_Form):
361
530
  return
362
531
 
363
532
  if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
364
-
365
533
  if self.tw_data_files.rowCount() == 1:
366
534
  row_idx = 0
367
535
  else:
@@ -381,9 +549,7 @@ class Observation(QDialog, Ui_Form):
381
549
  time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
382
550
  time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
383
551
 
384
- substract_first_value = self.tw_data_files.cellWidget(
385
- row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
386
- ).currentText()
552
+ substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
387
553
 
388
554
  plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
389
555
 
@@ -428,6 +594,13 @@ class Observation(QDialog, Ui_Form):
428
594
  else:
429
595
  QMessageBox.warning(self, cfg.programName, "Select a data file")
430
596
 
597
+ def not_editable_column_color(self):
598
+ """
599
+ return a color for the not editable column
600
+ """
601
+ window_color = QApplication.instance().palette().window().color()
602
+ return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
603
+
431
604
  def add_data_file(self, mode: str):
432
605
  """
433
606
  user select a data file to be plotted synchronously with media file
@@ -461,19 +634,14 @@ class Observation(QDialog, Ui_Form):
461
634
  QMessageBox.warning(
462
635
  self,
463
636
  cfg.programName,
464
- (
465
- "It is not yet possible to plot more than 2 external data sources"
466
- "This limitation will be removed in future"
467
- ),
637
+ ("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
468
638
  )
469
639
  return
470
640
 
471
641
  fd = QFileDialog()
472
642
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
473
643
 
474
- fn = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
475
- file_name = fn[0] if type(fn) is tuple else fn
476
-
644
+ file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
477
645
  if not file_name:
478
646
  return
479
647
 
@@ -490,16 +658,14 @@ class Observation(QDialog, Ui_Form):
490
658
  QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
491
659
  return
492
660
 
493
- header, footer = util.return_file_header_footer(
494
- file_name, file_row_number=file_parameters["rows number"], row_number=5
495
- )
661
+ header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
496
662
 
497
663
  if not header:
498
664
  QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
499
665
  return
500
666
 
501
667
  w = dialog.View_data()
502
- w.setWindowTitle(f"View data")
668
+ w.setWindowTitle("View data")
503
669
  w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
504
670
 
505
671
  w.tw.setColumnCount(file_parameters["fields number"])
@@ -518,9 +684,7 @@ class Observation(QDialog, Ui_Form):
518
684
 
519
685
  # stats
520
686
  try:
521
- df = pd.read_csv(
522
- file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0]
523
- )
687
+ df = pd.read_csv(file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0])
524
688
  # set columns names to based 1 index
525
689
  if not file_parameters["has header"]:
526
690
  df.columns = range(1, len(df.columns) + 1)
@@ -556,7 +720,6 @@ class Observation(QDialog, Ui_Form):
556
720
  self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
557
721
 
558
722
  if " rel " in mode:
559
-
560
723
  try:
561
724
  file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
562
725
  except ValueError:
@@ -585,15 +748,14 @@ class Observation(QDialog, Ui_Form):
585
748
  item = QTableWidgetItem(value)
586
749
  if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
587
750
  item.setFlags(Qt.ItemIsEnabled)
588
- item.setBackground(QColor(230, 230, 230))
751
+ # item.setBackground(QColor(230, 230, 230))
752
+ item.setBackground(self.not_editable_column_color())
589
753
  self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
590
754
 
591
755
  # substract first value
592
756
  combobox = QComboBox()
593
757
  combobox.addItems(["True", "False"])
594
- self.tw_data_files.setCellWidget(
595
- self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
596
- )
758
+ self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
597
759
 
598
760
  # plot line color
599
761
  combobox = QComboBox()
@@ -622,18 +784,16 @@ class Observation(QDialog, Ui_Form):
622
784
  if "error" in file_parameters:
623
785
  QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
624
786
  return
625
- header, footer = util.return_file_header_footer(
626
- data_file_path, file_row_number=file_parameters["rows number"], row_number=5
627
- )
787
+ header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
628
788
 
629
789
  if not header:
630
790
  QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
631
791
  return
632
792
 
633
793
  w = dialog.View_data()
634
- w.setWindowTitle(f"View data")
794
+ w.setWindowTitle("View data")
635
795
  w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
636
- w.pbOK.setText("Close")
796
+ w.pbOK.setText(cfg.CLOSE)
637
797
  w.label.setText("Index of columns to plot")
638
798
  w.le.setEnabled(False)
639
799
  w.le.setText(columns_to_plot)
@@ -676,73 +836,139 @@ class Observation(QDialog, Ui_Form):
676
836
  extract wav of all media files loaded in player #1
677
837
  """
678
838
 
679
- if self.cbVisualizeSpectrogram.isChecked() or self.cb_visualize_waveform.isChecked():
680
- flag_wav_produced = False
681
- # check if player 1 is selected
682
- flag_player1 = False
683
- for row in range(self.twVideo1.rowCount()):
684
- if self.twVideo1.cellWidget(row, 0).currentText() == "1":
685
- flag_player1 = True
839
+ if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
840
+ return
686
841
 
687
- if not flag_player1:
688
- QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
689
- self.cbVisualizeSpectrogram.setChecked(False)
690
- self.cb_visualize_waveform.setChecked(False)
691
- return
692
- """
693
- if dialog.MessageDialog(programName, ("You choose to visualize the spectrogram or waveform for the media in player #1.<br>"
694
- "The WAV will be extracted from the media files, be patient"), [YES, NO]) == YES:
695
- """
696
- if True:
842
+ flag_wav_produced = False
843
+ # check if player 1 is selected
844
+ flag_player1 = False
845
+ for row in range(self.twVideo1.rowCount()):
846
+ if self.twVideo1.cellWidget(row, 0).currentText() == "1":
847
+ flag_player1 = True
697
848
 
698
- w = dialog.Info_widget()
699
- w.resize(350, 100)
700
- # w.setWindowFlags(Qt.WindowStaysOnTopHint)
701
- w.setWindowTitle("BORIS")
702
- w.label.setText("Extracting WAV from media files...")
849
+ if not flag_player1:
850
+ QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
851
+ self.cbVisualizeSpectrogram.setChecked(False)
852
+ self.cb_visualize_waveform.setChecked(False)
853
+ return
703
854
 
704
- for row in range(self.twVideo1.rowCount()):
705
- # check if player 1
706
- if self.twVideo1.cellWidget(row, 0).currentText() != "1":
707
- continue
855
+ if True:
856
+ w = dialog.Info_widget()
857
+ w.resize(350, 100)
858
+ # w.setWindowFlags(Qt.WindowStaysOnTopHint)
859
+ w.setWindowTitle("BORIS")
860
+ w.label.setText("Extracting WAV from media files...")
708
861
 
709
- media_file_path = project_functions.full_path(
710
- self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path
711
- )
712
- if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
862
+ for row in range(self.twVideo1.rowCount()):
863
+ # check if player 1
864
+ if self.twVideo1.cellWidget(row, 0).currentText() != "1":
865
+ continue
866
+
867
+ media_file_path = project_functions.full_path(self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path)
868
+ if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
869
+ QMessageBox.critical(self, cfg.programName, f"The media file {media_file_path} does not seem to have audio")
870
+ flag_wav_produced = False
871
+ break
872
+
873
+ if os.path.isfile(media_file_path):
874
+ w.show()
875
+ QApplication.processEvents()
876
+
877
+ if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
713
878
  QMessageBox.critical(
714
- self, cfg.programName, f"The media file {media_file_path} does not seem to have audio"
879
+ self,
880
+ cfg.programName,
881
+ f"Error during extracting WAV of the media file {media_file_path}",
715
882
  )
716
883
  flag_wav_produced = False
717
884
  break
718
885
 
719
- if os.path.isfile(media_file_path):
720
- w.show()
721
- QApplication.processEvents()
886
+ w.hide()
722
887
 
723
- if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
724
- QMessageBox.critical(
725
- self,
726
- cfg.programName,
727
- f"Error during extracting WAV of the media file {media_file_path}",
728
- )
729
- flag_wav_produced = False
730
- break
888
+ flag_wav_produced = True
889
+ else:
890
+ QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
731
891
 
732
- w.hide()
892
+ if not flag_wav_produced:
893
+ self.cbVisualizeSpectrogram.setChecked(False)
894
+ self.cb_visualize_waveform.setChecked(False)
733
895
 
734
- flag_wav_produced = True
735
- else:
736
- QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
896
+ def check_creation_date(self) -> int:
897
+ """
898
+ check if media file exists
899
+ check if Creation Date tag is present in metadata of media file
737
900
 
738
- if not flag_wav_produced:
739
- self.cbVisualizeSpectrogram.setChecked(False)
740
- self.cb_visualize_waveform.setChecked(False)
741
- """
901
+ Returns:
902
+ int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
903
+
904
+ """
905
+
906
+ # check if media files exist
907
+
908
+ media_not_found_list: list = []
909
+ for row in range(self.twVideo1.rowCount()):
910
+ if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
911
+ media_not_found_list.append(self.twVideo1.item(row, 2).text())
912
+
913
+ """
914
+ if media_list:
915
+ dlg = dialog.Results_dialog()
916
+ dlg.setWindowTitle("BORIS")
917
+ dlg.pbOK.setText("OK")
918
+ dlg.pbCancel.setVisible(False)
919
+ dlg.ptText.clear()
920
+ dlg.ptText.appendHtml(
921
+ (
922
+ "Some media file(s) were not found:<br>"
923
+ f"{'<br>'.join(media_list)}<br><br>"
924
+ "You cannot select the <b>Use the media creation date/time option</b>."
925
+ )
926
+ )
927
+ dlg.ptText.moveCursor(QTextCursor.Start)
928
+ ret = dlg.exec_()
929
+ """
930
+
931
+ """
932
+ not_tagged_media_list: list = []
933
+ for row in range(self.twVideo1.rowCount()):
934
+ if self.twVideo1.item(row, 2).text() not in media_not_found_list:
935
+ media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
936
+ if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
937
+ not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
938
+ else:
939
+ creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
940
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
941
+
942
+ if not_tagged_media_list:
943
+ dlg = dialog.Results_dialog()
944
+ dlg.setWindowTitle("BORIS")
945
+ dlg.pbOK.setText("Yes")
946
+ dlg.pbCancel.setVisible(True)
947
+ dlg.pbCancel.setText("No")
948
+
949
+ dlg.ptText.clear()
950
+ dlg.ptText.appendHtml(
951
+ (
952
+ "Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
953
+ f"{'<br>'.join(not_tagged_media_list)}<br><br>"
954
+ "Use the media file date/time instead?"
955
+ )
956
+ )
957
+ dlg.ptText.moveCursor(QTextCursor.Start)
958
+ ret = dlg.exec_()
959
+
960
+ if ret == 1: # use file creation time
961
+ for media in not_tagged_media_list:
962
+ self.media_creation_time[media] = pl.Path(media).stat().st_ctime
963
+ return 0 # OK use media file creation date/time
742
964
  else:
743
- self.cbVisualizeSpectrogram.setChecked(False)
744
- self.cb_visualize_waveform.setChecked(False)
745
- """
965
+ self.cb_media_creation_date_as_offset.setChecked(False)
966
+ self.media_creation_time = {}
967
+ return 1
968
+ else:
969
+ return 0 # OK all media have a 'creation time' tag
970
+ """
971
+ return 0
746
972
 
747
973
  def closeEvent(self, event):
748
974
  """
@@ -761,11 +987,13 @@ class Observation(QDialog, Ui_Form):
761
987
  self.text = None
762
988
  self.reject()
763
989
 
764
- def check_parameters(self):
990
+ def check_parameters(self) -> bool:
765
991
  """
766
992
  check observation parameters
767
993
 
768
- return True if everything OK else False
994
+ Returns:
995
+ bool: True if everything is OK else False
996
+
769
997
  """
770
998
 
771
999
  def is_numeric(s):
@@ -786,21 +1014,40 @@ class Observation(QDialog, Ui_Form):
786
1014
 
787
1015
  # check if observation id not empty
788
1016
  if not self.leObservationId.text():
789
- self.qm = QMessageBox()
790
- self.qm.setIcon(QMessageBox.Critical)
791
- self.qm.setText("The <b>observation id</b> is mandatory and must be unique.")
792
- self.qm.exec_()
1017
+ QMessageBox.critical(
1018
+ self,
1019
+ cfg.programName,
1020
+ "The <b>observation id</b> is mandatory and must be unique.",
1021
+ )
793
1022
  return False
794
1023
 
795
1024
  # check if observation_type
796
1025
  if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
797
- self.qm = QMessageBox()
798
- self.qm.setIcon(QMessageBox.Critical)
799
- self.qm.setText("Choose an observation type.")
800
- self.qm.exec_()
1026
+ QMessageBox.critical(
1027
+ self,
1028
+ cfg.programName,
1029
+ "Choose an observation type.",
1030
+ )
801
1031
  return False
802
1032
 
1033
+ # check if offset is correct
1034
+ if self.cb_time_offset.isChecked():
1035
+ if self.obs_time_offset.get_time() is None:
1036
+ QMessageBox.critical(
1037
+ self,
1038
+ cfg.programName,
1039
+ "Check the time offset value.",
1040
+ )
1041
+ return False
1042
+
803
1043
  if self.rb_media_files.isChecked(): # observation based on media file(s)
1044
+ # check if media file exists
1045
+ media_file_not_found: list = []
1046
+ for row in range(self.twVideo1.rowCount()):
1047
+ # check if media file exists
1048
+ if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
1049
+ media_file_not_found.append(self.twVideo1.item(row, 2).text())
1050
+
804
1051
  # check player number
805
1052
  players_list: list = []
806
1053
  players: dict = {} # for storing duration
@@ -813,18 +1060,20 @@ class Observation(QDialog, Ui_Form):
813
1060
 
814
1061
  # check if player #1 is used
815
1062
  if not players_list or min(players_list) > 1:
816
- self.qm = QMessageBox()
817
- self.qm.setIcon(QMessageBox.Critical)
818
- self.qm.setText("A media file must be loaded in player #1")
819
- self.qm.exec_()
1063
+ QMessageBox.critical(
1064
+ self,
1065
+ cfg.programName,
1066
+ "A media file must be loaded in player #1",
1067
+ )
820
1068
  return False
821
1069
 
822
1070
  # check if players are used in crescent order
823
1071
  if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
824
- self.qm = QMessageBox()
825
- self.qm.setIcon(QMessageBox.Critical)
826
- self.qm.setText("Some player are not used. Please reorganize your media files")
827
- self.qm.exec_()
1072
+ QMessageBox.critical(
1073
+ self,
1074
+ cfg.programName,
1075
+ "Some player are not used. Please reorganize your media files",
1076
+ )
828
1077
  return False
829
1078
 
830
1079
  # check if more media in player #1 and media in other players
@@ -856,7 +1105,7 @@ class Observation(QDialog, Ui_Form):
856
1105
  return False
857
1106
 
858
1107
  # check that the longuest media is in player #1
859
- durations = []
1108
+ durations: list = []
860
1109
  for i in sorted(list(players.keys())):
861
1110
  durations.append(sum(players[i]))
862
1111
  if [x for x in durations[1:] if x > durations[0]]:
@@ -878,6 +1127,20 @@ class Observation(QDialog, Ui_Form):
878
1127
  )
879
1128
  return False
880
1129
 
1130
+ # check if offset set and only player #1 is used
1131
+ if len(set(players_list)) == 1:
1132
+ for row in range(self.twVideo1.rowCount()):
1133
+ if float(self.twVideo1.item(row, 1).text()):
1134
+ QMessageBox.critical(
1135
+ self,
1136
+ cfg.programName,
1137
+ (
1138
+ "It is not possible to use offset value(s) with only one player,<br>"
1139
+ "The offset values are use to synchronise various players."
1140
+ ),
1141
+ )
1142
+ return False
1143
+
881
1144
  # check offset for external data files
882
1145
  for row in range(self.tw_data_files.rowCount()):
883
1146
  if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
@@ -893,6 +1156,18 @@ class Observation(QDialog, Ui_Form):
893
1156
  )
894
1157
  return False
895
1158
 
1159
+ # check media creation time tag in metadata
1160
+ # Disable because the check will be made at the observation start
1161
+ """
1162
+ if self.cb_media_creation_date_as_offset.isChecked():
1163
+ if self.check_creation_date():
1164
+ return False
1165
+ """
1166
+
1167
+ # check media creation date time (if option enabled)
1168
+ if self.check_media_creation_date():
1169
+ return False
1170
+
896
1171
  if self.rb_images.isChecked(): # observation based on images directory
897
1172
  if not self.lw_images_directory.count():
898
1173
  QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
@@ -901,9 +1176,7 @@ class Observation(QDialog, Ui_Form):
901
1176
  # check if indep variables are correct type
902
1177
  for row in range(self.twIndepVariables.rowCount()):
903
1178
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
904
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
905
- self.twIndepVariables.item(row, 2).text()
906
- ):
1179
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
907
1180
  QMessageBox.critical(
908
1181
  self,
909
1182
  cfg.programName,
@@ -925,11 +1198,10 @@ class Observation(QDialog, Ui_Form):
925
1198
  )
926
1199
  return False
927
1200
 
1201
+ # check if numeric indep variable values are numeric
928
1202
  for row in range(self.twIndepVariables.rowCount()):
929
1203
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
930
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
931
- self.twIndepVariables.item(row, 2).text()
932
- ):
1204
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
933
1205
  QMessageBox.critical(
934
1206
  self,
935
1207
  cfg.programName,
@@ -941,7 +1213,7 @@ class Observation(QDialog, Ui_Form):
941
1213
 
942
1214
  def pbLaunch_clicked(self):
943
1215
  """
944
- Close window and start observation
1216
+ Close dialog and start the observation
945
1217
  """
946
1218
 
947
1219
  if self.check_parameters():
@@ -976,24 +1248,54 @@ class Observation(QDialog, Ui_Form):
976
1248
  str: error message or empty string
977
1249
  """
978
1250
 
1251
+ logging.debug(f"check_media function for {file_path}")
1252
+
979
1253
  media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
1254
+
1255
+ logging.debug(f"{media_info=}")
1256
+
980
1257
  if "error" in media_info:
981
- return False, media_info["error"]
1258
+ return (True, media_info["error"])
1259
+
1260
+ if media_info["format_long_name"] == "Tele-typewriter":
1261
+ return (True, "Text file")
1262
+
1263
+ if media_info["duration"] > 0:
1264
+ if " rel " in mode:
1265
+ # convert to relative path (relative to BORIS project file)
1266
+ file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
1267
+
1268
+ self.mediaDurations[file_path] = float(media_info["duration"])
1269
+ elif media_info["has_video"] is False and media_info["audio_duration"]:
1270
+ self.mediaDurations[file_path] = float(media_info["audio_duration"])
982
1271
  else:
983
- if media_info["duration"] > 0:
1272
+ return (True, "Media duration not available")
984
1273
 
985
- if " rel " in mode:
986
- # convert to relative path (relative to BORIS project file)
987
- file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
988
-
989
- self.mediaDurations[file_path] = float(media_info["duration"])
990
- self.mediaFPS[file_path] = float(media_info["fps"])
991
- self.mediaHasVideo[file_path] = media_info["has_video"]
992
- self.mediaHasAudio[file_path] = media_info["has_audio"]
993
- self.add_media_to_listview(file_path)
994
- return (False, "")
995
- else:
996
- return (True, "Media duration not available")
1274
+ self.mediaFPS[file_path] = float(media_info["fps"])
1275
+ self.mediaHasVideo[file_path] = media_info["has_video"]
1276
+ self.mediaHasAudio[file_path] = media_info["has_audio"]
1277
+
1278
+ logging.debug(f"{file_path=}")
1279
+
1280
+ self.add_media_to_listview(file_path)
1281
+ return (False, "")
1282
+
1283
+ def update_media_options(self):
1284
+ """
1285
+ update the media options
1286
+ """
1287
+ for w in (
1288
+ self.cbVisualizeSpectrogram,
1289
+ self.cb_visualize_waveform,
1290
+ self.cb_observation_time_interval,
1291
+ self.cb_media_creation_date_as_offset,
1292
+ ):
1293
+ w.setEnabled(self.twVideo1.rowCount() > 0)
1294
+
1295
+ # enable stop ongoing state events if n. media > 1
1296
+ self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(self.twVideo1.rowCount() > 0)
1297
+
1298
+ # self.creation_date_as_offset()
997
1299
 
998
1300
  def add_media(self, mode: str):
999
1301
  """
@@ -1021,9 +1323,7 @@ class Observation(QDialog, Ui_Form):
1021
1323
  QMessageBox.critical(
1022
1324
  self,
1023
1325
  cfg.programName,
1024
- (
1025
- "It is not possible to add a media file without path or with a relative path if the project is not already saved"
1026
- ),
1326
+ ("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
1027
1327
  )
1028
1328
  return
1029
1329
 
@@ -1034,9 +1334,9 @@ class Observation(QDialog, Ui_Form):
1034
1334
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
1035
1335
 
1036
1336
  if "media " in mode:
1337
+ file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
1037
1338
 
1038
- fn = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
1039
- file_paths = fn[0] if type(fn) is tuple else fn
1339
+ logging.debug(f"{file_paths=}")
1040
1340
 
1041
1341
  if file_paths:
1042
1342
  # store directory for next usage
@@ -1058,39 +1358,40 @@ class Observation(QDialog, Ui_Form):
1058
1358
  if error:
1059
1359
  QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
1060
1360
 
1061
- if "dir " in mode:
1062
-
1361
+ if "dir " in mode: # add media from dir
1063
1362
  dir_name = fd.getExistingDirectory(self, "Select directory")
1064
1363
  if dir_name:
1065
1364
  response = ""
1066
- for file_path in glob.glob(dir_name + os.sep + "*"):
1067
- (error, msg) = self.check_media(file_path, mode)
1365
+ for file_path in sorted(pl.Path(dir_name).glob("*")):
1366
+ if not file_path.is_file():
1367
+ continue
1368
+ (error, msg) = self.check_media(str(file_path), mode)
1068
1369
  if error:
1069
1370
  if response != "Skip all non media files":
1070
1371
  response = dialog.MessageDialog(
1071
1372
  cfg.programName,
1072
1373
  f"<b>{file_path}</b> {msg}",
1073
- ["Continue", "Skip all non media files", "Cancel"],
1374
+ ["Continue", "Skip all non media files", cfg.CANCEL],
1074
1375
  )
1075
- if response == "Cancel":
1376
+ if response == cfg.CANCEL:
1076
1377
  break
1378
+ # ask to use directory name / path as observation id
1379
+ if response != cfg.CANCEL:
1380
+ selected_obs_id = dialog.MessageDialog(
1381
+ cfg.programName,
1382
+ "Select the observation id",
1383
+ [dir_name, str(pl.Path(dir_name).name), cfg.CANCEL],
1384
+ )
1385
+ if selected_obs_id != cfg.CANCEL:
1386
+ self.leObservationId.setText(selected_obs_id)
1077
1387
 
1078
- for w in [
1079
- self.cbVisualizeSpectrogram,
1080
- self.cb_visualize_waveform,
1081
- self.cb_observation_time_interval,
1082
- self.cbCloseCurrentBehaviorsBetweenVideo,
1083
- ]:
1084
- w.setEnabled(self.twVideo1.rowCount() > 0)
1085
-
1086
- # disabled for problems
1087
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
1388
+ self.update_media_options()
1088
1389
 
1089
1390
  def add_media_to_listview(self, file_name):
1090
1391
  """
1091
1392
  add media file path to list widget
1092
1393
  """
1093
-
1394
+ # add a row
1094
1395
  self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
1095
1396
 
1096
1397
  for col_idx, s in enumerate(
@@ -1131,33 +1432,22 @@ class Observation(QDialog, Ui_Form):
1131
1432
  remove all selected media files from list widget
1132
1433
  """
1133
1434
 
1134
- if self.twVideo1.selectedIndexes():
1135
- rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
1136
- for row in sorted(rows_to_delete, reverse=True):
1137
- media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
1138
- self.twVideo1.removeRow(row)
1139
- if media_path not in [
1140
- self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())
1141
- ]:
1142
- try:
1143
- del self.mediaDurations[media_path]
1144
- except NameError:
1145
- pass
1146
- try:
1147
- del self.mediaFPS[media_path]
1148
- except NameError:
1149
- pass
1150
-
1151
- for w in [
1152
- self.cbVisualizeSpectrogram,
1153
- self.cb_visualize_waveform,
1154
- self.cb_observation_time_interval,
1155
- self.cbCloseCurrentBehaviorsBetweenVideo,
1156
- ]:
1157
- w.setEnabled(self.twVideo1.rowCount() > 0)
1158
-
1159
- # disabled for problems
1160
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
1161
-
1162
- else:
1435
+ if not self.twVideo1.selectedIndexes():
1163
1436
  QMessageBox.warning(self, cfg.programName, "No media file selected")
1437
+ return
1438
+
1439
+ rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
1440
+ for row in sorted(rows_to_delete, reverse=True):
1441
+ media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
1442
+ self.twVideo1.removeRow(row)
1443
+ if media_path not in [self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())]:
1444
+ try:
1445
+ del self.mediaDurations[media_path]
1446
+ except NameError:
1447
+ pass
1448
+ try:
1449
+ del self.mediaFPS[media_path]
1450
+ except NameError:
1451
+ pass
1452
+
1453
+ self.update_media_options()