lazylabel-gui 1.3.3__py3-none-any.whl → 1.3.4__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.
@@ -65,7 +65,7 @@ class FileManager:
65
65
  if not os.path.exists(npz_path):
66
66
  raise OSError(f"NPZ file was not created: {npz_path}")
67
67
 
68
- logger.info(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
68
+ logger.debug(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
69
69
  return npz_path
70
70
 
71
71
  def save_yolo_txt(
@@ -612,10 +612,10 @@ class ControlPanel(QWidget):
612
612
  layout.addWidget(crop_collapsible)
613
613
 
614
614
  # Channel Threshold - collapsible
615
- threshold_collapsible = SimpleCollapsible(
615
+ self.channel_threshold_collapsible = SimpleCollapsible(
616
616
  "Channel Threshold", self.channel_threshold_widget
617
617
  )
618
- layout.addWidget(threshold_collapsible)
618
+ layout.addWidget(self.channel_threshold_collapsible)
619
619
 
620
620
  # FFT Threshold - collapsible (default collapsed)
621
621
  self.fft_threshold_collapsible = SimpleCollapsible(
@@ -880,6 +880,11 @@ class ControlPanel(QWidget):
880
880
  """Update channel threshold widget for new image."""
881
881
  self.channel_threshold_widget.update_for_image(image_array)
882
882
 
883
+ # Auto-expand channel threshold panel when any image is loaded
884
+ if image_array is not None and hasattr(self, "channel_threshold_collapsible"):
885
+ # Find and expand the Channel Threshold panel
886
+ self.channel_threshold_collapsible.set_collapsed(False)
887
+
883
888
  def get_channel_threshold_widget(self):
884
889
  """Get the channel threshold widget."""
885
890
  return self.channel_threshold_widget
@@ -19,12 +19,14 @@ from PyQt6.QtGui import (
19
19
  QShortcut,
20
20
  )
21
21
  from PyQt6.QtWidgets import (
22
+ QApplication,
22
23
  QDialog,
23
24
  QFileDialog,
24
25
  QGraphicsEllipseItem,
25
26
  QGraphicsLineItem,
26
27
  QGraphicsPolygonItem,
27
28
  QGraphicsRectItem,
29
+ QGridLayout,
28
30
  QHBoxLayout,
29
31
  QLabel,
30
32
  QMainWindow,
@@ -772,6 +774,7 @@ class MainWindow(QMainWindow):
772
774
  logger.info("Step 5/8: Discovering available models...")
773
775
  self._setup_model_manager() # Just setup manager, don't load model
774
776
  self._setup_connections()
777
+ self._fix_fft_connection() # Workaround for FFT signal connection issue
775
778
  self._setup_shortcuts()
776
779
  self._load_settings()
777
780
 
@@ -877,12 +880,7 @@ class MainWindow(QMainWindow):
877
880
  grid_widget = QWidget()
878
881
 
879
882
  # Use grid layout for 4-view, horizontal layout for 2-view
880
- if use_grid:
881
- from PyQt6.QtWidgets import QGridLayout
882
-
883
- grid_layout = QGridLayout(grid_widget)
884
- else:
885
- grid_layout = QHBoxLayout(grid_widget)
883
+ grid_layout = QGridLayout(grid_widget) if use_grid else QHBoxLayout(grid_widget)
886
884
  grid_layout.setSpacing(5)
887
885
 
888
886
  self.multi_view_viewers = []
@@ -951,29 +949,30 @@ class MainWindow(QMainWindow):
951
949
  grid_mode_label = QLabel("View Mode:")
952
950
  controls_layout.addWidget(grid_mode_label)
953
951
 
954
- from PyQt6.QtWidgets import QComboBox
952
+ from lazylabel.ui.widgets.model_selection_widget import CustomDropdown
955
953
 
956
- self.grid_mode_combo = QComboBox()
954
+ self.grid_mode_combo = CustomDropdown()
955
+ self.grid_mode_combo.setText("View Mode") # Default text
957
956
  self.grid_mode_combo.addItem("2 Views (1x2)", "2_view")
958
957
  self.grid_mode_combo.addItem("4 Views (2x2)", "4_view")
959
958
 
960
959
  # Set current selection based on settings
961
960
  current_mode = self.settings.multi_view_grid_mode
962
- for i in range(self.grid_mode_combo.count()):
961
+ for i in range(len(self.grid_mode_combo.items)):
963
962
  if self.grid_mode_combo.itemData(i) == current_mode:
964
963
  self.grid_mode_combo.setCurrentIndex(i)
965
964
  break
966
965
 
967
- self.grid_mode_combo.currentTextChanged.connect(self._on_grid_mode_changed)
966
+ self.grid_mode_combo.activated.connect(self._on_grid_mode_changed)
968
967
  controls_layout.addWidget(self.grid_mode_combo)
969
968
 
970
969
  controls_layout.addStretch()
971
970
 
972
971
  layout.addWidget(controls_widget)
973
972
 
974
- def _on_grid_mode_changed(self):
973
+ def _on_grid_mode_changed(self, index):
975
974
  """Handle grid mode change from combo box."""
976
- current_data = self.grid_mode_combo.currentData()
975
+ current_data = self.grid_mode_combo.itemData(index)
977
976
  if current_data and current_data != self.settings.multi_view_grid_mode:
978
977
  # Update settings
979
978
  self.settings.multi_view_grid_mode = current_data
@@ -1062,8 +1061,7 @@ class MainWindow(QMainWindow):
1062
1061
  layout.deleteLater()
1063
1062
  except Exception as e:
1064
1063
  # If layout clearing fails, just reset the viewer lists
1065
- print(f"Layout clearing failed: {e}")
1066
- pass
1064
+ logger.error(f"Layout clearing failed: {e}")
1067
1065
 
1068
1066
  def _clear_multi_view_layout(self):
1069
1067
  """Clear the existing multi-view layout."""
@@ -1121,6 +1119,48 @@ class MainWindow(QMainWindow):
1121
1119
  # Switch to polygon mode if SAM is disabled and we're in SAM/AI mode
1122
1120
  self.set_polygon_mode()
1123
1121
 
1122
+ def _fix_fft_connection(self):
1123
+ """Fix FFT signal connection issue - workaround for connection timing problem."""
1124
+ try:
1125
+ # Get the FFT widget directly and connect to its signal
1126
+ fft_widget = self.control_panel.get_fft_threshold_widget()
1127
+ if fft_widget:
1128
+ # Direct connection bypass - connect FFT widget directly to main window handler
1129
+ # This bypasses the control panel signal forwarding which has timing issues
1130
+ # Use a wrapper to ensure the connection works reliably
1131
+ def fft_signal_wrapper():
1132
+ self._handle_fft_threshold_changed()
1133
+
1134
+ fft_widget.fft_threshold_changed.connect(fft_signal_wrapper)
1135
+
1136
+ logger.info("FFT signal connection bypass established successfully")
1137
+ else:
1138
+ logger.warning("FFT widget not found during connection fix")
1139
+ except Exception as e:
1140
+ logger.warning(f"Failed to establish FFT connection bypass: {e}")
1141
+
1142
+ # Also fix channel threshold connection for RGB images
1143
+ try:
1144
+ channel_widget = self.control_panel.get_channel_threshold_widget()
1145
+ if channel_widget:
1146
+ # Direct connection bypass for channel threshold widget too
1147
+ def channel_signal_wrapper():
1148
+ self._handle_channel_threshold_changed()
1149
+
1150
+ channel_widget.thresholdChanged.connect(channel_signal_wrapper)
1151
+
1152
+ logger.info(
1153
+ "Channel threshold signal connection bypass established successfully"
1154
+ )
1155
+ else:
1156
+ logger.warning(
1157
+ "Channel threshold widget not found during connection fix"
1158
+ )
1159
+ except Exception as e:
1160
+ logger.warning(
1161
+ f"Failed to establish channel threshold connection bypass: {e}"
1162
+ )
1163
+
1124
1164
  def _setup_connections(self):
1125
1165
  """Setup signal connections."""
1126
1166
  # Control panel connections
@@ -1169,9 +1209,13 @@ class MainWindow(QMainWindow):
1169
1209
  )
1170
1210
 
1171
1211
  # FFT threshold connections
1172
- self.control_panel.fft_threshold_changed.connect(
1173
- self._handle_fft_threshold_changed
1174
- )
1212
+ try:
1213
+ self.control_panel.fft_threshold_changed.connect(
1214
+ self._handle_fft_threshold_changed
1215
+ )
1216
+ logger.debug("FFT threshold connection established in _setup_connections")
1217
+ except Exception as e:
1218
+ logger.error(f"Failed to establish FFT threshold connection: {e}")
1175
1219
 
1176
1220
  # Right panel connections
1177
1221
  self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
@@ -1774,6 +1818,9 @@ class MainWindow(QMainWindow):
1774
1818
  # Update file selection in the file manager
1775
1819
  self.right_panel.select_file(Path(path))
1776
1820
 
1821
+ # Update threshold widgets for new image (this was missing!)
1822
+ self._update_channel_threshold_for_image(pixmap)
1823
+
1777
1824
  def _load_multi_view_from_path(self, path: str):
1778
1825
  """Load multi-view starting from a specific path using FastFileManager."""
1779
1826
  config = self._get_multi_view_config()
@@ -1948,6 +1995,9 @@ class MainWindow(QMainWindow):
1948
1995
  if changed_indices:
1949
1996
  self._fast_update_multi_view_images(changed_indices)
1950
1997
 
1998
+ # Update threshold widgets for the loaded images
1999
+ self._update_multi_view_channel_threshold_for_images()
2000
+
1951
2001
  # Load existing segments for all loaded images
1952
2002
  valid_image_paths = [path for path in image_paths if path is not None]
1953
2003
  if valid_image_paths:
@@ -2005,7 +2055,9 @@ class MainWindow(QMainWindow):
2005
2055
  all_segments.extend(viewer_segments)
2006
2056
  self.segment_manager.segments.clear()
2007
2057
  except Exception as e:
2008
- print(f"Error loading segments for viewer {viewer_index}: {e}")
2058
+ logger.error(
2059
+ f"Error loading segments for viewer {viewer_index}: {e}"
2060
+ )
2009
2061
 
2010
2062
  # Set all segments at once
2011
2063
  self.segment_manager.segments = all_segments
@@ -2025,7 +2077,7 @@ class MainWindow(QMainWindow):
2025
2077
  self._update_all_lists()
2026
2078
 
2027
2079
  except Exception as e:
2028
- print(f"Error in _load_multi_view_segments: {e}")
2080
+ logger.error(f"Error in _load_multi_view_segments: {e}")
2029
2081
  self.segment_manager.segments.clear()
2030
2082
 
2031
2083
  def _cancel_multi_view_sam_loading(self):
@@ -3353,6 +3405,8 @@ class MainWindow(QMainWindow):
3353
3405
  valid_paths = [Path(img) for img in self.multi_view_images if img]
3354
3406
  if valid_paths:
3355
3407
  self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
3408
+ # Force immediate GUI update
3409
+ QApplication.processEvents()
3356
3410
  # Clear the tracking list for next save
3357
3411
  self._saved_file_paths = []
3358
3412
 
@@ -3401,6 +3455,8 @@ class MainWindow(QMainWindow):
3401
3455
  )
3402
3456
  # Update UI immediately when files are deleted
3403
3457
  self._update_all_lists()
3458
+ # Force immediate GUI update
3459
+ QApplication.processEvents()
3404
3460
  else:
3405
3461
  self._show_warning_notification("No segments to save.")
3406
3462
  return
@@ -3478,15 +3534,16 @@ class MainWindow(QMainWindow):
3478
3534
  )
3479
3535
 
3480
3536
  # Update FastFileManager to show NPZ/TXT checkmarks
3481
- if (
3482
- (npz_path or txt_path)
3483
- and hasattr(self, "right_panel")
3484
- and hasattr(self.right_panel, "file_manager")
3537
+ # Always update file status after save attempt (regardless of what was saved)
3538
+ if hasattr(self, "right_panel") and hasattr(
3539
+ self.right_panel, "file_manager"
3485
3540
  ):
3486
3541
  # Update the file status in the FastFileManager
3487
3542
  self.right_panel.file_manager.updateFileStatus(
3488
3543
  Path(self.current_image_path)
3489
3544
  )
3545
+ # Force immediate GUI update
3546
+ QApplication.processEvents()
3490
3547
  except Exception as e:
3491
3548
  logger.error(f"Error saving file: {str(e)}", exc_info=True)
3492
3549
  self._show_error_notification(f"Error saving: {str(e)}")
@@ -5714,27 +5771,50 @@ class MainWindow(QMainWindow):
5714
5771
 
5715
5772
  def _update_channel_threshold_for_image(self, pixmap):
5716
5773
  """Update channel threshold widget for the given image pixmap."""
5717
- if pixmap.isNull():
5774
+ if pixmap.isNull() or not self.current_image_path:
5718
5775
  self.control_panel.update_channel_threshold_for_image(None)
5719
5776
  return
5720
5777
 
5721
- # Convert pixmap to numpy array
5722
- qimage = pixmap.toImage()
5723
- ptr = qimage.constBits()
5724
- ptr.setsize(qimage.bytesPerLine() * qimage.height())
5725
- image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5726
- # Convert from BGRA to RGB, ignore alpha
5727
- image_rgb = image_np[:, :, [2, 1, 0]]
5778
+ # Use cv2.imread for more robust loading instead of QPixmap conversion
5779
+ try:
5780
+ import cv2
5728
5781
 
5729
- # Check if image is grayscale (all channels are the same)
5730
- if np.array_equal(image_rgb[:, :, 0], image_rgb[:, :, 1]) and np.array_equal(
5731
- image_rgb[:, :, 1], image_rgb[:, :, 2]
5732
- ):
5733
- # Convert to single channel grayscale
5734
- image_array = image_rgb[:, :, 0]
5735
- else:
5736
- # Keep as RGB
5737
- image_array = image_rgb
5782
+ image_array = cv2.imread(self.current_image_path)
5783
+ if image_array is None:
5784
+ self.control_panel.update_channel_threshold_for_image(None)
5785
+ return
5786
+
5787
+ # Convert from BGR to RGB
5788
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
5789
+ image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
5790
+
5791
+ # Check if image is grayscale (all channels are the same)
5792
+ if (
5793
+ len(image_array.shape) == 3
5794
+ and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
5795
+ and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
5796
+ ):
5797
+ # Convert to single channel grayscale
5798
+ image_array = image_array[:, :, 0]
5799
+
5800
+ except Exception:
5801
+ # Fallback to QPixmap conversion if cv2 fails
5802
+ qimage = pixmap.toImage()
5803
+ ptr = qimage.constBits()
5804
+ ptr.setsize(qimage.bytesPerLine() * qimage.height())
5805
+ image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5806
+ # Convert from BGRA to RGB, ignore alpha
5807
+ image_rgb = image_np[:, :, [2, 1, 0]]
5808
+
5809
+ # Check if image is grayscale (all channels are the same)
5810
+ if np.array_equal(
5811
+ image_rgb[:, :, 0], image_rgb[:, :, 1]
5812
+ ) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
5813
+ # Convert to single channel grayscale
5814
+ image_array = image_rgb[:, :, 0]
5815
+ else:
5816
+ # Keep as RGB
5817
+ image_array = image_rgb
5738
5818
 
5739
5819
  # Update the channel threshold widget
5740
5820
  self.control_panel.update_channel_threshold_for_image(image_array)
@@ -5762,29 +5842,51 @@ class MainWindow(QMainWindow):
5762
5842
  self.control_panel.update_channel_threshold_for_image(None)
5763
5843
  return
5764
5844
 
5765
- # Load and convert the first image to update the widget
5766
- pixmap = QPixmap(first_image_path)
5767
- if pixmap.isNull():
5768
- self.control_panel.update_channel_threshold_for_image(None)
5769
- return
5845
+ # Use cv2.imread for more robust loading instead of QPixmap conversion
5846
+ try:
5847
+ import cv2
5770
5848
 
5771
- # Convert pixmap to numpy array
5772
- qimage = pixmap.toImage()
5773
- ptr = qimage.constBits()
5774
- ptr.setsize(qimage.bytesPerLine() * qimage.height())
5775
- image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5776
- # Convert from BGRA to RGB, ignore alpha
5777
- image_rgb = image_np[:, :, [2, 1, 0]]
5849
+ image_array = cv2.imread(first_image_path)
5850
+ if image_array is None:
5851
+ self.control_panel.update_channel_threshold_for_image(None)
5852
+ return
5778
5853
 
5779
- # Check if image is grayscale (all channels are the same)
5780
- if np.array_equal(image_rgb[:, :, 0], image_rgb[:, :, 1]) and np.array_equal(
5781
- image_rgb[:, :, 1], image_rgb[:, :, 2]
5782
- ):
5783
- # Convert to single channel grayscale
5784
- image_array = image_rgb[:, :, 0]
5785
- else:
5786
- # Keep as RGB
5787
- image_array = image_rgb
5854
+ # Convert from BGR to RGB
5855
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
5856
+ image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
5857
+
5858
+ # Check if image is grayscale (all channels are the same)
5859
+ if (
5860
+ len(image_array.shape) == 3
5861
+ and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
5862
+ and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
5863
+ ):
5864
+ # Convert to single channel grayscale
5865
+ image_array = image_array[:, :, 0]
5866
+
5867
+ except Exception:
5868
+ # Fallback to QPixmap conversion if cv2 fails
5869
+ pixmap = QPixmap(first_image_path)
5870
+ if pixmap.isNull():
5871
+ self.control_panel.update_channel_threshold_for_image(None)
5872
+ return
5873
+
5874
+ qimage = pixmap.toImage()
5875
+ ptr = qimage.constBits()
5876
+ ptr.setsize(qimage.bytesPerLine() * qimage.height())
5877
+ image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
5878
+ # Convert from BGRA to RGB, ignore alpha
5879
+ image_rgb = image_np[:, :, [2, 1, 0]]
5880
+
5881
+ # Check if image is grayscale (all channels are the same)
5882
+ if np.array_equal(
5883
+ image_rgb[:, :, 0], image_rgb[:, :, 1]
5884
+ ) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
5885
+ # Convert to single channel grayscale
5886
+ image_array = image_rgb[:, :, 0]
5887
+ else:
5888
+ # Keep as RGB
5889
+ image_array = image_rgb
5788
5890
 
5789
5891
  # Update the channel threshold widget
5790
5892
  self.control_panel.update_channel_threshold_for_image(image_array)
@@ -7097,7 +7199,6 @@ class MainWindow(QMainWindow):
7097
7199
  )
7098
7200
 
7099
7201
  # Load existing segments for the current image
7100
- print(f"Loading segments for single-view: {self.current_image_path}")
7101
7202
  try:
7102
7203
  # Clear any leftover multi-view segments first
7103
7204
  self.segment_manager.clear()
@@ -7114,7 +7215,7 @@ class MainWindow(QMainWindow):
7114
7215
  self._update_all_lists()
7115
7216
 
7116
7217
  except Exception as e:
7117
- print(f"Error loading segments for single-view: {e}")
7218
+ logger.error(f"Error loading segments for single-view: {e}")
7118
7219
 
7119
7220
  # Redisplay segments for single view
7120
7221
  if hasattr(self, "single_view_mode_handler"):
@@ -270,10 +270,6 @@ class MultiIndicatorSlider(QWidget):
270
270
  """Handle right-click to remove indicator."""
271
271
  slider_rect = self.get_slider_rect()
272
272
 
273
- # Only allow removal if more than 1 indicator
274
- if len(self.indicators) <= 1:
275
- return
276
-
277
273
  # Check if right-clicking on an indicator
278
274
  for i, value in enumerate(self.indicators):
279
275
  x = self.value_to_x(value)
@@ -473,7 +469,7 @@ class ChannelThresholdWidget(QWidget):
473
469
 
474
470
  if self.current_image_channels == 1:
475
471
  # Grayscale image
476
- if "Gray" in self.sliders:
472
+ if "Gray" in self.sliders and self.sliders["Gray"].is_enabled():
477
473
  result = self._apply_channel_thresholding(
478
474
  result, self.sliders["Gray"].get_indicators()
479
475
  )
@@ -481,7 +477,10 @@ class ChannelThresholdWidget(QWidget):
481
477
  # RGB image
482
478
  channel_names = ["Red", "Green", "Blue"]
483
479
  for i, channel_name in enumerate(channel_names):
484
- if channel_name in self.sliders:
480
+ if (
481
+ channel_name in self.sliders
482
+ and self.sliders[channel_name].is_enabled()
483
+ ):
485
484
  result[:, :, i] = self._apply_channel_thresholding(
486
485
  result[:, :, i], self.sliders[channel_name].get_indicators()
487
486
  )
@@ -494,7 +493,7 @@ class ChannelThresholdWidget(QWidget):
494
493
  if not indicators:
495
494
  return channel_data
496
495
 
497
- # Sort indicators
496
+ # Sort indicators
498
497
  sorted_indicators = sorted(indicators)
499
498
 
500
499
  # Create output array
@@ -527,9 +526,9 @@ class ChannelThresholdWidget(QWidget):
527
526
  return result
528
527
 
529
528
  def has_active_thresholding(self):
530
- """Check if any channel has active thresholding (indicators present)."""
529
+ """Check if any channel has active thresholding (enabled and indicators present)."""
531
530
  for slider_widget in self.sliders.values():
532
- if slider_widget.get_indicators():
531
+ if slider_widget.is_enabled() and slider_widget.get_indicators():
533
532
  return True
534
533
  return False
535
534
 
@@ -308,6 +308,8 @@ class FFTThresholdWidget(QWidget):
308
308
  self.status_label.setStyleSheet(
309
309
  "color: #F44336; font-size: 9px; font-style: italic;"
310
310
  )
311
+ # Disable FFT processing for color images
312
+ self.enable_checkbox.setChecked(False)
311
313
  else:
312
314
  # Unknown format
313
315
  self.current_image_channels = 0
@@ -315,6 +317,8 @@ class FFTThresholdWidget(QWidget):
315
317
  self.status_label.setStyleSheet(
316
318
  "color: #F44336; font-size: 9px; font-style: italic;"
317
319
  )
320
+ # Disable FFT processing for unsupported formats
321
+ self.enable_checkbox.setChecked(False)
318
322
 
319
323
  def is_active(self):
320
324
  """Check if FFT processing is active (checkbox enabled and image is grayscale)."""
@@ -111,6 +111,15 @@ class CustomDropdown(QToolButton):
111
111
  text, _ = self.items[index]
112
112
  self.setText(text)
113
113
 
114
+ def count(self):
115
+ """Get number of items."""
116
+ return len(self.items)
117
+
118
+ def currentData(self):
119
+ """Get data of currently selected item."""
120
+ current_idx = self.currentIndex()
121
+ return self.itemData(current_idx)
122
+
114
123
  def blockSignals(self, block):
115
124
  """Block/unblock signals."""
116
125
  super().blockSignals(block)
@@ -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.4
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
@@ -6,19 +6,19 @@ lazylabel/config/hotkeys.py,sha256=PwxBUy7RvIUU3HCXT74NnexPbg2VFQrbR5FTGek31h8,8
6
6
  lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
7
7
  lazylabel/config/settings.py,sha256=myBqkgnYv_P1OomieXt8luCOSNjBiDVN-TybSqHkAgA,1990
8
8
  lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
9
- lazylabel/core/file_manager.py,sha256=qVPCJdo9KEu9ESujHaWPmlpfVLjyaVXmDe9TmM9c8k4,7282
9
+ lazylabel/core/file_manager.py,sha256=Fs4IJIm2hHvYXeDWR7mqEu2EE21o6qwSPpECv8CYQ3o,7283
10
10
  lazylabel/core/model_manager.py,sha256=NmgnIrw9sb_vSwRVkBr2Z_Mc2kFfdFukFgUhqs2e-L0,6829
11
11
  lazylabel/core/segment_manager.py,sha256=M6kHcYeiub3WqL01NElCvKOc2GNmf72LUM1W8XwSaxc,6465
12
12
  lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
13
13
  lazylabel/models/sam2_model.py,sha256=lleJ6j7B2k9v4gqhfU7tP_3yxiVGAPTZG5Tq26AwKR4,15612
14
14
  lazylabel/models/sam_model.py,sha256=Q1GAaFG6n5JoZXhClcUZKN-gvA_wmtVuUYzELG7zhPg,8833
15
15
  lazylabel/ui/__init__.py,sha256=4qDIh9y6tABPmD8MAMGZn_G7oSRyrcHt2HkjoWgbGH4,268
16
- lazylabel/ui/control_panel.py,sha256=I2Bpij2Xe-4yW1t1iKBS0H3vomuSW_26QvkNhO0mj2I,33140
16
+ lazylabel/ui/control_panel.py,sha256=6WsCX9MzLtYzgi93Yat4utajHHjKuROg_Nf9iGLY05s,33456
17
17
  lazylabel/ui/editable_vertex.py,sha256=ofo3r8ZZ3b8oYV40vgzZuS3QnXYBNzE92ArC2wggJOM,5122
18
18
  lazylabel/ui/hotkey_dialog.py,sha256=U_B76HLOxWdWkfA4d2XgRUaZTJPAAE_m5fmwf7Rh-5Y,14743
19
19
  lazylabel/ui/hoverable_pixelmap_item.py,sha256=UbWVxpmCTaeae_AeA8gMOHYGUmAw40fZBFTS3sZlw48,1821
20
20
  lazylabel/ui/hoverable_polygon_item.py,sha256=gZalImJ_PJYM7xON0iiSjQ335ZBREOfSscKLVs-MSh8,2314
21
- lazylabel/ui/main_window.py,sha256=rOjzb2nLmbIPbmr7QJuKGXpd1qmvaCPtWP5FAuaU5u0,393283
21
+ lazylabel/ui/main_window.py,sha256=-PgpY1IVqi6p0gj67I_BD44YH6anvA2N1fWTm5wqg_Q,398187
22
22
  lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
23
23
  lazylabel/ui/photo_viewer.py,sha256=D_t2DS0AYckzlXjotTVC5ZxCsMYC-0xjBjeVQCrhqws,5631
24
24
  lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
@@ -30,20 +30,20 @@ lazylabel/ui/modes/single_view_mode.py,sha256=khGUXVQ_lv9cCXkOAewQN8iH7R_CPyIVtQ
30
30
  lazylabel/ui/widgets/__init__.py,sha256=6VDoMnanqVm3yjOovxie3WggPODuhUsIO75RtxOhQhI,688
31
31
  lazylabel/ui/widgets/adjustments_widget.py,sha256=5xldhdEArX3H2P7EmHPjURdwpV-Wa2WULFfspp0gxns,12750
32
32
  lazylabel/ui/widgets/border_crop_widget.py,sha256=8NTkHrk_L5_T1psVuXMspVVZRPTTeJyIEYBfVxdGeQA,7529
33
- lazylabel/ui/widgets/channel_threshold_widget.py,sha256=CSdqiv-x1povttaLn_mY7NtU9IlMBAsZ8RIJo8_GATs,20588
34
- lazylabel/ui/widgets/fft_threshold_widget.py,sha256=UT4QMMllPezK75hpUC7Fh1QqcD_bSHpn9fZEJCqc968,20630
33
+ lazylabel/ui/widgets/channel_threshold_widget.py,sha256=9KoUK0TytGuSmSU_lO6m8j1zNHx_m9NufgogG0gLYLM,20656
34
+ lazylabel/ui/widgets/fft_threshold_widget.py,sha256=mcbkNoP0aj-Pe57sgIWBnmg4JiEbTikrwG67Vgygnio,20859
35
35
  lazylabel/ui/widgets/fragment_threshold_widget.py,sha256=YtToua1eAUtEuJ3EwdCMvI-39TRrFnshkty4tnR4OMU,3492
36
- lazylabel/ui/widgets/model_selection_widget.py,sha256=mHaVMLOACL7Yc1IVA648OOYBu4yIxb6xnEGPgXTc2Ns,8160
36
+ lazylabel/ui/widgets/model_selection_widget.py,sha256=k4zyhWszi_7e-0TPa0Go1LLPZpTNGtrYSb-yT7uGiVU,8420
37
37
  lazylabel/ui/widgets/settings_widget.py,sha256=ShTaLJeXxwrSuTV4kmtV2JiWjfREil2D1nvPUIfAgDs,4859
38
38
  lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
39
39
  lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
40
40
  lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
41
- lazylabel/utils/fast_file_manager.py,sha256=CwojNwi8VcM2te8hceNuCa0rDaZtEh63Uj4yzHyXq0g,30689
41
+ lazylabel/utils/fast_file_manager.py,sha256=kzbWz_xKufG5bP6sjyZV1fmOKRWPPNeL-xLYZEu_8wE,44697
42
42
  lazylabel/utils/logger.py,sha256=R7z6ifgA-NY-9ZbLlNH0i19zzwXndJ_gkG2J1zpVEhg,1306
43
43
  lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
44
- lazylabel_gui-1.3.3.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
45
- lazylabel_gui-1.3.3.dist-info/METADATA,sha256=Dwol4IiBVsDeqCIyIHPgROMZSWIYWtPkxwbyMUb94b0,6898
46
- lazylabel_gui-1.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- lazylabel_gui-1.3.3.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
48
- lazylabel_gui-1.3.3.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
49
- lazylabel_gui-1.3.3.dist-info/RECORD,,
44
+ lazylabel_gui-1.3.4.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
45
+ lazylabel_gui-1.3.4.dist-info/METADATA,sha256=a8ILHz7bYF4Qb0ts_2O3mGc89-6xO8Y85KhomxxsGMk,6898
46
+ lazylabel_gui-1.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ lazylabel_gui-1.3.4.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
48
+ lazylabel_gui-1.3.4.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
49
+ lazylabel_gui-1.3.4.dist-info/RECORD,,