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.
- lazylabel/core/file_manager.py +1 -1
- lazylabel/ui/control_panel.py +7 -2
- lazylabel/ui/main_window.py +164 -63
- 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/utils/fast_file_manager.py +422 -78
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/RECORD +13 -13
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.4.dist-info}/top_level.txt +0 -0
lazylabel/core/file_manager.py
CHANGED
@@ -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.
|
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(
|
lazylabel/ui/control_panel.py
CHANGED
@@ -612,10 +612,10 @@ class ControlPanel(QWidget):
|
|
612
612
|
layout.addWidget(crop_collapsible)
|
613
613
|
|
614
614
|
# Channel Threshold - collapsible
|
615
|
-
|
615
|
+
self.channel_threshold_collapsible = SimpleCollapsible(
|
616
616
|
"Channel Threshold", self.channel_threshold_widget
|
617
617
|
)
|
618
|
-
layout.addWidget(
|
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
|
lazylabel/ui/main_window.py
CHANGED
@@ -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
|
952
|
+
from lazylabel.ui.widgets.model_selection_widget import CustomDropdown
|
955
953
|
|
956
|
-
self.grid_mode_combo =
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
1173
|
-
self.
|
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
|
-
|
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
|
-
|
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
|
-
|
3482
|
-
|
3483
|
-
|
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
|
-
#
|
5722
|
-
|
5723
|
-
|
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
|
-
|
5730
|
-
|
5731
|
-
|
5732
|
-
|
5733
|
-
|
5734
|
-
|
5735
|
-
|
5736
|
-
|
5737
|
-
|
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
|
-
#
|
5766
|
-
|
5767
|
-
|
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
|
-
|
5772
|
-
|
5773
|
-
|
5774
|
-
|
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
|
-
|
5780
|
-
|
5781
|
-
|
5782
|
-
|
5783
|
-
#
|
5784
|
-
|
5785
|
-
|
5786
|
-
|
5787
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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)
|
@@ -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=
|
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=
|
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
|
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=
|
34
|
-
lazylabel/ui/widgets/fft_threshold_widget.py,sha256=
|
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=
|
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=
|
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.
|
45
|
-
lazylabel_gui-1.3.
|
46
|
-
lazylabel_gui-1.3.
|
47
|
-
lazylabel_gui-1.3.
|
48
|
-
lazylabel_gui-1.3.
|
49
|
-
lazylabel_gui-1.3.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|