lazylabel-gui 1.3.1__py3-none-any.whl → 1.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lazylabel/core/file_manager.py +33 -1
- lazylabel/ui/main_window.py +213 -65
- lazylabel/ui/right_panel.py +29 -2
- lazylabel/utils/fast_file_manager.py +676 -0
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/RECORD +10 -10
- lazylabel/ui/test_hover.py +0 -48
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.1.dist-info → lazylabel_gui-1.3.2.dist-info}/top_level.txt +0 -0
lazylabel/core/file_manager.py
CHANGED
@@ -24,16 +24,48 @@ class FileManager:
|
|
24
24
|
crop_coords: tuple[int, int, int, int] | None = None,
|
25
25
|
) -> str:
|
26
26
|
"""Save segments as NPZ file."""
|
27
|
+
logger.debug(f"Saving NPZ for image: {image_path}")
|
28
|
+
logger.debug(f"Image size: {image_size}, Class order: {class_order}")
|
29
|
+
|
30
|
+
# Validate inputs
|
31
|
+
if not class_order:
|
32
|
+
raise ValueError("No classes defined for saving")
|
33
|
+
|
27
34
|
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
28
35
|
image_size, class_order
|
29
36
|
)
|
30
37
|
|
38
|
+
# Validate mask tensor
|
39
|
+
if final_mask_tensor.size == 0:
|
40
|
+
raise ValueError("Empty mask tensor generated")
|
41
|
+
|
42
|
+
logger.debug(f"Final mask tensor shape: {final_mask_tensor.shape}")
|
43
|
+
|
31
44
|
# Apply crop if coordinates are provided
|
32
45
|
if crop_coords:
|
33
46
|
final_mask_tensor = self._apply_crop_to_mask(final_mask_tensor, crop_coords)
|
47
|
+
logger.debug(f"Applied crop: {crop_coords}")
|
34
48
|
|
35
49
|
npz_path = os.path.splitext(image_path)[0] + ".npz"
|
36
|
-
|
50
|
+
|
51
|
+
# Create parent directory if it doesn't exist
|
52
|
+
parent_dir = os.path.dirname(npz_path)
|
53
|
+
if parent_dir: # Only create if there's actually a parent directory
|
54
|
+
os.makedirs(parent_dir, exist_ok=True)
|
55
|
+
logger.debug(f"Ensured directory exists: {parent_dir}")
|
56
|
+
|
57
|
+
# Save the NPZ file
|
58
|
+
try:
|
59
|
+
np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
|
60
|
+
logger.debug(f"Saved NPZ file: {npz_path}")
|
61
|
+
except Exception as e:
|
62
|
+
raise OSError(f"Failed to save NPZ file {npz_path}: {str(e)}") from e
|
63
|
+
|
64
|
+
# Verify the file was actually created
|
65
|
+
if not os.path.exists(npz_path):
|
66
|
+
raise OSError(f"NPZ file was not created: {npz_path}")
|
67
|
+
|
68
|
+
logger.info(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
|
37
69
|
return npz_path
|
38
70
|
|
39
71
|
def save_yolo_txt(
|
lazylabel/ui/main_window.py
CHANGED
@@ -1176,6 +1176,8 @@ class MainWindow(QMainWindow):
|
|
1176
1176
|
# Right panel connections
|
1177
1177
|
self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
|
1178
1178
|
self.right_panel.image_selected.connect(self._load_selected_image)
|
1179
|
+
# Connect new path-based signal from FastFileManager
|
1180
|
+
self.right_panel.image_path_selected.connect(self._load_image_from_path)
|
1179
1181
|
self.right_panel.merge_selection_requested.connect(
|
1180
1182
|
self._assign_selected_to_class
|
1181
1183
|
)
|
@@ -1625,6 +1627,13 @@ class MainWindow(QMainWindow):
|
|
1625
1627
|
self._start_background_image_discovery()
|
1626
1628
|
self.viewer.setFocus()
|
1627
1629
|
|
1630
|
+
def _load_image_from_path(self, file_path: Path):
|
1631
|
+
"""Load image from a Path object (used by FastFileManager)."""
|
1632
|
+
if file_path.is_file() and self.file_manager.is_image_file(str(file_path)):
|
1633
|
+
# Convert Path to QModelIndex for compatibility
|
1634
|
+
# This allows existing code to work while using the new file manager
|
1635
|
+
self._load_image_by_path(str(file_path))
|
1636
|
+
|
1628
1637
|
def _load_selected_image(self, index):
|
1629
1638
|
"""Load the selected image. Auto-saves previous work if enabled."""
|
1630
1639
|
|
@@ -1686,6 +1695,131 @@ class MainWindow(QMainWindow):
|
|
1686
1695
|
|
1687
1696
|
self._show_success_notification(f"Loaded: {Path(self.current_image_path).name}")
|
1688
1697
|
|
1698
|
+
def _load_image_by_path(self, path: str):
|
1699
|
+
"""Load image by file path directly (for FastFileManager)."""
|
1700
|
+
# Check if we're in multi-view mode
|
1701
|
+
if hasattr(self, "view_mode") and self.view_mode == "multi":
|
1702
|
+
# For multi-view, we need to handle differently
|
1703
|
+
# Load the selected image and consecutive ones
|
1704
|
+
self._load_multi_view_from_path(path)
|
1705
|
+
return
|
1706
|
+
|
1707
|
+
if path == self.current_image_path: # Only reset if loading a new image
|
1708
|
+
return
|
1709
|
+
|
1710
|
+
# Auto-save if enabled and we have a current image (not the first load)
|
1711
|
+
if self.current_image_path and self.control_panel.get_settings().get(
|
1712
|
+
"auto_save", True
|
1713
|
+
):
|
1714
|
+
self._save_output_to_npz()
|
1715
|
+
|
1716
|
+
self.segment_manager.clear()
|
1717
|
+
# Remove all scene items except the pixmap
|
1718
|
+
items_to_remove = [
|
1719
|
+
item
|
1720
|
+
for item in self.viewer.scene().items()
|
1721
|
+
if item is not self.viewer._pixmap_item
|
1722
|
+
]
|
1723
|
+
for item in items_to_remove:
|
1724
|
+
self.viewer.scene().removeItem(item)
|
1725
|
+
self.current_image_path = path
|
1726
|
+
|
1727
|
+
# Load the image
|
1728
|
+
original_image = cv2.imread(path)
|
1729
|
+
if original_image is None:
|
1730
|
+
logger.error(f"Failed to load image: {path}")
|
1731
|
+
return
|
1732
|
+
|
1733
|
+
# Convert BGR to RGB
|
1734
|
+
if len(original_image.shape) == 3:
|
1735
|
+
original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
|
1736
|
+
self.original_image = original_image
|
1737
|
+
|
1738
|
+
# Convert to QImage and display
|
1739
|
+
height, width = original_image.shape[:2]
|
1740
|
+
bytes_per_line = 3 * width if len(original_image.shape) == 3 else width
|
1741
|
+
|
1742
|
+
if len(original_image.shape) == 3:
|
1743
|
+
q_image = QImage(
|
1744
|
+
original_image.data.tobytes(),
|
1745
|
+
width,
|
1746
|
+
height,
|
1747
|
+
bytes_per_line,
|
1748
|
+
QImage.Format.Format_RGB888,
|
1749
|
+
)
|
1750
|
+
else:
|
1751
|
+
q_image = QImage(
|
1752
|
+
original_image.data.tobytes(),
|
1753
|
+
width,
|
1754
|
+
height,
|
1755
|
+
bytes_per_line,
|
1756
|
+
QImage.Format.Format_Grayscale8,
|
1757
|
+
)
|
1758
|
+
|
1759
|
+
pixmap = QPixmap.fromImage(q_image)
|
1760
|
+
self.viewer.set_photo(pixmap)
|
1761
|
+
|
1762
|
+
# Load existing segments and class aliases
|
1763
|
+
self.file_manager.load_class_aliases(path)
|
1764
|
+
self.file_manager.load_existing_mask(path)
|
1765
|
+
|
1766
|
+
# Update UI lists to reflect loaded segments
|
1767
|
+
self._update_all_lists()
|
1768
|
+
|
1769
|
+
# Update display if we have an update method
|
1770
|
+
if hasattr(self, "_update_display"):
|
1771
|
+
self._update_display()
|
1772
|
+
self._show_success_notification(f"Loaded: {Path(path).name}")
|
1773
|
+
|
1774
|
+
# Update file selection in the file manager
|
1775
|
+
self.right_panel.select_file(Path(path))
|
1776
|
+
|
1777
|
+
def _load_multi_view_from_path(self, path: str):
|
1778
|
+
"""Load multi-view starting from a specific path using FastFileManager."""
|
1779
|
+
config = self._get_multi_view_config()
|
1780
|
+
num_viewers = config["num_viewers"]
|
1781
|
+
|
1782
|
+
# Auto-save if enabled
|
1783
|
+
if (
|
1784
|
+
hasattr(self, "multi_view_images")
|
1785
|
+
and self.multi_view_images
|
1786
|
+
and self.multi_view_images[0]
|
1787
|
+
and self.control_panel.get_settings().get("auto_save", True)
|
1788
|
+
):
|
1789
|
+
self._save_multi_view_output()
|
1790
|
+
|
1791
|
+
# Get all files from the FastFileManager model
|
1792
|
+
file_manager_model = self.right_panel.file_manager._model
|
1793
|
+
all_files = []
|
1794
|
+
current_idx = -1
|
1795
|
+
|
1796
|
+
# Collect all files from the model
|
1797
|
+
for i in range(file_manager_model.rowCount()):
|
1798
|
+
file_info = file_manager_model.getFileInfo(i)
|
1799
|
+
if file_info:
|
1800
|
+
all_files.append(str(file_info.path))
|
1801
|
+
if str(file_info.path) == path:
|
1802
|
+
current_idx = i
|
1803
|
+
|
1804
|
+
if current_idx == -1:
|
1805
|
+
# File not found in current list
|
1806
|
+
return
|
1807
|
+
|
1808
|
+
# Load consecutive images
|
1809
|
+
images_to_load = []
|
1810
|
+
for i in range(num_viewers):
|
1811
|
+
idx = current_idx + i
|
1812
|
+
if idx < len(all_files):
|
1813
|
+
images_to_load.append(all_files[idx])
|
1814
|
+
else:
|
1815
|
+
images_to_load.append(None)
|
1816
|
+
|
1817
|
+
# Load the images
|
1818
|
+
self._load_multi_view_images(images_to_load)
|
1819
|
+
|
1820
|
+
# Update file manager selection
|
1821
|
+
self.right_panel.select_file(Path(path))
|
1822
|
+
|
1689
1823
|
def _load_selected_image_multi_view(self, index, path):
|
1690
1824
|
"""Load selected image in multi-view mode starting from the selected file."""
|
1691
1825
|
# Auto-save if enabled and we have current images (not the first load)
|
@@ -2141,17 +2275,9 @@ class MainWindow(QMainWindow):
|
|
2141
2275
|
self._load_next_multi_batch()
|
2142
2276
|
return
|
2143
2277
|
|
2144
|
-
|
2145
|
-
|
2146
|
-
|
2147
|
-
row = self.current_file_index.row()
|
2148
|
-
# Find next valid image file
|
2149
|
-
for next_row in range(row + 1, self.file_model.rowCount(parent)):
|
2150
|
-
next_index = self.file_model.index(next_row, 0, parent)
|
2151
|
-
path = self.file_model.filePath(next_index)
|
2152
|
-
if os.path.isfile(path) and self.file_manager.is_image_file(path):
|
2153
|
-
self._load_selected_image(next_index)
|
2154
|
-
return
|
2278
|
+
# Use new file manager navigation
|
2279
|
+
self.right_panel.navigate_next_image()
|
2280
|
+
return
|
2155
2281
|
|
2156
2282
|
def _load_previous_image(self):
|
2157
2283
|
"""Load previous image in the file list."""
|
@@ -2160,17 +2286,9 @@ class MainWindow(QMainWindow):
|
|
2160
2286
|
self._load_previous_multi_batch()
|
2161
2287
|
return
|
2162
2288
|
|
2163
|
-
|
2164
|
-
|
2165
|
-
|
2166
|
-
row = self.current_file_index.row()
|
2167
|
-
# Find previous valid image file
|
2168
|
-
for prev_row in range(row - 1, -1, -1):
|
2169
|
-
prev_index = self.file_model.index(prev_row, 0, parent)
|
2170
|
-
path = self.file_model.filePath(prev_index)
|
2171
|
-
if os.path.isfile(path) and self.file_manager.is_image_file(path):
|
2172
|
-
self._load_selected_image(prev_index)
|
2173
|
-
return
|
2289
|
+
# Use new file manager navigation
|
2290
|
+
self.right_panel.navigate_previous_image()
|
2291
|
+
return
|
2174
2292
|
|
2175
2293
|
# Segment management methods
|
2176
2294
|
def _assign_selected_to_class(self):
|
@@ -2823,20 +2941,34 @@ class MainWindow(QMainWindow):
|
|
2823
2941
|
|
2824
2942
|
def _handle_enter_press(self):
|
2825
2943
|
"""Handle enter key press."""
|
2944
|
+
logger.debug(f"Enter pressed - mode: {self.mode}, view_mode: {self.view_mode}")
|
2945
|
+
|
2826
2946
|
if self.mode == "polygon":
|
2827
|
-
|
2828
|
-
self.
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2947
|
+
logger.debug(
|
2948
|
+
f"Polygon mode - polygon_points: {len(self.polygon_points) if hasattr(self, 'polygon_points') and self.polygon_points else 0}"
|
2949
|
+
)
|
2950
|
+
if self.view_mode == "single":
|
2951
|
+
# If there are pending polygon points, finalize them first
|
2952
|
+
if self.polygon_points:
|
2953
|
+
logger.debug("Finalizing pending polygon")
|
2954
|
+
self._finalize_polygon()
|
2955
|
+
# Always save after handling pending work to show checkmarks and notifications
|
2956
|
+
logger.debug("Saving polygon segments to NPZ")
|
2957
|
+
self._save_output_to_npz()
|
2958
|
+
elif self.view_mode == "multi":
|
2832
2959
|
# Complete polygons for all viewers that have points
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
2837
|
-
self
|
2838
|
-
|
2839
|
-
|
2960
|
+
if hasattr(self, "multi_view_polygon_points"):
|
2961
|
+
for i, points in enumerate(self.multi_view_polygon_points):
|
2962
|
+
if points and len(points) >= 3:
|
2963
|
+
# Use the multi-view mode handler for proper pairing logic
|
2964
|
+
if hasattr(self, "multi_view_mode_handler"):
|
2965
|
+
self.multi_view_mode_handler._finalize_multi_view_polygon(
|
2966
|
+
i
|
2967
|
+
)
|
2968
|
+
else:
|
2969
|
+
self._finalize_multi_view_polygon(i)
|
2970
|
+
# Always save after handling pending work to show checkmarks and notifications
|
2971
|
+
self._save_output_to_npz()
|
2840
2972
|
else:
|
2841
2973
|
# First accept any AI segments (same as spacebar), then save
|
2842
2974
|
self._accept_ai_segment()
|
@@ -3198,7 +3330,13 @@ class MainWindow(QMainWindow):
|
|
3198
3330
|
try:
|
3199
3331
|
os.remove(file_path)
|
3200
3332
|
deleted_files.append(os.path.basename(file_path))
|
3201
|
-
|
3333
|
+
# Update FastFileManager after file deletion
|
3334
|
+
if hasattr(self, "right_panel") and hasattr(
|
3335
|
+
self.right_panel, "file_manager"
|
3336
|
+
):
|
3337
|
+
self.right_panel.file_manager.updateNpzStatus(
|
3338
|
+
Path(image_path)
|
3339
|
+
)
|
3202
3340
|
except Exception as e:
|
3203
3341
|
self._show_error_notification(
|
3204
3342
|
f"Error deleting {file_path}: {e}"
|
@@ -3213,20 +3351,15 @@ class MainWindow(QMainWindow):
|
|
3213
3351
|
f"Error saving {os.path.basename(image_path)}: {str(e)}"
|
3214
3352
|
)
|
3215
3353
|
|
3216
|
-
# Update
|
3217
|
-
if hasattr(self, "
|
3218
|
-
for
|
3219
|
-
if
|
3220
|
-
|
3221
|
-
self
|
3222
|
-
|
3223
|
-
|
3224
|
-
|
3225
|
-
self.file_model.set_highlighted_path(None)
|
3226
|
-
if self.file_model.highlighted_path == p
|
3227
|
-
else None
|
3228
|
-
),
|
3229
|
-
)
|
3354
|
+
# Update FastFileManager to show NPZ checkmarks for multi-view
|
3355
|
+
if hasattr(self, "multi_view_images") and self.multi_view_images:
|
3356
|
+
for image_path in self.multi_view_images:
|
3357
|
+
if (
|
3358
|
+
image_path
|
3359
|
+
and hasattr(self, "right_panel")
|
3360
|
+
and hasattr(self.right_panel, "file_manager")
|
3361
|
+
):
|
3362
|
+
self.right_panel.file_manager.updateNpzStatus(Path(image_path))
|
3230
3363
|
# Clear the tracking list for next save
|
3231
3364
|
self._saved_file_paths = []
|
3232
3365
|
|
@@ -3258,7 +3391,13 @@ class MainWindow(QMainWindow):
|
|
3258
3391
|
try:
|
3259
3392
|
os.remove(file_path)
|
3260
3393
|
deleted_files.append(file_path)
|
3261
|
-
|
3394
|
+
# Update FastFileManager after file deletion
|
3395
|
+
if hasattr(self, "right_panel") and hasattr(
|
3396
|
+
self.right_panel, "file_manager"
|
3397
|
+
):
|
3398
|
+
self.right_panel.file_manager.updateNpzStatus(
|
3399
|
+
Path(self.current_image_path)
|
3400
|
+
)
|
3262
3401
|
except Exception as e:
|
3263
3402
|
self._show_error_notification(
|
3264
3403
|
f"Error deleting {file_path}: {e}"
|
@@ -3276,24 +3415,35 @@ class MainWindow(QMainWindow):
|
|
3276
3415
|
try:
|
3277
3416
|
settings = self.control_panel.get_settings()
|
3278
3417
|
npz_path = None
|
3279
|
-
|
3418
|
+
|
3419
|
+
# Debug logging
|
3420
|
+
logger.debug(f"Starting save process for: {self.current_image_path}")
|
3421
|
+
logger.debug(f"Number of segments: {len(self.segment_manager.segments)}")
|
3422
|
+
|
3280
3423
|
if settings.get("save_npz", True):
|
3281
3424
|
h, w = (
|
3282
3425
|
self.viewer._pixmap_item.pixmap().height(),
|
3283
3426
|
self.viewer._pixmap_item.pixmap().width(),
|
3284
3427
|
)
|
3285
3428
|
class_order = self.segment_manager.get_unique_class_ids()
|
3429
|
+
logger.debug(f"Class order for saving: {class_order}")
|
3430
|
+
|
3286
3431
|
if class_order:
|
3432
|
+
logger.debug(
|
3433
|
+
f"Attempting to save NPZ to: {os.path.splitext(self.current_image_path)[0]}.npz"
|
3434
|
+
)
|
3287
3435
|
npz_path = self.file_manager.save_npz(
|
3288
3436
|
self.current_image_path,
|
3289
3437
|
(h, w),
|
3290
3438
|
class_order,
|
3291
3439
|
self.current_crop_coords,
|
3292
3440
|
)
|
3441
|
+
logger.debug(f"NPZ save completed: {npz_path}")
|
3293
3442
|
self._show_success_notification(
|
3294
3443
|
f"Saved: {os.path.basename(npz_path)}"
|
3295
3444
|
)
|
3296
3445
|
else:
|
3446
|
+
logger.warning("No classes defined for saving")
|
3297
3447
|
self._show_warning_notification("No classes defined for saving.")
|
3298
3448
|
if settings.get("save_txt", True):
|
3299
3449
|
h, w = (
|
@@ -3308,27 +3458,25 @@ class MainWindow(QMainWindow):
|
|
3308
3458
|
else:
|
3309
3459
|
class_labels = [str(cid) for cid in class_order]
|
3310
3460
|
if class_order:
|
3311
|
-
|
3461
|
+
self.file_manager.save_yolo_txt(
|
3312
3462
|
self.current_image_path,
|
3313
3463
|
(h, w),
|
3314
3464
|
class_order,
|
3315
3465
|
class_labels,
|
3316
3466
|
self.current_crop_coords,
|
3317
3467
|
)
|
3318
|
-
#
|
3319
|
-
|
3320
|
-
|
3321
|
-
|
3322
|
-
|
3323
|
-
|
3324
|
-
|
3325
|
-
|
3326
|
-
|
3327
|
-
|
3328
|
-
else None
|
3329
|
-
),
|
3330
|
-
)
|
3468
|
+
# Update FastFileManager to show NPZ checkmarks
|
3469
|
+
if (
|
3470
|
+
npz_path
|
3471
|
+
and hasattr(self, "right_panel")
|
3472
|
+
and hasattr(self.right_panel, "file_manager")
|
3473
|
+
):
|
3474
|
+
# Update the NPZ status in the FastFileManager
|
3475
|
+
self.right_panel.file_manager.updateNpzStatus(
|
3476
|
+
Path(self.current_image_path)
|
3477
|
+
)
|
3331
3478
|
except Exception as e:
|
3479
|
+
logger.error(f"Error saving file: {str(e)}", exc_info=True)
|
3332
3480
|
self._show_error_notification(f"Error saving: {str(e)}")
|
3333
3481
|
|
3334
3482
|
def _handle_merge_press(self):
|
lazylabel/ui/right_panel.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Right panel with file explorer and segment management."""
|
2
2
|
|
3
|
+
from pathlib import Path
|
4
|
+
|
3
5
|
from PyQt6.QtCore import Qt, pyqtSignal
|
4
6
|
from PyQt6.QtWidgets import (
|
5
7
|
QComboBox,
|
@@ -14,6 +16,7 @@ from PyQt6.QtWidgets import (
|
|
14
16
|
QWidget,
|
15
17
|
)
|
16
18
|
|
19
|
+
from ..utils.fast_file_manager import FastFileManager
|
17
20
|
from .reorderable_class_table import ReorderableClassTable
|
18
21
|
|
19
22
|
|
@@ -23,6 +26,7 @@ class RightPanel(QWidget):
|
|
23
26
|
# Signals
|
24
27
|
open_folder_requested = pyqtSignal()
|
25
28
|
image_selected = pyqtSignal("QModelIndex")
|
29
|
+
image_path_selected = pyqtSignal(Path) # New signal for path-based selection
|
26
30
|
merge_selection_requested = pyqtSignal()
|
27
31
|
delete_selection_requested = pyqtSignal()
|
28
32
|
segments_selection_changed = pyqtSignal()
|
@@ -109,8 +113,13 @@ class RightPanel(QWidget):
|
|
109
113
|
self.btn_open_folder.setToolTip("Open a directory of images")
|
110
114
|
layout.addWidget(self.btn_open_folder)
|
111
115
|
|
112
|
-
|
113
|
-
|
116
|
+
# Use new FastFileManager instead of QTreeView
|
117
|
+
self.file_manager = FastFileManager()
|
118
|
+
layout.addWidget(self.file_manager)
|
119
|
+
|
120
|
+
# Keep file_tree reference for compatibility
|
121
|
+
self.file_tree = QTreeView() # Hidden, for backward compatibility
|
122
|
+
self.file_tree.hide()
|
114
123
|
|
115
124
|
splitter.addWidget(file_explorer_widget)
|
116
125
|
|
@@ -195,6 +204,8 @@ class RightPanel(QWidget):
|
|
195
204
|
"""Connect internal signals."""
|
196
205
|
self.btn_open_folder.clicked.connect(self.open_folder_requested)
|
197
206
|
self.file_tree.doubleClicked.connect(self.image_selected)
|
207
|
+
# Connect new file manager signal
|
208
|
+
self.file_manager.fileSelected.connect(self.image_path_selected)
|
198
209
|
self.btn_merge_selection.clicked.connect(self.merge_selection_requested)
|
199
210
|
self.btn_delete_selection.clicked.connect(self.delete_selection_requested)
|
200
211
|
self.segment_table.itemSelectionChanged.connect(self.segments_selection_changed)
|
@@ -276,12 +287,28 @@ class RightPanel(QWidget):
|
|
276
287
|
|
277
288
|
def setup_file_model(self, file_model):
|
278
289
|
"""Setup the file model for the tree view."""
|
290
|
+
# Keep for backward compatibility
|
279
291
|
self.file_tree.setModel(file_model)
|
280
292
|
self.file_tree.setColumnWidth(0, 200)
|
281
293
|
|
282
294
|
def set_folder(self, folder_path, file_model):
|
283
295
|
"""Set the folder for file browsing."""
|
296
|
+
# Keep old tree view for compatibility
|
284
297
|
self.file_tree.setRootIndex(file_model.setRootPath(folder_path))
|
298
|
+
# Use new file manager
|
299
|
+
self.file_manager.setDirectory(Path(folder_path))
|
300
|
+
|
301
|
+
def navigate_next_image(self):
|
302
|
+
"""Navigate to next image in the file manager."""
|
303
|
+
self.file_manager.navigateNext()
|
304
|
+
|
305
|
+
def navigate_previous_image(self):
|
306
|
+
"""Navigate to previous image in the file manager."""
|
307
|
+
self.file_manager.navigatePrevious()
|
308
|
+
|
309
|
+
def select_file(self, file_path: Path):
|
310
|
+
"""Select a specific file in the file manager."""
|
311
|
+
self.file_manager.selectFile(file_path)
|
285
312
|
|
286
313
|
def get_selected_segment_indices(self):
|
287
314
|
"""Get indices of selected segments."""
|
@@ -0,0 +1,676 @@
|
|
1
|
+
"""
|
2
|
+
Fast file manager with lazy loading, sorting, and efficient navigation
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from datetime import datetime
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
from PyQt6.QtCore import (
|
11
|
+
QAbstractTableModel,
|
12
|
+
QModelIndex,
|
13
|
+
QSortFilterProxyModel,
|
14
|
+
Qt,
|
15
|
+
QThread,
|
16
|
+
pyqtSignal,
|
17
|
+
)
|
18
|
+
from PyQt6.QtGui import QPixmap
|
19
|
+
from PyQt6.QtWidgets import (
|
20
|
+
QComboBox,
|
21
|
+
QHBoxLayout,
|
22
|
+
QHeaderView,
|
23
|
+
QLabel,
|
24
|
+
QLineEdit,
|
25
|
+
QPushButton,
|
26
|
+
QTableView,
|
27
|
+
QVBoxLayout,
|
28
|
+
QWidget,
|
29
|
+
)
|
30
|
+
|
31
|
+
# Image extensions supported
|
32
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class FileInfo:
|
37
|
+
"""Information about a file"""
|
38
|
+
|
39
|
+
path: Path
|
40
|
+
name: str
|
41
|
+
size: int = 0 # Lazy load for speed
|
42
|
+
modified: float = 0.0 # Lazy load for speed
|
43
|
+
has_npz: bool = False
|
44
|
+
has_txt: bool = False
|
45
|
+
thumbnail: QPixmap | None = None
|
46
|
+
|
47
|
+
|
48
|
+
class FileScanner(QThread):
|
49
|
+
"""Background thread for scanning files"""
|
50
|
+
|
51
|
+
filesFound = pyqtSignal(list) # Emits batches of FileInfo
|
52
|
+
scanComplete = pyqtSignal(int) # Total file count
|
53
|
+
progress = pyqtSignal(int, int) # Current, total
|
54
|
+
|
55
|
+
def __init__(self, directory: Path):
|
56
|
+
super().__init__()
|
57
|
+
self.directory = directory
|
58
|
+
self._stop_flag = False
|
59
|
+
|
60
|
+
def run(self):
|
61
|
+
"""Scan directory in background - OPTIMIZED FOR SPEED"""
|
62
|
+
batch_size = 1000 # Larger batches = fewer UI updates = faster
|
63
|
+
batch = []
|
64
|
+
total_files = 0
|
65
|
+
|
66
|
+
try:
|
67
|
+
# Use os.scandir() - MUCH faster than Path.iterdir()
|
68
|
+
# Single pass to collect all file info
|
69
|
+
npz_stems = set()
|
70
|
+
txt_stems = set()
|
71
|
+
image_entries = []
|
72
|
+
|
73
|
+
with os.scandir(self.directory) as entries:
|
74
|
+
for entry in entries:
|
75
|
+
if self._stop_flag:
|
76
|
+
break
|
77
|
+
|
78
|
+
# Check file extension
|
79
|
+
name = entry.name
|
80
|
+
ext = os.path.splitext(name)[1].lower()
|
81
|
+
|
82
|
+
if ext == ".npz":
|
83
|
+
npz_stems.add(os.path.splitext(name)[0])
|
84
|
+
elif ext == ".txt":
|
85
|
+
txt_stems.add(os.path.splitext(name)[0])
|
86
|
+
elif ext in IMAGE_EXTENSIONS:
|
87
|
+
image_entries.append((entry.path, name))
|
88
|
+
|
89
|
+
# Process images in batches
|
90
|
+
total_count = len(image_entries)
|
91
|
+
|
92
|
+
for i, (path, name) in enumerate(image_entries):
|
93
|
+
if self._stop_flag:
|
94
|
+
break
|
95
|
+
|
96
|
+
stem = os.path.splitext(name)[0]
|
97
|
+
|
98
|
+
# Create FileInfo - NO STAT CALLS for speed!
|
99
|
+
file_info = FileInfo(
|
100
|
+
path=Path(path),
|
101
|
+
name=name,
|
102
|
+
has_npz=stem in npz_stems,
|
103
|
+
has_txt=stem in txt_stems,
|
104
|
+
)
|
105
|
+
|
106
|
+
batch.append(file_info)
|
107
|
+
total_files += 1
|
108
|
+
|
109
|
+
if len(batch) >= batch_size:
|
110
|
+
self.filesFound.emit(batch)
|
111
|
+
batch = []
|
112
|
+
|
113
|
+
# Progress updates less frequently
|
114
|
+
if i % 1000 == 0 and i > 0:
|
115
|
+
self.progress.emit(i, total_count)
|
116
|
+
|
117
|
+
# Emit remaining files
|
118
|
+
if batch:
|
119
|
+
self.filesFound.emit(batch)
|
120
|
+
|
121
|
+
self.scanComplete.emit(total_files)
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
print(f"Error scanning directory: {e}")
|
125
|
+
|
126
|
+
def stop(self):
|
127
|
+
"""Stop the scanning thread"""
|
128
|
+
self._stop_flag = True
|
129
|
+
|
130
|
+
|
131
|
+
class FastFileModel(QAbstractTableModel):
|
132
|
+
"""High-performance file model with background loading"""
|
133
|
+
|
134
|
+
fileSelected = pyqtSignal(Path)
|
135
|
+
|
136
|
+
def __init__(self):
|
137
|
+
super().__init__()
|
138
|
+
self._files: list[FileInfo] = []
|
139
|
+
self._scanner: FileScanner | None = None
|
140
|
+
|
141
|
+
def rowCount(self, parent=QModelIndex()):
|
142
|
+
return len(self._files)
|
143
|
+
|
144
|
+
def columnCount(self, parent=QModelIndex()):
|
145
|
+
return 5 # Name, Size, Modified, NPZ, TXT
|
146
|
+
|
147
|
+
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
148
|
+
if not index.isValid():
|
149
|
+
return None
|
150
|
+
|
151
|
+
file_info = self._files[index.row()]
|
152
|
+
col = index.column()
|
153
|
+
|
154
|
+
if role == Qt.ItemDataRole.DisplayRole:
|
155
|
+
if col == 0: # Name
|
156
|
+
return file_info.name
|
157
|
+
elif col == 1: # Size
|
158
|
+
# Lazy load size only when displayed
|
159
|
+
if file_info.size == 0:
|
160
|
+
try:
|
161
|
+
file_info.size = file_info.path.stat().st_size
|
162
|
+
except OSError:
|
163
|
+
file_info.size = -1 # Mark as error
|
164
|
+
return self._format_size(file_info.size) if file_info.size >= 0 else "-"
|
165
|
+
elif col == 2: # Modified
|
166
|
+
# Lazy load modified time only when displayed
|
167
|
+
if file_info.modified == 0.0:
|
168
|
+
try:
|
169
|
+
file_info.modified = file_info.path.stat().st_mtime
|
170
|
+
except OSError:
|
171
|
+
file_info.modified = -1 # Mark as error
|
172
|
+
return (
|
173
|
+
datetime.fromtimestamp(file_info.modified).strftime(
|
174
|
+
"%Y-%m-%d %H:%M"
|
175
|
+
)
|
176
|
+
if file_info.modified > 0
|
177
|
+
else "-"
|
178
|
+
)
|
179
|
+
elif col == 3: # NPZ
|
180
|
+
return "✓" if file_info.has_npz else ""
|
181
|
+
elif col == 4: # TXT
|
182
|
+
return "✓" if file_info.has_txt else ""
|
183
|
+
elif role == Qt.ItemDataRole.UserRole:
|
184
|
+
# Return the FileInfo object for custom access
|
185
|
+
return file_info
|
186
|
+
elif role == Qt.ItemDataRole.TextAlignmentRole and col in [
|
187
|
+
3,
|
188
|
+
4,
|
189
|
+
]: # Center checkmarks
|
190
|
+
return Qt.AlignmentFlag.AlignCenter
|
191
|
+
|
192
|
+
return None
|
193
|
+
|
194
|
+
def headerData(self, section, orientation, role):
|
195
|
+
if (
|
196
|
+
orientation == Qt.Orientation.Horizontal
|
197
|
+
and role == Qt.ItemDataRole.DisplayRole
|
198
|
+
):
|
199
|
+
headers = ["Name", "Size", "Modified", "NPZ", "TXT"]
|
200
|
+
return headers[section]
|
201
|
+
return None
|
202
|
+
|
203
|
+
def _format_size(self, size: int) -> str:
|
204
|
+
"""Format file size in human readable format"""
|
205
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
206
|
+
if size < 1024:
|
207
|
+
return f"{size:.1f} {unit}"
|
208
|
+
size /= 1024
|
209
|
+
return f"{size:.1f} TB"
|
210
|
+
|
211
|
+
def setDirectory(self, directory: Path):
|
212
|
+
"""Set directory to scan"""
|
213
|
+
# Stop previous scanner if running
|
214
|
+
if self._scanner and self._scanner.isRunning():
|
215
|
+
self._scanner.stop()
|
216
|
+
self._scanner.wait()
|
217
|
+
|
218
|
+
# Clear current files
|
219
|
+
self.beginResetModel()
|
220
|
+
self._files.clear()
|
221
|
+
self.endResetModel()
|
222
|
+
|
223
|
+
# Start new scan
|
224
|
+
self._scanner = FileScanner(directory)
|
225
|
+
self._scanner.filesFound.connect(self._on_files_found)
|
226
|
+
self._scanner.scanComplete.connect(self._on_scan_complete)
|
227
|
+
self._scanner.start()
|
228
|
+
|
229
|
+
def _on_files_found(self, files: list[FileInfo]):
|
230
|
+
"""Handle batch of files found"""
|
231
|
+
start_row = len(self._files)
|
232
|
+
end_row = start_row + len(files) - 1
|
233
|
+
self.beginInsertRows(QModelIndex(), start_row, end_row)
|
234
|
+
self._files.extend(files)
|
235
|
+
self.endInsertRows()
|
236
|
+
|
237
|
+
def _on_scan_complete(self, total: int):
|
238
|
+
"""Handle scan completion"""
|
239
|
+
print(f"Scan complete: {total} files found")
|
240
|
+
|
241
|
+
def getFileInfo(self, index: int) -> FileInfo | None:
|
242
|
+
"""Get file info at index"""
|
243
|
+
if 0 <= index < len(self._files):
|
244
|
+
return self._files[index]
|
245
|
+
return None
|
246
|
+
|
247
|
+
def updateNpzStatus(self, image_path: Path):
|
248
|
+
"""Update NPZ status for a specific image file"""
|
249
|
+
image_path_str = str(image_path)
|
250
|
+
npz_path = image_path.with_suffix(".npz")
|
251
|
+
has_npz = npz_path.exists()
|
252
|
+
|
253
|
+
# Find and update the file info
|
254
|
+
for i, file_info in enumerate(self._files):
|
255
|
+
if str(file_info.path) == image_path_str:
|
256
|
+
old_has_npz = file_info.has_npz
|
257
|
+
file_info.has_npz = has_npz
|
258
|
+
|
259
|
+
# Only emit dataChanged if status actually changed
|
260
|
+
if old_has_npz != has_npz:
|
261
|
+
index = self.index(i, 3) # NPZ column
|
262
|
+
self.dataChanged.emit(index, index)
|
263
|
+
break
|
264
|
+
|
265
|
+
def getFileIndex(self, path: Path) -> int:
|
266
|
+
"""Get index of file by path"""
|
267
|
+
for i, file_info in enumerate(self._files):
|
268
|
+
if file_info.path == path:
|
269
|
+
return i
|
270
|
+
return -1
|
271
|
+
|
272
|
+
|
273
|
+
class FileSortProxyModel(QSortFilterProxyModel):
|
274
|
+
"""Custom proxy model for proper sorting of file data."""
|
275
|
+
|
276
|
+
def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
|
277
|
+
"""Custom sorting comparison."""
|
278
|
+
# Get the file info objects
|
279
|
+
left_info = self.sourceModel().getFileInfo(left.row())
|
280
|
+
right_info = self.sourceModel().getFileInfo(right.row())
|
281
|
+
|
282
|
+
if not left_info or not right_info:
|
283
|
+
return False
|
284
|
+
|
285
|
+
col = left.column()
|
286
|
+
|
287
|
+
# Sort based on column
|
288
|
+
if col == 0: # Name
|
289
|
+
return left_info.name.lower() < right_info.name.lower()
|
290
|
+
elif col == 1: # Size
|
291
|
+
# Lazy load size if needed for sorting
|
292
|
+
if left_info.size == 0:
|
293
|
+
try:
|
294
|
+
left_info.size = left_info.path.stat().st_size
|
295
|
+
except OSError:
|
296
|
+
left_info.size = -1
|
297
|
+
if right_info.size == 0:
|
298
|
+
try:
|
299
|
+
right_info.size = right_info.path.stat().st_size
|
300
|
+
except OSError:
|
301
|
+
right_info.size = -1
|
302
|
+
return left_info.size < right_info.size
|
303
|
+
elif col == 2: # Modified
|
304
|
+
# Lazy load modified time if needed for sorting
|
305
|
+
if left_info.modified == 0.0:
|
306
|
+
try:
|
307
|
+
left_info.modified = left_info.path.stat().st_mtime
|
308
|
+
except OSError:
|
309
|
+
left_info.modified = -1
|
310
|
+
if right_info.modified == 0.0:
|
311
|
+
try:
|
312
|
+
right_info.modified = right_info.path.stat().st_mtime
|
313
|
+
except OSError:
|
314
|
+
right_info.modified = -1
|
315
|
+
return left_info.modified < right_info.modified
|
316
|
+
elif col == 3: # NPZ
|
317
|
+
return left_info.has_npz < right_info.has_npz
|
318
|
+
elif col == 4: # TXT
|
319
|
+
return left_info.has_txt < right_info.has_txt
|
320
|
+
|
321
|
+
return False
|
322
|
+
|
323
|
+
|
324
|
+
class FastFileManager(QWidget):
|
325
|
+
"""Main file manager widget with improved performance"""
|
326
|
+
|
327
|
+
fileSelected = pyqtSignal(Path)
|
328
|
+
|
329
|
+
def __init__(self):
|
330
|
+
super().__init__()
|
331
|
+
self._current_directory = None
|
332
|
+
self._init_ui()
|
333
|
+
|
334
|
+
def _init_ui(self):
|
335
|
+
"""Initialize the UI"""
|
336
|
+
layout = QVBoxLayout(self)
|
337
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
338
|
+
|
339
|
+
# Header with controls
|
340
|
+
header = self._create_header()
|
341
|
+
layout.addWidget(header)
|
342
|
+
|
343
|
+
# File table view
|
344
|
+
self._table_view = QTableView()
|
345
|
+
self._table_view.setAlternatingRowColors(True)
|
346
|
+
self._table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
347
|
+
self._table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
348
|
+
self._table_view.setSortingEnabled(False) # We'll handle sorting manually
|
349
|
+
self._table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
350
|
+
|
351
|
+
# Set up model and proxy
|
352
|
+
self._model = FastFileModel()
|
353
|
+
self._model.fileSelected.connect(self.fileSelected)
|
354
|
+
|
355
|
+
# Set up custom sorting proxy
|
356
|
+
self._proxy_model = FileSortProxyModel()
|
357
|
+
self._proxy_model.setSourceModel(self._model)
|
358
|
+
self._table_view.setModel(self._proxy_model)
|
359
|
+
|
360
|
+
# Enable sorting
|
361
|
+
self._table_view.setSortingEnabled(True)
|
362
|
+
self._table_view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
|
363
|
+
|
364
|
+
# Configure headers
|
365
|
+
header = self._table_view.horizontalHeader()
|
366
|
+
header.setSectionResizeMode(
|
367
|
+
0, QHeaderView.ResizeMode.Stretch
|
368
|
+
) # Name column stretches
|
369
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Size
|
370
|
+
header.setSectionResizeMode(
|
371
|
+
2, QHeaderView.ResizeMode.ResizeToContents
|
372
|
+
) # Modified
|
373
|
+
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # NPZ
|
374
|
+
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # TXT
|
375
|
+
header.resizeSection(3, 50)
|
376
|
+
header.resizeSection(4, 50)
|
377
|
+
|
378
|
+
# Style the table to match the existing UI
|
379
|
+
self._table_view.setStyleSheet("""
|
380
|
+
QTableView {
|
381
|
+
background-color: transparent;
|
382
|
+
alternate-background-color: rgba(255, 255, 255, 0.03);
|
383
|
+
gridline-color: rgba(255, 255, 255, 0.1);
|
384
|
+
color: #E0E0E0;
|
385
|
+
}
|
386
|
+
QTableView::item {
|
387
|
+
padding: 2px;
|
388
|
+
color: #E0E0E0;
|
389
|
+
}
|
390
|
+
QTableView::item:selected {
|
391
|
+
background-color: rgba(100, 100, 200, 0.5);
|
392
|
+
}
|
393
|
+
QHeaderView::section {
|
394
|
+
background-color: rgba(60, 60, 60, 0.5);
|
395
|
+
color: #E0E0E0;
|
396
|
+
padding: 4px;
|
397
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
398
|
+
font-weight: bold;
|
399
|
+
}
|
400
|
+
""")
|
401
|
+
|
402
|
+
# Connect selection
|
403
|
+
self._table_view.clicked.connect(self._on_item_clicked)
|
404
|
+
self._table_view.doubleClicked.connect(self._on_item_double_clicked)
|
405
|
+
|
406
|
+
layout.addWidget(self._table_view)
|
407
|
+
|
408
|
+
# Status bar
|
409
|
+
self._status_label = QLabel("No folder selected")
|
410
|
+
self._status_label.setStyleSheet(
|
411
|
+
"padding: 5px; background: rgba(60, 60, 60, 0.3); color: #E0E0E0;"
|
412
|
+
)
|
413
|
+
layout.addWidget(self._status_label)
|
414
|
+
|
415
|
+
def _create_header(self) -> QWidget:
|
416
|
+
"""Create header with controls"""
|
417
|
+
header = QWidget()
|
418
|
+
header.setStyleSheet("background: rgba(60, 60, 60, 0.3); padding: 5px;")
|
419
|
+
layout = QHBoxLayout(header)
|
420
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
421
|
+
|
422
|
+
# Search box
|
423
|
+
self._search_box = QLineEdit()
|
424
|
+
self._search_box.setPlaceholderText("Search files...")
|
425
|
+
self._search_box.textChanged.connect(self._on_search_changed)
|
426
|
+
self._search_box.setStyleSheet("""
|
427
|
+
QLineEdit {
|
428
|
+
background-color: rgba(50, 50, 50, 0.5);
|
429
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
430
|
+
color: #E0E0E0;
|
431
|
+
padding: 4px;
|
432
|
+
border-radius: 3px;
|
433
|
+
}
|
434
|
+
""")
|
435
|
+
layout.addWidget(self._search_box)
|
436
|
+
|
437
|
+
# Sort dropdown
|
438
|
+
sort_label = QLabel("Sort:")
|
439
|
+
sort_label.setStyleSheet("color: #E0E0E0;")
|
440
|
+
layout.addWidget(sort_label)
|
441
|
+
|
442
|
+
self._sort_combo = QComboBox()
|
443
|
+
self._sort_combo.addItems(
|
444
|
+
[
|
445
|
+
"Name (A-Z)",
|
446
|
+
"Name (Z-A)",
|
447
|
+
"Date (Oldest)",
|
448
|
+
"Date (Newest)",
|
449
|
+
"Size (Smallest)",
|
450
|
+
"Size (Largest)",
|
451
|
+
]
|
452
|
+
)
|
453
|
+
self._sort_combo.currentIndexChanged.connect(self._on_sort_changed)
|
454
|
+
self._sort_combo.setStyleSheet("""
|
455
|
+
QComboBox {
|
456
|
+
background-color: rgba(50, 50, 50, 0.5);
|
457
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
458
|
+
color: #E0E0E0;
|
459
|
+
padding: 4px;
|
460
|
+
border-radius: 3px;
|
461
|
+
}
|
462
|
+
QComboBox::drop-down {
|
463
|
+
border: none;
|
464
|
+
}
|
465
|
+
QComboBox::down-arrow {
|
466
|
+
width: 12px;
|
467
|
+
height: 12px;
|
468
|
+
}
|
469
|
+
QComboBox QAbstractItemView {
|
470
|
+
background-color: rgba(50, 50, 50, 0.9);
|
471
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
472
|
+
color: #E0E0E0;
|
473
|
+
selection-background-color: rgba(100, 100, 200, 0.5);
|
474
|
+
}
|
475
|
+
""")
|
476
|
+
layout.addWidget(self._sort_combo)
|
477
|
+
|
478
|
+
# Refresh button
|
479
|
+
refresh_btn = QPushButton("Refresh")
|
480
|
+
refresh_btn.clicked.connect(self._refresh)
|
481
|
+
refresh_btn.setStyleSheet("""
|
482
|
+
QPushButton {
|
483
|
+
background-color: rgba(70, 70, 70, 0.6);
|
484
|
+
border: 1px solid rgba(80, 80, 80, 0.4);
|
485
|
+
color: #E0E0E0;
|
486
|
+
padding: 4px 8px;
|
487
|
+
border-radius: 3px;
|
488
|
+
}
|
489
|
+
QPushButton:hover {
|
490
|
+
background-color: rgba(90, 90, 90, 0.8);
|
491
|
+
}
|
492
|
+
QPushButton:pressed {
|
493
|
+
background-color: rgba(50, 50, 50, 0.8);
|
494
|
+
}
|
495
|
+
""")
|
496
|
+
layout.addWidget(refresh_btn)
|
497
|
+
|
498
|
+
layout.addStretch()
|
499
|
+
|
500
|
+
return header
|
501
|
+
|
502
|
+
def setDirectory(self, directory: Path):
|
503
|
+
"""Set the directory to display"""
|
504
|
+
self._current_directory = directory
|
505
|
+
self._model.setDirectory(directory)
|
506
|
+
self._update_status(f"Loading: {directory.name}")
|
507
|
+
|
508
|
+
# Connect to scan complete signal
|
509
|
+
if self._model._scanner:
|
510
|
+
self._model._scanner.scanComplete.connect(
|
511
|
+
lambda count: self._update_status(f"{count} files in {directory.name}")
|
512
|
+
)
|
513
|
+
|
514
|
+
def _on_search_changed(self, text: str):
|
515
|
+
"""Handle search text change"""
|
516
|
+
self._proxy_model.setFilterFixedString(text)
|
517
|
+
self._proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
518
|
+
self._proxy_model.setFilterKeyColumn(0) # Filter on name column
|
519
|
+
|
520
|
+
def _on_sort_changed(self, index: int):
|
521
|
+
"""Handle sort order change"""
|
522
|
+
# Map combo index to column and order
|
523
|
+
column_map = {
|
524
|
+
0: 0,
|
525
|
+
1: 0,
|
526
|
+
2: 2,
|
527
|
+
3: 2,
|
528
|
+
4: 1,
|
529
|
+
5: 1,
|
530
|
+
} # Name, Name, Date, Date, Size, Size
|
531
|
+
order_map = {
|
532
|
+
0: Qt.SortOrder.AscendingOrder,
|
533
|
+
1: Qt.SortOrder.DescendingOrder,
|
534
|
+
2: Qt.SortOrder.AscendingOrder,
|
535
|
+
3: Qt.SortOrder.DescendingOrder,
|
536
|
+
4: Qt.SortOrder.AscendingOrder,
|
537
|
+
5: Qt.SortOrder.DescendingOrder,
|
538
|
+
}
|
539
|
+
|
540
|
+
column = column_map.get(index, 0)
|
541
|
+
order = order_map.get(index, Qt.SortOrder.AscendingOrder)
|
542
|
+
|
543
|
+
self._table_view.sortByColumn(column, order)
|
544
|
+
|
545
|
+
def _refresh(self):
|
546
|
+
"""Refresh current directory"""
|
547
|
+
if self._current_directory:
|
548
|
+
self.setDirectory(self._current_directory)
|
549
|
+
|
550
|
+
def updateNpzStatus(self, image_path: Path):
|
551
|
+
"""Update NPZ status for a specific image file"""
|
552
|
+
self._model.updateNpzStatus(image_path)
|
553
|
+
|
554
|
+
def refreshFile(self, image_path: Path):
|
555
|
+
"""Refresh status for a specific file (alias for updateNpzStatus)"""
|
556
|
+
self.updateNpzStatus(image_path)
|
557
|
+
|
558
|
+
def _on_item_clicked(self, index: QModelIndex):
|
559
|
+
"""Handle item click"""
|
560
|
+
# Map proxy index to source index
|
561
|
+
source_index = self._proxy_model.mapToSource(index)
|
562
|
+
file_info = self._model.getFileInfo(source_index.row())
|
563
|
+
if file_info:
|
564
|
+
self.fileSelected.emit(file_info.path)
|
565
|
+
|
566
|
+
def _on_item_double_clicked(self, index: QModelIndex):
|
567
|
+
"""Handle item double click"""
|
568
|
+
# Map proxy index to source index
|
569
|
+
source_index = self._proxy_model.mapToSource(index)
|
570
|
+
file_info = self._model.getFileInfo(source_index.row())
|
571
|
+
if file_info:
|
572
|
+
self.fileSelected.emit(file_info.path)
|
573
|
+
|
574
|
+
def _update_status(self, text: str):
|
575
|
+
"""Update status label"""
|
576
|
+
self._status_label.setText(text)
|
577
|
+
|
578
|
+
def selectFile(self, path: Path):
|
579
|
+
"""Select a specific file in the view"""
|
580
|
+
index = self._model.getFileIndex(path)
|
581
|
+
if index >= 0:
|
582
|
+
source_index = self._model.index(index, 0)
|
583
|
+
proxy_index = self._proxy_model.mapFromSource(source_index)
|
584
|
+
self._table_view.setCurrentIndex(proxy_index)
|
585
|
+
self._table_view.scrollTo(proxy_index)
|
586
|
+
# Select the entire row
|
587
|
+
selection_model = self._table_view.selectionModel()
|
588
|
+
selection_model.select(
|
589
|
+
proxy_index,
|
590
|
+
selection_model.SelectionFlag.ClearAndSelect
|
591
|
+
| selection_model.SelectionFlag.Rows,
|
592
|
+
)
|
593
|
+
|
594
|
+
def getSelectedFile(self) -> Path | None:
|
595
|
+
"""Get currently selected file"""
|
596
|
+
index = self._table_view.currentIndex()
|
597
|
+
if index.isValid():
|
598
|
+
source_index = self._proxy_model.mapToSource(index)
|
599
|
+
file_info = self._model.getFileInfo(source_index.row())
|
600
|
+
if file_info:
|
601
|
+
return file_info.path
|
602
|
+
return None
|
603
|
+
|
604
|
+
def navigateNext(self):
|
605
|
+
"""Navigate to next file"""
|
606
|
+
current = self._table_view.currentIndex()
|
607
|
+
|
608
|
+
# If no current selection and we have files, select first
|
609
|
+
if not current.isValid() and self._proxy_model.rowCount() > 0:
|
610
|
+
first_index = self._proxy_model.index(0, 0)
|
611
|
+
self._table_view.setCurrentIndex(first_index)
|
612
|
+
selection_model = self._table_view.selectionModel()
|
613
|
+
selection_model.select(
|
614
|
+
first_index,
|
615
|
+
selection_model.SelectionFlag.ClearAndSelect
|
616
|
+
| selection_model.SelectionFlag.Rows,
|
617
|
+
)
|
618
|
+
# Emit file selection
|
619
|
+
source_index = self._proxy_model.mapToSource(first_index)
|
620
|
+
file_info = self._model.getFileInfo(source_index.row())
|
621
|
+
if file_info:
|
622
|
+
self.fileSelected.emit(file_info.path)
|
623
|
+
return
|
624
|
+
|
625
|
+
if current.isValid() and current.row() < self._proxy_model.rowCount() - 1:
|
626
|
+
next_index = self._proxy_model.index(current.row() + 1, 0)
|
627
|
+
self._table_view.setCurrentIndex(next_index)
|
628
|
+
# Select entire row
|
629
|
+
selection_model = self._table_view.selectionModel()
|
630
|
+
selection_model.select(
|
631
|
+
next_index,
|
632
|
+
selection_model.SelectionFlag.ClearAndSelect
|
633
|
+
| selection_model.SelectionFlag.Rows,
|
634
|
+
)
|
635
|
+
# Emit file selection
|
636
|
+
source_index = self._proxy_model.mapToSource(next_index)
|
637
|
+
file_info = self._model.getFileInfo(source_index.row())
|
638
|
+
if file_info:
|
639
|
+
self.fileSelected.emit(file_info.path)
|
640
|
+
|
641
|
+
def navigatePrevious(self):
|
642
|
+
"""Navigate to previous file"""
|
643
|
+
current = self._table_view.currentIndex()
|
644
|
+
|
645
|
+
# If no current selection and we have files, select last
|
646
|
+
if not current.isValid() and self._proxy_model.rowCount() > 0:
|
647
|
+
last_index = self._proxy_model.index(self._proxy_model.rowCount() - 1, 0)
|
648
|
+
self._table_view.setCurrentIndex(last_index)
|
649
|
+
selection_model = self._table_view.selectionModel()
|
650
|
+
selection_model.select(
|
651
|
+
last_index,
|
652
|
+
selection_model.SelectionFlag.ClearAndSelect
|
653
|
+
| selection_model.SelectionFlag.Rows,
|
654
|
+
)
|
655
|
+
# Emit file selection
|
656
|
+
source_index = self._proxy_model.mapToSource(last_index)
|
657
|
+
file_info = self._model.getFileInfo(source_index.row())
|
658
|
+
if file_info:
|
659
|
+
self.fileSelected.emit(file_info.path)
|
660
|
+
return
|
661
|
+
|
662
|
+
if current.isValid() and current.row() > 0:
|
663
|
+
prev_index = self._proxy_model.index(current.row() - 1, 0)
|
664
|
+
self._table_view.setCurrentIndex(prev_index)
|
665
|
+
# Select entire row
|
666
|
+
selection_model = self._table_view.selectionModel()
|
667
|
+
selection_model.select(
|
668
|
+
prev_index,
|
669
|
+
selection_model.SelectionFlag.ClearAndSelect
|
670
|
+
| selection_model.SelectionFlag.Rows,
|
671
|
+
)
|
672
|
+
# Emit file selection
|
673
|
+
source_index = self._proxy_model.mapToSource(prev_index)
|
674
|
+
file_info = self._model.getFileInfo(source_index.row())
|
675
|
+
if file_info:
|
676
|
+
self.fileSelected.emit(file_info.path)
|
@@ -6,7 +6,7 @@ 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=qVPCJdo9KEu9ESujHaWPmlpfVLjyaVXmDe9TmM9c8k4,7282
|
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
|
@@ -18,12 +18,11 @@ lazylabel/ui/editable_vertex.py,sha256=ofo3r8ZZ3b8oYV40vgzZuS3QnXYBNzE92ArC2wggJ
|
|
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=b30amNwHkSXDi2_WvXFYlmzd6TTlRnjNRgShGIoatiQ,387531
|
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
|
25
|
-
lazylabel/ui/right_panel.py,sha256
|
26
|
-
lazylabel/ui/test_hover.py,sha256=5H7J2jhMlhDYwbvGJXgfTupwO9VRylK30ESAG85Ju7I,1521
|
25
|
+
lazylabel/ui/right_panel.py,sha256=D69XgPXpLleflsIl8xCtoBZzFleLqx0SezdwfcEJhUg,14280
|
27
26
|
lazylabel/ui/modes/__init__.py,sha256=ikg47aeexLQavSda_3tYn79xGJW38jKoUCLXRe2w8ok,219
|
28
27
|
lazylabel/ui/modes/base_mode.py,sha256=0R3AkjN_WpYwetF3uuOvkTxb6Q1HB-Z1NQPvLh9eUTY,1315
|
29
28
|
lazylabel/ui/modes/multi_view_mode.py,sha256=TKdqTHokIk267aFVvwXikAtAcuxpG6ezIOlQwoque9c,50606
|
@@ -39,11 +38,12 @@ lazylabel/ui/widgets/settings_widget.py,sha256=ShTaLJeXxwrSuTV4kmtV2JiWjfREil2D1
|
|
39
38
|
lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
|
40
39
|
lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
|
41
40
|
lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
|
41
|
+
lazylabel/utils/fast_file_manager.py,sha256=lOc7Lu7lcOFME-5EO3K6PiEE_oW6I3LhxpeizwgkhsM,24628
|
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.2.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
45
|
+
lazylabel_gui-1.3.2.dist-info/METADATA,sha256=LflKLfur3j-0agOkaMDITRzavzagyFrKI_JzZJIBCk0,6898
|
46
|
+
lazylabel_gui-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
47
|
+
lazylabel_gui-1.3.2.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
48
|
+
lazylabel_gui-1.3.2.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
49
|
+
lazylabel_gui-1.3.2.dist-info/RECORD,,
|
lazylabel/ui/test_hover.py
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Test script to check hover functionality.
|
4
|
-
Run this before starting LazyLabel to enable debug logging.
|
5
|
-
"""
|
6
|
-
|
7
|
-
# First, enable debug logging
|
8
|
-
import logging
|
9
|
-
import os
|
10
|
-
import sys
|
11
|
-
|
12
|
-
# Add the src directory to path so we can import LazyLabel modules
|
13
|
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
14
|
-
|
15
|
-
from lazylabel.utils.logger import logger
|
16
|
-
|
17
|
-
# Set logger to DEBUG level
|
18
|
-
logger.setLevel(logging.DEBUG)
|
19
|
-
|
20
|
-
# Add console handler if not already present
|
21
|
-
if not logger.handlers:
|
22
|
-
console_handler = logging.StreamHandler()
|
23
|
-
console_handler.setLevel(logging.DEBUG)
|
24
|
-
formatter = logging.Formatter(
|
25
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
26
|
-
)
|
27
|
-
console_handler.setFormatter(formatter)
|
28
|
-
logger.addHandler(console_handler)
|
29
|
-
|
30
|
-
print("=" * 50)
|
31
|
-
print("HOVER DEBUG MODE ENABLED")
|
32
|
-
print("=" * 50)
|
33
|
-
print("Logger level:", logger.level)
|
34
|
-
print("Logger handlers:", len(logger.handlers))
|
35
|
-
print()
|
36
|
-
print("Now run LazyLabel and test hover functionality:")
|
37
|
-
print("1. Load images in multi-view mode")
|
38
|
-
print("2. Create some segments (AI or polygon)")
|
39
|
-
print("3. Try hovering over segments")
|
40
|
-
print("4. Watch the console for debug messages")
|
41
|
-
print()
|
42
|
-
print("Expected debug messages:")
|
43
|
-
print("- HoverablePolygonItem.set_segment_info")
|
44
|
-
print("- HoverablePixmapItem.set_segment_info")
|
45
|
-
print("- HoverablePolygonItem.hoverEnterEvent")
|
46
|
-
print("- HoverablePixmapItem.hoverEnterEvent")
|
47
|
-
print("- _trigger_segment_hover called")
|
48
|
-
print("=" * 50)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|