boris-behav-obs 9.0.2__py2.py3-none-any.whl → 9.0.5__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +2 -2
  4. boris/add_modifier.py +1 -1
  5. boris/add_modifier_ui.py +22 -22
  6. boris/advanced_event_filtering.py +1 -1
  7. boris/analysis_plugins/number_of_occurences.py +6 -3
  8. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +6 -4
  9. boris/analysis_plugins/time_budget.py +24 -14
  10. boris/behav_coding_map_creator.py +1 -1
  11. boris/behavior_binary_table.py +1 -1
  12. boris/behaviors_coding_map.py +1 -1
  13. boris/boris_cli.py +1 -1
  14. boris/cmd_arguments.py +1 -1
  15. boris/coding_pad.py +1 -1
  16. boris/config.py +16 -9
  17. boris/config_file.py +1 -1
  18. boris/connections.py +1 -1
  19. boris/converters.py +1 -1
  20. boris/cooccurence.py +1 -1
  21. boris/core.py +144 -176
  22. boris/db_functions.py +1 -1
  23. boris/dialog.py +10 -277
  24. boris/edit_event.py +8 -1
  25. boris/event_operations.py +1 -1
  26. boris/events_cursor.py +1 -1
  27. boris/events_snapshots.py +1 -1
  28. boris/exclusion_matrix.py +1 -1
  29. boris/export_events.py +1 -1
  30. boris/export_observation.py +1 -1
  31. boris/external_processes.py +1 -1
  32. boris/geometric_measurement.py +1 -1
  33. boris/gui_utilities.py +1 -1
  34. boris/image_overlay.py +1 -1
  35. boris/import_observations.py +1 -1
  36. boris/irr.py +1 -1
  37. boris/latency.py +1 -1
  38. boris/map_creator.py +1 -1
  39. boris/measurement_widget.py +1 -1
  40. boris/media_file.py +1 -1
  41. boris/menu_options.py +1 -1
  42. boris/modifiers_coding_map.py +1 -1
  43. boris/observation.py +1 -1
  44. boris/observation_operations.py +530 -425
  45. boris/observations_list.py +3 -3
  46. boris/otx_parser.py +1 -1
  47. boris/param_panel.py +1 -1
  48. boris/player_dock_widget.py +13 -9
  49. boris/plot_data_module.py +1 -1
  50. boris/plot_events.py +1 -1
  51. boris/plot_events_rt.py +1 -1
  52. boris/plot_spectrogram_rt.py +1 -1
  53. boris/plot_waveform_rt.py +1 -1
  54. boris/plugins.py +2 -2
  55. boris/preferences.py +6 -1
  56. boris/preferences_ui.py +7 -1
  57. boris/project.py +1 -1
  58. boris/project_functions.py +73 -76
  59. boris/project_import_export.py +1 -1
  60. boris/select_modifiers.py +1 -1
  61. boris/select_observations.py +6 -6
  62. boris/select_subj_behav.py +1 -1
  63. boris/state_events.py +1 -1
  64. boris/subjects_pad.py +1 -1
  65. boris/synthetic_time_budget.py +1 -1
  66. boris/time_budget_functions.py +1 -1
  67. boris/time_budget_widget.py +5 -5
  68. boris/transitions.py +1 -1
  69. boris/utilities.py +1 -1
  70. boris/version.py +3 -3
  71. boris/video_equalizer.py +1 -1
  72. boris/video_operations.py +1 -1
  73. boris/write_event.py +1 -1
  74. {boris_behav_obs-9.0.2.dist-info → boris_behav_obs-9.0.5.dist-info}/METADATA +1 -1
  75. boris_behav_obs-9.0.5.dist-info/RECORD +103 -0
  76. boris_behav_obs-9.0.2.dist-info/RECORD +0 -103
  77. {boris_behav_obs-9.0.2.dist-info → boris_behav_obs-9.0.5.dist-info}/LICENSE.TXT +0 -0
  78. {boris_behav_obs-9.0.2.dist-info → boris_behav_obs-9.0.5.dist-info}/WHEEL +0 -0
  79. {boris_behav_obs-9.0.2.dist-info → boris_behav_obs-9.0.5.dist-info}/entry_points.txt +0 -0
  80. {boris_behav_obs-9.0.2.dist-info → boris_behav_obs-9.0.5.dist-info}/top_level.txt +0 -0
boris/core.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2024 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -34,6 +34,7 @@ import json
34
34
  import logging
35
35
  import pathlib as pl
36
36
  import platform
37
+ import importlib
37
38
  import re
38
39
  import PIL.Image
39
40
  import PIL.ImageEnhance
@@ -116,6 +117,7 @@ from . import config_file
116
117
  from . import select_subj_behav
117
118
  from . import observation_operations
118
119
  from . import write_event
120
+ from . import view_df
119
121
 
120
122
 
121
123
  # matplotlib.pyplot.switch_backend("Qt5Agg")
@@ -282,7 +284,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
282
284
  plot_colors = cfg.BEHAVIORS_PLOT_COLORS
283
285
  behav_category_colors = cfg.CATEGORY_COLORS_LIST
284
286
 
285
- measurement_w = None
287
+ # measurement_w = None
286
288
  current_image_size = None
287
289
 
288
290
  media_scan_sampling_mem: list = []
@@ -311,7 +313,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
311
313
  playerType: str = "" # cfg.MEDIA, cfg.LIVE, cfg.VIEWER
312
314
 
313
315
  # spectrogram
314
- chunk_length = 60 # spectrogram chunk length in seconds
316
+ chunk_length: float = 60 # spectrogram chunk length in seconds
315
317
 
316
318
  close_the_same_current_event: bool = False
317
319
  tcp_port: int = 0
@@ -323,7 +325,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
323
325
 
324
326
  dw_player: list = []
325
327
 
326
- save_project_json_started = False
328
+ save_project_json_started: bool = False
327
329
 
328
330
  mem_hash_obs: int = 0
329
331
 
@@ -537,11 +539,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
537
539
  media_file_available=ib.elements["Test media file accessibility"].isChecked(),
538
540
  )
539
541
  if msg:
540
- msg = f"Some issues were found in the project<br><br>{msg}"
541
542
  self.results = dialog.Results_dialog()
542
543
  self.results.setWindowTitle("Check project integrity")
543
544
  self.results.ptText.clear()
544
- self.results.ptText.appendHtml(msg)
545
+ self.results.ptText.appendHtml(f"Some issues were found in the project<br><br>{msg}")
545
546
  self.results.show()
546
547
  else:
547
548
  QMessageBox.information(self, cfg.programName, "The current project has no issues")
@@ -1649,9 +1650,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1649
1650
  """
1650
1651
 
1651
1652
  if self.observationId and self.projectFileName:
1652
- logging.info("autosave project")
1653
-
1654
- self.save_project_activated()
1653
+ if not self.save_project_activated():
1654
+ logging.info("project autosaved")
1655
+ else:
1656
+ logging.warning("Error autosaving project")
1655
1657
  else:
1656
1658
  logging.debug((f"project not autosaved: observation id: {self.observationId} project file name: {self.projectFileName}"))
1657
1659
 
@@ -1690,43 +1692,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1690
1692
  break
1691
1693
  return currentMedia, round(frameCurrentMedia)
1692
1694
 
1693
- '''
1694
- def extract_exif_DateTimeOriginal(self, file_path: str) -> int:
1695
- """
1696
- extract the EXIF DateTimeOriginal tag
1697
- return epoch time
1698
- if the tag is not available return -1
1699
-
1700
- Args:
1701
- file_path (str): path of the media file
1702
-
1703
- Returns:
1704
- int: timestamp
1705
-
1706
- """
1707
- try:
1708
- with open(file_path, "rb") as f_in:
1709
- tags = exifread.process_file(f_in, details=False, stop_tag="EXIF DateTimeOriginal")
1710
- if "EXIF DateTimeOriginal" in tags:
1711
- date_time_original = (
1712
- f'{tags["EXIF DateTimeOriginal"].values[:4]}-'
1713
- f'{tags["EXIF DateTimeOriginal"].values[5:7]}-'
1714
- f'{tags["EXIF DateTimeOriginal"].values[8:10]} '
1715
- f'{tags["EXIF DateTimeOriginal"].values.split(" ")[-1]}'
1716
- )
1717
- return int(datetime.datetime.strptime(date_time_original, "%Y-%m-%d %H:%M:%S").timestamp())
1718
- else:
1719
- try:
1720
- # read from file name (YYYY-MM-DD_HHMMSS)
1721
- return int(datetime.datetime.strptime(pl.Path(file_path).stem, "%Y-%m-%d_%H%M%S").timestamp())
1722
- except Exception:
1723
- # read from file name (YYYY-MM-DD_HH:MM:SS)
1724
- return int(datetime.datetime.strptime(pl.Path(file_path).stem, "%Y-%m-%d_%H:%M:%S").timestamp())
1725
-
1726
- except Exception:
1727
- return -1
1728
- '''
1729
-
1730
1695
  def extract_frame(self, dw):
1731
1696
  """
1732
1697
  for MEDIA obs: extract frame from video and visualize it in frame_viewer
@@ -1743,9 +1708,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1743
1708
  )
1744
1709
 
1745
1710
  if self.playerType == cfg.IMAGES:
1746
- # print(f"{self.images_list=}")
1747
- # print(f"{self.image_idx=}")
1748
-
1749
1711
  if self.image_idx >= len(self.images_list):
1750
1712
  QMessageBox.critical(
1751
1713
  None,
@@ -1769,7 +1731,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1769
1731
  else:
1770
1732
  msg += "<br>EXIF Date/Time Original: <b>NA</b>"
1771
1733
 
1772
- # self.image_time_ref = 0
1773
1734
  if self.image_idx == 0 and date_time_original != -1:
1774
1735
  self.image_time_ref = date_time_original
1775
1736
 
@@ -2257,47 +2218,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2257
2218
  set_and_update_pan_and_zoom(pan_x=0, pan_y=0, zoom=0)
2258
2219
  return
2259
2220
 
2260
- # def read_tw_event_field(self, row_idx: int, player_type: str, field_type: str) -> Union[str, None, int, dec]:
2261
- # """
2262
- # return value of field for event in TW or NA if not available
2263
- # """
2264
- # if field_type not in cfg.TW_EVENTS_FIELDS[player_type]:
2265
- # return None
2266
- #
2267
- # return self.twEvents.item(row_idx, cfg.TW_OBS_FIELD[player_type][field_type]).text()
2268
-
2269
- # def configure_twevents_columns(self):
2270
- # """
2271
- # configure the visible columns of twEvent tablewidget
2272
- # configuration for playerType is recorded in self.config_param[f"{self.playerType} tw fields"]
2273
- # """
2274
-
2275
- # dlg = dialog.Input_dialog(
2276
- # label_caption="Select the columns to show",
2277
- # elements_list=[
2278
- # (
2279
- # "cb",
2280
- # x,
2281
- # # default state
2282
- # x
2283
- # in self.config_param.get(
2284
- # f"{self.playerType} tw fields",
2285
- # cfg.TW_EVENTS_FIELDS[self.playerType],
2286
- # ),
2287
- # )
2288
- # for x in cfg.TW_EVENTS_FIELDS[self.playerType]
2289
- # ],
2290
- # title="Select the column to show",
2291
- # )
2292
- # if not dlg.exec_():
2293
- # return
2294
-
2295
- # self.config_param[f"{self.playerType} tw fields"] = tuple(
2296
- # field for field in cfg.TW_EVENTS_FIELDS[self.playerType] if dlg.elements[field].isChecked()
2297
- # )
2298
-
2299
- # self.load_tw_events(self.observationId)
2300
-
2301
2221
  def configure_tvevents_columns(self):
2302
2222
  """
2303
2223
  configure the visible columns of tv_events tableview
@@ -2405,7 +2325,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2405
2325
  # self.table.setSortingEnabled(True)
2406
2326
  # self.table.sortByColumn(0, Qt.AscendingOrder)
2407
2327
 
2408
- def load_tw_events(self, obs_id):
2328
+ def load_tw_events(self, obs_id) -> None:
2409
2329
  """
2410
2330
  load events in table view and update START/STOP
2411
2331
 
@@ -2531,76 +2451,77 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2531
2451
  self.plot_data[pd].close_plot()
2532
2452
 
2533
2453
  except Exception:
2534
- pass
2535
- """
2536
- while self.plot_data:
2537
- self.plot_data[0].close_plot()
2538
- time.sleep(1)
2539
- del self.plot_data[0]
2540
- """
2454
+ logging.warning("Error closing plot window")
2541
2455
 
2542
2456
  if hasattr(self, "measurement_w"):
2543
2457
  try:
2544
2458
  self.measurement_w.close()
2545
- del self.codingpad
2459
+ del self.measurement_w
2546
2460
  except Exception:
2547
- pass
2461
+ logging.warning("Error closing measurement window")
2548
2462
 
2549
2463
  if hasattr(self, "codingpad"):
2550
2464
  try:
2551
2465
  self.codingpad.close()
2552
2466
  del self.codingpad
2553
2467
  except Exception:
2554
- pass
2468
+ logging.warning("Error closing coding pad window")
2555
2469
 
2556
2470
  if hasattr(self, "subjects_pad"):
2557
2471
  try:
2558
2472
  self.subjects_pad.close()
2559
2473
  del self.subjects_pad
2560
2474
  except Exception:
2561
- pass
2475
+ logging.warning("Error closing subjects pad window")
2562
2476
 
2563
2477
  if hasattr(self, "spectro"):
2564
2478
  try:
2565
2479
  self.spectro.close()
2566
2480
  del self.spectro
2567
2481
  except Exception:
2568
- pass
2482
+ logging.warning("Error closing spectrogram window")
2569
2483
 
2570
2484
  if hasattr(self, "waveform"):
2571
2485
  try:
2572
2486
  self.waveform.close()
2573
2487
  del self.waveform
2574
2488
  except Exception:
2575
- pass
2489
+ logging.warning("Error closing waveform window")
2576
2490
 
2577
2491
  if hasattr(self, "plot_events"):
2578
2492
  try:
2579
2493
  self.plot_events.close()
2580
2494
  del self.plot_events
2581
2495
  except Exception:
2582
- pass
2496
+ logging.warning("Error closing plot events window")
2583
2497
 
2584
2498
  if hasattr(self, "results"):
2585
2499
  try:
2586
2500
  self.results.close()
2587
2501
  del self.results
2588
2502
  except Exception:
2589
- pass
2503
+ logging.warning("Error closing results window")
2590
2504
 
2591
2505
  if hasattr(self, "mapCreatorWindow"):
2592
2506
  try:
2593
2507
  self.mapCreatorWindow.close()
2594
2508
  del self.mapCreatorWindow
2595
2509
  except Exception:
2596
- pass
2510
+ logging.warning("Error closing map creator window")
2597
2511
 
2598
2512
  if hasattr(self, "video_equalizer_wgt"):
2599
2513
  try:
2600
2514
  self.video_equalizer_wgt.close()
2601
2515
  del self.video_equalizer_wgt
2602
2516
  except Exception:
2603
- pass
2517
+ logging.warning("Error closing video equalizer window")
2518
+
2519
+ if hasattr(self, "view_dataframe"):
2520
+ try:
2521
+ self.view_dataframe.close()
2522
+ del self.view_dataframe
2523
+ except Exception:
2524
+ logging.warning("Error closing the plugin results window")
2604
2525
 
2605
2526
  # delete behavior coding map
2606
2527
  for idx in self.bcm_dict:
@@ -2671,22 +2592,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2671
2592
  except Exception:
2672
2593
  logging.debug("error in observation time interval")
2673
2594
 
2674
- # TODO: replace by event_type in project_functions
2675
- def eventType(self, code):
2676
- """
2677
- returns type of event for code
2678
- """
2679
- for idx in self.pj[cfg.ETHOGRAM]:
2680
- if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == code:
2681
- return self.pj[cfg.ETHOGRAM][idx][cfg.TYPE]
2682
- return None
2683
-
2684
2595
  def extract_observed_behaviors(self, selected_observations, selectedSubjects):
2685
2596
  """
2686
2597
  extract unique behaviors codes from obs_id observation
2687
2598
  """
2688
2599
 
2689
- observed_behaviors = []
2600
+ observed_behaviors: list = []
2690
2601
 
2691
2602
  # extract events from selected observations
2692
2603
  all_events = [self.pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in self.pj[cfg.OBSERVATIONS] if x in selected_observations]
@@ -2752,8 +2663,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
2752
2663
  return
2753
2664
 
2754
2665
  # select dir if many observations
2755
- plot_directory = ""
2756
- file_format = "png"
2666
+ plot_directory: str = ""
2667
+ file_format: str = "png"
2757
2668
  if len(selected_observations) > 1:
2758
2669
  plot_directory = QFileDialog.getExistingDirectory(
2759
2670
  self,
@@ -3026,12 +2937,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3026
2937
  flag_all_upper = True
3027
2938
  if pj[cfg.ETHOGRAM]:
3028
2939
  for idx in pj[cfg.ETHOGRAM]:
3029
- if pj[cfg.ETHOGRAM][idx]["key"].islower():
2940
+ if pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_KEY].islower():
3030
2941
  flag_all_upper = False
3031
2942
 
3032
2943
  if pj[cfg.SUBJECTS]:
3033
2944
  for idx in pj[cfg.SUBJECTS]:
3034
- if pj[cfg.SUBJECTS][idx]["key"].islower():
2945
+ if pj[cfg.SUBJECTS][idx][cfg.SUBJECT_KEY].islower():
3035
2946
  flag_all_upper = False
3036
2947
 
3037
2948
  if (
@@ -3048,7 +2959,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3048
2959
  == cfg.YES
3049
2960
  ):
3050
2961
  for idx in pj[cfg.ETHOGRAM]:
3051
- pj[cfg.ETHOGRAM][idx]["key"] = pj[cfg.ETHOGRAM][idx]["key"].lower()
2962
+ pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_KEY] = pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_KEY].lower()
3052
2963
  # convert modifier short cuts to lower case
3053
2964
  for modifier_set in pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]:
3054
2965
  try:
@@ -3065,7 +2976,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3065
2976
  logging.warning("error during convertion of modifier short cut to lower case")
3066
2977
 
3067
2978
  for idx in pj[cfg.SUBJECTS]:
3068
- pj[cfg.SUBJECTS][idx]["key"] = pj[cfg.SUBJECTS][idx]["key"].lower()
2979
+ pj[cfg.SUBJECTS][idx][cfg.SUBJECT_KEY] = pj[cfg.SUBJECTS][idx][cfg.SUBJECT_KEY].lower()
2980
+
2981
+ # check project integrity
2982
+ if self.config_param.get(cfg.CHECK_PROJECT_INTEGRITY, True):
2983
+ msg = project_functions.check_project_integrity(
2984
+ pj,
2985
+ self.timeFormat,
2986
+ project_path,
2987
+ media_file_available=True,
2988
+ )
2989
+ if msg:
2990
+ self.results = dialog.Results_dialog()
2991
+ self.results.setWindowTitle("Project integrity results")
2992
+ self.results.ptText.clear()
2993
+ self.results.ptText.appendHtml(f"Some issues were found in the project<br><br>{msg}")
2994
+ self.results.show()
3069
2995
 
3070
2996
  self.load_project(project_path, project_changed, pj)
3071
2997
  del pj
@@ -3412,19 +3338,19 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3412
3338
 
3413
3339
  del newProjectWindow
3414
3340
 
3415
- def save_project_json(self, projectFileName: str) -> int:
3341
+ def save_project_json(self, project_file_name: str) -> int:
3416
3342
  """
3417
3343
  save project to JSON file
3418
3344
  convert Decimal type in float
3419
3345
 
3420
3346
  Args:
3421
- projectFileName (str): path of project to save
3347
+ project_file_name (str): path of project to save
3422
3348
 
3423
3349
  Returns:
3424
3350
  str:
3425
3351
  """
3426
3352
 
3427
- logging.debug(f"init save_project_json function {projectFileName}")
3353
+ logging.debug(f"init save_project_json function {project_file_name}")
3428
3354
 
3429
3355
  if self.save_project_json_started:
3430
3356
  logging.warning("Function save_project_json already launched")
@@ -3447,8 +3373,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3447
3373
  # project file indentation
3448
3374
  file_indentation = self.config_param.get(cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
3449
3375
  try:
3450
- if projectFileName.endswith(".boris.gz"):
3451
- with gzip.open(projectFileName, mode="wt", encoding="utf-8") as f_out:
3376
+ if project_file_name.endswith(".boris.gz"):
3377
+ with gzip.open(project_file_name, mode="wt", encoding="utf-8") as f_out:
3452
3378
  f_out.write(
3453
3379
  json.dumps(
3454
3380
  self.pj,
@@ -3457,7 +3383,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3457
3383
  )
3458
3384
  )
3459
3385
  else: # .boris and other extensions
3460
- with open(projectFileName, "w") as f_out:
3386
+ with open(project_file_name, "w") as f_out:
3461
3387
  f_out.write(
3462
3388
  json.dumps(
3463
3389
  self.pj,
@@ -3547,11 +3473,36 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3547
3473
 
3548
3474
  if self.save_project_json(project_new_file_name) == 0:
3549
3475
  self.projectFileName = project_new_file_name
3476
+
3477
+ self.check_project_integrity_open_save()
3478
+
3550
3479
  # update windows title
3551
3480
  menu_options.update_windows_title(self)
3552
3481
  else:
3553
3482
  return "Not saved"
3554
3483
 
3484
+ def check_project_integrity_open_save(self) -> None:
3485
+ """
3486
+ check project integrity
3487
+ to be used after opening or saving the current project
3488
+ """
3489
+ if self.automaticBackup:
3490
+ return
3491
+
3492
+ if self.config_param.get(cfg.CHECK_PROJECT_INTEGRITY, True):
3493
+ msg = project_functions.check_project_integrity(
3494
+ self.pj,
3495
+ self.timeFormat,
3496
+ self.projectFileName,
3497
+ media_file_available=True,
3498
+ )
3499
+ if msg:
3500
+ self.results = dialog.Results_dialog()
3501
+ self.results.setWindowTitle("Project integrity results")
3502
+ self.results.ptText.clear()
3503
+ self.results.ptText.appendHtml(f"Some issues were found in the project<br><br>{msg}")
3504
+ self.results.show()
3505
+
3555
3506
  def save_project_activated(self):
3556
3507
  """
3557
3508
  save current project
@@ -3617,8 +3568,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3617
3568
  if r:
3618
3569
  self.projectFileName = ""
3619
3570
  return r
3571
+ self.check_project_integrity_open_save()
3572
+
3620
3573
  else:
3621
- return self.save_project_json(self.projectFileName)
3574
+ r = self.save_project_json(self.projectFileName)
3575
+ self.check_project_integrity_open_save()
3576
+ return r
3622
3577
 
3623
3578
  return ""
3624
3579
 
@@ -4007,7 +3962,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4007
3962
  """
4008
3963
  returns frame index for player player_idx
4009
3964
  """
4010
- estimated_frame_number = self.dw_player[player_idx].player.estimated_frame_number
3965
+
3966
+ if not sys.platform.startswith(cfg.MACOS_CODE):
3967
+ estimated_frame_number = self.dw_player[player_idx].player.estimated_frame_number
3968
+ else:
3969
+ estimated_frame_number = observation_operations.send_command({"command": ["get_property", "estimated_frame_number"]})
4011
3970
  if estimated_frame_number is not None:
4012
3971
  return estimated_frame_number
4013
3972
  else:
@@ -4340,13 +4299,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4340
4299
 
4341
4300
  ct0 = cumulative_time_pos
4342
4301
 
4343
- if self.dw_player[0].player.time_pos is not None:
4344
- for n_player in range(1, len(self.dw_player)):
4345
- ct = self.getLaps(n_player=n_player)
4302
+ if not sys.platform.startswith(cfg.MACOS_CODE):
4303
+ if self.dw_player[0].player.time_pos is not None:
4304
+ for n_player in range(1, len(self.dw_player)):
4305
+ ct = self.getLaps(n_player=n_player)
4346
4306
 
4347
- # sync players 2..8 if time diff >= 1 s
4348
- if abs(ct0 - (ct + dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OFFSET][str(n_player + 1)]))) >= 1:
4349
- self.sync_time(n_player, ct0) # self.seek_mediaplayer(ct0, n_player)
4307
+ # sync players 2..8 if time diff >= 1 s
4308
+ if (
4309
+ abs(ct0 - (ct + dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OFFSET][str(n_player + 1)])))
4310
+ >= 1
4311
+ ):
4312
+ self.sync_time(n_player, ct0) # self.seek_mediaplayer(ct0, n_player)
4350
4313
 
4351
4314
  currentTimeOffset = dec(cumulative_time_pos + self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET])
4352
4315
 
@@ -4743,9 +4706,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4743
4706
 
4744
4707
  if self.playerType == cfg.MEDIA:
4745
4708
  # cumulative time
4746
- mem_laps = sum(self.dw_player[n_player].media_durations[0 : self.dw_player[n_player].player.playlist_pos]) + (
4747
- 0 if self.dw_player[n_player].player.time_pos is None else self.dw_player[n_player].player.time_pos * 1000
4748
- )
4709
+ if not sys.platform.startswith(cfg.MACOS_CODE):
4710
+ mem_laps = sum(self.dw_player[n_player].media_durations[0 : self.dw_player[n_player].player.playlist_pos]) + (
4711
+ 0 if self.dw_player[n_player].player.time_pos is None else self.dw_player[n_player].player.time_pos * 1000
4712
+ )
4713
+ else:
4714
+ time_pos = observation_operations.send_command({"command": ["get_property", "time-pos"]})
4715
+ # TODO: fix!
4716
+ return dec(time_pos)
4749
4717
 
4750
4718
  return dec(str(round(mem_laps / 1000, 3)))
4751
4719
 
@@ -5780,9 +5748,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5780
5748
  )
5781
5749
  return
5782
5750
 
5783
- import importlib
5784
-
5785
- print(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
5751
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
5786
5752
 
5787
5753
  plugin_name = self.sender().text()
5788
5754
  if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
@@ -5790,7 +5756,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5790
5756
  return
5791
5757
 
5792
5758
  plugin_path = self.config_param.get(cfg.ANALYSIS_PLUGINS, {})[plugin_name]
5793
- print(f"{plugin_path=}")
5759
+
5760
+ logging.debug(f"{plugin_path=}")
5761
+
5794
5762
  if not pl.Path(plugin_path).is_file():
5795
5763
  QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
5796
5764
  return
@@ -5801,56 +5769,45 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5801
5769
 
5802
5770
  spec = importlib.util.spec_from_file_location(module_name, plugin_path)
5803
5771
  plugin_module = importlib.util.module_from_spec(spec)
5804
- print(f"{plugin_module=}")
5772
+
5773
+ logging.debug(f"{plugin_module=}")
5774
+
5805
5775
  spec.loader.exec_module(plugin_module)
5806
5776
 
5807
- print(
5777
+ logging.info(
5808
5778
  f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
5809
5779
  )
5810
5780
 
5811
- """
5812
- plugins_dir = pl.Path(__file__).parent / "analysis_plugins"
5813
-
5814
- print(f"{plugins_dir=}")
5815
-
5816
- module_path = f"{plugins_dir.name}.{plugin}"
5817
-
5818
- print(f"{module_path=}")
5819
-
5820
- try:
5821
- plugin_module = importlib.import_module(module_path)
5822
- print(f"{plugin} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}")
5823
- except Exception:
5824
- QMessageBox.critical(self, cfg.programName, f"Error loding the plugin {plugin}")
5825
- return
5826
-
5827
- print(f"{plugin_module=}")
5828
- """
5829
-
5830
5781
  selected_observations, parameters = self.obs_param()
5831
5782
  if not selected_observations:
5832
5783
  return
5833
5784
 
5834
5785
  df = project_functions.project2dataframe(self.pj, selected_observations)
5835
5786
 
5836
- print(f"{df.head()=}")
5787
+ logging.debug("dataframe info")
5788
+ logging.debug(f"{df.info()}")
5789
+ logging.debug(f"{df.head()}")
5837
5790
 
5838
- df_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5839
-
5840
- from . import view_df
5791
+ df_results, str_results = plugin_module.main(df, observations_list=selected_observations, parameters=parameters)
5841
5792
 
5842
5793
  self.view_dataframe = view_df.View_df(
5843
5794
  self.sender().text(), f"{plugin_module.__version__} ({plugin_module.__version_date__})", df_results
5844
5795
  )
5845
5796
  self.view_dataframe.show()
5846
5797
 
5847
- # print(f"{results=}")
5798
+ if str_results:
5799
+ self.results = dialog.Results_dialog()
5800
+ self.results.setWindowTitle(self.sender().text())
5801
+ self.results.ptText.clear()
5802
+ self.results.ptText.appendPlainText(str_results)
5803
+ self.results.show()
5848
5804
 
5849
5805
 
5850
5806
  def main():
5851
5807
  # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
5852
5808
 
5853
5809
  app = QApplication(sys.argv)
5810
+ app.setStyle("Fusion")
5854
5811
 
5855
5812
  locale.setlocale(locale.LC_NUMERIC, "C")
5856
5813
 
@@ -5884,9 +5841,6 @@ def main():
5884
5841
 
5885
5842
  window = MainWindow(ffmpeg_bin)
5886
5843
 
5887
- # if window.config_param.get(cfg.DARK_MODE, cfg.DARK_MODE_DEFAULT_VALUE):
5888
- # app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="PySide6"))
5889
-
5890
5844
  # open project/start observation on command line
5891
5845
 
5892
5846
  project_to_open: str = ""
@@ -5916,6 +5870,20 @@ def main():
5916
5870
  QMessageBox.information(window, cfg.programName, msg)
5917
5871
  window.load_project(project_path, project_changed, pj)
5918
5872
 
5873
+ # check project integrity
5874
+ msg = project_functions.check_project_integrity(
5875
+ pj,
5876
+ "S",
5877
+ project_path,
5878
+ media_file_available=True,
5879
+ )
5880
+ if msg:
5881
+ results = dialog.Results_dialog()
5882
+ results.setWindowTitle("Project integrity results")
5883
+ results.ptText.clear()
5884
+ results.ptText.appendHtml(f"Some issues were found in the project<br><br>{msg}")
5885
+ results.show()
5886
+
5919
5887
  window.show()
5920
5888
  window.raise_()
5921
5889
 
boris/db_functions.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2024 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
 
7
7
  This program is free software; you can redistribute it and/or modify