boris-behav-obs 9.7.7__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.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  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 +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/dialog.py ADDED
@@ -0,0 +1,1108 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import datetime as dt
24
+ from decimal import Decimal as dec
25
+ import logging
26
+ import math
27
+ import pathlib as pl
28
+ import platform
29
+ import sys
30
+ import traceback
31
+ from typing import Union
32
+
33
+ from PySide6.QtCore import Qt, Signal, qVersion, QRect, QTime, QDateTime, QSize
34
+ from PySide6.QtWidgets import (
35
+ QApplication,
36
+ QAbstractItemView,
37
+ QCheckBox,
38
+ QComboBox,
39
+ QDialog,
40
+ QFileDialog,
41
+ QHBoxLayout,
42
+ QLabel,
43
+ QLineEdit,
44
+ QListWidget,
45
+ QListWidgetItem,
46
+ QMessageBox,
47
+ QPlainTextEdit,
48
+ QPushButton,
49
+ QSizePolicy,
50
+ QSpacerItem,
51
+ QSpinBox,
52
+ QDoubleSpinBox,
53
+ QTableView,
54
+ QTableWidget,
55
+ QVBoxLayout,
56
+ QWidget,
57
+ QDateTimeEdit,
58
+ QTimeEdit,
59
+ QAbstractSpinBox,
60
+ QRadioButton,
61
+ QStackedWidget,
62
+ QFrame,
63
+ )
64
+ from PySide6.QtGui import QFont, QTextCursor
65
+
66
+ from . import config as cfg
67
+ from . import version
68
+ from . import utilities as util
69
+
70
+
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
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
+ """
83
+ message = QMessageBox()
84
+ message.setWindowTitle(title)
85
+ message.setText(text)
86
+ message.setIcon(QMessageBox.Question)
87
+ for button in buttons:
88
+ message.addButton(button, QMessageBox.YesRole)
89
+
90
+ message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
91
+ message.exec()
92
+ return message.clickedButton().text()
93
+
94
+
95
+ def global_error_message(exception_type, exception_value, traceback_object):
96
+ """
97
+ Global error management
98
+ save error using loggin.critical and stdout
99
+ """
100
+
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"
104
+ error_text += "".join(traceback.format_exception(exception_type, exception_value, traceback_object))
105
+
106
+ # write to stdout
107
+ logging.critical(error_text)
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'}")
115
+
116
+ # copy to clipboard
117
+ cb = QApplication.clipboard()
118
+ cb.clear()
119
+ cb.setText(error_text)
120
+
121
+ text: str = (
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"
127
+ f"{error_text}"
128
+ )
129
+
130
+ errorbox = Results_dialog()
131
+
132
+ errorbox.setWindowTitle("BORIS - An error occured")
133
+ errorbox.pbOK.setText("Abort")
134
+ errorbox.pbCancel.setVisible(True)
135
+ errorbox.pbCancel.setText("Ignore and try to continue")
136
+
137
+ font = QFont()
138
+ font.setFamily("monospace")
139
+ errorbox.ptText.setFont(font)
140
+ errorbox.ptText.clear()
141
+ errorbox.ptText.appendPlainText(text)
142
+
143
+ errorbox.ptText.moveCursor(QTextCursor.Start)
144
+
145
+ ret = errorbox.exec_()
146
+
147
+ if ret == 1: # Abort
148
+ sys.exit(1)
149
+
150
+
151
+ class Info_widget(QWidget):
152
+ def __init__(self, parent=None):
153
+ super().__init__(parent)
154
+
155
+ self.setWindowTitle("BORIS")
156
+ layout = QVBoxLayout()
157
+ self.label = QLabel()
158
+ layout.addWidget(self.label)
159
+ self.lwi = QListWidget()
160
+ layout.addWidget(self.lwi)
161
+ self.setLayout(layout)
162
+
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
+
477
+ class Video_overlay_dialog(QDialog):
478
+ """
479
+ dialog to ask image file and position for video overlay
480
+ """
481
+
482
+ def __init__(self):
483
+ super().__init__()
484
+
485
+ vlayout = QVBoxLayout()
486
+ self.cb_player = QComboBox()
487
+ vlayout.addWidget(self.cb_player)
488
+
489
+ hbox = QHBoxLayout()
490
+ hbox.addWidget(QLabel("Image file"))
491
+ self.le_file_path = QLineEdit()
492
+ hbox.addWidget(self.le_file_path)
493
+ vlayout.addLayout(hbox)
494
+
495
+ hbox = QHBoxLayout()
496
+ self.pb_browse = QPushButton("Browse", self, clicked=self.browse)
497
+ hbox.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
498
+ hbox.addWidget(self.pb_browse)
499
+ vlayout.addLayout(hbox)
500
+
501
+ hbox = QHBoxLayout()
502
+ hbox.addWidget(QLabel("Position: x,y"))
503
+ self.le_overlay_position = QLineEdit()
504
+ hbox.addWidget(self.le_overlay_position)
505
+ vlayout.addLayout(hbox)
506
+
507
+ hbox = QHBoxLayout()
508
+ hbox.addWidget(QLabel("Transparency %"))
509
+ self.sb_overlay_transparency = QSpinBox()
510
+ self.sb_overlay_transparency.setRange(0, 100)
511
+ self.sb_overlay_transparency.setSingleStep(1)
512
+ self.sb_overlay_transparency.setValue(90)
513
+ hbox.addWidget(self.sb_overlay_transparency)
514
+ vlayout.addLayout(hbox)
515
+
516
+ # self.sb_overlay_transparency.setEnabled(False)
517
+
518
+ hbox = QHBoxLayout()
519
+ self.pb_cancel = QPushButton("Cancel", self, clicked=self.reject)
520
+ hbox.addWidget(self.pb_cancel)
521
+ self.pb_OK = QPushButton("OK", clicked=self.ok)
522
+
523
+ self.pb_OK.setDefault(True)
524
+ hbox.addWidget(self.pb_OK)
525
+
526
+ vlayout.addLayout(hbox)
527
+
528
+ self.setLayout(vlayout)
529
+
530
+ def browse(self):
531
+ file_name, _ = QFileDialog.getOpenFileName(self, "Choose an image file", "", "PNG files (*.png);;All files (*)")
532
+ if file_name:
533
+ self.le_file_path.setText(file_name)
534
+
535
+ def ok(self):
536
+ if not self.le_file_path.text():
537
+ QMessageBox.warning(
538
+ None,
539
+ cfg.programName,
540
+ "Select a file containing a PNG image",
541
+ QMessageBox.Ok | QMessageBox.Default,
542
+ QMessageBox.NoButton,
543
+ )
544
+ return
545
+
546
+ if self.le_overlay_position.text() and "," not in self.le_overlay_position.text():
547
+ QMessageBox.warning(
548
+ None,
549
+ cfg.programName,
550
+ "The overlay position must be in x,y format",
551
+ QMessageBox.Ok | QMessageBox.Default,
552
+ QMessageBox.NoButton,
553
+ )
554
+ return
555
+ if self.le_overlay_position.text():
556
+ try:
557
+ [int(x.strip()) for x in self.le_overlay_position.text().split(",")]
558
+ except Exception:
559
+ QMessageBox.warning(
560
+ None,
561
+ cfg.programName,
562
+ "The overlay position must be in x,y format",
563
+ QMessageBox.Ok | QMessageBox.Default,
564
+ QMessageBox.NoButton,
565
+ )
566
+ return
567
+ self.accept()
568
+
569
+
570
+ class Input_dialog(QDialog):
571
+ """
572
+ dialog for user input. Elements can be:
573
+ checkbox (cb): Tuple(str, str, bool)
574
+ lineedit (le): Tuple(str, str)
575
+ spinbox (sb)
576
+ doubleSpinbox (dsb)
577
+ items list (il)
578
+
579
+ """
580
+
581
+ def __init__(self, label_caption: str, elements_list: list, title: str = ""):
582
+ super().__init__()
583
+
584
+ self.setWindowTitle(title)
585
+
586
+ hbox = QVBoxLayout()
587
+ self.label = QLabel()
588
+ self.label.setText(label_caption)
589
+ hbox.addWidget(self.label)
590
+
591
+ self.elements: dict = {}
592
+ for element in elements_list:
593
+ if element[0] == "cb": # checkbox
594
+ self.elements[element[1]] = QCheckBox(element[1])
595
+ self.elements[element[1]].setChecked(element[2])
596
+ hbox.addWidget(self.elements[element[1]])
597
+
598
+ if element[0] == "le": # line edit
599
+ lb = QLabel(element[1])
600
+ hbox.addWidget(lb)
601
+ self.elements[element[1]] = QLineEdit()
602
+ hbox.addWidget(self.elements[element[1]])
603
+
604
+ if element[0] == "sb": # spinbox
605
+ # 1 - Label
606
+ # 2 - minimum value
607
+ # 3 - maximum value
608
+ # 4 - step
609
+ # 5 - initial value
610
+
611
+ lb = QLabel(element[1])
612
+ hbox.addWidget(lb)
613
+ self.elements[element[1]] = QSpinBox()
614
+ self.elements[element[1]].setRange(element[2], element[3])
615
+ self.elements[element[1]].setSingleStep(element[4])
616
+ self.elements[element[1]].setValue(element[5])
617
+ hbox.addWidget(self.elements[element[1]])
618
+
619
+ if element[0] == "dsb": # doubleSpinbox
620
+ # 1 - Label
621
+ # 2 - minimum value
622
+ # 3 - maximum value
623
+ # 4 - step
624
+ # 5 - initial value
625
+ # 6 - number of decimals
626
+
627
+ lb = QLabel(element[1])
628
+ hbox.addWidget(lb)
629
+ self.elements[element[1]] = QDoubleSpinBox()
630
+ self.elements[element[1]].setRange(element[2], element[3])
631
+ self.elements[element[1]].setSingleStep(element[4])
632
+ self.elements[element[1]].setValue(element[5])
633
+ self.elements[element[1]].setDecimals(element[6])
634
+ hbox.addWidget(self.elements[element[1]])
635
+
636
+ if element[0] == "il": # items list
637
+ # 1 - Label
638
+ # 2 - Values (tuple of tuple: 0 - value; 1 - "", "selected")
639
+ lb = QLabel(element[1])
640
+ hbox.addWidget(lb)
641
+ self.elements[element[1]] = QComboBox()
642
+ self.elements[element[1]].addItems([x[0] for x in element[2]]) # take first element of tuple
643
+ try:
644
+ self.elements[element[1]].setCurrentIndex([idx for idx, x in enumerate(element[2]) if x[1] == "selected"][0])
645
+ except Exception:
646
+ self.elements[element[1]].setCurrentIndex(0)
647
+ hbox.addWidget(self.elements[element[1]])
648
+
649
+ hbox2 = QHBoxLayout()
650
+
651
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
652
+
653
+ self.pbCancel = QPushButton(cfg.CANCEL)
654
+ self.pbCancel.clicked.connect(self.reject)
655
+ hbox2.addWidget(self.pbCancel)
656
+
657
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
658
+ self.pbOK.setDefault(True)
659
+ hbox2.addWidget(self.pbOK)
660
+
661
+ hbox.addLayout(hbox2)
662
+
663
+ self.setLayout(hbox)
664
+
665
+
666
+ class Duplicate_items(QDialog):
667
+ """
668
+ let user show between behaviors/subjects that are coded by same key
669
+ """
670
+
671
+ def __init__(self, text, codes_list):
672
+ super(Duplicate_items, self).__init__()
673
+
674
+ self.setWindowTitle(cfg.programName)
675
+ self.setWindowFlags(Qt.WindowStaysOnTopHint)
676
+
677
+ Vlayout = QVBoxLayout()
678
+ widget = QWidget(self)
679
+ widget.setLayout(Vlayout)
680
+
681
+ label = QLabel()
682
+ label.setText(text)
683
+ Vlayout.addWidget(label)
684
+
685
+ self.lw = QListWidget(widget)
686
+ self.lw.setObjectName("lw_modifiers")
687
+ # TODO: to be enabled
688
+ # lw.installEventFilter(self)
689
+
690
+ for code in codes_list:
691
+ item = QListWidgetItem(code)
692
+ self.lw.addItem(item)
693
+
694
+ Vlayout.addWidget(self.lw)
695
+
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)
703
+ pbOK.setDefault(True)
704
+ hlayout.addWidget(pbOK)
705
+
706
+ Vlayout.addLayout(hlayout)
707
+
708
+ self.setLayout(Vlayout)
709
+
710
+ # self.installEventFilter(self)
711
+
712
+ self.setMaximumSize(1024, 960)
713
+
714
+ def getCode(self):
715
+ """
716
+ get selected behavior code
717
+ """
718
+ if self.lw.selectedItems():
719
+ return self.lw.selectedItems()[0].text()
720
+ else:
721
+ return None
722
+
723
+
724
+ class ChooseObservationsToImport(QDialog):
725
+ """
726
+ dialog for selectiong items
727
+ """
728
+
729
+ def __init__(self, text, observations_list):
730
+ super(ChooseObservationsToImport, self).__init__()
731
+
732
+ self.setWindowTitle(cfg.programName)
733
+
734
+ Vlayout = QVBoxLayout()
735
+ widget = QWidget(self)
736
+ widget.setLayout(Vlayout)
737
+
738
+ label = QLabel()
739
+ label.setText(text)
740
+ Vlayout.addWidget(label)
741
+
742
+ self.lw = QListWidget(widget)
743
+ self.lw.setObjectName("lw_observations")
744
+ self.lw.setSelectionMode(QAbstractItemView.ExtendedSelection)
745
+ # TODO: to be enabled
746
+ # lw.installEventFilter(self)
747
+
748
+ for code in observations_list:
749
+ item = QListWidgetItem(code)
750
+ self.lw.addItem(item)
751
+
752
+ Vlayout.addWidget(self.lw)
753
+
754
+ hlayout = QHBoxLayout()
755
+ hlayout.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
756
+ pbCancel = QPushButton("Cancel", clicked=self.reject)
757
+ hlayout.addWidget(pbCancel)
758
+ pbOK = QPushButton("OK", clicked=self.accept)
759
+ pbOK.setDefault(True)
760
+ hlayout.addWidget(pbOK)
761
+
762
+ Vlayout.addLayout(hlayout)
763
+
764
+ self.setLayout(Vlayout)
765
+
766
+ # self.installEventFilter(self)
767
+
768
+ self.setMaximumSize(1024, 960)
769
+
770
+ def get_selected_observations(self):
771
+ """
772
+ get selected_observations
773
+ """
774
+ return [item.text() for item in self.lw.selectedItems()]
775
+
776
+
777
+ class FindInEvents(QWidget):
778
+ """
779
+ "find in events" dialog box
780
+ """
781
+
782
+ clickSignal = Signal(str)
783
+ currentIdx: int = -1
784
+
785
+ def __init__(self):
786
+ super().__init__()
787
+
788
+ self.setWindowTitle("Find in events")
789
+
790
+ hbox = QVBoxLayout()
791
+
792
+ self.cbSubject = QCheckBox("Subject")
793
+ self.cbSubject.setChecked(True)
794
+ hbox.addWidget(self.cbSubject)
795
+
796
+ self.cbBehavior = QCheckBox("Behavior")
797
+ self.cbBehavior.setChecked(True)
798
+ hbox.addWidget(self.cbBehavior)
799
+
800
+ self.cbModifier = QCheckBox("Modifiers")
801
+ self.cbModifier.setChecked(True)
802
+ hbox.addWidget(self.cbModifier)
803
+
804
+ self.cbComment = QCheckBox("Comment")
805
+ self.cbComment.setChecked(True)
806
+ hbox.addWidget(self.cbComment)
807
+
808
+ self.lbFind = QLabel("Find")
809
+ hbox.addWidget(self.lbFind)
810
+
811
+ self.findText = QLineEdit()
812
+ hbox.addWidget(self.findText)
813
+
814
+ self.cbFindInSelectedEvents = QCheckBox("Find in selected events")
815
+ self.cbFindInSelectedEvents.setChecked(False)
816
+ hbox.addWidget(self.cbFindInSelectedEvents)
817
+
818
+ self.cb_case_sensitive = QCheckBox("Case sensitive")
819
+ self.cb_case_sensitive.setChecked(False)
820
+ hbox.addWidget(self.cb_case_sensitive)
821
+
822
+ self.lb_message = QLabel()
823
+ hbox.addWidget(self.lb_message)
824
+
825
+ hbox2 = QHBoxLayout()
826
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
827
+ self.pbOK = QPushButton("Find")
828
+ self.pbOK.clicked.connect(lambda: self.click("FIND"))
829
+ self.pbCancel = QPushButton(cfg.CLOSE)
830
+ self.pbCancel.clicked.connect(lambda: self.click("CLOSE"))
831
+ hbox2.addWidget(self.pbCancel)
832
+ hbox2.addWidget(self.pbOK)
833
+ hbox.addLayout(hbox2)
834
+
835
+ self.setLayout(hbox)
836
+
837
+ def click(self, msg):
838
+ self.clickSignal.emit(msg)
839
+
840
+
841
+ class FindReplaceEvents(QWidget):
842
+ """
843
+ "find replace events" dialog box
844
+ """
845
+
846
+ clickSignal = Signal(str)
847
+
848
+ def __init__(self):
849
+ super().__init__()
850
+
851
+ self.setWindowTitle("Find/Replace events")
852
+
853
+ hbox = QVBoxLayout()
854
+
855
+ self.combo_fields = QComboBox()
856
+ self.combo_fields.addItems(("Choose a field", "Subject", "Behavior", "Modifiers", "Comment"))
857
+ hbox.addWidget(self.combo_fields)
858
+
859
+ self.lbFind = QLabel("Find")
860
+ hbox.addWidget(self.lbFind)
861
+
862
+ self.findText = QLineEdit()
863
+ hbox.addWidget(self.findText)
864
+
865
+ self.lbReplace = QLabel("Replace")
866
+ hbox.addWidget(self.lbReplace)
867
+
868
+ self.replaceText = QLineEdit()
869
+ hbox.addWidget(self.replaceText)
870
+
871
+ self.cbFindInSelectedEvents = QCheckBox("Find/Replace only in selected events")
872
+ self.cbFindInSelectedEvents.setChecked(False)
873
+ hbox.addWidget(self.cbFindInSelectedEvents)
874
+
875
+ self.cb_case_sensitive = QCheckBox("Case sensitive")
876
+ self.cb_case_sensitive.setChecked(False)
877
+ hbox.addWidget(self.cb_case_sensitive)
878
+
879
+ hbox2 = QHBoxLayout()
880
+
881
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
882
+
883
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=lambda: self.click("CANCEL"))
884
+ hbox2.addWidget(self.pbCancel)
885
+
886
+ self.pbOK = QPushButton("Find and replace", clicked=lambda: self.click("FIND_REPLACE"))
887
+ hbox2.addWidget(self.pbOK)
888
+
889
+ self.pbFindReplaceAll = QPushButton("Find and replace all", clicked=lambda: self.click("FIND_REPLACE_ALL"))
890
+ hbox2.addWidget(self.pbFindReplaceAll)
891
+
892
+ hbox.addLayout(hbox2)
893
+
894
+ self.lb_results = QLabel(" ")
895
+ hbox.addWidget(self.lb_results)
896
+
897
+ self.setLayout(hbox)
898
+
899
+ def click(self, msg):
900
+ self.clickSignal.emit(msg)
901
+
902
+
903
+ class Results_dialog(QDialog):
904
+ """
905
+ widget for visualizing text output in HTML
906
+ """
907
+
908
+ def __init__(self):
909
+ super().__init__()
910
+
911
+ self.dataset = False
912
+
913
+ self.setWindowTitle("")
914
+
915
+ hbox = QVBoxLayout()
916
+
917
+ self.lb = QLabel("")
918
+ hbox.addWidget(self.lb)
919
+
920
+ self.ptText = QPlainTextEdit()
921
+ self.ptText.setReadOnly(True)
922
+ hbox.addWidget(self.ptText)
923
+
924
+ hbox2 = QHBoxLayout()
925
+ hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
926
+
927
+ self.pbSave = QPushButton("Save results", clicked=self.save_results)
928
+ hbox2.addWidget(self.pbSave)
929
+
930
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.reject)
931
+ hbox2.addWidget(self.pbCancel)
932
+ self.pbCancel.setVisible(False)
933
+
934
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
935
+ hbox2.addWidget(self.pbOK)
936
+
937
+ hbox.addLayout(hbox2)
938
+ self.setLayout(hbox)
939
+
940
+ self.resize(800, 640)
941
+
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):
963
+ """
964
+ widget for visualizing text output
965
+ """
966
+
967
+ def __init__(self):
968
+ super().__init__()
969
+
970
+ self.dataset = False
971
+
972
+ self.setWindowTitle("")
973
+
974
+ hbox = QVBoxLayout()
975
+
976
+ self.lb = QLabel("")
977
+ hbox.addWidget(self.lb)
978
+
979
+ self.ptText = QPlainTextEdit()
980
+ self.ptText.setReadOnly(True)
981
+ hbox.addWidget(self.ptText)
982
+
983
+ hbox2 = QHBoxLayout()
984
+ hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
985
+
986
+ self.pbSave = QPushButton("Save results", clicked=self.save_results)
987
+ hbox2.addWidget(self.pbSave)
988
+
989
+ self.pb1 = QPushButton("1", clicked=lambda: self.done_(1))
990
+ hbox2.addWidget(self.pb1)
991
+
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)
997
+
998
+ hbox.addLayout(hbox2)
999
+ self.setLayout(hbox)
1000
+
1001
+ self.resize(800, 640)
1002
+
1003
+ def done_(self, status):
1004
+ self.done(status)
1005
+
1006
+ def save_results(self):
1007
+ """
1008
+ save content of self.ptText
1009
+ """
1010
+
1011
+ if not self.dataset:
1012
+ file_name, _ = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
1013
+
1014
+ if not file_name:
1015
+ return
1016
+ try:
1017
+ with open(file_name, "w") as f:
1018
+ f.write(self.ptText.toPlainText())
1019
+ except Exception:
1020
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
1021
+
1022
+ else:
1023
+ self.done(cfg.SAVE_DATASET)
1024
+
1025
+
1026
+ class View_data(QDialog):
1027
+ """
1028
+ widget for visualizing rows of data file
1029
+ """
1030
+
1031
+ def __init__(self):
1032
+ super().__init__()
1033
+
1034
+ self.setWindowTitle("")
1035
+
1036
+ vbox = QVBoxLayout()
1037
+
1038
+ self.lb = QLabel("")
1039
+ vbox.addWidget(self.lb)
1040
+
1041
+ self.tw = QTableWidget()
1042
+ self.tw.verticalHeader().hide()
1043
+ vbox.addWidget(self.tw)
1044
+
1045
+ vbox.addWidget(QLabel("Descriptive statistics"))
1046
+
1047
+ self.stats = QPlainTextEdit()
1048
+ font = QFont()
1049
+ font.setFamily("Monospace")
1050
+ self.stats.setFont(font)
1051
+
1052
+ vbox.addWidget(self.stats)
1053
+
1054
+ self.label = QLabel("Enter the column indices to plot (time, value) separated by comma (,)")
1055
+ vbox.addWidget(self.label)
1056
+
1057
+ self.le = QLineEdit()
1058
+ vbox.addWidget(self.le)
1059
+
1060
+ hbox2 = QHBoxLayout()
1061
+
1062
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
1063
+
1064
+ self.pbCancel = QPushButton(cfg.CANCEL, clicked=self.reject)
1065
+ hbox2.addWidget(self.pbCancel)
1066
+
1067
+ self.pbOK = QPushButton(cfg.OK, clicked=self.accept)
1068
+ hbox2.addWidget(self.pbOK)
1069
+
1070
+ vbox.addLayout(hbox2)
1071
+
1072
+ self.setLayout(vbox)
1073
+
1074
+ self.resize(800, 640)
1075
+
1076
+
1077
+ class View_explore_project_results(QWidget):
1078
+ """
1079
+ widget for visualizing results of explore project
1080
+ """
1081
+
1082
+ double_click_signal = Signal(str, int)
1083
+
1084
+ def __init__(self):
1085
+ super().__init__()
1086
+
1087
+ self.setWindowTitle("")
1088
+
1089
+ vbox = QVBoxLayout()
1090
+
1091
+ self.lb = QLabel("")
1092
+ vbox.addWidget(self.lb)
1093
+
1094
+ self.tw = QTableWidget()
1095
+ self.tw.setSelectionBehavior(QTableView.SelectRows)
1096
+ self.tw.cellDoubleClicked[int, int].connect(self.tw_cellDoubleClicked)
1097
+ vbox.addWidget(self.tw)
1098
+
1099
+ hbox2 = QHBoxLayout()
1100
+ hbox2.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
1101
+ hbox2.addWidget(QPushButton(cfg.CLOSE, clicked=self.close))
1102
+
1103
+ vbox.addLayout(hbox2)
1104
+
1105
+ self.setLayout(vbox)
1106
+
1107
+ def tw_cellDoubleClicked(self, r, c):
1108
+ self.double_click_signal.emit(self.tw.item(r, 0).text(), int(self.tw.item(r, 1).text()))