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/dialog.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,17 +20,18 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
+ import datetime as dt
24
+ from decimal import Decimal as dec
23
25
  import logging
24
- import sys
26
+ import math
25
27
  import pathlib as pl
26
- import traceback
27
28
  import platform
28
- import datetime as dt
29
- import subprocess
30
- from decimal import Decimal as dec
29
+ import sys
30
+ import traceback
31
+ from typing import Union
31
32
 
32
- from PyQt5.QtCore import Qt, pyqtSignal, QT_VERSION_STR, PYQT_VERSION_STR
33
- from PyQt5.QtWidgets import (
33
+ from PySide6.QtCore import Qt, Signal, qVersion, QRect, QTime, QDateTime, QSize
34
+ from PySide6.QtWidgets import (
34
35
  QApplication,
35
36
  QAbstractItemView,
36
37
  QCheckBox,
@@ -53,16 +54,32 @@ from PyQt5.QtWidgets import (
53
54
  QTableWidget,
54
55
  QVBoxLayout,
55
56
  QWidget,
57
+ QDateTimeEdit,
58
+ QTimeEdit,
59
+ QAbstractSpinBox,
60
+ QRadioButton,
61
+ QStackedWidget,
62
+ QFrame,
56
63
  )
57
- from PyQt5.QtGui import QFont, QTextCursor
64
+ from PySide6.QtGui import QFont, QTextCursor
58
65
 
59
66
  from . import config as cfg
60
- from . import duration_widget
61
67
  from . import version
68
+ from . import utilities as util
62
69
 
63
70
 
64
71
  def MessageDialog(title: str, text: str, buttons: tuple) -> str:
72
+ """
73
+ show a generic message dialog and returns the text of the clicked button
65
74
 
75
+ Args:
76
+ title (str): Title of the dialog box
77
+ text (str): text of the dialog box
78
+ buttons (tuple): text for buttons
79
+
80
+ Return
81
+ str: text of the clicked button
82
+ """
66
83
  message = QMessageBox()
67
84
  message.setWindowTitle(title)
68
85
  message.setText(text)
@@ -70,133 +87,61 @@ def MessageDialog(title: str, text: str, buttons: tuple) -> str:
70
87
  for button in buttons:
71
88
  message.addButton(button, QMessageBox.YesRole)
72
89
 
73
- # message.setWindowFlags(Qt.WindowStaysOnTopHint)
74
- message.exec_()
90
+ message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
91
+ message.exec()
75
92
  return message.clickedButton().text()
76
93
 
77
94
 
78
- '''
79
- def error_message_box(task, error_type, error_file_name, error_lineno):
80
- # do NOT use this function directly, use error_message function
81
- """
82
- show a critical dialog
83
-
84
- """
85
- QMessageBox.critical(None, cfg.programName, (f"BORIS version: {version.__version__}<br>"
86
- f"An error occured during the execution of <b>{task}</b>.<br>"
87
- f"Error: {error_type}<br>"
88
- f"in {error_file_name} "
89
- f"at line # {error_lineno}<br><br>"
90
- "to improve the software please report this problem at:<br>"
91
- '<a href="https://github.com/olivierfriard/BORIS/issues">'
92
- 'https://github.com/olivierfriard/BORIS/issues</a><br>'
93
- "or by email (See the About page on the BORIS web site.<br><br>"
94
- "Thank you for your collaboration!"))
95
- '''
96
-
97
-
98
- def error_message_box(error_traceback):
99
- # do NOT use this function directly, use error_message function
100
- """
101
- show a critical dialog
102
-
103
- """
104
- QMessageBox.critical(
105
- None,
106
- cfg.programName,
107
- (
108
- f"BORIS version: {version.__version__}<br><br>"
109
- f"<b>An error has occured</b>:<br>"
110
- f"{error_traceback}<br><br>"
111
- "to improve the software please report this problem at:<br>"
112
- '<a href="https://github.com/olivierfriard/BORIS/issues">'
113
- "https://github.com/olivierfriard/BORIS/issues</a><br>"
114
- "or by email (See the About page on the BORIS web site.<br><br>"
115
- "Thank you for your collaboration!"
116
- ),
117
- )
118
-
119
-
120
- def error_message() -> None:
121
- """
122
- Show details about the error in a message box
123
- write entry to log as CRITICAL
124
- """
125
-
126
- error_traceback = traceback.format_exc().replace("Traceback (most recent call last):", "").replace("\n", " ")
127
-
128
- logging.critical(error_traceback)
129
- error_message_box(error_traceback)
130
-
131
-
132
95
  def global_error_message(exception_type, exception_value, traceback_object):
133
96
  """
134
- global error management
97
+ Global error management
135
98
  save error using loggin.critical and stdout
136
99
  """
137
100
 
138
- error_text: str = (
139
- f"BORIS version: {version.__version__}\n"
140
- f"OS: {platform.uname().system} {platform.uname().release} {platform.uname().version}\n"
141
- f"CPU: {platform.uname().machine} {platform.uname().processor}\n"
142
- f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})\n"
143
- f"Qt {QT_VERSION_STR} - PyQt {PYQT_VERSION_STR}\n"
144
- f"{dt.datetime.now():%Y-%m-%d %H:%M}\n\n"
145
- )
101
+ error_text = "\n\nSystem info\n===========\n\n"
102
+ error_text += util.get_systeminfo()
103
+ error_text += f"Error succeded at {dt.datetime.now():%Y-%m-%d %H:%M}\n\n"
146
104
  error_text += "".join(traceback.format_exception(exception_type, exception_value, traceback_object))
147
105
 
148
- # system info
149
- if sys.platform.startswith("win"):
150
- systeminfo = subprocess.getoutput("systeminfo")
151
- if sys.platform.startswith("linux"):
152
- systeminfo = subprocess.getoutput("cat /etc/*rel*; uname -a")
153
-
106
+ # write to stdout
154
107
  logging.critical(error_text)
155
- logging.critical(systeminfo)
108
+
109
+ # write to $HOME/boris_error.log
110
+ try:
111
+ with open(pl.Path.home() / "boris_error.log", "w") as f_error:
112
+ f_error.write(error_text)
113
+ except Exception:
114
+ logging.critical(f"Impossible to write to {pl.Path.home() / 'boris_error.log'}")
156
115
 
157
116
  # copy to clipboard
158
117
  cb = QApplication.clipboard()
159
- cb.clear(mode=cb.Clipboard)
160
- cb.setText(error_text + "\nSystem info:\n" + systeminfo, mode=cb.Clipboard)
161
-
162
- error_text: str = error_text.replace("\r\n", "\n").replace("\n", "<br>")
118
+ cb.clear()
119
+ cb.setText(error_text)
163
120
 
164
121
  text: str = (
165
- f"<b>An error has occured</b><br><br>"
166
- "to improve the software please report this problem at:<br>"
167
- '<a href="https://github.com/olivierfriard/BORIS/issues">'
168
- "https://github.com/olivierfriard/BORIS/issues</a><br>"
169
- "Please no screenshot, the error message was copied to the clipboard.<br><br>"
170
- "Thank you for your collaboration!"
171
- "<br><br>"
172
- "<pre>"
122
+ f"An error has occured!\n\n"
123
+ "to improve the software please report this problem at:\n"
124
+ "https://github.com/olivierfriard/BORIS/issues\n\n"
125
+ "Please no screenshot, the error message was copied to the clipboard.\n\n"
126
+ "Thank you for your collaboration!\n\n"
173
127
  f"{error_text}"
174
- "<hr>"
175
- "<b>System info</b><br>"
176
- f"{systeminfo}"
177
- "</pre>"
178
128
  )
179
129
 
180
130
  errorbox = Results_dialog()
131
+
181
132
  errorbox.setWindowTitle("BORIS - An error occured")
182
133
  errorbox.pbOK.setText("Abort")
183
134
  errorbox.pbCancel.setVisible(True)
184
135
  errorbox.pbCancel.setText("Ignore and try to continue")
185
136
 
137
+ font = QFont()
138
+ font.setFamily("monospace")
139
+ errorbox.ptText.setFont(font)
186
140
  errorbox.ptText.clear()
187
- errorbox.ptText.appendHtml(text)
141
+ errorbox.ptText.appendPlainText(text)
188
142
 
189
143
  errorbox.ptText.moveCursor(QTextCursor.Start)
190
144
 
191
- """
192
- errorbox = QMessageBox()
193
- errorbox.setWindowTitle("BORIS error occured")
194
- errorbox.setText(text)
195
- errorbox.setTextFormat(Qt.RichText)
196
- errorbox.setStandardButtons(QMessageBox.Abort)
197
- _ = errorbox.addButton("Ignore and try to continue", QMessageBox.RejectRole)
198
- """
199
-
200
145
  ret = errorbox.exec_()
201
146
 
202
147
  if ret == 1: # Abort
@@ -216,6 +161,319 @@ class Info_widget(QWidget):
216
161
  self.setLayout(layout)
217
162
 
218
163
 
164
+ class get_time_widget(QWidget):
165
+ """
166
+ widget for selecting a time in various formats: seconds, HH:MM:SS:ZZZ or YYYY-mm-DD HH:MM:SS:ZZZ
167
+ """
168
+
169
+ def __init__(self, time_value=dec(0), parent=None):
170
+ super().__init__(parent)
171
+
172
+ self.setWindowTitle("BORIS")
173
+
174
+ self.widget = QWidget()
175
+ self.widget.setObjectName("widget")
176
+ self.widget.setGeometry(QRect(130, 220, 302, 63))
177
+ self.verticalLayout_3 = QVBoxLayout(self.widget)
178
+ self.verticalLayout_3.setObjectName("verticalLayout_3")
179
+ self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
180
+ self.horizontalLayout_3 = QHBoxLayout()
181
+ self.horizontalLayout_3.setObjectName("horizontalLayout_3")
182
+ self.verticalLayout_2 = QVBoxLayout()
183
+ self.verticalLayout_2.setObjectName("verticalLayout_2")
184
+ self.label = QLabel(self.widget)
185
+ self.label.setObjectName("label")
186
+
187
+ self.verticalLayout_2.addWidget(self.label)
188
+
189
+ self.pb_sign = QPushButton(self.widget)
190
+ self.pb_sign.setObjectName("pb_sign")
191
+ self.pb_sign.setMaximumSize(QSize(40, 16777215))
192
+
193
+ self.verticalLayout_2.addWidget(self.pb_sign)
194
+
195
+ self.horizontalLayout_3.addLayout(self.verticalLayout_2)
196
+
197
+ self.verticalLayout = QVBoxLayout()
198
+ self.verticalLayout.setObjectName("verticalLayout")
199
+ self.horizontalLayout = QHBoxLayout()
200
+ self.horizontalLayout.setObjectName("horizontalLayout")
201
+ self.rb_seconds = QRadioButton(self.widget)
202
+ self.rb_seconds.setObjectName("rb_seconds")
203
+
204
+ self.horizontalLayout.addWidget(self.rb_seconds)
205
+
206
+ self.rb_time = QRadioButton(self.widget)
207
+ self.rb_time.setObjectName("rb_time")
208
+
209
+ self.horizontalLayout.addWidget(self.rb_time)
210
+
211
+ self.rb_datetime = QRadioButton(self.widget)
212
+ self.rb_datetime.setObjectName("rb_datetime")
213
+
214
+ self.horizontalLayout.addWidget(self.rb_datetime)
215
+
216
+ self.horizontalLayout.addStretch()
217
+
218
+ self.verticalLayout.addLayout(self.horizontalLayout)
219
+
220
+ self.stackedWidget = QStackedWidget(self.widget)
221
+ self.stackedWidget.setObjectName("stackedWidget")
222
+ self.stackedWidget.setFrameShape(QFrame.NoFrame)
223
+ self.seconds = QWidget()
224
+ self.seconds.setObjectName("seconds")
225
+ self.widget1 = QWidget(self.seconds)
226
+ self.widget1.setObjectName("widget1")
227
+ # self.widget1.setGeometry(QRect(10, 0, 163, 27))
228
+ self.horizontalLayout_4 = QHBoxLayout(self.widget1)
229
+ self.horizontalLayout_4.setObjectName("horizontalLayout_4")
230
+ self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
231
+ self.le_seconds = QLineEdit(self.widget1)
232
+ self.le_seconds.setObjectName("le_seconds")
233
+
234
+ self.horizontalLayout_4.addWidget(self.le_seconds)
235
+
236
+ self.lb_seconds = QLabel(self.widget1)
237
+ self.lb_seconds.setObjectName("lb_seconds")
238
+
239
+ self.horizontalLayout_4.addWidget(self.lb_seconds)
240
+
241
+ self.horizontalLayout_4.addStretch()
242
+
243
+ self.stackedWidget.addWidget(self.seconds)
244
+ self.hhmmss = QWidget()
245
+ self.hhmmss.setObjectName("hhmmss")
246
+ self.widget2 = QWidget(self.hhmmss)
247
+ self.widget2.setMinimumWidth(500)
248
+ self.widget2.setObjectName("widget2")
249
+ self.widget2.setGeometry(QRect(0, 0, 213, 28))
250
+ self.horizontalLayout_2 = QHBoxLayout(self.widget2)
251
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
252
+ self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
253
+ self.lb_hour = QLabel(self.widget2)
254
+ self.lb_hour.setObjectName("lb_hour")
255
+
256
+ self.horizontalLayout_2.addWidget(self.lb_hour)
257
+
258
+ self.sb_hour = QSpinBox(self.widget2)
259
+ self.sb_hour.setObjectName("sb_hour")
260
+ self.sb_hour.setButtonSymbols(QAbstractSpinBox.NoButtons)
261
+
262
+ self.horizontalLayout_2.addWidget(self.sb_hour)
263
+
264
+ self.lb_hhmmss = QLabel(self.widget2)
265
+ self.lb_hhmmss.setObjectName("lb_hhmmss")
266
+
267
+ self.horizontalLayout_2.addWidget(self.lb_hhmmss)
268
+
269
+ self.te_time = QTimeEdit(self.widget2)
270
+ self.te_time.setObjectName("te_time")
271
+ self.te_time.adjustSize()
272
+ # self.te_time.setMinimumWidth(200)
273
+ # self.widget2.adjustSize()
274
+
275
+ self.horizontalLayout_2.addWidget(self.te_time)
276
+
277
+ self.horizontalLayout_2.addStretch()
278
+
279
+ self.stackedWidget.addWidget(self.hhmmss)
280
+ self.page_2 = QWidget()
281
+ self.page_2.setObjectName("page_2")
282
+ self.dte = QDateTimeEdit(self.page_2)
283
+
284
+ self.dte.setObjectName("dte")
285
+ # self.dte.setGeometry(QRect(10, 0, 164, 26))
286
+ self.stackedWidget.addWidget(self.page_2)
287
+
288
+ self.verticalLayout.addWidget(self.stackedWidget)
289
+
290
+ self.horizontalLayout_3.addLayout(self.verticalLayout)
291
+
292
+ self.verticalLayout_3.addLayout(self.horizontalLayout_3)
293
+
294
+ self.line = QFrame(self.widget)
295
+ self.line.setObjectName("line")
296
+ self.line.setFrameShape(QFrame.HLine)
297
+ self.line.setFrameShadow(QFrame.Sunken)
298
+
299
+ self.verticalLayout_3.addWidget(self.line)
300
+
301
+ self.setLayout(self.verticalLayout_3)
302
+
303
+ self.stackedWidget.setCurrentIndex(0)
304
+
305
+ self.label.setText("")
306
+ self.pb_sign.setText("+")
307
+ self.rb_seconds.setText("Seconds")
308
+ self.rb_time.setText("hh:mm:ss")
309
+ self.rb_datetime.setText("Date time")
310
+ self.le_seconds.setText("")
311
+ self.lb_seconds.setText("seconds")
312
+ self.lb_hour.setText("hour")
313
+ self.lb_hhmmss.setText("mm:ss.ms")
314
+ self.te_time.setDisplayFormat("mm:ss.zzz")
315
+ self.dte.setDisplayFormat("yyyy-MM-dd hh:mm:ss:zzz")
316
+ self.dte.adjustSize()
317
+ font = QFont()
318
+ font.setPointSize(14)
319
+ self.pb_sign.setFont(font)
320
+
321
+ self.rb_seconds.toggled.connect(self.format_changed)
322
+ self.rb_time.toggled.connect(self.format_changed)
323
+ self.rb_datetime.toggled.connect(self.format_changed)
324
+ self.sb_hour.setMaximum(cfg.HOUR_CUTOFF)
325
+ self.pb_sign.clicked.connect(self.pb_sign_clicked)
326
+
327
+ if time_value:
328
+ self.set_time(time_value)
329
+
330
+ self.format_changed()
331
+
332
+ self.adjustSize()
333
+
334
+ def format_changed(self):
335
+ if self.rb_seconds.isChecked():
336
+ self.stackedWidget.setCurrentIndex(0)
337
+ if self.rb_time.isChecked():
338
+ self.stackedWidget.setCurrentIndex(1)
339
+ if self.rb_datetime.isChecked():
340
+ self.stackedWidget.setCurrentIndex(2)
341
+
342
+ self.le_seconds.setEnabled(self.rb_seconds.isChecked())
343
+ self.le_seconds.adjustSize()
344
+ self.lb_seconds.setEnabled(self.rb_seconds.isChecked())
345
+ self.sb_hour.setEnabled(self.rb_time.isChecked())
346
+ self.te_time.setEnabled(self.rb_time.isChecked())
347
+ self.lb_hour.setEnabled(self.rb_time.isChecked())
348
+ self.lb_hhmmss.setEnabled(self.rb_time.isChecked())
349
+ self.dte.setEnabled(self.rb_datetime.isChecked())
350
+
351
+ def pb_sign_clicked(self):
352
+ if self.pb_sign.text() == "+":
353
+ self.pb_sign.setText("-")
354
+ else:
355
+ self.pb_sign.setText("+")
356
+
357
+ def get_time(self) -> Union[dec, None]:
358
+ """
359
+ Get time from the selected format in the time widget
360
+
361
+ Returns:
362
+ dec: time in seconds (None if no format selected)
363
+ """
364
+
365
+ time_sec = dec("NaN")
366
+
367
+ if self.rb_seconds.isChecked():
368
+ try:
369
+ time_sec = float(self.le_seconds.text())
370
+ except Exception:
371
+ QMessageBox.warning(
372
+ None,
373
+ cfg.programName,
374
+ f"The value of seconds ({self.le_seconds.text()}) is not a decimal number",
375
+ QMessageBox.Ok | QMessageBox.Default,
376
+ QMessageBox.NoButton,
377
+ )
378
+ return dec("NaN")
379
+
380
+ if self.rb_time.isChecked():
381
+ time_sec = self.sb_hour.value() * 3600
382
+ time_sec += self.te_time.time().msecsSinceStartOfDay() / 1000
383
+
384
+ if self.rb_datetime.isChecked():
385
+ time_sec = self.dte.dateTime().toMSecsSinceEpoch() / 1000
386
+
387
+ if self.pb_sign.text() == "-":
388
+ time_sec = -time_sec
389
+
390
+ return dec(time_sec).quantize(dec("0.001")) # if time_sec is not None else None
391
+
392
+ def set_time(self, new_time: dec) -> None:
393
+ """
394
+ set time on time widget
395
+ """
396
+
397
+ if math.isnan(new_time):
398
+ return
399
+
400
+ self.pb_sign.setText("-" if new_time < 0 else "+")
401
+
402
+ # seconds
403
+ self.le_seconds.setText(f"{new_time:0.3f}")
404
+
405
+ if new_time <= cfg.DATE_CUTOFF: # hh:mm:ss.zzz
406
+ h = int(abs(new_time) // 3600)
407
+ m = int((abs(new_time) - h * 3600) // 60)
408
+ s = int((abs(new_time) - h * 3600 - m * 60))
409
+ ms = round((abs(new_time) - h * 3600 - m * 60 - s) * 1000)
410
+
411
+ self.sb_hour.setValue(h)
412
+ self.te_time.setTime(QTime(0, m, s, ms))
413
+ else:
414
+ self.sb_hour.setValue(0)
415
+ self.te_time.setTime(QTime(0, 0, 0, 0))
416
+
417
+ self.dte.setDateTime(QDateTime().fromMSecsSinceEpoch(int(new_time * 1000)))
418
+ self.rb_datetime.setChecked(True)
419
+
420
+
421
+ class Ask_time(QDialog):
422
+ """
423
+ Qdialog class for asking time to user
424
+ User can select a time format between seconds, HHMMSS.zzz or YYY-mm-DD HH:MM:SS.zzz
425
+ """
426
+
427
+ def __init__(self, time_value=0):
428
+ super().__init__()
429
+ self.setWindowTitle("")
430
+
431
+ hbox = QVBoxLayout(self)
432
+ self.label = QLabel()
433
+ self.label.setText("")
434
+ hbox.addWidget(self.label)
435
+
436
+ self.time_widget = get_time_widget(time_value)
437
+
438
+ hbox.addWidget(self.time_widget)
439
+
440
+ self.pbOK = QPushButton(cfg.OK, clicked=self.pb_ok_clicked)
441
+ self.pbOK.setDefault(True)
442
+
443
+ self.pbCancel = QPushButton(cfg.CANCEL)
444
+ self.pbCancel.clicked.connect(self.reject)
445
+
446
+ self.hbox2 = QHBoxLayout(self)
447
+ self.hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
448
+ self.hbox2.addWidget(self.pbCancel)
449
+ self.hbox2.addWidget(self.pbOK)
450
+ hbox.addLayout(self.hbox2)
451
+ self.setLayout(hbox)
452
+
453
+ # if time_value:
454
+ # self.time_widget.set_time(time_value)
455
+
456
+ def pb_ok_clicked(self):
457
+ if (
458
+ not self.time_widget.rb_seconds.isChecked()
459
+ and not self.time_widget.rb_time.isChecked()
460
+ and not self.time_widget.rb_datetime.isChecked()
461
+ ):
462
+ QMessageBox.warning(
463
+ None,
464
+ cfg.programName,
465
+ "Select an option",
466
+ QMessageBox.Ok | QMessageBox.Default,
467
+ QMessageBox.NoButton,
468
+ )
469
+ return
470
+ # test time value
471
+ if self.time_widget.get_time().is_nan():
472
+ return
473
+
474
+ self.accept()
475
+
476
+
219
477
  class Video_overlay_dialog(QDialog):
220
478
  """
221
479
  dialog to ask image file and position for video overlay
@@ -270,8 +528,7 @@ class Video_overlay_dialog(QDialog):
270
528
  self.setLayout(vlayout)
271
529
 
272
530
  def browse(self):
273
- fn = QFileDialog().getOpenFileName(self, "Choose an image file", "", "PNG files (*.png);;All files (*)")
274
- file_name = fn[0] if type(fn) is tuple else fn
531
+ file_name, _ = QFileDialog.getOpenFileName(self, "Choose an image file", "", "PNG files (*.png);;All files (*)")
275
532
  if file_name:
276
533
  self.le_file_path.setText(file_name)
277
534
 
@@ -315,7 +572,8 @@ class Input_dialog(QDialog):
315
572
  dialog for user input. Elements can be:
316
573
  checkbox (cb): Tuple(str, str, bool)
317
574
  lineedit (le): Tuple(str, str)
318
- spinbox (sp)
575
+ spinbox (sb)
576
+ doubleSpinbox (dsb)
319
577
  items list (il)
320
578
 
321
579
  """
@@ -332,7 +590,6 @@ class Input_dialog(QDialog):
332
590
 
333
591
  self.elements: dict = {}
334
592
  for element in elements_list:
335
-
336
593
  if element[0] == "cb": # checkbox
337
594
  self.elements[element[1]] = QCheckBox(element[1])
338
595
  self.elements[element[1]].setChecked(element[2])
@@ -365,7 +622,7 @@ class Input_dialog(QDialog):
365
622
  # 3 - maximum value
366
623
  # 4 - step
367
624
  # 5 - initial value
368
- # 6 - number of decimas
625
+ # 6 - number of decimals
369
626
 
370
627
  lb = QLabel(element[1])
371
628
  hbox.addWidget(lb)
@@ -384,15 +641,15 @@ class Input_dialog(QDialog):
384
641
  self.elements[element[1]] = QComboBox()
385
642
  self.elements[element[1]].addItems([x[0] for x in element[2]]) # take first element of tuple
386
643
  try:
387
- self.elements[element[1]].setCurrentIndex(
388
- [idx for idx, x in enumerate(element[2]) if x[1] == "selected"][0]
389
- )
390
- except:
644
+ self.elements[element[1]].setCurrentIndex([idx for idx, x in enumerate(element[2]) if x[1] == "selected"][0])
645
+ except Exception:
391
646
  self.elements[element[1]].setCurrentIndex(0)
392
647
  hbox.addWidget(self.elements[element[1]])
393
648
 
394
649
  hbox2 = QHBoxLayout()
395
650
 
651
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
652
+
396
653
  self.pbCancel = QPushButton(cfg.CANCEL)
397
654
  self.pbCancel.clicked.connect(self.reject)
398
655
  hbox2.addWidget(self.pbCancel)
@@ -406,14 +663,13 @@ class Input_dialog(QDialog):
406
663
  self.setLayout(hbox)
407
664
 
408
665
 
409
- class DuplicateBehaviorCode(QDialog):
666
+ class Duplicate_items(QDialog):
410
667
  """
411
- let user show between behaviors that are coded by same key
668
+ let user show between behaviors/subjects that are coded by same key
412
669
  """
413
670
 
414
671
  def __init__(self, text, codes_list):
415
-
416
- super(DuplicateBehaviorCode, self).__init__()
672
+ super(Duplicate_items, self).__init__()
417
673
 
418
674
  self.setWindowTitle(cfg.programName)
419
675
  self.setWindowFlags(Qt.WindowStaysOnTopHint)
@@ -437,13 +693,17 @@ class DuplicateBehaviorCode(QDialog):
437
693
 
438
694
  Vlayout.addWidget(self.lw)
439
695
 
440
- pbCancel = QPushButton("Cancel")
441
- pbCancel.clicked.connect(self.reject)
442
- Vlayout.addWidget(pbCancel)
443
- pbOK = QPushButton("OK")
696
+ hlayout = QHBoxLayout()
697
+ hlayout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
698
+
699
+ pbCancel = QPushButton("Cancel", clicked=self.reject)
700
+ hlayout.addWidget(pbCancel)
701
+
702
+ pbOK = QPushButton("OK", clicked=self.accept)
444
703
  pbOK.setDefault(True)
445
- pbOK.clicked.connect(self.pbOK_clicked)
446
- Vlayout.addWidget(pbOK)
704
+ hlayout.addWidget(pbOK)
705
+
706
+ Vlayout.addLayout(hlayout)
447
707
 
448
708
  self.setLayout(Vlayout)
449
709
 
@@ -457,9 +717,8 @@ class DuplicateBehaviorCode(QDialog):
457
717
  """
458
718
  if self.lw.selectedItems():
459
719
  return self.lw.selectedItems()[0].text()
460
-
461
- def pbOK_clicked(self):
462
- self.accept()
720
+ else:
721
+ return None
463
722
 
464
723
 
465
724
  class ChooseObservationsToImport(QDialog):
@@ -468,7 +727,6 @@ class ChooseObservationsToImport(QDialog):
468
727
  """
469
728
 
470
729
  def __init__(self, text, observations_list):
471
-
472
730
  super(ChooseObservationsToImport, self).__init__()
473
731
 
474
732
  self.setWindowTitle(cfg.programName)
@@ -516,50 +774,13 @@ class ChooseObservationsToImport(QDialog):
516
774
  return [item.text() for item in self.lw.selectedItems()]
517
775
 
518
776
 
519
- class Ask_time(QDialog):
520
- """
521
- "Ask time" dialog box using duration widget in duration_widget module
522
- """
523
-
524
- def __init__(self, time_format):
525
- super().__init__()
526
- hbox = QVBoxLayout(self)
527
- self.label = QLabel()
528
- self.label.setText("Go to time")
529
- hbox.addWidget(self.label)
530
-
531
- self.time_widget = duration_widget.Duration_widget()
532
- if time_format == cfg.HHMMSS:
533
- self.time_widget.set_format_hhmmss()
534
- if time_format == cfg.S:
535
- self.time_widget.set_format_s()
536
-
537
- hbox.addWidget(self.time_widget)
538
-
539
- self.pbOK = QPushButton("OK")
540
- self.pbOK.clicked.connect(self.accept)
541
- self.pbOK.setDefault(True)
542
-
543
- self.pbCancel = QPushButton("Cancel")
544
- self.pbCancel.clicked.connect(self.reject)
545
-
546
- self.hbox2 = QHBoxLayout(self)
547
- self.hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
548
- self.hbox2.addWidget(self.pbCancel)
549
- self.hbox2.addWidget(self.pbOK)
550
- hbox.addLayout(self.hbox2)
551
- self.setLayout(hbox)
552
- self.setWindowTitle("Time")
553
-
554
-
555
777
  class FindInEvents(QWidget):
556
778
  """
557
779
  "find in events" dialog box
558
780
  """
559
781
 
560
- clickSignal = pyqtSignal(str)
561
-
562
- currentIdx = -1
782
+ clickSignal = Signal(str)
783
+ currentIdx: int = -1
563
784
 
564
785
  def __init__(self):
565
786
  super().__init__()
@@ -602,9 +823,10 @@ class FindInEvents(QWidget):
602
823
  hbox.addWidget(self.lb_message)
603
824
 
604
825
  hbox2 = QHBoxLayout()
826
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
605
827
  self.pbOK = QPushButton("Find")
606
828
  self.pbOK.clicked.connect(lambda: self.click("FIND"))
607
- self.pbCancel = QPushButton("Close")
829
+ self.pbCancel = QPushButton(cfg.CLOSE)
608
830
  self.pbCancel.clicked.connect(lambda: self.click("CLOSE"))
609
831
  hbox2.addWidget(self.pbCancel)
610
832
  hbox2.addWidget(self.pbOK)
@@ -621,10 +843,7 @@ class FindReplaceEvents(QWidget):
621
843
  "find replace events" dialog box
622
844
  """
623
845
 
624
- clickSignal = pyqtSignal(str)
625
- """
626
- sendEventSignal = pyqtSignal(QEvent)
627
- """
846
+ clickSignal = Signal(str)
628
847
 
629
848
  def __init__(self):
630
849
  super().__init__()
@@ -633,21 +852,9 @@ class FindReplaceEvents(QWidget):
633
852
 
634
853
  hbox = QVBoxLayout()
635
854
 
636
- self.cbSubject = QCheckBox("Subject")
637
- self.cbSubject.setChecked(False)
638
- hbox.addWidget(self.cbSubject)
639
-
640
- self.cbBehavior = QCheckBox("Behavior")
641
- self.cbBehavior.setChecked(False)
642
- hbox.addWidget(self.cbBehavior)
643
-
644
- self.cbModifier = QCheckBox("Modifiers")
645
- self.cbModifier.setChecked(False)
646
- hbox.addWidget(self.cbModifier)
647
-
648
- self.cbComment = QCheckBox("Comment")
649
- self.cbComment.setChecked(False)
650
- hbox.addWidget(self.cbComment)
855
+ self.combo_fields = QComboBox()
856
+ self.combo_fields.addItems(("Choose a field", "Subject", "Behavior", "Modifiers", "Comment"))
857
+ hbox.addWidget(self.combo_fields)
651
858
 
652
859
  self.lbFind = QLabel("Find")
653
860
  hbox.addWidget(self.lbFind)
@@ -661,7 +868,7 @@ class FindReplaceEvents(QWidget):
661
868
  self.replaceText = QLineEdit()
662
869
  hbox.addWidget(self.replaceText)
663
870
 
664
- self.cbFindInSelectedEvents = QCheckBox("Find/Replace in selected events")
871
+ self.cbFindInSelectedEvents = QCheckBox("Find/Replace only in selected events")
665
872
  self.cbFindInSelectedEvents.setChecked(False)
666
873
  hbox.addWidget(self.cbFindInSelectedEvents)
667
874
 
@@ -671,6 +878,8 @@ class FindReplaceEvents(QWidget):
671
878
 
672
879
  hbox2 = QHBoxLayout()
673
880
 
881
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
882
+
674
883
  self.pbCancel = QPushButton(cfg.CANCEL, clicked=lambda: self.click("CANCEL"))
675
884
  hbox2.addWidget(self.pbCancel)
676
885
 
@@ -681,68 +890,76 @@ class FindReplaceEvents(QWidget):
681
890
  hbox2.addWidget(self.pbFindReplaceAll)
682
891
 
683
892
  hbox.addLayout(hbox2)
893
+
894
+ self.lb_results = QLabel(" ")
895
+ hbox.addWidget(self.lb_results)
896
+
684
897
  self.setLayout(hbox)
685
898
 
686
899
  def click(self, msg):
687
900
  self.clickSignal.emit(msg)
688
901
 
689
902
 
690
- '''
691
- class explore_project_dialog(QDialog):
903
+ class Results_dialog(QDialog):
692
904
  """
693
- "explore project" dialog box
905
+ widget for visualizing text output in HTML
694
906
  """
695
907
 
696
908
  def __init__(self):
697
909
  super().__init__()
698
910
 
699
- self.setWindowTitle("Explore project")
700
-
701
- hbox = QVBoxLayout()
702
-
703
- hbox.addWidget(QLabel("Search in all observations"))
911
+ self.dataset = False
704
912
 
705
- self.lb_subject = QLabel("Subject")
706
- hbox.addWidget(self.lb_subject)
707
- self.find_subject = QLineEdit()
708
- hbox.addWidget(self.find_subject)
913
+ self.setWindowTitle("")
709
914
 
710
- self.lb_behav = QLabel("Behaviors")
711
- hbox.addWidget(self.lb_behav)
712
- self.find_behavior = QLineEdit()
713
- hbox.addWidget(self.find_behavior)
915
+ hbox = QVBoxLayout()
714
916
 
715
- self.lb_modifier = QLabel("Modifier")
716
- hbox.addWidget(self.lb_modifier)
717
- self.find_modifier = QLineEdit()
718
- hbox.addWidget(self.find_modifier)
917
+ self.lb = QLabel("")
918
+ hbox.addWidget(self.lb)
719
919
 
720
- self.lb_comment = QLabel("Comment")
721
- hbox.addWidget(self.lb_comment)
722
- self.find_comment = QLineEdit()
723
- hbox.addWidget(self.find_comment)
920
+ self.ptText = QPlainTextEdit()
921
+ self.ptText.setReadOnly(True)
922
+ hbox.addWidget(self.ptText)
724
923
 
725
- self.cb_case_sensitive = QCheckBox("Case sensitive")
726
- self.cb_case_sensitive.setChecked(False)
727
- hbox.addWidget(self.cb_case_sensitive)
924
+ hbox2 = QHBoxLayout()
925
+ hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
728
926
 
729
- self.lb_message = QLabel()
730
- hbox.addWidget(self.lb_message)
927
+ self.pbSave = QPushButton("Save results", clicked=self.save_results)
928
+ hbox2.addWidget(self.pbSave)
731
929
 
732
- hbox2 = QHBoxLayout()
733
- self.pbOK = QPushButton("Find")
734
- self.pbOK.clicked.connect(self.accept)
735
- self.pbCancel = QPushButton("Cancel")
736
- self.pbCancel.clicked.connect(self.reject)
930
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.reject)
737
931
  hbox2.addWidget(self.pbCancel)
932
+ self.pbCancel.setVisible(False)
933
+
934
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
738
935
  hbox2.addWidget(self.pbOK)
739
- hbox.addLayout(hbox2)
740
936
 
937
+ hbox.addLayout(hbox2)
741
938
  self.setLayout(hbox)
742
- '''
743
939
 
940
+ self.resize(800, 640)
744
941
 
745
- class Results_dialog(QDialog):
942
+ def save_results(self):
943
+ """
944
+ save content of self.ptText
945
+ """
946
+
947
+ if not self.dataset:
948
+ file_name, _ = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
949
+
950
+ if not file_name:
951
+ return
952
+ try:
953
+ with open(file_name, "w") as f:
954
+ f.write(self.ptText.toPlainText())
955
+ except Exception:
956
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
957
+
958
+ else:
959
+ self.done(cfg.SAVE_DATASET)
960
+
961
+
962
+ class Results_dialog_exit_code(QDialog):
746
963
  """
747
964
  widget for visualizing text output
748
965
  """
@@ -769,26 +986,30 @@ class Results_dialog(QDialog):
769
986
  self.pbSave = QPushButton("Save results", clicked=self.save_results)
770
987
  hbox2.addWidget(self.pbSave)
771
988
 
772
- self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.reject)
773
- hbox2.addWidget(self.pbCancel)
774
- self.pbCancel.setVisible(False)
989
+ self.pb1 = QPushButton("1", clicked=lambda: self.done_(1))
990
+ hbox2.addWidget(self.pb1)
775
991
 
776
- self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
777
- hbox2.addWidget(self.pbOK)
992
+ self.pb2 = QPushButton("2", clicked=lambda: self.done_(2))
993
+ hbox2.addWidget(self.pb2)
994
+
995
+ self.pb3 = QPushButton("3", clicked=lambda: self.done_(3))
996
+ hbox2.addWidget(self.pb3)
778
997
 
779
998
  hbox.addLayout(hbox2)
780
999
  self.setLayout(hbox)
781
1000
 
782
1001
  self.resize(800, 640)
783
1002
 
1003
+ def done_(self, status):
1004
+ self.done(status)
1005
+
784
1006
  def save_results(self):
785
1007
  """
786
1008
  save content of self.ptText
787
1009
  """
788
1010
 
789
1011
  if not self.dataset:
790
- fn = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
791
- file_name = fn[0] if type(fn) is tuple else fn
1012
+ file_name, _ = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
792
1013
 
793
1014
  if not file_name:
794
1015
  return
@@ -840,10 +1061,10 @@ class View_data(QDialog):
840
1061
 
841
1062
  hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
842
1063
 
843
- self.pbCancel = QPushButton("Cancel", clicked=self.reject)
1064
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.reject)
844
1065
  hbox2.addWidget(self.pbCancel)
845
1066
 
846
- self.pbOK = QPushButton("OK", clicked=self.accept)
1067
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
847
1068
  hbox2.addWidget(self.pbOK)
848
1069
 
849
1070
  vbox.addLayout(hbox2)
@@ -858,7 +1079,7 @@ class View_explore_project_results(QWidget):
858
1079
  widget for visualizing results of explore project
859
1080
  """
860
1081
 
861
- double_click_signal = pyqtSignal(str, int)
1082
+ double_click_signal = Signal(str, int)
862
1083
 
863
1084
  def __init__(self):
864
1085
  super().__init__()
@@ -876,14 +1097,12 @@ class View_explore_project_results(QWidget):
876
1097
  vbox.addWidget(self.tw)
877
1098
 
878
1099
  hbox2 = QHBoxLayout()
879
- hbox2.addWidget(QPushButton("OK", clicked=self.close))
1100
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
1101
+ hbox2.addWidget(QPushButton(cfg.CLOSE, clicked=self.close))
880
1102
 
881
1103
  vbox.addLayout(hbox2)
882
1104
 
883
1105
  self.setLayout(vbox)
884
1106
 
885
- # self.resize(540, 640)
886
-
887
1107
  def tw_cellDoubleClicked(self, r, c):
888
-
889
1108
  self.double_click_signal.emit(self.tw.item(r, 0).text(), int(self.tw.item(r, 1).text()))