panorama-tools 0.0.2__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.
panorama/__init__.py ADDED
File without changes
panorama/binary.py ADDED
@@ -0,0 +1,577 @@
1
+
2
+ from PyQt6.QtCore import *
3
+ from PyQt6.QtGui import *
4
+ from PyQt6.QtWidgets import *
5
+ from panorama import signals
6
+ import html
7
+ import string
8
+
9
+
10
+ PRINTABLE = string.ascii_letters + string.digits + string.punctuation + " "
11
+
12
+ def formatChar(byte: int) -> str:
13
+ """
14
+ Returns the character that is shown in the ascii view for a given byte.
15
+ """
16
+ char = chr(byte)
17
+ if char in PRINTABLE:
18
+ return html.escape(char)
19
+ return "."
20
+
21
+
22
+ class TextStream:
23
+ _text: str
24
+ _color: str | None
25
+ _bgcolor: str | None
26
+
27
+ def __init__(self):
28
+ self._text = ""
29
+ self._color = None
30
+ self._bgcolor = None
31
+
32
+ def setColor(self, color: str | None) -> None:
33
+ if color != self._color:
34
+ if self._color or self._bgcolor:
35
+ self._text += "</span>"
36
+ self._color = color
37
+ self._applyStyle()
38
+
39
+ def setBackground(self, color: str | None) -> None:
40
+ if color != self._bgcolor:
41
+ if self._color or self._bgcolor:
42
+ self._text += "</span>"
43
+ self._bgcolor = color
44
+ self._applyStyle()
45
+
46
+ def write(self, text: str) -> None:
47
+ self._text += text.replace("\n", "<br>").replace(" ", "&nbsp;")
48
+
49
+ def get(self) -> str:
50
+ if self._color or self._bgcolor:
51
+ self._text += "</span>"
52
+ self._color = None
53
+ self._bgcolor = None
54
+ return self._text
55
+
56
+ def _applyStyle(self) -> None:
57
+ style = []
58
+ if self._color:
59
+ style.append(f"color: {self._color}")
60
+ if self._bgcolor:
61
+ style.append(f"background-color: {self._bgcolor}")
62
+
63
+ if style:
64
+ self._text += f"<span style='{"; ".join(style)}'>"
65
+
66
+
67
+ class BinaryContextMenu(QMenu):
68
+ copy: QAction
69
+ selectAll: QAction
70
+
71
+ def __init__(self, parent: QWidget | None = None):
72
+ super().__init__(parent)
73
+ self.copy = QAction("Copy")
74
+ self.copy.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditCopy))
75
+ self.copy.setShortcut(QKeySequence.StandardKey.Copy)
76
+
77
+ self.selectAll = QAction("Select All")
78
+ self.selectAll.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditSelectAll))
79
+ self.selectAll.setShortcut(QKeySequence.StandardKey.SelectAll)
80
+
81
+ self.addAction(self.copy)
82
+ self.addSeparator()
83
+ self.addAction(self.selectAll)
84
+
85
+
86
+ class BinaryView(QTextEdit):
87
+ viewChanged: signals.Signal
88
+ """Emitted when the size of the view changes."""
89
+
90
+ wheel: signals.Signal[QWheelEvent]
91
+ """Emitted when a scroll wheel event occurs on the view."""
92
+
93
+ scrollRequest: signals.Signal[int]
94
+ """
95
+ Emitted when the view should scroll by the given number of rows, either due
96
+ to mouse of keyboard events.
97
+ """
98
+
99
+ jump: signals.Signal[int]
100
+ """Emitted when the view should jump to the given offset in the file."""
101
+
102
+ _data: bytes
103
+ """Content of the file that is shown in the binary view."""
104
+
105
+ _base: int
106
+ """Current base address of the view."""
107
+
108
+ _rows: int
109
+ """Number of rows that are currently displayed."""
110
+
111
+ _end: int
112
+ """End address of the range that is currently displayed."""
113
+
114
+ _cursorAddr: int
115
+ """Address of the cursor."""
116
+
117
+ _cursorOffset: int
118
+ """Position of the cursor in the current byte."""
119
+
120
+ _cursorAscii: bool
121
+ """Whether the cursor is active in the hex or ascii view."""
122
+
123
+ _selectionAnchor: int
124
+ """The address at which the user began making a selection."""
125
+
126
+ _selectionStart: int
127
+ """The lowest address of the selection."""
128
+
129
+ _selectionEnd: int
130
+ """The highest address of the selection."""
131
+
132
+ _selectionAscii: bool
133
+ """Whether the user is making a selection in the hex or ascii view."""
134
+
135
+ _mousePos: int
136
+ """
137
+ Current position of the mouse relative to the top of the view, while the
138
+ user is dragging a selection. This is used to determine how fast the view
139
+ should scroll when the user moves the mouse outside of the view.
140
+ """
141
+
142
+ _timer: QTimer
143
+ """
144
+ Timer that periodically scrolls the view while the user drags a selection
145
+ outside of the view.
146
+ """
147
+
148
+ def __init__(self, data: bytes):
149
+ super().__init__()
150
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
151
+ self.verticalScrollBar().valueChanged.connect(self._resetScroll)
152
+
153
+ self._data = data
154
+
155
+ self.viewChanged = signals.Signal()
156
+ self.wheel = signals.Signal()
157
+ self.scrollRequest = signals.Signal()
158
+ self.jump = signals.Signal()
159
+
160
+ self._base = 0
161
+ self._rows = 0
162
+ self._end = 0
163
+
164
+ self._cursorAddr = 0
165
+ self._cursorOffset = 0
166
+ self._cursorAscii = False
167
+
168
+ self._selectionAnchor = 0
169
+ self._selectionStart = 0
170
+ self._selectionEnd = 0
171
+ self._selectionAscii = False
172
+
173
+ self._mousePos = 0
174
+
175
+ self._timer = QTimer()
176
+ self._timer.timeout.connect(self._updateScroll)
177
+ self._timer.setInterval(50)
178
+ self._timer.start()
179
+
180
+ def setRange(self, offset: int, rows: int) -> None:
181
+ self._base = offset
182
+ self._rows = rows
183
+ self._end = min(offset + rows * 16, len(self._data) + 1)
184
+ self._updateText()
185
+
186
+ def resizeEvent(self, e: QResizeEvent) -> None:
187
+ super().resizeEvent(e)
188
+ self.viewChanged.emit()
189
+
190
+ def wheelEvent(self, e: QWheelEvent) -> None:
191
+ self.wheel.emit(e)
192
+
193
+ def contextMenuEvent(self, e: QContextMenuEvent) -> None:
194
+ menu = BinaryContextMenu(self)
195
+ menu.copy.setEnabled(self._selectionEnd > self._selectionStart)
196
+
197
+ menu.copy.triggered.connect(self._handleCopy)
198
+ menu.selectAll.triggered.connect(self._handleSelectAll)
199
+
200
+ menu.exec(e.globalPos())
201
+
202
+ def keyPressEvent(self, e: QKeyEvent) -> None:
203
+ self._handleKeyPress(e)
204
+ self._updateText()
205
+
206
+ def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
207
+ self.mousePressEvent(e)
208
+
209
+ def mousePressEvent(self, e: QMouseEvent) -> None:
210
+ if e.button() == Qt.MouseButton.LeftButton:
211
+ cursor = self.cursorForPosition(e.pos())
212
+ self._cursorAddr, self._cursorOffset, self._cursorAscii = \
213
+ self._getCursorInfo(cursor.position())
214
+
215
+ self._clampCursor()
216
+
217
+ self._selectionAnchor = self._selectionAddr(
218
+ self._cursorAddr, self._cursorOffset, self._cursorAscii
219
+ )
220
+ self._selectionStart = self._selectionAnchor
221
+ self._selectionEnd = self._selectionAnchor
222
+ self._selectionAscii = self._cursorAscii
223
+
224
+ self._updateText()
225
+
226
+ def mouseMoveEvent(self, e):
227
+ if e.buttons() & Qt.MouseButton.LeftButton:
228
+ self._mousePos = e.y()
229
+
230
+ cursor = self.cursorForPosition(e.pos())
231
+ self._cursorAddr, self._cursorOffset, self._cursorAscii = \
232
+ self._getCursorInfo(cursor.position(), self._cursorAscii)
233
+
234
+ self._clampCursor()
235
+
236
+ selectionPos = self._selectionAddr(
237
+ self._cursorAddr, self._cursorOffset, self._cursorAscii
238
+ )
239
+ self._moveSelectionTo(selectionPos)
240
+
241
+ self._updateText()
242
+
243
+ def mouseReleaseEvent(self, e):
244
+ if e.button() == Qt.MouseButton.LeftButton:
245
+ self._mousePos = 0
246
+
247
+ def _resetScroll(self) -> None:
248
+ """Disables functionality of the scroll bar."""
249
+ self.verticalScrollBar().setValue(0)
250
+
251
+ def _updateScroll(self) -> None:
252
+ """
253
+ Called every 50 milliseconds. This scrolls the view while the user drags
254
+ a selection outside of the view.
255
+ """
256
+
257
+ if self._mousePos < 0:
258
+ self.scrollRequest.emit(self._mousePos // 10)
259
+ elif self._mousePos > self.height():
260
+ self.scrollRequest.emit((self._mousePos - self.height()) // 10)
261
+
262
+ def _handleKeyPress(self, e: QKeyEvent) -> None:
263
+ self._checkKeySequence(e)
264
+ self._checkNavigationKeys(e)
265
+
266
+ def _checkKeySequence(self, e: QKeyEvent) -> None:
267
+ if e.matches(QKeySequence.StandardKey.Copy):
268
+ self._handleCopy()
269
+ elif e.matches(QKeySequence.StandardKey.SelectAll):
270
+ self._handleSelectAll()
271
+
272
+ def _checkNavigationKeys(self, e: QKeyEvent) -> None:
273
+ key = e.key()
274
+
275
+ if key == Qt.Key.Key_Left:
276
+ if self._cursorOffset != 0:
277
+ self._cursorOffset = 0
278
+ elif self._cursorAddr > 0:
279
+ self._cursorAddr -= 1
280
+
281
+ elif key == Qt.Key.Key_Right:
282
+ self._cursorOffset = 0
283
+ if self._cursorAddr < len(self._data):
284
+ self._cursorAddr += 1
285
+
286
+ elif key == Qt.Key.Key_Up:
287
+ if self._cursorAddr >= 16:
288
+ self._cursorAddr -= 16
289
+
290
+ elif key == Qt.Key.Key_Down:
291
+ if self._cursorAddr <= len(self._data) - 16:
292
+ self._cursorAddr += 16
293
+
294
+ elif key == Qt.Key.Key_PageUp:
295
+ maxRows = self._cursorAddr // 16
296
+ self._cursorAddr -= min(maxRows, self._rows) * 16
297
+ self.scrollRequest.emit(-self._rows)
298
+
299
+ elif key == Qt.Key.Key_PageDown:
300
+ maxRows = (len(self._data) - self._cursorAddr) // 16
301
+ self._cursorAddr += min(maxRows, self._rows) * 16
302
+ self.scrollRequest.emit(self._rows)
303
+
304
+ elif key == Qt.Key.Key_Home:
305
+ self._cursorAddr &= ~15
306
+ self._cursorOffset = 0
307
+
308
+ elif key == Qt.Key.Key_End:
309
+ self._cursorAddr = ((self._cursorAddr + 16) & ~15)
310
+
311
+ self._cursorOffset = 0
312
+ if self._cursorAddr % 16 == 0 and self._cursorAddr != 0:
313
+ self._cursorAddr -= 1
314
+ self._cursorOffset = 2 - self._cursorAscii
315
+
316
+ if key in [
317
+ Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
318
+ Qt.Key.Key_PageUp, Qt.Key.Key_PageDown, Qt.Key.Key_Home,
319
+ Qt.Key.Key_End
320
+ ]:
321
+ modifiers = e.modifiers()
322
+ shift = bool(modifiers & Qt.KeyboardModifier.ShiftModifier)
323
+
324
+ self._clampCursor()
325
+
326
+ if shift:
327
+ selectionPos = self._selectionAddr(
328
+ self._cursorAddr, self._cursorOffset, self._cursorAscii
329
+ )
330
+ self._moveSelectionTo(selectionPos)
331
+ else:
332
+ self._clearSelection()
333
+
334
+ self._jumpToCursor()
335
+
336
+ def _handleCopy(self) -> None:
337
+ data = self._data[self._selectionStart : self._selectionEnd]
338
+ if not data:
339
+ return
340
+
341
+ if self._selectionAscii:
342
+ text = data.decode("charmap")
343
+ else:
344
+ text = data.hex()
345
+
346
+ clipboard = QGuiApplication.clipboard()
347
+ clipboard.setText(text)
348
+
349
+ def _handleSelectAll(self) -> None:
350
+ self._cursorAddr = len(self._data)
351
+ self._cursorOffset = 0
352
+
353
+ self._selectionStart = 0
354
+ self._selectionAnchor = 0
355
+ self._selectionEnd = len(self._data)
356
+
357
+ self._clampCursor()
358
+ self._updateText()
359
+ self._jumpToCursor()
360
+
361
+ def _jumpToCursor(self) -> None:
362
+ if self._cursorAddr < self._base:
363
+ self.jump.emit(self._cursorAddr & ~0xF)
364
+ elif self._cursorAddr >= self._end:
365
+ addr = (self._cursorAddr & ~0xF) - (self._rows - 1) * 16
366
+ self.jump.emit(addr)
367
+
368
+ def _clearSelection(self) -> None:
369
+ """
370
+ Clears the selection, e.g. when the user presses an arrow key without
371
+ holding shift.
372
+ """
373
+ addr = self._selectionAddr(
374
+ self._cursorAddr, self._cursorOffset, self._cursorAscii
375
+ )
376
+ self._selectionAnchor = addr
377
+ self._selectionStart = addr
378
+ self._selectionEnd = addr
379
+
380
+ def _moveSelectionTo(self, addr):
381
+ if addr < self._selectionAnchor:
382
+ self._selectionStart = addr
383
+ self._selectionEnd = self._selectionAnchor
384
+ else:
385
+ self._selectionStart = self._selectionAnchor
386
+ self._selectionEnd = addr
387
+
388
+ def _selectionAddr(self, pos, offset, view):
389
+ if pos % 16 == 15 or pos == len(self._data) - 1:
390
+ if view and offset:
391
+ return pos + 1
392
+ return pos + (offset == 2)
393
+ return pos
394
+
395
+ def _clampCursor(self):
396
+ if self._cursorAddr > len(self._data):
397
+ self._cursorAddr = len(self._data)
398
+
399
+ if self._cursorAddr == len(self._data):
400
+ self._cursorOffset = 0
401
+ if self._cursorAddr != 0:
402
+ self._cursorAddr -= 1
403
+ if self._cursorAscii:
404
+ self._cursorOffset = 1
405
+ else:
406
+ self._cursorOffset = 2
407
+
408
+ def _getCursorInfo(
409
+ self, pos: int, ascii: bool | None = None
410
+ ) -> tuple[int, int, bool]:
411
+ """
412
+ From the given cursor position in the text, computes the address of the
413
+ byte under the cursor, the location of the cursor within that byte, and
414
+ whether the cursor is in the hex or ascii view.
415
+
416
+ If the ascii parameter is provided and not None, it forces the cursor to
417
+ be either in the hex or ascii view.
418
+ """
419
+
420
+ row = max(pos // 76, 2)
421
+ col = max(pos % 76, 10)
422
+
423
+ if ascii is not None:
424
+ if ascii:
425
+ col = max(col, 59)
426
+ else:
427
+ col = min(col, 57)
428
+
429
+ base = self._base + (row - 2) * 16
430
+ if col < 57:
431
+ return base + (col - 9) // 3, col % 3 == 2, False
432
+ if col == 57:
433
+ return base + 15, 2, False
434
+ if col == 58:
435
+ return base, 0, True
436
+ if col < 75:
437
+ return base + col - 59, 0, True
438
+ return base + 15, 1, True
439
+
440
+ def _getCursorPos(self, addr: int, offset: int, ascii: bool) -> int:
441
+ """
442
+ Computes the position for a cursor in the text that corresponds to the
443
+ given byte address, cursor offset within that byte, and view.
444
+ """
445
+ row = 76 * ((addr - self._base) // 16 + 2)
446
+ if ascii:
447
+ return row + 59 + addr % 16 + offset
448
+ return row + 10 + 3 * (addr % 16) + offset
449
+
450
+ def _updateCursor(self) -> None:
451
+ if self._cursorAddr >= self._base and self._cursorAddr < self._end:
452
+ pos = self._getCursorPos(
453
+ self._cursorAddr, self._cursorOffset, self._cursorAscii
454
+ )
455
+
456
+ cursor = QTextCursor(self.document())
457
+ cursor.setPosition(pos)
458
+ self.setTextCursor(cursor)
459
+ else:
460
+ self.setTextCursor(QTextCursor())
461
+
462
+ def _updateColor(self, stream: TextStream, addr: int, ascii: bool) -> None:
463
+ if self._selectionStart <= addr < self._selectionEnd:
464
+ if ascii == self._selectionAscii:
465
+ stream.setBackground("blue")
466
+ else:
467
+ stream.setBackground("lightblue")
468
+ stream.setColor("white")
469
+
470
+ else:
471
+ if not ascii and addr % 2:
472
+ stream.setColor("gray")
473
+ else:
474
+ stream.setColor(None)
475
+ stream.setBackground(None)
476
+
477
+ def _updateText(self) -> None:
478
+ stream = TextStream()
479
+ stream.setColor("blue")
480
+ stream.write("Binary ")
481
+ stream.write("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
482
+ stream.write(" ")
483
+ stream.write("0123456789ABCDEF\n")
484
+ stream.write(" " * 75 + "\n")
485
+
486
+ addr = self._base
487
+ data = self._data[self._base : self._end]
488
+ while data:
489
+ stream.setColor("blue")
490
+ stream.write(f"{addr:08X} ")
491
+
492
+ for i in range(min(len(data), 16)):
493
+ stream.write(" ")
494
+ self._updateColor(stream, addr + i, False)
495
+ stream.write(f"{data[i]:02X}")
496
+
497
+ stream.setColor(None)
498
+ stream.setBackground(None)
499
+ stream.write(" " * (2 + (15 - i) * 3))
500
+
501
+ for i, byte in enumerate(data[:16]):
502
+ self._updateColor(stream, addr + i, True)
503
+ stream.write(formatChar(byte))
504
+
505
+ stream.setColor(None)
506
+ stream.setBackground(None)
507
+ stream.write("\n")
508
+
509
+ data = data[16:]
510
+ addr += 16
511
+
512
+ if not self._data:
513
+ stream.setColor("blue")
514
+ stream.write("00000000 " + " " * 50)
515
+
516
+ self.setText(stream.get())
517
+
518
+ self._updateCursor()
519
+
520
+
521
+ class BinaryWidget(QWidget):
522
+ _scrollBar: QScrollBar
523
+ _font: QFont
524
+ _metrics: QFontMetrics
525
+ _view: BinaryView
526
+ _layout: QHBoxLayout
527
+
528
+ def __init__(self, data):
529
+ super().__init__()
530
+ self._data = data
531
+
532
+ self._scrollBar = QScrollBar()
533
+ self._scrollBar.setRange(0, len(data) // 16)
534
+ self._scrollBar.valueChanged.connect(self._updateView)
535
+
536
+ self._font = \
537
+ QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
538
+ self._font.setPixelSize(14)
539
+ self.setFont(self._font)
540
+
541
+ self._metrics = QFontMetrics(self._font)
542
+
543
+ width = self._metrics.boundingRect("_" * 75).width() + 10
544
+
545
+ self._view = BinaryView(data)
546
+ self._view.setMinimumWidth(width)
547
+ self._view.viewChanged.connect(self._updateView)
548
+ self._view.wheel.connect(self._scrollBar.wheelEvent)
549
+ self._view.scrollRequest.connect(self._moveScroll)
550
+ self._view.jump.connect(self._handleJump)
551
+
552
+ self._layout = QHBoxLayout(self)
553
+ self._layout.addWidget(self._view)
554
+ self._layout.addWidget(self._scrollBar)
555
+
556
+ self._updateView()
557
+
558
+ def _moveScroll(self, offset: int) -> None:
559
+ self._scrollBar.setValue(self._scrollBar.value() + offset)
560
+ self._updateView()
561
+
562
+ def _handleJump(self, address: int) -> None:
563
+ rows = int(
564
+ (self._view.height() - 10) / (self._metrics.lineSpacing() + 1)
565
+ )
566
+ rows -= 2
567
+
568
+ self._view.setRange(address, rows)
569
+
570
+ scroll = max((len(self._data) + 15) // 16 - rows, 0)
571
+ self._scrollBar.setVisible(scroll != 0)
572
+ self._scrollBar.setRange(0, scroll)
573
+ self._scrollBar.setValue(address // 16)
574
+
575
+ def _updateView(self) -> None:
576
+ offset = self._scrollBar.value() * 16
577
+ self._handleJump(offset)
panorama/colors.py ADDED
@@ -0,0 +1,7 @@
1
+
2
+ AUDIO = "#c00"
3
+ ARCHIVE = "#a50"
4
+ COMPRESSION = "#888"
5
+ GRAPHICS = "#f88"
6
+ PROPERTIES = "#c00"
7
+ TEXT = "#00c"
panorama/filesystem.py ADDED
@@ -0,0 +1,95 @@
1
+
2
+ """
3
+ Provides nodes for files and folders that are stored on disk.
4
+ """
5
+
6
+ from PyQt6.QtWidgets import *
7
+ from panorama import nodes, plugins
8
+ import io
9
+ import mmap
10
+ import os
11
+ import qtawesome
12
+
13
+
14
+ class FileReader(nodes.Reader):
15
+ """Reader for files that are stored on disk."""
16
+
17
+ _path: str
18
+
19
+ def __init__(self, path: str):
20
+ self._path = path
21
+
22
+ def path(self) -> str:
23
+ return self._path
24
+
25
+ def filename(self) -> str:
26
+ return os.path.basename(self._path)
27
+
28
+ def read(self) -> bytes | mmap.mmap:
29
+ """
30
+ Opens the file. Files that are smaller than 1 MB are read completely
31
+ into memory. Larger files are mapped with mmap.
32
+ """
33
+ with open(self._path, "rb") as f:
34
+ f.seek(0, io.SEEK_END)
35
+ size = f.tell()
36
+ f.seek(0)
37
+
38
+ if size < 1024 * 1024:
39
+ return f.read()
40
+ return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
41
+
42
+
43
+ class Folder(nodes.Node):
44
+ """Represents a folder on disk."""
45
+
46
+ _plugins: plugins.Plugins
47
+ _path: str
48
+
49
+ def __init__(self, plugins: plugins.Plugins, path: str):
50
+ super().__init__()
51
+ self._plugins = plugins
52
+ self._path = path
53
+
54
+ self.setText(0, os.path.basename(path))
55
+ self.setIcon(0, qtawesome.icon("fa5s.folder", color="#fc0"))
56
+
57
+ self.showIndicator(bool(os.listdir(self._path)))
58
+
59
+ def path(self) -> str:
60
+ return self._path
61
+
62
+ def priority(self) -> int:
63
+ return -1 # Show folders before everything else
64
+
65
+ def createChildren(self) -> None:
66
+ for name in os.listdir(self._path):
67
+ childpath = os.path.join(self._path, name)
68
+ if os.path.isdir(childpath):
69
+ self.addChild(Folder(self._plugins, childpath))
70
+ elif os.path.isfile(childpath):
71
+ reader = FileReader(childpath)
72
+ self.addChild(self._plugins.create(reader))
73
+ else:
74
+ self.addChild(Other(childpath))
75
+
76
+
77
+ class Other(nodes.Node):
78
+ """
79
+ Special filesystem node.
80
+
81
+ Represents a filesystem node that is neither a folder nor a regular file.
82
+ For example, this could be a symbolic link or a Unix socket file.
83
+ """
84
+
85
+ _path: str
86
+
87
+ def __init__(self, path: str):
88
+ super().__init__()
89
+ self._path = path
90
+
91
+ self.setText(0, os.path.basename(path))
92
+ self.setIcon(0, qtawesome.icon("fa5s.question"))
93
+
94
+ def path(self) -> str:
95
+ return self._path