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.
@@ -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
- print(f"Error scanning directory: {e}")
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 5 # Name, Size, Modified, NPZ, TXT
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
- col = index.column()
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 col == 0: # Name
361
+ if column_name == "Name":
157
362
  return file_info.name
158
- elif col == 1: # Size
159
- # Lazy load size only when displayed
160
- if file_info.size == 0:
161
- try:
162
- file_info.size = file_info.path.stat().st_size
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 col == 3: # NPZ
181
- return "✓" if file_info.has_npz else ""
182
- elif col == 4: # TXT
183
- return "✓" if file_info.has_txt else ""
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 col in [
188
- 3,
189
- 4,
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
- headers = ["Name", "Size", "Modified", "NPZ", "TXT"]
201
- return headers[section]
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
- print(f"Scan complete: {total} files found")
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
- col = left.column()
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 col == 0: # Name
583
+ # Sort based on column type
584
+ if column_name == "Name":
365
585
  return left_info.name.lower() < right_info.name.lower()
366
- elif col == 1: # Size
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 col == 2: # Modified
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 col == 3: # NPZ
612
+ elif column_name == "NPZ":
393
613
  return left_info.has_npz < right_info.has_npz
394
- elif col == 4: # TXT
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.setSectionResizeMode(
443
- 0, QHeaderView.ResizeMode.Stretch
444
- ) # Name column stretches
445
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Size
446
- header.setSectionResizeMode(
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
- self._sort_combo = QComboBox()
519
- self._sort_combo.addItems(
520
- [
521
- "Name (A-Z)",
522
- "Name (Z-A)",
523
- "Date (Oldest)",
524
- "Date (Newest)",
525
- "Size (Smallest)",
526
- "Size (Largest)",
527
- ]
528
- )
529
- self._sort_combo.currentIndexChanged.connect(self._on_sort_changed)
530
- self._sort_combo.setStyleSheet("""
531
- QComboBox {
532
- background-color: rgba(50, 50, 50, 0.5);
533
- border: 1px solid rgba(80, 80, 80, 0.4);
534
- color: #E0E0E0;
535
- padding: 4px;
536
- border-radius: 3px;
537
- }
538
- QComboBox::drop-down {
539
- border: none;
540
- }
541
- QComboBox::down-arrow {
542
- width: 12px;
543
- height: 12px;
544
- }
545
- QComboBox QAbstractItemView {
546
- background-color: rgba(50, 50, 50, 0.9);
547
- border: 1px solid rgba(80, 80, 80, 0.4);
548
- color: #E0E0E0;
549
- selection-background-color: rgba(100, 100, 200, 0.5);
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._update_status(f"{count} files in {directory.name}")
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.3
3
+ Version: 1.3.5
4
4
  Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License