lazylabel-gui 1.3.3__py3-none-any.whl → 1.3.5__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.
- lazylabel/core/file_manager.py +1 -1
- lazylabel/models/sam2_model.py +253 -134
- lazylabel/ui/control_panel.py +7 -2
- lazylabel/ui/main_window.py +264 -593
- lazylabel/ui/photo_viewer.py +35 -11
- lazylabel/ui/widgets/channel_threshold_widget.py +8 -9
- lazylabel/ui/widgets/fft_threshold_widget.py +4 -0
- lazylabel/ui/widgets/model_selection_widget.py +9 -0
- lazylabel/ui/workers/__init__.py +15 -0
- lazylabel/ui/workers/image_discovery_worker.py +66 -0
- lazylabel/ui/workers/multi_view_sam_init_worker.py +135 -0
- lazylabel/ui/workers/multi_view_sam_update_worker.py +158 -0
- lazylabel/ui/workers/sam_update_worker.py +129 -0
- lazylabel/ui/workers/single_view_sam_init_worker.py +61 -0
- lazylabel/utils/fast_file_manager.py +422 -78
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/RECORD +21 -15
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/top_level.txt +0 -0
@@ -17,21 +17,159 @@ from PyQt6.QtCore import (
|
|
17
17
|
)
|
18
18
|
from PyQt6.QtGui import QPixmap
|
19
19
|
from PyQt6.QtWidgets import (
|
20
|
-
QComboBox,
|
21
20
|
QHBoxLayout,
|
22
21
|
QHeaderView,
|
23
22
|
QLabel,
|
24
23
|
QLineEdit,
|
24
|
+
QMenu,
|
25
25
|
QPushButton,
|
26
26
|
QTableView,
|
27
|
+
QToolButton,
|
27
28
|
QVBoxLayout,
|
28
29
|
QWidget,
|
29
30
|
)
|
30
31
|
|
32
|
+
from ..utils.logger import logger
|
33
|
+
|
31
34
|
# Image extensions supported
|
32
35
|
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
33
36
|
|
34
37
|
|
38
|
+
class CustomDropdown(QToolButton):
|
39
|
+
"""Custom dropdown using QToolButton + QMenu for reliable closing behavior."""
|
40
|
+
|
41
|
+
activated = pyqtSignal(int)
|
42
|
+
|
43
|
+
def __init__(self, parent=None):
|
44
|
+
super().__init__(parent)
|
45
|
+
self.setText("⚏") # Grid/settings icon
|
46
|
+
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
47
|
+
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
|
48
|
+
|
49
|
+
# Create the menu
|
50
|
+
self.menu = QMenu(self)
|
51
|
+
self.setMenu(self.menu)
|
52
|
+
|
53
|
+
# Store items for access
|
54
|
+
self.items = []
|
55
|
+
|
56
|
+
# Style to match app theme (dark theme with consistent colors)
|
57
|
+
self.setStyleSheet("""
|
58
|
+
QToolButton {
|
59
|
+
background-color: rgba(40, 40, 40, 0.8);
|
60
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
61
|
+
border-radius: 6px;
|
62
|
+
color: #E0E0E0;
|
63
|
+
font-size: 10px;
|
64
|
+
padding: 5px 8px;
|
65
|
+
text-align: left;
|
66
|
+
min-width: 30px;
|
67
|
+
max-width: 30px;
|
68
|
+
}
|
69
|
+
QToolButton:hover {
|
70
|
+
background-color: rgba(60, 60, 60, 0.8);
|
71
|
+
border-color: rgba(90, 120, 150, 0.8);
|
72
|
+
}
|
73
|
+
QToolButton:pressed {
|
74
|
+
background-color: rgba(70, 100, 130, 0.8);
|
75
|
+
}
|
76
|
+
QToolButton::menu-indicator {
|
77
|
+
subcontrol-origin: padding;
|
78
|
+
subcontrol-position: top right;
|
79
|
+
width: 16px;
|
80
|
+
border-left: 1px solid rgba(80, 80, 80, 0.6);
|
81
|
+
}
|
82
|
+
QMenu {
|
83
|
+
background-color: rgba(50, 50, 50, 0.9);
|
84
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
85
|
+
color: #E0E0E0;
|
86
|
+
}
|
87
|
+
QMenu::item {
|
88
|
+
padding: 4px 8px;
|
89
|
+
}
|
90
|
+
QMenu::item:selected {
|
91
|
+
background-color: rgba(100, 100, 200, 0.5);
|
92
|
+
}
|
93
|
+
""")
|
94
|
+
|
95
|
+
def addCheckableItem(self, text, checked=True, data=None):
|
96
|
+
"""Add a checkable item to the dropdown."""
|
97
|
+
action = self.menu.addAction(text)
|
98
|
+
action.setCheckable(True)
|
99
|
+
action.setChecked(checked)
|
100
|
+
action.setData(data)
|
101
|
+
self.items.append((text, data, action))
|
102
|
+
|
103
|
+
# Connect to selection handler
|
104
|
+
action.triggered.connect(
|
105
|
+
lambda checked_state, idx=len(self.items) - 1: self._on_item_toggled(
|
106
|
+
idx, checked_state
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
110
|
+
def clear(self):
|
111
|
+
"""Clear all items."""
|
112
|
+
self.menu.clear()
|
113
|
+
self.items.clear()
|
114
|
+
|
115
|
+
def _on_item_toggled(self, index, checked):
|
116
|
+
"""Handle item toggle."""
|
117
|
+
if 0 <= index < len(self.items):
|
118
|
+
self.activated.emit(index)
|
119
|
+
|
120
|
+
def isItemChecked(self, index):
|
121
|
+
"""Check if item at index is checked."""
|
122
|
+
if 0 <= index < len(self.items):
|
123
|
+
return self.items[index][2].isChecked()
|
124
|
+
return False
|
125
|
+
|
126
|
+
def setItemChecked(self, index, checked):
|
127
|
+
"""Set checked state of item at index."""
|
128
|
+
if 0 <= index < len(self.items):
|
129
|
+
self.items[index][2].setChecked(checked)
|
130
|
+
|
131
|
+
def addItem(self, text, data=None):
|
132
|
+
"""Add a non-checkable item to the dropdown (QComboBox compatibility)."""
|
133
|
+
action = self.menu.addAction(text)
|
134
|
+
action.setCheckable(False)
|
135
|
+
action.setData(data)
|
136
|
+
self.items.append((text, data, action))
|
137
|
+
|
138
|
+
# Connect to selection handler
|
139
|
+
action.triggered.connect(lambda: self._on_item_selected(len(self.items) - 1))
|
140
|
+
|
141
|
+
def _on_item_selected(self, index):
|
142
|
+
"""Handle item selection (for non-checkable items)."""
|
143
|
+
if 0 <= index < len(self.items):
|
144
|
+
text, data, action = self.items[index]
|
145
|
+
self.setText(text)
|
146
|
+
self.activated.emit(index)
|
147
|
+
|
148
|
+
def count(self):
|
149
|
+
"""Return number of items (QComboBox compatibility)."""
|
150
|
+
return len(self.items)
|
151
|
+
|
152
|
+
def itemData(self, index):
|
153
|
+
"""Get data for item at index (QComboBox compatibility)."""
|
154
|
+
if 0 <= index < len(self.items):
|
155
|
+
return self.items[index][1]
|
156
|
+
return None
|
157
|
+
|
158
|
+
def setCurrentIndex(self, index):
|
159
|
+
"""Set current selection index (QComboBox compatibility)."""
|
160
|
+
if 0 <= index < len(self.items):
|
161
|
+
text, data, action = self.items[index]
|
162
|
+
self.setText(text)
|
163
|
+
|
164
|
+
def currentIndex(self):
|
165
|
+
"""Get current selection index (QComboBox compatibility)."""
|
166
|
+
current_text = self.text()
|
167
|
+
for i, (text, _data, _action) in enumerate(self.items):
|
168
|
+
if text == current_text:
|
169
|
+
return i
|
170
|
+
return -1
|
171
|
+
|
172
|
+
|
35
173
|
@dataclass
|
36
174
|
class FileInfo:
|
37
175
|
"""Information about a file"""
|
@@ -121,7 +259,7 @@ class FileScanner(QThread):
|
|
121
259
|
self.scanComplete.emit(total_files)
|
122
260
|
|
123
261
|
except Exception as e:
|
124
|
-
|
262
|
+
logger.error(f"Error scanning directory: {e}")
|
125
263
|
|
126
264
|
def stop(self):
|
127
265
|
"""Stop the scanning thread"""
|
@@ -139,31 +277,94 @@ class FastFileModel(QAbstractTableModel):
|
|
139
277
|
self._path_to_index: dict[str, int] = {} # For O(1) lookups
|
140
278
|
self._scanner: FileScanner | None = None
|
141
279
|
|
280
|
+
# Column management - New order: Name, NPZ, TXT, Modified, Size
|
281
|
+
self._all_columns = ["Name", "NPZ", "TXT", "Modified", "Size"]
|
282
|
+
self._column_map = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} # logical to physical mapping
|
283
|
+
self._visible_columns = [True, True, True, True, True] # Default all visible
|
284
|
+
|
142
285
|
def rowCount(self, parent=QModelIndex()):
|
143
286
|
return len(self._files)
|
144
287
|
|
145
288
|
def columnCount(self, parent=QModelIndex()):
|
146
|
-
return
|
289
|
+
return sum(self._visible_columns) # Count visible columns
|
290
|
+
|
291
|
+
def getVisibleColumnIndex(self, logical_column):
|
292
|
+
"""Convert logical column index to visible column index"""
|
293
|
+
if (
|
294
|
+
logical_column >= len(self._visible_columns)
|
295
|
+
or not self._visible_columns[logical_column]
|
296
|
+
):
|
297
|
+
return -1
|
298
|
+
|
299
|
+
visible_index = 0
|
300
|
+
for i in range(logical_column):
|
301
|
+
if self._visible_columns[i]:
|
302
|
+
visible_index += 1
|
303
|
+
return visible_index
|
304
|
+
|
305
|
+
def getLogicalColumnIndex(self, visible_column):
|
306
|
+
"""Convert visible column index to logical column index"""
|
307
|
+
visible_count = 0
|
308
|
+
for i, visible in enumerate(self._visible_columns):
|
309
|
+
if visible:
|
310
|
+
if visible_count == visible_column:
|
311
|
+
return i
|
312
|
+
visible_count += 1
|
313
|
+
return -1
|
314
|
+
|
315
|
+
def setColumnVisible(self, column, visible):
|
316
|
+
"""Set column visibility"""
|
317
|
+
if (
|
318
|
+
0 <= column < len(self._visible_columns)
|
319
|
+
and self._visible_columns[column] != visible
|
320
|
+
):
|
321
|
+
self.beginResetModel()
|
322
|
+
self._visible_columns[column] = visible
|
323
|
+
self.endResetModel()
|
324
|
+
|
325
|
+
def isColumnVisible(self, column):
|
326
|
+
"""Check if column is visible"""
|
327
|
+
if 0 <= column < len(self._visible_columns):
|
328
|
+
return self._visible_columns[column]
|
329
|
+
return False
|
330
|
+
|
331
|
+
def moveColumn(self, from_column, to_column):
|
332
|
+
"""Move column to new position"""
|
333
|
+
if (
|
334
|
+
0 <= from_column < len(self._all_columns)
|
335
|
+
and 0 <= to_column < len(self._all_columns)
|
336
|
+
and from_column != to_column
|
337
|
+
):
|
338
|
+
self.beginResetModel()
|
339
|
+
# Move in all arrays
|
340
|
+
self._all_columns.insert(to_column, self._all_columns.pop(from_column))
|
341
|
+
self._visible_columns.insert(
|
342
|
+
to_column, self._visible_columns.pop(from_column)
|
343
|
+
)
|
344
|
+
self.endResetModel()
|
147
345
|
|
148
346
|
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
149
347
|
if not index.isValid():
|
150
348
|
return None
|
151
349
|
|
152
350
|
file_info = self._files[index.row()]
|
153
|
-
|
351
|
+
visible_col = index.column()
|
352
|
+
|
353
|
+
# Convert visible column to logical column
|
354
|
+
logical_col = self.getLogicalColumnIndex(visible_col)
|
355
|
+
if logical_col == -1:
|
356
|
+
return None
|
357
|
+
|
358
|
+
column_name = self._all_columns[logical_col]
|
154
359
|
|
155
360
|
if role == Qt.ItemDataRole.DisplayRole:
|
156
|
-
if
|
361
|
+
if column_name == "Name":
|
157
362
|
return file_info.name
|
158
|
-
elif
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
except OSError:
|
164
|
-
file_info.size = -1 # Mark as error
|
165
|
-
return self._format_size(file_info.size) if file_info.size >= 0 else "-"
|
166
|
-
elif col == 2: # Modified
|
363
|
+
elif column_name == "NPZ":
|
364
|
+
return "✓" if file_info.has_npz else ""
|
365
|
+
elif column_name == "TXT":
|
366
|
+
return "✓" if file_info.has_txt else ""
|
367
|
+
elif column_name == "Modified":
|
167
368
|
# Lazy load modified time only when displayed
|
168
369
|
if file_info.modified == 0.0:
|
169
370
|
try:
|
@@ -177,16 +378,20 @@ class FastFileModel(QAbstractTableModel):
|
|
177
378
|
if file_info.modified > 0
|
178
379
|
else "-"
|
179
380
|
)
|
180
|
-
elif
|
181
|
-
|
182
|
-
|
183
|
-
|
381
|
+
elif column_name == "Size":
|
382
|
+
# Lazy load size only when displayed
|
383
|
+
if file_info.size == 0:
|
384
|
+
try:
|
385
|
+
file_info.size = file_info.path.stat().st_size
|
386
|
+
except OSError:
|
387
|
+
file_info.size = -1 # Mark as error
|
388
|
+
return self._format_size(file_info.size) if file_info.size >= 0 else "-"
|
184
389
|
elif role == Qt.ItemDataRole.UserRole:
|
185
390
|
# Return the FileInfo object for custom access
|
186
391
|
return file_info
|
187
|
-
elif role == Qt.ItemDataRole.TextAlignmentRole and
|
188
|
-
|
189
|
-
|
392
|
+
elif role == Qt.ItemDataRole.TextAlignmentRole and column_name in [
|
393
|
+
"NPZ",
|
394
|
+
"TXT",
|
190
395
|
]: # Center checkmarks
|
191
396
|
return Qt.AlignmentFlag.AlignCenter
|
192
397
|
|
@@ -197,8 +402,9 @@ class FastFileModel(QAbstractTableModel):
|
|
197
402
|
orientation == Qt.Orientation.Horizontal
|
198
403
|
and role == Qt.ItemDataRole.DisplayRole
|
199
404
|
):
|
200
|
-
|
201
|
-
|
405
|
+
logical_col = self.getLogicalColumnIndex(section)
|
406
|
+
if logical_col >= 0 and logical_col < len(self._all_columns):
|
407
|
+
return self._all_columns[logical_col]
|
202
408
|
return None
|
203
409
|
|
204
410
|
def _format_size(self, size: int) -> str:
|
@@ -244,7 +450,14 @@ class FastFileModel(QAbstractTableModel):
|
|
244
450
|
|
245
451
|
def _on_scan_complete(self, total: int):
|
246
452
|
"""Handle scan completion"""
|
247
|
-
|
453
|
+
pass # Scan completion is handled by the UI status update
|
454
|
+
|
455
|
+
def getFileCounts(self):
|
456
|
+
"""Get counts of total files, NPZ files, and TXT files"""
|
457
|
+
total_files = len(self._files)
|
458
|
+
npz_count = sum(1 for file_info in self._files if file_info.has_npz)
|
459
|
+
txt_count = sum(1 for file_info in self._files if file_info.has_txt)
|
460
|
+
return total_files, npz_count, txt_count
|
248
461
|
|
249
462
|
def getFileInfo(self, index: int) -> FileInfo | None:
|
250
463
|
"""Get file info at index"""
|
@@ -358,12 +571,19 @@ class FileSortProxyModel(QSortFilterProxyModel):
|
|
358
571
|
if not left_info or not right_info:
|
359
572
|
return False
|
360
573
|
|
361
|
-
|
574
|
+
visible_col = left.column()
|
575
|
+
|
576
|
+
# Convert visible column to logical column
|
577
|
+
logical_col = self.sourceModel().getLogicalColumnIndex(visible_col)
|
578
|
+
if logical_col == -1:
|
579
|
+
return False
|
580
|
+
|
581
|
+
column_name = self.sourceModel()._all_columns[logical_col]
|
362
582
|
|
363
|
-
# Sort based on column
|
364
|
-
if
|
583
|
+
# Sort based on column type
|
584
|
+
if column_name == "Name":
|
365
585
|
return left_info.name.lower() < right_info.name.lower()
|
366
|
-
elif
|
586
|
+
elif column_name == "Size":
|
367
587
|
# Lazy load size if needed for sorting
|
368
588
|
if left_info.size == 0:
|
369
589
|
try:
|
@@ -376,7 +596,7 @@ class FileSortProxyModel(QSortFilterProxyModel):
|
|
376
596
|
except OSError:
|
377
597
|
right_info.size = -1
|
378
598
|
return left_info.size < right_info.size
|
379
|
-
elif
|
599
|
+
elif column_name == "Modified":
|
380
600
|
# Lazy load modified time if needed for sorting
|
381
601
|
if left_info.modified == 0.0:
|
382
602
|
try:
|
@@ -389,9 +609,9 @@ class FileSortProxyModel(QSortFilterProxyModel):
|
|
389
609
|
except OSError:
|
390
610
|
right_info.modified = -1
|
391
611
|
return left_info.modified < right_info.modified
|
392
|
-
elif
|
612
|
+
elif column_name == "NPZ":
|
393
613
|
return left_info.has_npz < right_info.has_npz
|
394
|
-
elif
|
614
|
+
elif column_name == "TXT":
|
395
615
|
return left_info.has_txt < right_info.has_txt
|
396
616
|
|
397
617
|
return False
|
@@ -437,19 +657,13 @@ class FastFileManager(QWidget):
|
|
437
657
|
self._table_view.setSortingEnabled(True)
|
438
658
|
self._table_view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
|
439
659
|
|
440
|
-
# Configure headers
|
660
|
+
# Configure headers with drag-and-drop reordering
|
441
661
|
header = self._table_view.horizontalHeader()
|
442
|
-
header.
|
443
|
-
|
444
|
-
|
445
|
-
header
|
446
|
-
|
447
|
-
2, QHeaderView.ResizeMode.ResizeToContents
|
448
|
-
) # Modified
|
449
|
-
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # NPZ
|
450
|
-
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # TXT
|
451
|
-
header.resizeSection(3, 50)
|
452
|
-
header.resizeSection(4, 50)
|
662
|
+
header.setSectionsMovable(True) # Enable drag-and-drop reordering
|
663
|
+
header.sectionMoved.connect(self._on_column_moved)
|
664
|
+
|
665
|
+
# Initial header sizing (will be updated by _update_header_sizing)
|
666
|
+
self._update_header_sizing()
|
453
667
|
|
454
668
|
# Style the table to match the existing UI
|
455
669
|
self._table_view.setStyleSheet("""
|
@@ -515,42 +729,95 @@ class FastFileManager(QWidget):
|
|
515
729
|
sort_label.setStyleSheet("color: #E0E0E0;")
|
516
730
|
layout.addWidget(sort_label)
|
517
731
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
"
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
732
|
+
# Create custom sort dropdown
|
733
|
+
class SortDropdown(QToolButton):
|
734
|
+
activated = pyqtSignal(int)
|
735
|
+
|
736
|
+
def __init__(self, parent=None):
|
737
|
+
super().__init__(parent)
|
738
|
+
self.setText("Name (A-Z)")
|
739
|
+
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
740
|
+
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
|
741
|
+
|
742
|
+
self.menu = QMenu(self)
|
743
|
+
self.setMenu(self.menu)
|
744
|
+
self.items = []
|
745
|
+
|
746
|
+
# Same style as CustomDropdown
|
747
|
+
self.setStyleSheet("""
|
748
|
+
QToolButton {
|
749
|
+
background-color: rgba(40, 40, 40, 0.8);
|
750
|
+
border: 1px solid rgba(80, 80, 80, 0.6);
|
751
|
+
border-radius: 6px;
|
752
|
+
color: #E0E0E0;
|
753
|
+
font-size: 10px;
|
754
|
+
padding: 5px 8px;
|
755
|
+
text-align: left;
|
756
|
+
min-width: 70px;
|
757
|
+
max-width: 70px;
|
758
|
+
}
|
759
|
+
QToolButton:hover {
|
760
|
+
background-color: rgba(60, 60, 60, 0.8);
|
761
|
+
border-color: rgba(90, 120, 150, 0.8);
|
762
|
+
}
|
763
|
+
QToolButton:pressed {
|
764
|
+
background-color: rgba(70, 100, 130, 0.8);
|
765
|
+
}
|
766
|
+
QToolButton::menu-indicator {
|
767
|
+
subcontrol-origin: padding;
|
768
|
+
subcontrol-position: top right;
|
769
|
+
width: 16px;
|
770
|
+
border-left: 1px solid rgba(80, 80, 80, 0.6);
|
771
|
+
}
|
772
|
+
QMenu {
|
773
|
+
background-color: rgba(50, 50, 50, 0.9);
|
774
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
775
|
+
color: #E0E0E0;
|
776
|
+
}
|
777
|
+
QMenu::item {
|
778
|
+
padding: 4px 8px;
|
779
|
+
}
|
780
|
+
QMenu::item:selected {
|
781
|
+
background-color: rgba(100, 100, 200, 0.5);
|
782
|
+
}
|
783
|
+
""")
|
784
|
+
|
785
|
+
def addItem(self, text, data=None):
|
786
|
+
action = self.menu.addAction(text)
|
787
|
+
action.setData(data)
|
788
|
+
self.items.append((text, data))
|
789
|
+
action.triggered.connect(
|
790
|
+
lambda checked, idx=len(self.items) - 1: self._on_item_selected(idx)
|
791
|
+
)
|
792
|
+
if len(self.items) == 1:
|
793
|
+
self.setText(text)
|
794
|
+
|
795
|
+
def _on_item_selected(self, index):
|
796
|
+
if 0 <= index < len(self.items):
|
797
|
+
text, data = self.items[index]
|
798
|
+
self.setText(text)
|
799
|
+
self.activated.emit(index)
|
800
|
+
|
801
|
+
self._sort_combo = SortDropdown()
|
802
|
+
self._sort_combo.addItem("Name (A-Z)", 0)
|
803
|
+
self._sort_combo.addItem("Name (Z-A)", 1)
|
804
|
+
self._sort_combo.addItem("Date (Oldest)", 2)
|
805
|
+
self._sort_combo.addItem("Date (Newest)", 3)
|
806
|
+
self._sort_combo.addItem("Size (Smallest)", 4)
|
807
|
+
self._sort_combo.addItem("Size (Largest)", 5)
|
808
|
+
self._sort_combo.activated.connect(self._on_sort_changed)
|
552
809
|
layout.addWidget(self._sort_combo)
|
553
810
|
|
811
|
+
# Column visibility dropdown
|
812
|
+
self._column_dropdown = CustomDropdown()
|
813
|
+
self._column_dropdown.addCheckableItem("Name", True, 0)
|
814
|
+
self._column_dropdown.addCheckableItem("NPZ", True, 1)
|
815
|
+
self._column_dropdown.addCheckableItem("TXT", True, 2)
|
816
|
+
self._column_dropdown.addCheckableItem("Modified", True, 3)
|
817
|
+
self._column_dropdown.addCheckableItem("Size", True, 4)
|
818
|
+
self._column_dropdown.activated.connect(self._on_column_visibility_changed)
|
819
|
+
layout.addWidget(self._column_dropdown)
|
820
|
+
|
554
821
|
# Refresh button
|
555
822
|
refresh_btn = QPushButton("Refresh")
|
556
823
|
refresh_btn.clicked.connect(self._refresh)
|
@@ -584,7 +851,7 @@ class FastFileManager(QWidget):
|
|
584
851
|
# Connect to scan complete signal
|
585
852
|
if self._model._scanner:
|
586
853
|
self._model._scanner.scanComplete.connect(
|
587
|
-
lambda count: self.
|
854
|
+
lambda count: self._update_detailed_status(directory.name)
|
588
855
|
)
|
589
856
|
|
590
857
|
def _on_search_changed(self, text: str):
|
@@ -623,6 +890,50 @@ class FastFileManager(QWidget):
|
|
623
890
|
if self._current_directory:
|
624
891
|
self.setDirectory(self._current_directory)
|
625
892
|
|
893
|
+
def _on_column_visibility_changed(self, column_index):
|
894
|
+
"""Handle column visibility toggle"""
|
895
|
+
is_checked = self._column_dropdown.isItemChecked(column_index)
|
896
|
+
self._model.setColumnVisible(column_index, is_checked)
|
897
|
+
|
898
|
+
# Update header sizing for visible columns
|
899
|
+
self._update_header_sizing()
|
900
|
+
|
901
|
+
def _update_header_sizing(self):
|
902
|
+
"""Update header column sizing for visible columns"""
|
903
|
+
header = self._table_view.horizontalHeader()
|
904
|
+
visible_columns = sum(self._model._visible_columns)
|
905
|
+
|
906
|
+
if visible_columns == 0:
|
907
|
+
return
|
908
|
+
|
909
|
+
# Set all columns to Interactive mode for manual resizing
|
910
|
+
for i in range(visible_columns):
|
911
|
+
# Find logical column for this visible index
|
912
|
+
logical_col = self._model.getLogicalColumnIndex(i)
|
913
|
+
if logical_col >= 0:
|
914
|
+
column_name = self._model._all_columns[logical_col]
|
915
|
+
# All columns are interactive (manually resizable)
|
916
|
+
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Interactive)
|
917
|
+
|
918
|
+
# Set appropriate default sizes
|
919
|
+
if column_name == "Name":
|
920
|
+
header.resizeSection(i, 200) # Default name column width
|
921
|
+
elif column_name in ["NPZ", "TXT"]:
|
922
|
+
header.resizeSection(i, 50) # Compact for checkmarks
|
923
|
+
elif column_name == "Modified":
|
924
|
+
header.resizeSection(i, 120) # Date needs more space
|
925
|
+
elif column_name == "Size":
|
926
|
+
header.resizeSection(i, 80) # Size needs moderate space
|
927
|
+
|
928
|
+
# Disable stretch last section to allow all columns to be manually resized
|
929
|
+
header.setStretchLastSection(False)
|
930
|
+
|
931
|
+
def _on_column_moved(self, logical_index, old_visual_index, new_visual_index):
|
932
|
+
"""Handle column reordering via drag-and-drop"""
|
933
|
+
# For now, just update the header sizing to maintain proper resize modes
|
934
|
+
# The QHeaderView handles the visual reordering automatically
|
935
|
+
self._update_header_sizing()
|
936
|
+
|
626
937
|
def updateNpzStatus(self, image_path: Path):
|
627
938
|
"""Update NPZ status for a specific image file"""
|
628
939
|
self._model.updateNpzStatus(image_path)
|
@@ -630,6 +941,12 @@ class FastFileManager(QWidget):
|
|
630
941
|
def updateFileStatus(self, image_path: Path):
|
631
942
|
"""Update both NPZ and TXT status for a specific image file"""
|
632
943
|
self._model.updateFileStatus(image_path)
|
944
|
+
# Update status counts when file status changes
|
945
|
+
if self._current_directory:
|
946
|
+
self._update_detailed_status(self._current_directory.name)
|
947
|
+
# Force table view to repaint immediately
|
948
|
+
self._table_view.viewport().update()
|
949
|
+
self._table_view.repaint()
|
633
950
|
|
634
951
|
def refreshFile(self, image_path: Path):
|
635
952
|
"""Refresh status for a specific file"""
|
@@ -638,6 +955,12 @@ class FastFileManager(QWidget):
|
|
638
955
|
def batchUpdateFileStatus(self, image_paths: list[Path]):
|
639
956
|
"""Batch update file status for multiple files"""
|
640
957
|
self._model.batchUpdateFileStatus(image_paths)
|
958
|
+
# Update status counts after batch update
|
959
|
+
if self._current_directory:
|
960
|
+
self._update_detailed_status(self._current_directory.name)
|
961
|
+
# Force table view to repaint immediately
|
962
|
+
self._table_view.viewport().update()
|
963
|
+
self._table_view.repaint()
|
641
964
|
|
642
965
|
def getSurroundingFiles(self, current_path: Path, count: int) -> list[Path]:
|
643
966
|
"""Get files in current sorted/filtered order surrounding the given path"""
|
@@ -730,6 +1053,27 @@ class FastFileManager(QWidget):
|
|
730
1053
|
"""Update status label"""
|
731
1054
|
self._status_label.setText(text)
|
732
1055
|
|
1056
|
+
def _update_detailed_status(self, directory_name: str):
|
1057
|
+
"""Update status label with detailed file counts"""
|
1058
|
+
total_files, npz_count, txt_count = self._model.getFileCounts()
|
1059
|
+
|
1060
|
+
if total_files == 0:
|
1061
|
+
status_text = f"No files in {directory_name}"
|
1062
|
+
else:
|
1063
|
+
# Build the status message parts
|
1064
|
+
parts = []
|
1065
|
+
parts.append(f"{total_files} image{'s' if total_files != 1 else ''}")
|
1066
|
+
|
1067
|
+
if npz_count > 0:
|
1068
|
+
parts.append(f"{npz_count} npz")
|
1069
|
+
|
1070
|
+
if txt_count > 0:
|
1071
|
+
parts.append(f"{txt_count} txt")
|
1072
|
+
|
1073
|
+
status_text = f"{', '.join(parts)} in {directory_name}"
|
1074
|
+
|
1075
|
+
self._status_label.setText(status_text)
|
1076
|
+
|
733
1077
|
def selectFile(self, path: Path):
|
734
1078
|
"""Select a specific file in the view"""
|
735
1079
|
index = self._model.getFileIndex(path)
|