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