lazylabel-gui 1.3.1__py3-none-any.whl → 1.3.3__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.
@@ -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,112 @@ 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 surrounding files in current sorted/filtered order
1792
+ file_manager = self.right_panel.file_manager
1793
+ surrounding_files = file_manager.getSurroundingFiles(Path(path), num_viewers)
1794
+
1795
+ # Convert to strings for loading
1796
+ images_to_load = [str(p) if p else None for p in surrounding_files]
1797
+
1798
+ # Load the images
1799
+ self._load_multi_view_images(images_to_load)
1800
+
1801
+ # Update file manager selection
1802
+ self.right_panel.select_file(Path(path))
1803
+
1689
1804
  def _load_selected_image_multi_view(self, index, path):
1690
1805
  """Load selected image in multi-view mode starting from the selected file."""
1691
1806
  # Auto-save if enabled and we have current images (not the first load)
@@ -2141,17 +2256,9 @@ class MainWindow(QMainWindow):
2141
2256
  self._load_next_multi_batch()
2142
2257
  return
2143
2258
 
2144
- if not self.current_file_index.isValid():
2145
- return
2146
- parent = self.current_file_index.parent()
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
2259
+ # Use new file manager navigation
2260
+ self.right_panel.navigate_next_image()
2261
+ return
2155
2262
 
2156
2263
  def _load_previous_image(self):
2157
2264
  """Load previous image in the file list."""
@@ -2160,17 +2267,9 @@ class MainWindow(QMainWindow):
2160
2267
  self._load_previous_multi_batch()
2161
2268
  return
2162
2269
 
2163
- if not self.current_file_index.isValid():
2164
- return
2165
- parent = self.current_file_index.parent()
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
2270
+ # Use new file manager navigation
2271
+ self.right_panel.navigate_previous_image()
2272
+ return
2174
2273
 
2175
2274
  # Segment management methods
2176
2275
  def _assign_selected_to_class(self):
@@ -2823,20 +2922,34 @@ class MainWindow(QMainWindow):
2823
2922
 
2824
2923
  def _handle_enter_press(self):
2825
2924
  """Handle enter key press."""
2925
+ logger.debug(f"Enter pressed - mode: {self.mode}, view_mode: {self.view_mode}")
2926
+
2826
2927
  if self.mode == "polygon":
2827
- if self.view_mode == "single" and self.polygon_points:
2828
- self._finalize_polygon()
2829
- elif self.view_mode == "multi" and hasattr(
2830
- self, "multi_view_polygon_points"
2831
- ):
2928
+ logger.debug(
2929
+ f"Polygon mode - polygon_points: {len(self.polygon_points) if hasattr(self, 'polygon_points') and self.polygon_points else 0}"
2930
+ )
2931
+ if self.view_mode == "single":
2932
+ # If there are pending polygon points, finalize them first
2933
+ if self.polygon_points:
2934
+ logger.debug("Finalizing pending polygon")
2935
+ self._finalize_polygon()
2936
+ # Always save after handling pending work to show checkmarks and notifications
2937
+ logger.debug("Saving polygon segments to NPZ")
2938
+ self._save_output_to_npz()
2939
+ elif self.view_mode == "multi":
2832
2940
  # Complete polygons for all viewers that have points
2833
- for i, points in enumerate(self.multi_view_polygon_points):
2834
- if points and len(points) >= 3:
2835
- # Use the multi-view mode handler for proper pairing logic
2836
- if hasattr(self, "multi_view_mode_handler"):
2837
- self.multi_view_mode_handler._finalize_multi_view_polygon(i)
2838
- else:
2839
- self._finalize_multi_view_polygon(i)
2941
+ if hasattr(self, "multi_view_polygon_points"):
2942
+ for i, points in enumerate(self.multi_view_polygon_points):
2943
+ if points and len(points) >= 3:
2944
+ # Use the multi-view mode handler for proper pairing logic
2945
+ if hasattr(self, "multi_view_mode_handler"):
2946
+ self.multi_view_mode_handler._finalize_multi_view_polygon(
2947
+ i
2948
+ )
2949
+ else:
2950
+ self._finalize_multi_view_polygon(i)
2951
+ # Always save after handling pending work to show checkmarks and notifications
2952
+ self._save_output_to_npz()
2840
2953
  else:
2841
2954
  # First accept any AI segments (same as spacebar), then save
2842
2955
  self._accept_ai_segment()
@@ -3188,6 +3301,18 @@ class MainWindow(QMainWindow):
3188
3301
  # Restore original segments
3189
3302
  self.segment_manager.segments = original_segments
3190
3303
 
3304
+ # Save class aliases if enabled
3305
+ if settings.get("save_class_aliases", False) and viewer_segments:
3306
+ # Temporarily set segments for this viewer to save correct aliases
3307
+ original_segments = self.segment_manager.segments
3308
+ self.segment_manager.segments = viewer_segments
3309
+
3310
+ aliases_path = self.file_manager.save_class_aliases(image_path)
3311
+ saved_files.append(os.path.basename(aliases_path))
3312
+
3313
+ # Restore original segments
3314
+ self.segment_manager.segments = original_segments
3315
+
3191
3316
  # If no segments for this viewer, delete associated files
3192
3317
  if not viewer_segments:
3193
3318
  base, _ = os.path.splitext(image_path)
@@ -3198,7 +3323,13 @@ class MainWindow(QMainWindow):
3198
3323
  try:
3199
3324
  os.remove(file_path)
3200
3325
  deleted_files.append(os.path.basename(file_path))
3201
- self.file_model.update_cache_for_path(file_path)
3326
+ # Update FastFileManager after file deletion
3327
+ if hasattr(self, "right_panel") and hasattr(
3328
+ self.right_panel, "file_manager"
3329
+ ):
3330
+ self.right_panel.file_manager.updateFileStatus(
3331
+ Path(image_path)
3332
+ )
3202
3333
  except Exception as e:
3203
3334
  self._show_error_notification(
3204
3335
  f"Error deleting {file_path}: {e}"
@@ -3213,20 +3344,15 @@ class MainWindow(QMainWindow):
3213
3344
  f"Error saving {os.path.basename(image_path)}: {str(e)}"
3214
3345
  )
3215
3346
 
3216
- # Update file list with green flash and tick marks (like single view mode)
3217
- if hasattr(self, "_saved_file_paths") and self._saved_file_paths:
3218
- for path in self._saved_file_paths:
3219
- if path:
3220
- self.file_model.update_cache_for_path(path)
3221
- self.file_model.set_highlighted_path(path)
3222
- QTimer.singleShot(
3223
- 1500,
3224
- lambda p=path: (
3225
- self.file_model.set_highlighted_path(None)
3226
- if self.file_model.highlighted_path == p
3227
- else None
3228
- ),
3229
- )
3347
+ # Update FastFileManager to show NPZ checkmarks for multi-view
3348
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
3349
+ if hasattr(self, "right_panel") and hasattr(
3350
+ self.right_panel, "file_manager"
3351
+ ):
3352
+ # Batch update for better performance
3353
+ valid_paths = [Path(img) for img in self.multi_view_images if img]
3354
+ if valid_paths:
3355
+ self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
3230
3356
  # Clear the tracking list for next save
3231
3357
  self._saved_file_paths = []
3232
3358
 
@@ -3258,7 +3384,13 @@ class MainWindow(QMainWindow):
3258
3384
  try:
3259
3385
  os.remove(file_path)
3260
3386
  deleted_files.append(file_path)
3261
- self.file_model.update_cache_for_path(file_path)
3387
+ # Update FastFileManager after file deletion
3388
+ if hasattr(self, "right_panel") and hasattr(
3389
+ self.right_panel, "file_manager"
3390
+ ):
3391
+ self.right_panel.file_manager.updateFileStatus(
3392
+ Path(self.current_image_path)
3393
+ )
3262
3394
  except Exception as e:
3263
3395
  self._show_error_notification(
3264
3396
  f"Error deleting {file_path}: {e}"
@@ -3276,25 +3408,38 @@ class MainWindow(QMainWindow):
3276
3408
  try:
3277
3409
  settings = self.control_panel.get_settings()
3278
3410
  npz_path = None
3279
- txt_path = None
3411
+
3412
+ # Debug logging
3413
+ logger.debug(f"Starting save process for: {self.current_image_path}")
3414
+ logger.debug(f"Number of segments: {len(self.segment_manager.segments)}")
3415
+
3280
3416
  if settings.get("save_npz", True):
3281
3417
  h, w = (
3282
3418
  self.viewer._pixmap_item.pixmap().height(),
3283
3419
  self.viewer._pixmap_item.pixmap().width(),
3284
3420
  )
3285
3421
  class_order = self.segment_manager.get_unique_class_ids()
3422
+ logger.debug(f"Class order for saving: {class_order}")
3423
+
3286
3424
  if class_order:
3425
+ logger.debug(
3426
+ f"Attempting to save NPZ to: {os.path.splitext(self.current_image_path)[0]}.npz"
3427
+ )
3287
3428
  npz_path = self.file_manager.save_npz(
3288
3429
  self.current_image_path,
3289
3430
  (h, w),
3290
3431
  class_order,
3291
3432
  self.current_crop_coords,
3292
3433
  )
3434
+ logger.debug(f"NPZ save completed: {npz_path}")
3293
3435
  self._show_success_notification(
3294
3436
  f"Saved: {os.path.basename(npz_path)}"
3295
3437
  )
3296
3438
  else:
3439
+ logger.warning("No classes defined for saving")
3297
3440
  self._show_warning_notification("No classes defined for saving.")
3441
+ # Save TXT file if enabled
3442
+ txt_path = None
3298
3443
  if settings.get("save_txt", True):
3299
3444
  h, w = (
3300
3445
  self.viewer._pixmap_item.pixmap().height(),
@@ -3315,20 +3460,35 @@ class MainWindow(QMainWindow):
3315
3460
  class_labels,
3316
3461
  self.current_crop_coords,
3317
3462
  )
3318
- # Efficiently update file list tickboxes and highlight
3319
- for path in [npz_path, txt_path]:
3320
- if path:
3321
- self.file_model.update_cache_for_path(path)
3322
- self.file_model.set_highlighted_path(path)
3323
- QTimer.singleShot(
3324
- 1500,
3325
- lambda p=path: (
3326
- self.file_model.set_highlighted_path(None)
3327
- if self.file_model.highlighted_path == p
3328
- else None
3329
- ),
3463
+ if txt_path:
3464
+ logger.debug(f"TXT save completed: {txt_path}")
3465
+ self._show_success_notification(
3466
+ f"Saved: {os.path.basename(txt_path)}"
3467
+ )
3468
+
3469
+ # Save class aliases if enabled
3470
+ if settings.get("save_class_aliases", False):
3471
+ aliases_path = self.file_manager.save_class_aliases(
3472
+ self.current_image_path
3473
+ )
3474
+ if aliases_path:
3475
+ logger.debug(f"Class aliases saved: {aliases_path}")
3476
+ self._show_success_notification(
3477
+ f"Saved: {os.path.basename(aliases_path)}"
3330
3478
  )
3479
+
3480
+ # Update FastFileManager to show NPZ/TXT checkmarks
3481
+ if (
3482
+ (npz_path or txt_path)
3483
+ and hasattr(self, "right_panel")
3484
+ and hasattr(self.right_panel, "file_manager")
3485
+ ):
3486
+ # Update the file status in the FastFileManager
3487
+ self.right_panel.file_manager.updateFileStatus(
3488
+ Path(self.current_image_path)
3489
+ )
3331
3490
  except Exception as e:
3491
+ logger.error(f"Error saving file: {str(e)}", exc_info=True)
3332
3492
  self._show_error_notification(f"Error saving: {str(e)}")
3333
3493
 
3334
3494
  def _handle_merge_press(self):
@@ -7088,24 +7248,73 @@ class MainWindow(QMainWindow):
7088
7248
  self._load_multi_view_images(image_paths)
7089
7249
 
7090
7250
  def _load_next_multi_batch(self):
7091
- """Load the next batch of images using cached list."""
7092
- if not self.current_file_index.isValid():
7093
- return
7094
-
7095
- # Auto-save if enabled and we have current images (not the first load)
7096
- if self.multi_view_images[0] and self.control_panel.get_settings().get(
7097
- "auto_save", True
7251
+ """Load the next batch of images using fast file manager or cached list."""
7252
+ # Auto-save if enabled and we have current images
7253
+ if (
7254
+ hasattr(self, "multi_view_images")
7255
+ and self.multi_view_images
7256
+ and self.multi_view_images[0]
7257
+ and self.control_panel.get_settings().get("auto_save", True)
7098
7258
  ):
7099
7259
  self._save_multi_view_output()
7100
7260
 
7101
- # If cached list isn't ready, fall back to file model navigation
7261
+ # Get current image path - look for any valid image in multi-view state
7262
+ current_path = None
7263
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
7264
+ # Find the first valid image path in the current multi-view state
7265
+ for img_path in self.multi_view_images:
7266
+ if img_path:
7267
+ current_path = img_path
7268
+ break
7269
+
7270
+ # Fallback to current_image_path if no valid multi-view images found
7271
+ if (
7272
+ not current_path
7273
+ and hasattr(self, "current_image_path")
7274
+ and self.current_image_path
7275
+ ):
7276
+ current_path = self.current_image_path
7277
+
7278
+ # If no valid path found, can't navigate forward
7279
+ if not current_path:
7280
+ return
7281
+
7282
+ # Get the number of viewers for multi-view mode
7283
+ config = self._get_multi_view_config()
7284
+ num_viewers = config["num_viewers"]
7285
+
7286
+ # Try to use fast file manager first (respects sorting/filtering)
7287
+ try:
7288
+ if (
7289
+ hasattr(self, "right_panel")
7290
+ and hasattr(self.right_panel, "file_manager")
7291
+ and hasattr(self.right_panel.file_manager, "getSurroundingFiles")
7292
+ ):
7293
+ file_manager = self.right_panel.file_manager
7294
+ surrounding_files = file_manager.getSurroundingFiles(
7295
+ Path(current_path), num_viewers * 2
7296
+ )
7297
+
7298
+ if len(surrounding_files) > num_viewers:
7299
+ # Skip ahead by num_viewers to get the next batch
7300
+ next_batch = surrounding_files[num_viewers : num_viewers * 2]
7301
+
7302
+ # Convert to strings and load
7303
+ images_to_load = [str(p) if p else None for p in next_batch]
7304
+ self._load_multi_view_images(images_to_load)
7305
+
7306
+ # Update file manager selection to first image of new batch
7307
+ if next_batch and next_batch[0]:
7308
+ self.right_panel.select_file(next_batch[0])
7309
+ return
7310
+ except Exception:
7311
+ pass # Fall back to cached list approach
7312
+
7313
+ # Fall back to cached list approach (for backward compatibility / tests)
7102
7314
  if not self.cached_image_paths:
7103
7315
  self._load_next_multi_batch_fallback()
7104
7316
  return
7105
7317
 
7106
- # Get current image path
7107
- current_path = self.file_model.filePath(self.current_file_index)
7108
-
7109
7318
  # Find current position in cached list
7110
7319
  try:
7111
7320
  current_index = self.cached_image_paths.index(current_path)
@@ -7114,13 +7323,14 @@ class MainWindow(QMainWindow):
7114
7323
  self._load_next_multi_batch_fallback()
7115
7324
  return
7116
7325
 
7117
- # Get the number of viewers for multi-view mode
7118
- config = self._get_multi_view_config()
7119
- num_viewers = config["num_viewers"]
7120
-
7121
7326
  # Skip num_viewers positions ahead in cached list
7122
7327
  target_index = current_index + num_viewers
7123
7328
 
7329
+ # Check if we can navigate forward (at least one valid image at target position)
7330
+ if target_index >= len(self.cached_image_paths):
7331
+ return # Can't navigate forward - at or past the end
7332
+
7333
+ # Check if we have at least one valid image at the target position
7124
7334
  if target_index < len(self.cached_image_paths):
7125
7335
  # Collect consecutive images
7126
7336
  image_paths = []
@@ -7130,39 +7340,88 @@ class MainWindow(QMainWindow):
7130
7340
  else:
7131
7341
  image_paths.append(None)
7132
7342
 
7133
- # Load all images
7134
- self._load_multi_view_images(image_paths)
7135
-
7136
- # Update current file index to the first image of the new batch
7137
- # Find the file model index for this path
7138
- if image_paths and image_paths[0]:
7139
- parent_index = self.current_file_index.parent()
7140
- for row in range(self.file_model.rowCount(parent_index)):
7141
- index = self.file_model.index(row, 0, parent_index)
7142
- if self.file_model.filePath(index) == image_paths[0]:
7143
- self.current_file_index = index
7144
- self.right_panel.file_tree.setCurrentIndex(index)
7145
- break
7343
+ # Only proceed if we have at least one valid image (prevent all-None batches)
7344
+ if any(path is not None for path in image_paths):
7345
+ # Load all images
7346
+ self._load_multi_view_images(image_paths)
7347
+
7348
+ # Update current file index to the first image of the new batch
7349
+ # Find the file model index for this path
7350
+ if image_paths and image_paths[0]:
7351
+ parent_index = self.current_file_index.parent()
7352
+ for row in range(self.file_model.rowCount(parent_index)):
7353
+ index = self.file_model.index(row, 0, parent_index)
7354
+ if self.file_model.filePath(index) == image_paths[0]:
7355
+ self.current_file_index = index
7356
+ self.right_panel.file_tree.setCurrentIndex(index)
7357
+ break
7358
+ # If all would be None, don't navigate (stay at current position)
7146
7359
 
7147
7360
  def _load_previous_multi_batch(self):
7148
- """Load the previous batch of images using cached list."""
7149
- if not self.current_file_index.isValid():
7150
- return
7151
-
7152
- # Auto-save if enabled and we have current images (not the first load)
7153
- if self.multi_view_images[0] and self.control_panel.get_settings().get(
7154
- "auto_save", True
7361
+ """Load the previous batch of images using fast file manager or cached list."""
7362
+ # Auto-save if enabled and we have current images
7363
+ if (
7364
+ hasattr(self, "multi_view_images")
7365
+ and self.multi_view_images
7366
+ and self.multi_view_images[0]
7367
+ and self.control_panel.get_settings().get("auto_save", True)
7155
7368
  ):
7156
7369
  self._save_multi_view_output()
7157
7370
 
7158
- # If cached list isn't ready, fall back to file model navigation
7371
+ # Get current image path - look for any valid image in multi-view state
7372
+ current_path = None
7373
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
7374
+ # Find the first valid image path in the current multi-view state
7375
+ for img_path in self.multi_view_images:
7376
+ if img_path:
7377
+ current_path = img_path
7378
+ break
7379
+
7380
+ # Fallback to current_image_path if no valid multi-view images found
7381
+ if (
7382
+ not current_path
7383
+ and hasattr(self, "current_image_path")
7384
+ and self.current_image_path
7385
+ ):
7386
+ current_path = self.current_image_path
7387
+
7388
+ # If no valid path found, can't navigate backward
7389
+ if not current_path:
7390
+ return
7391
+
7392
+ # Get the number of viewers for multi-view mode
7393
+ config = self._get_multi_view_config()
7394
+ num_viewers = config["num_viewers"]
7395
+
7396
+ # Try to use fast file manager first (respects sorting/filtering)
7397
+ try:
7398
+ if (
7399
+ hasattr(self, "right_panel")
7400
+ and hasattr(self.right_panel, "file_manager")
7401
+ and hasattr(self.right_panel.file_manager, "getPreviousFiles")
7402
+ ):
7403
+ file_manager = self.right_panel.file_manager
7404
+ previous_batch = file_manager.getPreviousFiles(
7405
+ Path(current_path), num_viewers
7406
+ )
7407
+
7408
+ if previous_batch and any(previous_batch):
7409
+ # Convert to strings and load
7410
+ images_to_load = [str(p) if p else None for p in previous_batch]
7411
+ self._load_multi_view_images(images_to_load)
7412
+
7413
+ # Update file manager selection to first image of new batch
7414
+ if previous_batch and previous_batch[0]:
7415
+ self.right_panel.select_file(previous_batch[0])
7416
+ return
7417
+ except Exception:
7418
+ pass # Fall back to cached list approach
7419
+
7420
+ # Fall back to cached list approach (for backward compatibility / tests)
7159
7421
  if not self.cached_image_paths:
7160
7422
  self._load_previous_multi_batch_fallback()
7161
7423
  return
7162
7424
 
7163
- # Get current image path
7164
- current_path = self.file_model.filePath(self.current_file_index)
7165
-
7166
7425
  # Find current position in cached list
7167
7426
  try:
7168
7427
  current_index = self.cached_image_paths.index(current_path)
@@ -7171,10 +7430,6 @@ class MainWindow(QMainWindow):
7171
7430
  self._load_previous_multi_batch_fallback()
7172
7431
  return
7173
7432
 
7174
- # Get the number of viewers for multi-view mode
7175
- config = self._get_multi_view_config()
7176
- num_viewers = config["num_viewers"]
7177
-
7178
7433
  # Skip num_viewers positions back in cached list
7179
7434
  target_index = current_index - num_viewers
7180
7435
  if target_index < 0:
@@ -7183,24 +7438,30 @@ class MainWindow(QMainWindow):
7183
7438
  # Collect consecutive images
7184
7439
  image_paths = []
7185
7440
  for i in range(num_viewers):
7186
- if target_index + i < len(self.cached_image_paths):
7441
+ if (
7442
+ target_index + i < len(self.cached_image_paths)
7443
+ and target_index + i >= 0
7444
+ ):
7187
7445
  image_paths.append(self.cached_image_paths[target_index + i])
7188
7446
  else:
7189
7447
  image_paths.append(None)
7190
7448
 
7191
- # Load all images
7192
- self._load_multi_view_images(image_paths)
7449
+ # Only proceed if we have at least one valid image (prevent all-None batches)
7450
+ if any(path is not None for path in image_paths):
7451
+ # Load all images
7452
+ self._load_multi_view_images(image_paths)
7193
7453
 
7194
- # Update current file index to the first image of the new batch
7195
- # Find the file model index for this path
7196
- if image_paths and image_paths[0]:
7197
- parent_index = self.current_file_index.parent()
7198
- for row in range(self.file_model.rowCount(parent_index)):
7199
- index = self.file_model.index(row, 0, parent_index)
7200
- if self.file_model.filePath(index) == image_paths[0]:
7201
- self.current_file_index = index
7202
- self.right_panel.file_tree.setCurrentIndex(index)
7203
- break
7454
+ # Update current file index to the first image of the new batch
7455
+ # Find the file model index for this path
7456
+ if image_paths and image_paths[0]:
7457
+ parent_index = self.current_file_index.parent()
7458
+ for row in range(self.file_model.rowCount(parent_index)):
7459
+ index = self.file_model.index(row, 0, parent_index)
7460
+ if self.file_model.filePath(index) == image_paths[0]:
7461
+ self.current_file_index = index
7462
+ self.right_panel.file_tree.setCurrentIndex(index)
7463
+ break
7464
+ # If all would be None, don't navigate (stay at current position)
7204
7465
 
7205
7466
  def _load_next_multi_batch_fallback(self):
7206
7467
  """Fallback navigation using file model when cached list isn't available."""