lazylabel-gui 1.3.0__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.
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License