boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +25 -33
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -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 +306 -83
  80. boris/preferences_ui.py +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +627 -236
  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 +95 -29
  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.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.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 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1013 @@
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
+ # to run this file, use
24
+ # python -m boris.modifier_coding_map_creator
25
+
26
+ import binascii
27
+ import io
28
+ import json
29
+ from pathlib import Path
30
+ import re
31
+ import gui_utilities
32
+
33
+ from PySide6.QtCore import (
34
+ Qt,
35
+ Signal,
36
+ QPoint,
37
+ QByteArray,
38
+ QBuffer,
39
+ QIODevice,
40
+ QLineF,
41
+ )
42
+ from PySide6.QtGui import QColor, QBrush, QMouseEvent, QPixmap, QIcon, QPen, QPolygon, QPolygonF, QAction
43
+ from PySide6.QtWidgets import (
44
+ QGraphicsPolygonItem,
45
+ QGraphicsEllipseItem,
46
+ QGraphicsPixmapItem,
47
+ QGraphicsLineItem,
48
+ QMainWindow,
49
+ QGraphicsView,
50
+ QPushButton,
51
+ QLabel,
52
+ QHBoxLayout,
53
+ QLineEdit,
54
+ QSlider,
55
+ QGraphicsScene,
56
+ QWidget,
57
+ QColorDialog,
58
+ QVBoxLayout,
59
+ QMessageBox,
60
+ QInputDialog,
61
+ QFileDialog,
62
+ QApplication,
63
+ QListWidget,
64
+ QSplitter,
65
+ QSpacerItem,
66
+ QSizePolicy,
67
+ QFrame,
68
+ )
69
+
70
+ from . import config as cfg
71
+ from . import dialog
72
+ from . import utilities as util
73
+
74
+ designColor = QColor(255, 0, 0, 128) # red opacity: 50%
75
+ penWidth: int = 0
76
+ penStyle = Qt.NoPen
77
+ selectedBrush = QBrush()
78
+ selectedBrush.setStyle(Qt.SolidPattern)
79
+ selectedBrush.setColor(QColor(255, 255, 0, 255))
80
+
81
+
82
+ class ModifiersMapCreatorWindow(QMainWindow):
83
+ closed = Signal()
84
+
85
+ class View(QGraphicsView):
86
+ """
87
+ class for handling mousepress event in QGraphicsView
88
+ """
89
+
90
+ mousePress = Signal(QMouseEvent)
91
+
92
+ def mousePressEvent(self, event):
93
+ self.mousePress.emit(event)
94
+
95
+ _start: int = 0
96
+ elList: list = []
97
+ points: list = []
98
+
99
+ def __init__(self, parent):
100
+ QGraphicsView.__init__(self, parent)
101
+ self.setBackgroundBrush(QColor(128, 128, 128))
102
+ self.setScene(QGraphicsScene(self))
103
+ self.scene().update()
104
+
105
+ bitmapFileName: str = ""
106
+ mapName: str = ""
107
+ fileName: str = ""
108
+ flagNewArea: bool = False
109
+ flag_map_changed: bool = False
110
+ areasList: dict = {}
111
+ polygonsList2: dict = {}
112
+ areaColor = QColor("lime")
113
+
114
+ def __init__(self):
115
+ super(ModifiersMapCreatorWindow, self).__init__()
116
+
117
+ self.pixmap = QPixmap()
118
+ self.closedPolygon = None
119
+ self.selectedPolygon = None
120
+
121
+ self.setWindowTitle("BORIS - Modifiers map creator")
122
+
123
+ self.newMapAction = QAction(QIcon(), "&New modifiers map", self)
124
+ self.newMapAction.setShortcut("Ctrl+N")
125
+ self.newMapAction.setStatusTip("Create a new modifiers map")
126
+ self.newMapAction.triggered.connect(self.newMap)
127
+
128
+ self.openMapAction = QAction(QIcon(), "&Open modifiers map", self)
129
+ self.openMapAction.setShortcut("Ctrl+O")
130
+ self.openMapAction.setStatusTip("Open a modifiers map")
131
+ self.openMapAction.triggered.connect(self.openMap)
132
+
133
+ self.saveMapAction = QAction(QIcon(), "&Save modifiers map", self)
134
+ self.saveMapAction.setShortcut("Ctrl+S")
135
+ self.saveMapAction.setStatusTip("Save modifiers map")
136
+ self.saveMapAction.setEnabled(False)
137
+ self.saveMapAction.triggered.connect(self.saveMap_clicked)
138
+
139
+ self.saveAsMapAction = QAction(QIcon(), "Save modifiers map as", self)
140
+ self.saveAsMapAction.setStatusTip("Save modifiers map as")
141
+ self.saveAsMapAction.setEnabled(False)
142
+ self.saveAsMapAction.triggered.connect(self.saveAsMap_clicked)
143
+
144
+ self.mapNameAction = QAction(QIcon(), "&Modifiers map name", self)
145
+ self.mapNameAction.setShortcut("Ctrl+M")
146
+ self.mapNameAction.setStatusTip("Change modifiers map name")
147
+ self.mapNameAction.setEnabled(False)
148
+ self.mapNameAction.triggered.connect(self.mapName_clicked)
149
+
150
+ self.exitAction = QAction(QIcon(), "&Close", self)
151
+ self.exitAction.setStatusTip("Close modifiers map creator")
152
+ self.exitAction.triggered.connect(self.close)
153
+
154
+ menubar = self.menuBar()
155
+ fileMenu = menubar.addMenu("&Modifiers Map creator")
156
+ fileMenu.addAction(self.newMapAction)
157
+ fileMenu.addAction(self.openMapAction)
158
+ fileMenu.addAction(self.saveMapAction)
159
+ fileMenu.addAction(self.saveAsMapAction)
160
+ fileMenu.addSeparator()
161
+ fileMenu.addAction(self.mapNameAction)
162
+ fileMenu.addSeparator()
163
+ fileMenu.addAction(self.exitAction)
164
+
165
+ splitter1 = QSplitter(Qt.Vertical)
166
+
167
+ self.view = self.View(self)
168
+ self.view.mousePress.connect(self.viewMousePressEvent)
169
+ splitter1.addWidget(self.view)
170
+
171
+ vlayout_list = QVBoxLayout()
172
+ vlayout_list.addWidget(QLabel("List of modifiers"))
173
+
174
+ self.area_list = QListWidget(self)
175
+ self.area_list.itemClicked.connect(self.area_list_item_click)
176
+ vlayout_list.addWidget(self.area_list)
177
+ w = QWidget()
178
+ w.setLayout(vlayout_list)
179
+ splitter1.addWidget(w)
180
+ splitter1.setSizes([300, 100])
181
+ splitter1.setStretchFactor(2, 8)
182
+
183
+ hlayout_cmd = QHBoxLayout()
184
+
185
+ self.btNewArea = QPushButton("New modifier", self)
186
+ self.btNewArea.clicked.connect(self.newArea)
187
+ self.btNewArea.setVisible(False)
188
+ hlayout_cmd.addWidget(self.btNewArea)
189
+
190
+ self.btSaveArea = QPushButton("Save modifier", self)
191
+ self.btSaveArea.clicked.connect(self.saveArea)
192
+ self.btSaveArea.setVisible(False)
193
+ hlayout_cmd.addWidget(self.btSaveArea)
194
+
195
+ self.btCancelAreaCreation = QPushButton("Cancel new modifier", self)
196
+ self.btCancelAreaCreation.clicked.connect(self.cancelAreaCreation)
197
+ self.btCancelAreaCreation.setVisible(False)
198
+ hlayout_cmd.addWidget(self.btCancelAreaCreation)
199
+
200
+ hlayout_cmd.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
201
+
202
+ self.btDeleteArea = QPushButton("Delete selected modifier", self)
203
+ self.btDeleteArea.clicked.connect(self.deleteArea)
204
+ self.btDeleteArea.setVisible(False)
205
+ self.btDeleteArea.setEnabled(False)
206
+ hlayout_cmd.addWidget(self.btDeleteArea)
207
+
208
+ hlayout_area = QHBoxLayout()
209
+
210
+ self.lb = QLabel("Modifier")
211
+ self.lb.setVisible(False)
212
+ hlayout_area.addWidget(self.lb)
213
+
214
+ self.leAreaCode = QLineEdit(self)
215
+ self.leAreaCode.setVisible(False)
216
+ hlayout_area.addWidget(self.leAreaCode)
217
+
218
+ self.btColor = QPushButton()
219
+ self.btColor.clicked.connect(self.chooseColor)
220
+ self.btColor.setVisible(False)
221
+ self.btColor.setStyleSheet(f"QWidget {{ background-color:{self.areaColor.name()} }}")
222
+ hlayout_area.addWidget(self.btColor)
223
+
224
+ self.slAlpha = QSlider(Qt.Horizontal)
225
+ self.slAlpha.setRange(20, 100)
226
+ self.slAlpha.setValue(50)
227
+ self.slAlpha.valueChanged.connect(self.slAlpha_changed)
228
+ self.slAlpha.setVisible(False)
229
+ hlayout_area.addWidget(self.slAlpha)
230
+
231
+ self.slAlpha_changed(50)
232
+
233
+ vlayout_frame = QVBoxLayout()
234
+ vlayout_frame.addLayout(hlayout_cmd)
235
+ vlayout_frame.addLayout(hlayout_area)
236
+ vlayout_frame.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
237
+
238
+ frame = QFrame()
239
+ frame.setFrameStyle(QFrame.Panel | QFrame.Plain)
240
+ frame.setMinimumHeight(120)
241
+ frame.setMaximumHeight(120)
242
+
243
+ frame.setLayout(vlayout_frame)
244
+
245
+ vlayout = QVBoxLayout()
246
+
247
+ vlayout.addWidget(splitter1)
248
+ vlayout.addWidget(frame)
249
+
250
+ main_widget = QWidget(self)
251
+ main_widget.setLayout(vlayout)
252
+ self.setCentralWidget(main_widget)
253
+
254
+ self.statusBar().showMessage("")
255
+
256
+ def area_list_item_click(self, item):
257
+ """
258
+ select the polygon corresponding to the clicked area
259
+ """
260
+
261
+ if self.selectedPolygon:
262
+ self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
263
+ self.selectedPolygon = None
264
+ self.selectedPolygonMemBrush = None
265
+
266
+ modifier_name = item.text()
267
+
268
+ self.selectedPolygon = self.polygonsList2[item.text()]
269
+
270
+ self.selectedPolygonMemBrush = self.selectedPolygon.brush()
271
+
272
+ self.selectedPolygonAreaCode = modifier_name
273
+
274
+ self.selectedPolygon.setPen(QPen(QColor(255, 0, 0, 255), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
275
+
276
+ self.leAreaCode.setText(modifier_name)
277
+ for widget in (self.lb, self.leAreaCode, self.btSaveArea, self.btDeleteArea, self.btColor, self.slAlpha):
278
+ widget.setVisible(True)
279
+
280
+ self.btSaveArea.setText("Update modifier")
281
+ self.btDeleteArea.setEnabled(True)
282
+
283
+ self.areaColor = self.selectedPolygon.brush().color()
284
+ self.btColor.setStyleSheet(f"QWidget {{background-color:{self.selectedPolygon.brush().color().name()}}}")
285
+
286
+ self.slAlpha.setValue(int(self.areaColor.alpha() / 255 * 100))
287
+
288
+ def get_current_alpha_value(self) -> str | None:
289
+ """
290
+ returns current alpha value (from button text)
291
+ """
292
+ match = re.search(r"(\d+)", self.btColor.text())
293
+
294
+ if match:
295
+ opacity_value = int(match.group(1)) # Convert the extracted string to an integer
296
+ return opacity_value
297
+ else:
298
+ return None
299
+
300
+ def slAlpha_changed(self, val):
301
+ """
302
+ opacity slider value changed
303
+ """
304
+
305
+ self.btColor.setText(f"Opacity: {val}")
306
+ self.areaColor.setAlpha(int(val / 100 * 255))
307
+
308
+ if self.selectedPolygon:
309
+ self.selectedPolygon.setBrush(self.areaColor)
310
+
311
+ # self.areasList[self.leAreaCode.text()]["color"] = self.areaColor.rgba()
312
+
313
+ if self.closedPolygon:
314
+ self.closedPolygon.setBrush(self.areaColor)
315
+
316
+ def chooseColor(self):
317
+ """
318
+ area color button clicked
319
+ """
320
+ cd = QColorDialog()
321
+ cd.setWindowFlags(Qt.WindowStaysOnTopHint)
322
+ cd.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
323
+
324
+ if cd.exec():
325
+ self.areaColor = cd.currentColor()
326
+ self.areaColor.setAlpha(int(self.get_current_alpha_value() / 100 * 255))
327
+
328
+ self.btColor.setStyleSheet(f"QWidget {{background-color:{self.areaColor.name()}}}")
329
+
330
+ if self.selectedPolygon:
331
+ self.selectedPolygon.setBrush(self.areaColor)
332
+ self.areasList[self.leAreaCode.text()]["color"] = self.areaColor.rgba()
333
+
334
+ if self.closedPolygon:
335
+ self.closedPolygon.setBrush(self.areaColor)
336
+
337
+ def closeEvent(self, event):
338
+ if self.flag_map_changed:
339
+ response = dialog.MessageDialog(
340
+ "BORIS - Modifiers map creator",
341
+ "What to do about the current unsaved modifiers coding map?",
342
+ [cfg.SAVE, cfg.DISCARD, cfg.CANCEL],
343
+ )
344
+
345
+ if response == cfg.SAVE:
346
+ if not self.saveMap_clicked():
347
+ event.ignore()
348
+
349
+ if response == cfg.CANCEL:
350
+ event.ignore()
351
+ return
352
+
353
+ self.closed.emit()
354
+ event.accept()
355
+
356
+ def viewMousePressEvent(self, event):
357
+ """
358
+ check if area selected with mouse
359
+ """
360
+
361
+ def new_polygon():
362
+ """
363
+ create a newpolygon
364
+ """
365
+ newPolygon = QPolygonF()
366
+ for p in self.view.points:
367
+ newPolygon.append(QPoint(p[0], p[1]))
368
+
369
+ # draw polygon a red polygon
370
+ self.closedPolygon = QGraphicsPolygonItem(newPolygon)
371
+ self.closedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
372
+ self.closedPolygon.setBrush(self.areaColor)
373
+ self.view.scene().addItem(self.closedPolygon)
374
+
375
+ def add_last_line():
376
+ """
377
+ add last line to start point
378
+ """
379
+ line = QGraphicsLineItem(
380
+ QLineF(
381
+ self.view._start,
382
+ QPoint(self.view.points[0][0], self.view.points[0][1]),
383
+ )
384
+ )
385
+ line.setPen(
386
+ QPen(
387
+ designColor,
388
+ penWidth,
389
+ Qt.SolidLine,
390
+ Qt.RoundCap,
391
+ Qt.RoundJoin,
392
+ )
393
+ )
394
+
395
+ self.view.scene().addItem(line)
396
+ self.view.elList.append(line)
397
+
398
+ self.statusBar().showMessage("Area completed")
399
+
400
+ if not self.bitmapFileName:
401
+ return
402
+
403
+ self.btDeleteArea.setEnabled(False)
404
+
405
+ # test = self.view.mapToScene(event.pos()).toPoint()
406
+ test = self.view.mapToScene(event.position().toPoint()).toPoint()
407
+
408
+ if test.x() < 0 or test.y() < 0 or test.x() > self.pixmap.size().width() or test.y() > self.pixmap.size().height():
409
+ return
410
+
411
+ if not self.flagNewArea: # test clicked point for areas
412
+ txt = ""
413
+
414
+ # reset selected polygon to default pen
415
+ if self.selectedPolygon:
416
+ self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
417
+ self.selectedPolygon = None
418
+ self.selectedPolygonMemBrush = None
419
+
420
+ for idx, areaCode in enumerate(sorted(self.polygonsList2.keys())):
421
+ for widget in (self.lb, self.leAreaCode, self.btSaveArea, self.btDeleteArea, self.btColor, self.slAlpha):
422
+ widget.setVisible(False)
423
+
424
+ if self.polygonsList2[areaCode].contains(test):
425
+ txt += "," if txt else ""
426
+
427
+ txt += areaCode
428
+ self.selectedPolygon = self.polygonsList2[areaCode]
429
+ self.selectedPolygonAreaCode = areaCode
430
+
431
+ self.selectedPolygonMemBrush = self.selectedPolygon.brush()
432
+
433
+ self.selectedPolygon.setPen(
434
+ QPen(
435
+ QColor(255, 0, 0, 255),
436
+ 2,
437
+ Qt.SolidLine,
438
+ Qt.RoundCap,
439
+ Qt.RoundJoin,
440
+ )
441
+ )
442
+
443
+ self.leAreaCode.setText(areaCode)
444
+ self.btDeleteArea.setEnabled(True)
445
+
446
+ # select area in list widget
447
+ item = self.area_list.item(idx)
448
+ self.area_list.setCurrentItem(item)
449
+
450
+ for widget in (self.lb, self.leAreaCode, self.btSaveArea, self.btDeleteArea, self.btColor, self.slAlpha):
451
+ widget.setVisible(True)
452
+
453
+ self.areaColor = self.selectedPolygon.brush().color()
454
+ self.btColor.setStyleSheet(f"QWidget {{background-color:{self.selectedPolygon.brush().color().name()} }}")
455
+
456
+ self.slAlpha.setValue(int(self.areaColor.alpha() / 255 * 100))
457
+
458
+ break
459
+
460
+ if txt:
461
+ self.statusBar().showMessage(f"Modifier{'s' if ',' in txt else ''}: {txt}")
462
+ else:
463
+ self.statusBar().showMessage("")
464
+
465
+ if not self.selectedPolygon:
466
+ self.leAreaCode.setVisible(False)
467
+ self.btColor.setVisible(False)
468
+ self.slAlpha.setVisible(False)
469
+ return
470
+
471
+ # right button: delete last line item
472
+ if (event.buttons() & Qt.RightButton) and not self.closedPolygon:
473
+ if self.view.points:
474
+ self.view.points = self.view.points[0:-1]
475
+
476
+ if self.view.points:
477
+ self.view._start = QPoint(self.view.points[-1][0], self.view.points[-1][1])
478
+ else:
479
+ self.view._start = None
480
+
481
+ # remove graphical elements
482
+ if self.view.elList:
483
+ self.view.scene().removeItem(self.view.elList[-1])
484
+ self.view.elList = self.view.elList[0:-1]
485
+
486
+ # middle button automatically close the polygon
487
+ if (event.buttons() & Qt.MiddleButton) and not self.closedPolygon:
488
+ add_last_line()
489
+ new_polygon()
490
+
491
+ # add line item
492
+ if event.buttons() == Qt.LeftButton and not self.closedPolygon:
493
+ if self.view._start:
494
+ end = test
495
+
496
+ # test is polygon is crossed
497
+ if len(self.view.points) >= 3:
498
+ for idx, point in enumerate(self.view.points[:-2]):
499
+ if util.intersection(
500
+ self.view.points[idx],
501
+ self.view.points[idx + 1],
502
+ self.view.points[-1],
503
+ (int(end.x()), int(end.y())),
504
+ ):
505
+ QMessageBox.critical(self, "", "The polygon edges can not be intersected")
506
+ return
507
+
508
+ # test if polygon closed (dist min 10 px)
509
+ if abs(end.x() - self.view.points[0][0]) < 10 and abs(end.y() - self.view.points[0][1]) < 10:
510
+ add_last_line()
511
+ new_polygon()
512
+
513
+ return
514
+
515
+ self.view.points.append((int(end.x()), int(end.y())))
516
+
517
+ line = QGraphicsLineItem(QLineF(self.view._start, end))
518
+
519
+ line.setPen(QPen(designColor, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
520
+
521
+ self.view.scene().addItem(line)
522
+ self.view.elList.append(line)
523
+
524
+ self.view._start = test
525
+
526
+ else: # first point
527
+ self.view._start = test
528
+
529
+ ellipse = QGraphicsEllipseItem(self.view._start.x(), self.view._start.y(), 3, 3)
530
+ ellipse.setPen(QPen(designColor, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
531
+
532
+ brush = QBrush()
533
+ brush.setStyle(Qt.SolidPattern)
534
+ brush.setColor(designColor)
535
+ ellipse.setBrush(brush)
536
+
537
+ self.view.scene().addItem(ellipse)
538
+ self.view.elList.append(ellipse)
539
+
540
+ self.view.points.append((self.view._start.x(), self.view._start.y()))
541
+
542
+ def mapName_clicked(self):
543
+ """
544
+ change map name
545
+ """
546
+ text, ok = QInputDialog.getText(
547
+ self,
548
+ "Modifiers map name",
549
+ "Enter a name for the modifiers map",
550
+ QLineEdit.Normal,
551
+ self.mapName,
552
+ )
553
+ if ok:
554
+ self.mapName = text
555
+ self.setWindowTitle(f"{cfg.programName} - Modifiers map creator tool - {self.mapName}")
556
+
557
+ def newMap(self):
558
+ """
559
+ create a new map
560
+ """
561
+
562
+ if self.flag_map_changed:
563
+ response = dialog.MessageDialog(
564
+ cfg.programName + " - Modifiers map creator",
565
+ "What to do about the current unsaved coding map?",
566
+ ["Save", "Discard", "Cancel"],
567
+ )
568
+
569
+ if response == "Save":
570
+ if not self.saveMap_clicked():
571
+ return
572
+
573
+ if response == "Cancel":
574
+ return
575
+
576
+ self.cancelMap()
577
+
578
+ text, ok = QInputDialog.getText(self, "Map name", "Enter a name for the new map")
579
+ if ok:
580
+ self.mapName = text
581
+ else:
582
+ return
583
+
584
+ if self.mapName == "":
585
+ QMessageBox.critical(self, "", "You must define a name for the new map")
586
+ return
587
+
588
+ if self.mapName in ["areas", "bitmap"]:
589
+ QMessageBox.critical(self, "", "This name is not allowed")
590
+ return
591
+
592
+ self.setWindowTitle(cfg.programName + " - Map creator tool - " + self.mapName)
593
+
594
+ self.loadBitmap()
595
+
596
+ def openMap(self):
597
+ """
598
+ load bitmap from data
599
+ show it in view scene
600
+ """
601
+ if self.flag_map_changed:
602
+ response = dialog.MessageDialog(
603
+ cfg.programName + " - Map creator",
604
+ "What to do about the current unsaved coding map?",
605
+ (cfg.SAVE, cfg.DISCARD, cfg.CANCEL),
606
+ )
607
+
608
+ if response == cfg.SAVE:
609
+ if not self.saveMap_clicked():
610
+ return
611
+
612
+ if response == cfg.CANCEL:
613
+ return
614
+
615
+ fileName, _ = QFileDialog().getOpenFileName(
616
+ self,
617
+ "Open a coding map",
618
+ "",
619
+ "BORIS coding map (*.boris_map);;All files (*)",
620
+ )
621
+
622
+ if not fileName:
623
+ return
624
+ try:
625
+ self.codingMap = json.loads(open(fileName, "r").read())
626
+ except Exception:
627
+ QMessageBox.critical(
628
+ self,
629
+ cfg.programName,
630
+ f"The file {fileName} seems not a behaviors coding map...",
631
+ )
632
+ return
633
+
634
+ self.cancelMap()
635
+
636
+ self.mapName = self.codingMap["name"]
637
+
638
+ self.setWindowTitle(cfg.programName + " - Map creator tool - " + self.mapName)
639
+
640
+ self.bitmapFileName = True
641
+
642
+ self.fileName = fileName
643
+
644
+ self.areasList = self.codingMap["areas"] # dictionary of dictionaries
645
+
646
+ bitmapContent = binascii.a2b_base64(self.codingMap["bitmap"])
647
+
648
+ self.pixmap.loadFromData(bitmapContent)
649
+
650
+ self.btDeleteArea.setEnabled(False)
651
+
652
+ self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height())
653
+ pixItem = QGraphicsPixmapItem(self.pixmap)
654
+ pixItem.setPos(0, 0)
655
+ self.view.scene().addItem(pixItem)
656
+
657
+ for areaCode in self.areasList:
658
+ points = self.areasList[areaCode]["geometry"]
659
+
660
+ newPolygon = QPolygonF()
661
+ for p in points:
662
+ newPolygon.append(QPoint(p[0], p[1]))
663
+
664
+ clr = QColor()
665
+ clr.setRgba(self.areasList[areaCode]["color"])
666
+
667
+ # draw polygon
668
+ polygon = QGraphicsPolygonItem()
669
+
670
+ polygon.setPolygon(newPolygon)
671
+
672
+ polygon.setPen(QPen(clr, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
673
+
674
+ polygon.setBrush(QBrush(clr, Qt.SolidPattern))
675
+
676
+ self.view.scene().addItem(polygon)
677
+ self.polygonsList2[areaCode] = polygon
678
+
679
+ self.btNewArea.setVisible(True)
680
+
681
+ self.saveMapAction.setEnabled(True)
682
+ self.saveAsMapAction.setEnabled(True)
683
+ self.mapNameAction.setEnabled(True)
684
+
685
+ self.update_area_list()
686
+
687
+ self.statusBar().showMessage('Click the "New modifier" button to create a new modifier')
688
+
689
+ def saveMap(self) -> bool:
690
+ """
691
+ save the modifier coding map on file
692
+ """
693
+ if not self.fileName:
694
+ return False
695
+ # create dict with map name key
696
+ mapDict = {"name": self.mapName}
697
+
698
+ # add areas
699
+ mapDict["areas"] = self.areasList
700
+
701
+ # Save QPixmap to QByteArray via QBuffer.
702
+ byte_array = QByteArray()
703
+ buffer = QBuffer(byte_array)
704
+ buffer.open(QIODevice.WriteOnly)
705
+ self.pixmap.save(buffer, "PNG")
706
+
707
+ string_io = io.BytesIO(byte_array)
708
+
709
+ string_io.seek(0)
710
+
711
+ # add bitmap
712
+ mapDict["bitmap"] = binascii.b2a_base64(string_io.read()).decode("utf-8")
713
+
714
+ with open(self.fileName, "w") as outfile:
715
+ outfile.write(json.dumps(mapDict))
716
+
717
+ self.flag_map_changed = False
718
+
719
+ return True
720
+
721
+ def saveAsMap_clicked(self):
722
+ filters = "Modifiers map (*.boris_map);;All files (*)"
723
+
724
+ fn = QFileDialog(self).getSaveFileName(self, "Save modifiers map as", "", filters)
725
+ if type(fn) is tuple:
726
+ self.fileName, _ = fn
727
+ else:
728
+ self.fileName = fn
729
+
730
+ if self.fileName:
731
+ # if os.path.splitext(self.fileName)[1] != ".boris_map":
732
+ if Path(self.fileName).suffix != ".boris_map":
733
+ self.fileName += ".boris_map"
734
+ self.saveMap()
735
+
736
+ def saveMap_clicked(self):
737
+ if not self.fileName:
738
+ fn = QFileDialog(self).getSaveFileName(
739
+ self,
740
+ "Save modifiers map",
741
+ self.mapName + ".boris_map",
742
+ "BORIS MAP (*.boris_map);;All files (*)",
743
+ )
744
+ if type(fn) is tuple:
745
+ self.fileName, _ = fn
746
+ else:
747
+ self.fileName = fn
748
+
749
+ if self.fileName and Path(self.fileName).suffix() != ".boris_map":
750
+ self.fileName += ".boris_map"
751
+
752
+ if self.fileName:
753
+ return self.saveMap()
754
+
755
+ return False
756
+
757
+ def newArea(self):
758
+ """
759
+ create a new area
760
+ """
761
+ if not self.bitmapFileName:
762
+ QMessageBox.critical(self, cfg.programName, "An image must be loaded before to define areas")
763
+ return
764
+
765
+ if self.selectedPolygon:
766
+ self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
767
+ self.selectedPolygon = None
768
+
769
+ self.flagNewArea = True
770
+ self.leAreaCode.clear()
771
+
772
+ for widget in (
773
+ self.btSaveArea,
774
+ self.btCancelAreaCreation,
775
+ self.lb,
776
+ self.leAreaCode,
777
+ self.btColor,
778
+ self.slAlpha,
779
+ ):
780
+ widget.setVisible(True)
781
+
782
+ self.btSaveArea.setText("Save modifier")
783
+
784
+ self.btNewArea.setVisible(False)
785
+ self.btDeleteArea.setVisible(False)
786
+
787
+ self.statusBar().showMessage(
788
+ "Draw a polygon by clicking the vertices (right-click will delete the last point, middle-click will close the polygon)"
789
+ )
790
+
791
+ def saveArea(self):
792
+ """
793
+ save the new modifier in the list
794
+ """
795
+
796
+ # check if not allowed character
797
+ for c in cfg.CHAR_FORBIDDEN_IN_MODIFIERS:
798
+ if c in self.leAreaCode.text():
799
+ QMessageBox.critical(
800
+ self,
801
+ cfg.programName,
802
+ f"The modifier contains a character that is not allowed <b>{cfg.CHAR_FORBIDDEN_IN_MODIFIERS}</b>.",
803
+ )
804
+ return
805
+
806
+ # check if modification
807
+ if "save" in self.btSaveArea.text().lower():
808
+ if not self.closedPolygon:
809
+ QMessageBox.critical(
810
+ self,
811
+ cfg.programName,
812
+ "You must close your area before saving it.\nThe last vertex must correspond to the first one.",
813
+ )
814
+
815
+ if len(self.view.points) < 3:
816
+ QMessageBox.critical(self, cfg.programName, "You must define a closed area")
817
+ return
818
+
819
+ # check if no modifier name
820
+ if not self.leAreaCode.text():
821
+ QMessageBox.critical(self, cfg.programName, "You must define a code for the new modifier")
822
+ return
823
+
824
+ # check if modifier name already used
825
+ if self.leAreaCode.text() in self.areasList:
826
+ QMessageBox.critical(self, cfg.programName, "The modifier name is already in use")
827
+ return
828
+
829
+ # create polygon
830
+ newPolygon = QPolygon()
831
+ for p in self.view.points:
832
+ newPolygon.append(QPoint(p[0], p[1]))
833
+
834
+ self.areasList[self.leAreaCode.text()] = {
835
+ "geometry": self.view.points,
836
+ "color": self.areaColor.rgba(),
837
+ }
838
+
839
+ # remove all lines
840
+ for line in self.view.elList:
841
+ self.view.scene().removeItem(line)
842
+
843
+ # draw polygon
844
+ self.closedPolygon.setBrush(QBrush(self.areaColor, Qt.SolidPattern))
845
+ self.polygonsList2[self.leAreaCode.text()] = self.closedPolygon
846
+ self.closedPolygon = None
847
+ self.view._start = 0
848
+ self.view.points = []
849
+ self.view.elList = []
850
+ self.flagNewArea = False
851
+
852
+ self.statusBar().showMessage("New modifier saved", 5000)
853
+
854
+ else: # modification
855
+ if self.leAreaCode.text() not in self.polygonsList2:
856
+ self.polygonsList2[self.leAreaCode.text()] = self.polygonsList2.pop(self.area_list.currentItem().text())
857
+ self.polygonsList2[self.leAreaCode.text()].setBrush(self.areaColor)
858
+
859
+ self.statusBar().showMessage("Modifier modified", 5000)
860
+
861
+ for widget in (
862
+ self.btSaveArea,
863
+ self.btCancelAreaCreation,
864
+ self.lb,
865
+ self.leAreaCode,
866
+ self.btColor,
867
+ self.slAlpha,
868
+ self.btDeleteArea,
869
+ self.btNewArea,
870
+ ):
871
+ widget.setVisible(False)
872
+
873
+ self.btNewArea.setVisible(True)
874
+ self.leAreaCode.setText("")
875
+ self.update_area_list()
876
+ self.flag_map_changed = True
877
+
878
+ def cancelAreaCreation(self):
879
+ if self.closedPolygon:
880
+ self.view.scene().removeItem(self.closedPolygon)
881
+ self.closedPolygon = None
882
+
883
+ # remove all lines
884
+ for line in self.view.elList:
885
+ self.view.scene().removeItem(line)
886
+
887
+ self.view.elList = []
888
+
889
+ self.view._start = 0
890
+ self.view.points = []
891
+ self.flagNewArea = False
892
+ self.btCancelAreaCreation.setVisible(False)
893
+ self.btDeleteArea.setVisible(True)
894
+ self.btSaveArea.setVisible(False)
895
+ self.lb.setVisible(False)
896
+
897
+ self.btColor.setVisible(False)
898
+ self.slAlpha.setVisible(False)
899
+ self.btNewArea.setVisible(True)
900
+
901
+ self.leAreaCode.setVisible(False)
902
+ self.leAreaCode.setText("")
903
+
904
+ def update_area_list(self):
905
+ """
906
+ update the area list widget
907
+ """
908
+ self.area_list.clear()
909
+
910
+ for modifier_name in sorted(self.polygonsList2.keys()):
911
+ self.area_list.addItem(modifier_name)
912
+
913
+ def deleteArea(self):
914
+ """
915
+ remove selected area from map
916
+ """
917
+
918
+ if self.selectedPolygon:
919
+ self.view.scene().removeItem(self.selectedPolygon)
920
+
921
+ if self.selectedPolygonAreaCode:
922
+ self.view.scene().removeItem(self.polygonsList2[self.selectedPolygonAreaCode])
923
+ del self.polygonsList2[self.selectedPolygonAreaCode]
924
+ del self.areasList[self.selectedPolygonAreaCode]
925
+
926
+ self.selectedPolygonAreaCode = None
927
+ self.selectedPolygon = None
928
+
929
+ self.flag_map_changed = True
930
+
931
+ self.view.elList = []
932
+
933
+ self.view._start = 0
934
+ self.view.points = []
935
+ self.flagNewArea = False
936
+
937
+ for widget in (self.btSaveArea, self.lb, self.btColor, self.slAlpha, self.leAreaCode):
938
+ widget.setVisible(False)
939
+
940
+ self.btNewArea.setVisible(True)
941
+
942
+ self.leAreaCode.setText("")
943
+ self.statusBar().showMessage("")
944
+
945
+ self.update_area_list()
946
+
947
+ def cancelMap(self):
948
+ """
949
+ remove current map
950
+ """
951
+ self.flagNewArea = False
952
+ self.areasList = {}
953
+ self.polygonsList2 = {}
954
+ self.view.scene().clear()
955
+ self.btDeleteArea.setVisible(False)
956
+ self.btNewArea.setVisible(False)
957
+ self.saveMapAction.setEnabled(False)
958
+ self.saveAsMapAction.setEnabled(False)
959
+ self.mapNameAction.setEnabled(False)
960
+ self.statusBar().showMessage("")
961
+ self.flag_map_changed = False
962
+
963
+ def loadBitmap(self):
964
+ """
965
+ load bitmap as background for coding map
966
+ """
967
+
968
+ fileName, _ = QFileDialog().getOpenFileName(self, "Load bitmap", "", "bitmap files (*.png *.jpg);;All files (*)")
969
+
970
+ if not fileName:
971
+ return
972
+ self.bitmapFileName = fileName
973
+
974
+ self.pixmap.load(self.bitmapFileName)
975
+
976
+ # scale image
977
+ """
978
+ if self.pixmap.size().width() > maxSize or self.pixmap.size().height() > maxSize:
979
+ self.pixmap = self.pixmap.scaled(maxSize, maxSize, Qt.KeepAspectRatio)
980
+ QMessageBox.information(
981
+ self,
982
+ cfg.programName,
983
+ "The bitmap was resized to %d x %d pixels\nThe original file was not modified"
984
+ % (self.pixmap.size().width(), self.pixmap.size().height()),
985
+ )
986
+ """
987
+
988
+ self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height())
989
+ pixitem = QGraphicsPixmapItem(self.pixmap)
990
+ pixitem.setPos(0, 0)
991
+ self.view.scene().addItem(pixitem)
992
+
993
+ self.btNewArea.setVisible(True)
994
+
995
+ self.saveMapAction.setEnabled(True)
996
+ self.saveAsMapAction.setEnabled(True)
997
+ self.mapNameAction.setEnabled(True)
998
+
999
+ self.statusBar().showMessage("""Click "New modifier" to create a new modifier""")
1000
+
1001
+ self.flag_map_changed = True
1002
+
1003
+
1004
+ if __name__ == "__main__":
1005
+ import sys
1006
+
1007
+ app = QApplication(sys.argv)
1008
+ window = ModifiersMapCreatorWindow()
1009
+
1010
+ gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1011
+
1012
+ window.show()
1013
+ sys.exit(app.exec())