lazylabel-gui 1.1.8__py3-none-any.whl → 1.2.0__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.
@@ -1,861 +1,920 @@
1
- """Left control panel with mode controls and settings."""
2
-
3
- from PyQt6.QtCore import Qt, pyqtSignal
4
- from PyQt6.QtWidgets import (
5
- QFrame,
6
- QHBoxLayout,
7
- QLabel,
8
- QPushButton,
9
- QScrollArea,
10
- QTabWidget,
11
- QVBoxLayout,
12
- QWidget,
13
- )
14
-
15
- from .widgets import (
16
- AdjustmentsWidget,
17
- BorderCropWidget,
18
- ChannelThresholdWidget,
19
- FragmentThresholdWidget,
20
- ModelSelectionWidget,
21
- SettingsWidget,
22
- )
23
-
24
-
25
- class SimpleCollapsible(QWidget):
26
- """A simple collapsible widget for use within tabs."""
27
-
28
- def __init__(self, title: str, content_widget: QWidget, parent=None):
29
- super().__init__(parent)
30
- self.content_widget = content_widget
31
- self.is_collapsed = False
32
-
33
- layout = QVBoxLayout(self)
34
- layout.setContentsMargins(0, 0, 0, 0)
35
- layout.setSpacing(2)
36
-
37
- # Header with toggle button
38
- header_layout = QHBoxLayout()
39
- header_layout.setContentsMargins(2, 2, 2, 2)
40
-
41
- self.toggle_button = QPushButton("▼")
42
- self.toggle_button.setMaximumWidth(16)
43
- self.toggle_button.setMaximumHeight(16)
44
- self.toggle_button.setStyleSheet(
45
- """
46
- QPushButton {
47
- border: 1px solid rgba(120, 120, 120, 0.5);
48
- background: rgba(70, 70, 70, 0.6);
49
- color: #E0E0E0;
50
- font-size: 10px;
51
- font-weight: bold;
52
- border-radius: 2px;
53
- }
54
- QPushButton:hover {
55
- background: rgba(90, 90, 90, 0.8);
56
- border: 1px solid rgba(140, 140, 140, 0.8);
57
- color: #FFF;
58
- }
59
- QPushButton:pressed {
60
- background: rgba(50, 50, 50, 0.8);
61
- border: 1px solid rgba(100, 100, 100, 0.6);
62
- }
63
- """
64
- )
65
- self.toggle_button.clicked.connect(self.toggle_collapse)
66
-
67
- title_label = QLabel(title)
68
- title_label.setStyleSheet(
69
- """
70
- QLabel {
71
- color: #E0E0E0;
72
- font-weight: bold;
73
- font-size: 11px;
74
- background: transparent;
75
- border: none;
76
- padding: 2px;
77
- }
78
- """
79
- )
80
-
81
- header_layout.addWidget(self.toggle_button)
82
- header_layout.addWidget(title_label)
83
- header_layout.addStretch()
84
-
85
- self.header_widget = QWidget()
86
- self.header_widget.setLayout(header_layout)
87
- self.header_widget.setStyleSheet(
88
- """
89
- QWidget {
90
- background-color: rgba(60, 60, 60, 0.3);
91
- border-radius: 3px;
92
- border: 1px solid rgba(80, 80, 80, 0.4);
93
- }
94
- """
95
- )
96
- self.header_widget.setFixedHeight(20)
97
-
98
- layout.addWidget(self.header_widget)
99
- layout.addWidget(content_widget)
100
-
101
- # Add some spacing below content
102
- layout.addSpacing(4)
103
-
104
- def toggle_collapse(self):
105
- """Toggle the collapsed state."""
106
- self.is_collapsed = not self.is_collapsed
107
- self.content_widget.setVisible(not self.is_collapsed)
108
- self.toggle_button.setText("▶" if self.is_collapsed else "▼")
109
-
110
-
111
- class ProfessionalCard(QFrame):
112
- """A professional-looking card widget for containing controls."""
113
-
114
- def __init__(self, title: str = "", parent=None):
115
- super().__init__(parent)
116
- self.setFrameStyle(QFrame.Shape.Box)
117
- self.setStyleSheet(
118
- """
119
- QFrame {
120
- background-color: rgba(40, 40, 40, 0.8);
121
- border: 1px solid rgba(80, 80, 80, 0.6);
122
- border-radius: 8px;
123
- margin: 2px;
124
- }
125
- """
126
- )
127
-
128
- layout = QVBoxLayout(self)
129
- layout.setContentsMargins(8, 8, 8, 8)
130
- layout.setSpacing(6)
131
-
132
- if title:
133
- title_label = QLabel(title)
134
- title_label.setStyleSheet(
135
- """
136
- QLabel {
137
- color: #E0E0E0;
138
- font-weight: bold;
139
- font-size: 11px;
140
- border: none;
141
- background: transparent;
142
- padding: 0px;
143
- margin-bottom: 4px;
144
- }
145
- """
146
- )
147
- layout.addWidget(title_label)
148
-
149
- self.content_layout = layout
150
-
151
- def addWidget(self, widget):
152
- """Add a widget to the card."""
153
- self.content_layout.addWidget(widget)
154
-
155
- def addLayout(self, layout):
156
- """Add a layout to the card."""
157
- self.content_layout.addLayout(layout)
158
-
159
-
160
- class ControlPanel(QWidget):
161
- """Left control panel with mode controls and settings."""
162
-
163
- # Signals
164
- sam_mode_requested = pyqtSignal()
165
- polygon_mode_requested = pyqtSignal()
166
- bbox_mode_requested = pyqtSignal()
167
- selection_mode_requested = pyqtSignal()
168
- edit_mode_requested = pyqtSignal()
169
- clear_points_requested = pyqtSignal()
170
- fit_view_requested = pyqtSignal()
171
- browse_models_requested = pyqtSignal()
172
- refresh_models_requested = pyqtSignal()
173
- model_selected = pyqtSignal(str)
174
- annotation_size_changed = pyqtSignal(int)
175
- pan_speed_changed = pyqtSignal(int)
176
- join_threshold_changed = pyqtSignal(int)
177
- fragment_threshold_changed = pyqtSignal(int)
178
- brightness_changed = pyqtSignal(int)
179
- contrast_changed = pyqtSignal(int)
180
- gamma_changed = pyqtSignal(int)
181
- reset_adjustments_requested = pyqtSignal()
182
- image_adjustment_changed = pyqtSignal()
183
- hotkeys_requested = pyqtSignal()
184
- pop_out_requested = pyqtSignal()
185
- settings_changed = pyqtSignal()
186
- # Border crop signals
187
- crop_draw_requested = pyqtSignal()
188
- crop_clear_requested = pyqtSignal()
189
- crop_applied = pyqtSignal(int, int, int, int) # x1, y1, x2, y2
190
- # Channel threshold signals
191
- channel_threshold_changed = pyqtSignal()
192
-
193
- def __init__(self, parent=None):
194
- super().__init__(parent)
195
- self.setMinimumWidth(260) # Slightly wider for better layout
196
- self.preferred_width = 280
197
- self._setup_ui()
198
- self._connect_signals()
199
-
200
- def _setup_ui(self):
201
- """Setup the professional UI layout."""
202
- layout = QVBoxLayout(self)
203
- layout.setContentsMargins(8, 8, 8, 8)
204
- layout.setSpacing(8)
205
-
206
- # Create widgets first
207
- self.model_widget = ModelSelectionWidget()
208
- self.crop_widget = BorderCropWidget()
209
- self.channel_threshold_widget = ChannelThresholdWidget()
210
- self.settings_widget = SettingsWidget()
211
- self.adjustments_widget = AdjustmentsWidget()
212
- self.fragment_widget = FragmentThresholdWidget()
213
-
214
- # Top header with pop-out button
215
- header_layout = QHBoxLayout()
216
- header_layout.addStretch()
217
-
218
- self.btn_popout = QPushButton("⋯")
219
- self.btn_popout.setToolTip("Pop out panel to separate window")
220
- self.btn_popout.setMaximumWidth(30)
221
- self.btn_popout.setMaximumHeight(25)
222
- self.btn_popout.setStyleSheet(
223
- """
224
- QPushButton {
225
- background-color: rgba(60, 60, 60, 0.8);
226
- border: 1px solid rgba(80, 80, 80, 0.6);
227
- border-radius: 4px;
228
- color: #E0E0E0;
229
- }
230
- QPushButton:hover {
231
- background-color: rgba(80, 80, 80, 0.9);
232
- }
233
- QPushButton:pressed {
234
- background-color: rgba(40, 40, 40, 0.9);
235
- }
236
- """
237
- )
238
- header_layout.addWidget(self.btn_popout)
239
- layout.addLayout(header_layout)
240
-
241
- # Fixed Mode Controls Section (Always Visible)
242
- mode_card = self._create_mode_card()
243
- layout.addWidget(mode_card)
244
-
245
- # Tabbed Interface for Everything Else
246
- self.tab_widget = QTabWidget()
247
- self.tab_widget.setStyleSheet(
248
- """
249
- QTabWidget::pane {
250
- border: 1px solid rgba(80, 80, 80, 0.6);
251
- border-radius: 6px;
252
- background-color: rgba(35, 35, 35, 0.9);
253
- margin-top: 2px;
254
- }
255
- QTabWidget::tab-bar {
256
- alignment: center;
257
- }
258
- QTabBar::tab {
259
- background-color: rgba(50, 50, 50, 0.8);
260
- border: 1px solid rgba(70, 70, 70, 0.6);
261
- border-bottom: none;
262
- border-radius: 4px 4px 0 0;
263
- padding: 4px 8px;
264
- margin-right: 1px;
265
- color: #B0B0B0;
266
- font-size: 9px;
267
- min-width: 45px;
268
- max-width: 80px;
269
- }
270
- QTabBar::tab:selected {
271
- background-color: rgba(70, 70, 70, 0.9);
272
- color: #E0E0E0;
273
- font-weight: bold;
274
- }
275
- QTabBar::tab:hover:!selected {
276
- background-color: rgba(60, 60, 60, 0.8);
277
- color: #D0D0D0;
278
- }
279
- """
280
- )
281
-
282
- # AI & Settings Tab
283
- ai_tab = self._create_ai_tab()
284
- self.tab_widget.addTab(ai_tab, "🤖 AI")
285
-
286
- # Processing & Adjustments Tab
287
- processing_tab = self._create_processing_tab()
288
- self.tab_widget.addTab(processing_tab, "🛠️ Tools")
289
-
290
- layout.addWidget(self.tab_widget, 1)
291
-
292
- # Status label at bottom
293
- self.notification_label = QLabel("")
294
- self.notification_label.setStyleSheet(
295
- """
296
- QLabel {
297
- color: #FFA500;
298
- font-style: italic;
299
- font-size: 9px;
300
- background: transparent;
301
- border: none;
302
- padding: 4px;
303
- }
304
- """
305
- )
306
- self.notification_label.setWordWrap(True)
307
- layout.addWidget(self.notification_label)
308
-
309
- def _create_mode_card(self):
310
- """Create the fixed mode controls card."""
311
- mode_card = ProfessionalCard("Mode Controls")
312
-
313
- # Mode buttons in a clean grid
314
- buttons_layout = QVBoxLayout()
315
- buttons_layout.setSpacing(4)
316
-
317
- # First row: AI and Polygon
318
- row1_layout = QHBoxLayout()
319
- row1_layout.setSpacing(4)
320
-
321
- self.btn_sam_mode = self._create_mode_button(
322
- "AI", "1", "Switch to AI Mode for AI segmentation"
323
- )
324
- self.btn_sam_mode.setCheckable(True)
325
- self.btn_sam_mode.setChecked(True) # Default mode
326
-
327
- self.btn_polygon_mode = self._create_mode_button(
328
- "Poly", "2", "Switch to Polygon Drawing Mode"
329
- )
330
- self.btn_polygon_mode.setCheckable(True)
331
-
332
- row1_layout.addWidget(self.btn_sam_mode)
333
- row1_layout.addWidget(self.btn_polygon_mode)
334
- buttons_layout.addLayout(row1_layout)
335
-
336
- # Second row: BBox and Selection
337
- row2_layout = QHBoxLayout()
338
- row2_layout.setSpacing(4)
339
-
340
- self.btn_bbox_mode = self._create_mode_button(
341
- "Box", "3", "Switch to Bounding Box Drawing Mode"
342
- )
343
- self.btn_bbox_mode.setCheckable(True)
344
-
345
- self.btn_selection_mode = self._create_mode_button(
346
- "Select", "E", "Toggle segment selection"
347
- )
348
- self.btn_selection_mode.setCheckable(True)
349
-
350
- row2_layout.addWidget(self.btn_bbox_mode)
351
- row2_layout.addWidget(self.btn_selection_mode)
352
- buttons_layout.addLayout(row2_layout)
353
-
354
- mode_card.addLayout(buttons_layout)
355
-
356
- # Bottom utility row: Edit and Hotkeys
357
- utility_layout = QHBoxLayout()
358
- utility_layout.setSpacing(4)
359
-
360
- self.btn_edit_mode = self._create_utility_button(
361
- "Edit", "R", "Edit segments and polygons"
362
- )
363
- self.btn_edit_mode.setCheckable(True) # Make edit button checkable
364
-
365
- self.btn_hotkeys = self._create_utility_button(
366
- "⌨️ Hotkeys", "", "Configure keyboard shortcuts"
367
- )
368
-
369
- utility_layout.addWidget(self.btn_edit_mode)
370
- utility_layout.addWidget(self.btn_hotkeys)
371
-
372
- mode_card.addLayout(utility_layout)
373
-
374
- return mode_card
375
-
376
- def _create_mode_button(self, text, key, tooltip):
377
- """Create a professional mode button."""
378
- button = QPushButton(f"{text} ({key})")
379
- button.setToolTip(f"{tooltip} ({key})")
380
- button.setFixedHeight(28)
381
- button.setFixedWidth(75) # Fixed width for consistency
382
- button.setStyleSheet(
383
- """
384
- QPushButton {
385
- background-color: rgba(60, 90, 120, 0.8);
386
- border: 1px solid rgba(80, 110, 140, 0.8);
387
- border-radius: 6px;
388
- color: #E0E0E0;
389
- font-weight: bold;
390
- font-size: 10px;
391
- padding: 4px 8px;
392
- }
393
- QPushButton:hover {
394
- background-color: rgba(80, 110, 140, 0.9);
395
- border-color: rgba(100, 130, 160, 0.9);
396
- }
397
- QPushButton:pressed {
398
- background-color: rgba(40, 70, 100, 0.9);
399
- }
400
- QPushButton:checked {
401
- background-color: rgba(120, 170, 220, 1.0);
402
- border: 2px solid rgba(150, 200, 250, 1.0);
403
- color: #FFFFFF;
404
- font-weight: bold;
405
- }
406
- QPushButton:checked:hover {
407
- background-color: rgba(140, 190, 240, 1.0);
408
- border: 2px solid rgba(170, 220, 255, 1.0);
409
- }
410
- """
411
- )
412
- return button
413
-
414
- def _create_utility_button(self, text, key, tooltip):
415
- """Create a utility button with consistent styling."""
416
- if key:
417
- button_text = f"{text} ({key})"
418
- tooltip_text = f"{tooltip} ({key})"
419
- else:
420
- button_text = text
421
- tooltip_text = tooltip
422
-
423
- button = QPushButton(button_text)
424
- button.setToolTip(tooltip_text)
425
- button.setFixedHeight(28)
426
- button.setFixedWidth(75) # Fixed width for consistency
427
- button.setStyleSheet(
428
- """
429
- QPushButton {
430
- background-color: rgba(70, 100, 130, 0.8);
431
- border: 1px solid rgba(90, 120, 150, 0.8);
432
- border-radius: 6px;
433
- color: #E0E0E0;
434
- font-weight: bold;
435
- font-size: 10px;
436
- padding: 4px 8px;
437
- }
438
- QPushButton:hover {
439
- background-color: rgba(90, 120, 150, 0.9);
440
- border-color: rgba(110, 140, 170, 0.9);
441
- }
442
- QPushButton:pressed {
443
- background-color: rgba(50, 80, 110, 0.9);
444
- }
445
- QPushButton:checked {
446
- background-color: rgba(120, 170, 220, 1.0);
447
- border: 2px solid rgba(150, 200, 250, 1.0);
448
- color: #FFFFFF;
449
- font-weight: bold;
450
- }
451
- QPushButton:checked:hover {
452
- background-color: rgba(140, 190, 240, 1.0);
453
- border: 2px solid rgba(170, 220, 255, 1.0);
454
- }
455
- """
456
- )
457
- return button
458
-
459
- def _get_mode_sized_button_style(self):
460
- """Get styling for utility buttons that matches mode button size."""
461
- return """
462
- QPushButton {
463
- background-color: rgba(80, 80, 80, 0.8);
464
- border: 1px solid rgba(100, 100, 100, 0.6);
465
- border-radius: 6px;
466
- color: #E0E0E0;
467
- font-weight: bold;
468
- font-size: 10px;
469
- padding: 4px 8px;
470
- min-height: 22px;
471
- }
472
- QPushButton:hover {
473
- background-color: rgba(100, 100, 100, 0.9);
474
- border-color: rgba(120, 120, 120, 0.8);
475
- }
476
- QPushButton:pressed {
477
- background-color: rgba(60, 60, 60, 0.9);
478
- }
479
- """
480
-
481
- def _get_utility_button_style(self):
482
- """Get styling for utility buttons."""
483
- return """
484
- QPushButton {
485
- background-color: rgba(80, 80, 80, 0.8);
486
- border: 1px solid rgba(100, 100, 100, 0.6);
487
- border-radius: 5px;
488
- color: #E0E0E0;
489
- font-size: 10px;
490
- padding: 4px 8px;
491
- min-height: 22px;
492
- }
493
- QPushButton:hover {
494
- background-color: rgba(100, 100, 100, 0.9);
495
- }
496
- QPushButton:pressed {
497
- background-color: rgba(60, 60, 60, 0.9);
498
- }
499
- """
500
-
501
- def _create_ai_tab(self):
502
- """Create the AI & Settings tab."""
503
- tab_widget = QWidget()
504
-
505
- # Create scroll area for AI and settings
506
- scroll = QScrollArea()
507
- scroll.setWidgetResizable(True)
508
- scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
509
- scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
510
- scroll.setStyleSheet(
511
- """
512
- QScrollArea {
513
- border: none;
514
- background: transparent;
515
- }
516
- QScrollBar:vertical {
517
- background-color: rgba(60, 60, 60, 0.5);
518
- width: 8px;
519
- border-radius: 4px;
520
- }
521
- QScrollBar::handle:vertical {
522
- background-color: rgba(120, 120, 120, 0.7);
523
- border-radius: 4px;
524
- min-height: 20px;
525
- }
526
- QScrollBar::handle:vertical:hover {
527
- background-color: rgba(140, 140, 140, 0.8);
528
- }
529
- """
530
- )
531
-
532
- scroll_content = QWidget()
533
- layout = QVBoxLayout(scroll_content)
534
- layout.setContentsMargins(6, 6, 6, 6)
535
- layout.setSpacing(8)
536
-
537
- # AI Model Selection - collapsible
538
- model_collapsible = SimpleCollapsible("AI Model Selection", self.model_widget)
539
- layout.addWidget(model_collapsible)
540
-
541
- # AI Fragment Filter - collapsible
542
- fragment_collapsible = SimpleCollapsible(
543
- "AI Fragment Filter", self.fragment_widget
544
- )
545
- layout.addWidget(fragment_collapsible)
546
-
547
- # Application Settings - collapsible
548
- settings_collapsible = SimpleCollapsible(
549
- "Application Settings", self.settings_widget
550
- )
551
- layout.addWidget(settings_collapsible)
552
-
553
- layout.addStretch()
554
-
555
- scroll.setWidget(scroll_content)
556
-
557
- tab_layout = QVBoxLayout(tab_widget)
558
- tab_layout.setContentsMargins(0, 0, 0, 0)
559
- tab_layout.addWidget(scroll)
560
-
561
- return tab_widget
562
-
563
- def _create_processing_tab(self):
564
- """Create the Processing & Tools tab."""
565
- tab_widget = QWidget()
566
-
567
- # Create scroll area for processing controls
568
- scroll = QScrollArea()
569
- scroll.setWidgetResizable(True)
570
- scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
571
- scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
572
- scroll.setStyleSheet(
573
- """
574
- QScrollArea {
575
- border: none;
576
- background: transparent;
577
- }
578
- QScrollBar:vertical {
579
- background-color: rgba(60, 60, 60, 0.5);
580
- width: 8px;
581
- border-radius: 4px;
582
- }
583
- QScrollBar::handle:vertical {
584
- background-color: rgba(120, 120, 120, 0.7);
585
- border-radius: 4px;
586
- min-height: 20px;
587
- }
588
- QScrollBar::handle:vertical:hover {
589
- background-color: rgba(140, 140, 140, 0.8);
590
- }
591
- """
592
- )
593
-
594
- scroll_content = QWidget()
595
- layout = QVBoxLayout(scroll_content)
596
- layout.setContentsMargins(6, 6, 6, 6)
597
- layout.setSpacing(8)
598
-
599
- # Border Crop - collapsible
600
- crop_collapsible = SimpleCollapsible("Border Crop", self.crop_widget)
601
- layout.addWidget(crop_collapsible)
602
-
603
- # Channel Threshold - collapsible
604
- threshold_collapsible = SimpleCollapsible(
605
- "Channel Threshold", self.channel_threshold_widget
606
- )
607
- layout.addWidget(threshold_collapsible)
608
-
609
- # Image Adjustments - collapsible
610
- adjustments_collapsible = SimpleCollapsible(
611
- "Image Adjustments", self.adjustments_widget
612
- )
613
- layout.addWidget(adjustments_collapsible)
614
-
615
- layout.addStretch()
616
-
617
- scroll.setWidget(scroll_content)
618
-
619
- tab_layout = QVBoxLayout(tab_widget)
620
- tab_layout.setContentsMargins(0, 0, 0, 0)
621
- tab_layout.addWidget(scroll)
622
-
623
- return tab_widget
624
-
625
- def _connect_signals(self):
626
- """Connect internal signals."""
627
- self.btn_sam_mode.clicked.connect(self._on_sam_mode_clicked)
628
- self.btn_polygon_mode.clicked.connect(self._on_polygon_mode_clicked)
629
- self.btn_bbox_mode.clicked.connect(self._on_bbox_mode_clicked)
630
- self.btn_selection_mode.clicked.connect(self._on_selection_mode_clicked)
631
- self.btn_edit_mode.clicked.connect(self._on_edit_mode_clicked)
632
- self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
633
- self.btn_popout.clicked.connect(self.pop_out_requested)
634
-
635
- # Model widget signals
636
- self.model_widget.browse_requested.connect(self.browse_models_requested)
637
- self.model_widget.refresh_requested.connect(self.refresh_models_requested)
638
- self.model_widget.model_selected.connect(self.model_selected)
639
-
640
- # Settings widget signals
641
- self.settings_widget.settings_changed.connect(self.settings_changed)
642
-
643
- # Adjustments widget signals
644
- self.adjustments_widget.annotation_size_changed.connect(
645
- self.annotation_size_changed
646
- )
647
- self.adjustments_widget.pan_speed_changed.connect(self.pan_speed_changed)
648
- self.adjustments_widget.join_threshold_changed.connect(
649
- self.join_threshold_changed
650
- )
651
- self.adjustments_widget.brightness_changed.connect(self.brightness_changed)
652
- self.adjustments_widget.contrast_changed.connect(self.contrast_changed)
653
- self.adjustments_widget.gamma_changed.connect(self.gamma_changed)
654
- self.adjustments_widget.reset_requested.connect(
655
- self.reset_adjustments_requested
656
- )
657
- self.adjustments_widget.image_adjustment_changed.connect(
658
- self.image_adjustment_changed
659
- )
660
-
661
- # Fragment threshold widget signals
662
- self.fragment_widget.fragment_threshold_changed.connect(
663
- self.fragment_threshold_changed
664
- )
665
-
666
- # Border crop signals
667
- self.crop_widget.crop_draw_requested.connect(self.crop_draw_requested)
668
- self.crop_widget.crop_clear_requested.connect(self.crop_clear_requested)
669
- self.crop_widget.crop_applied.connect(self.crop_applied)
670
-
671
- # Channel threshold signals
672
- self.channel_threshold_widget.thresholdChanged.connect(
673
- self.channel_threshold_changed
674
- )
675
-
676
- def _on_sam_mode_clicked(self):
677
- """Handle AI mode button click."""
678
- self._set_active_mode_button(self.btn_sam_mode)
679
- self.sam_mode_requested.emit()
680
-
681
- def _on_polygon_mode_clicked(self):
682
- """Handle polygon mode button click."""
683
- self._set_active_mode_button(self.btn_polygon_mode)
684
- self.polygon_mode_requested.emit()
685
-
686
- def _on_bbox_mode_clicked(self):
687
- """Handle bbox mode button click."""
688
- self._set_active_mode_button(self.btn_bbox_mode)
689
- self.bbox_mode_requested.emit()
690
-
691
- def _on_selection_mode_clicked(self):
692
- """Handle selection mode button click."""
693
- self._set_active_mode_button(self.btn_selection_mode)
694
- self.selection_mode_requested.emit()
695
-
696
- def _on_edit_mode_clicked(self):
697
- """Handle edit mode button click."""
698
- # For now, emit the signal - the main window will handle polygon checking
699
- self.edit_mode_requested.emit()
700
-
701
- def _set_active_mode_button(self, active_button):
702
- """Set the active mode button and deactivate others."""
703
- mode_buttons = [
704
- self.btn_sam_mode,
705
- self.btn_polygon_mode,
706
- self.btn_bbox_mode,
707
- self.btn_selection_mode,
708
- ]
709
-
710
- # Clear all mode buttons
711
- for button in mode_buttons:
712
- button.setChecked(button == active_button if active_button else False)
713
-
714
- # Clear edit button when setting mode buttons
715
- if active_button and active_button != self.btn_edit_mode:
716
- self.btn_edit_mode.setChecked(False)
717
-
718
- def mouseDoubleClickEvent(self, event):
719
- """Handle double-click to expand collapsed panel."""
720
- if (
721
- self.width() < 50
722
- and self.parent()
723
- and hasattr(self.parent(), "_expand_left_panel")
724
- ):
725
- self.parent()._expand_left_panel()
726
- super().mouseDoubleClickEvent(event)
727
-
728
- def show_notification(self, message: str, duration: int = 3000):
729
- """Show a notification message."""
730
- self.notification_label.setText(message)
731
- # Note: Timer should be handled by the caller
732
-
733
- def clear_notification(self):
734
- """Clear the notification message."""
735
- self.notification_label.clear()
736
-
737
- def set_mode_text(self, mode: str):
738
- """Set the active mode by highlighting the corresponding button."""
739
- # Map internal mode names to buttons
740
- mode_buttons = {
741
- "sam_points": self.btn_sam_mode,
742
- "polygon": self.btn_polygon_mode,
743
- "bbox": self.btn_bbox_mode,
744
- "selection": self.btn_selection_mode,
745
- "edit": self.btn_edit_mode,
746
- }
747
-
748
- active_button = mode_buttons.get(mode)
749
- if active_button:
750
- # Clear all mode buttons first
751
- self._set_active_mode_button(None)
752
- # Set edit button separately if it's edit mode
753
- if mode == "edit":
754
- self.btn_edit_mode.setChecked(True)
755
- else:
756
- self._set_active_mode_button(active_button)
757
-
758
- def set_edit_mode_active(self, active: bool):
759
- """Set edit mode button as active or inactive."""
760
- if active:
761
- # Clear all mode buttons and set edit as active
762
- self._set_active_mode_button(None)
763
- self.btn_edit_mode.setChecked(True)
764
- else:
765
- self.btn_edit_mode.setChecked(False)
766
-
767
- # Delegate methods for sub-widgets
768
- def populate_models(self, models):
769
- """Populate the models combo box."""
770
- self.model_widget.populate_models(models)
771
-
772
- def set_current_model(self, model_name):
773
- """Set the current model display."""
774
- self.model_widget.set_current_model(model_name)
775
-
776
- def get_settings(self):
777
- """Get current settings from the settings widget."""
778
- return self.settings_widget.get_settings()
779
-
780
- def set_settings(self, settings):
781
- """Set settings in the settings widget."""
782
- self.settings_widget.set_settings(settings)
783
-
784
- def get_annotation_size(self):
785
- """Get current annotation size."""
786
- return self.adjustments_widget.get_annotation_size()
787
-
788
- def set_annotation_size(self, value):
789
- """Set annotation size."""
790
- self.adjustments_widget.set_annotation_size(value)
791
-
792
- def set_pan_speed(self, value):
793
- """Set pan speed."""
794
- self.adjustments_widget.set_pan_speed(value)
795
-
796
- def set_join_threshold(self, value):
797
- """Set join threshold."""
798
- self.adjustments_widget.set_join_threshold(value)
799
-
800
- def set_fragment_threshold(self, value):
801
- """Set fragment threshold."""
802
- self.fragment_widget.set_fragment_threshold(value)
803
-
804
- def set_brightness(self, value):
805
- """Set brightness."""
806
- self.adjustments_widget.set_brightness(value)
807
-
808
- def set_contrast(self, value):
809
- """Set contrast."""
810
- self.adjustments_widget.set_contrast(value)
811
-
812
- def set_gamma(self, value):
813
- """Set gamma."""
814
- self.adjustments_widget.set_gamma(value)
815
-
816
- def set_sam_mode_enabled(self, enabled: bool):
817
- """Enable or disable the SAM mode button."""
818
- self.btn_sam_mode.setEnabled(enabled)
819
- if not enabled:
820
- self.btn_sam_mode.setToolTip("AI Mode (SAM model not available)")
821
- else:
822
- self.btn_sam_mode.setToolTip("Switch to AI Mode for AI segmentation (1)")
823
-
824
- def set_popout_mode(self, is_popped_out: bool):
825
- """Update the pop-out button based on panel state."""
826
- if is_popped_out:
827
- self.btn_popout.setText("")
828
- self.btn_popout.setToolTip("Return panel to main window")
829
- else:
830
- self.btn_popout.setText("⋯")
831
- self.btn_popout.setToolTip("Pop out panel to separate window")
832
-
833
- # Border crop delegate methods
834
- def set_crop_coordinates(self, x1, y1, x2, y2):
835
- """Set crop coordinates in the crop widget."""
836
- self.crop_widget.set_crop_coordinates(x1, y1, x2, y2)
837
-
838
- def clear_crop_coordinates(self):
839
- """Clear crop coordinates."""
840
- self.crop_widget.clear_crop_coordinates()
841
-
842
- def get_crop_coordinates(self):
843
- """Get current crop coordinates."""
844
- return self.crop_widget.get_crop_coordinates()
845
-
846
- def has_crop(self):
847
- """Check if crop coordinates are set."""
848
- return self.crop_widget.has_crop()
849
-
850
- def set_crop_status(self, message):
851
- """Set crop status message."""
852
- self.crop_widget.set_status(message)
853
-
854
- # Channel threshold delegate methods
855
- def update_channel_threshold_for_image(self, image_array):
856
- """Update channel threshold widget for new image."""
857
- self.channel_threshold_widget.update_for_image(image_array)
858
-
859
- def get_channel_threshold_widget(self):
860
- """Get the channel threshold widget."""
861
- return self.channel_threshold_widget
1
+ """Left control panel with mode controls and settings."""
2
+
3
+ from PyQt6.QtCore import Qt, pyqtSignal
4
+ from PyQt6.QtWidgets import (
5
+ QFrame,
6
+ QHBoxLayout,
7
+ QLabel,
8
+ QPushButton,
9
+ QScrollArea,
10
+ QTabWidget,
11
+ QVBoxLayout,
12
+ QWidget,
13
+ )
14
+
15
+ from .widgets import (
16
+ AdjustmentsWidget,
17
+ BorderCropWidget,
18
+ ChannelThresholdWidget,
19
+ FFTThresholdWidget,
20
+ FragmentThresholdWidget,
21
+ ModelSelectionWidget,
22
+ SettingsWidget,
23
+ )
24
+
25
+
26
+ class SimpleCollapsible(QWidget):
27
+ """A simple collapsible widget for use within tabs."""
28
+
29
+ def __init__(self, title: str, content_widget: QWidget, parent=None):
30
+ super().__init__(parent)
31
+ self.content_widget = content_widget
32
+ self.is_collapsed = False
33
+
34
+ layout = QVBoxLayout(self)
35
+ layout.setContentsMargins(0, 0, 0, 0)
36
+ layout.setSpacing(2)
37
+
38
+ # Header with toggle button
39
+ header_layout = QHBoxLayout()
40
+ header_layout.setContentsMargins(2, 2, 2, 2)
41
+
42
+ self.toggle_button = QPushButton("▼")
43
+ self.toggle_button.setMaximumWidth(16)
44
+ self.toggle_button.setMaximumHeight(16)
45
+ self.toggle_button.setStyleSheet(
46
+ """
47
+ QPushButton {
48
+ border: 1px solid rgba(120, 120, 120, 0.5);
49
+ background: rgba(70, 70, 70, 0.6);
50
+ color: #E0E0E0;
51
+ font-size: 10px;
52
+ font-weight: bold;
53
+ border-radius: 2px;
54
+ }
55
+ QPushButton:hover {
56
+ background: rgba(90, 90, 90, 0.8);
57
+ border: 1px solid rgba(140, 140, 140, 0.8);
58
+ color: #FFF;
59
+ }
60
+ QPushButton:pressed {
61
+ background: rgba(50, 50, 50, 0.8);
62
+ border: 1px solid rgba(100, 100, 100, 0.6);
63
+ }
64
+ """
65
+ )
66
+ self.toggle_button.clicked.connect(self.toggle_collapse)
67
+
68
+ title_label = QLabel(title)
69
+ title_label.setStyleSheet(
70
+ """
71
+ QLabel {
72
+ color: #E0E0E0;
73
+ font-weight: bold;
74
+ font-size: 11px;
75
+ background: transparent;
76
+ border: none;
77
+ padding: 2px;
78
+ }
79
+ """
80
+ )
81
+
82
+ header_layout.addWidget(self.toggle_button)
83
+ header_layout.addWidget(title_label)
84
+ header_layout.addStretch()
85
+
86
+ self.header_widget = QWidget()
87
+ self.header_widget.setLayout(header_layout)
88
+ self.header_widget.setStyleSheet(
89
+ """
90
+ QWidget {
91
+ background-color: rgba(60, 60, 60, 0.3);
92
+ border-radius: 3px;
93
+ border: 1px solid rgba(80, 80, 80, 0.4);
94
+ }
95
+ """
96
+ )
97
+ self.header_widget.setFixedHeight(20)
98
+
99
+ layout.addWidget(self.header_widget)
100
+ layout.addWidget(content_widget)
101
+
102
+ # Add some spacing below content
103
+ layout.addSpacing(4)
104
+
105
+ def toggle_collapse(self):
106
+ """Toggle the collapsed state."""
107
+ self.is_collapsed = not self.is_collapsed
108
+ self.content_widget.setVisible(not self.is_collapsed)
109
+ self.toggle_button.setText("▶" if self.is_collapsed else "▼")
110
+
111
+ def set_collapsed(self, collapsed: bool):
112
+ """Set the collapsed state programmatically."""
113
+ if self.is_collapsed != collapsed:
114
+ self.is_collapsed = collapsed
115
+ self.content_widget.setVisible(not self.is_collapsed)
116
+ self.toggle_button.setText("▶" if self.is_collapsed else "▼")
117
+
118
+
119
+ class ProfessionalCard(QFrame):
120
+ """A professional-looking card widget for containing controls."""
121
+
122
+ def __init__(self, title: str = "", parent=None):
123
+ super().__init__(parent)
124
+ self.setFrameStyle(QFrame.Shape.Box)
125
+ self.setStyleSheet(
126
+ """
127
+ QFrame {
128
+ background-color: rgba(40, 40, 40, 0.8);
129
+ border: 1px solid rgba(80, 80, 80, 0.6);
130
+ border-radius: 8px;
131
+ margin: 2px;
132
+ }
133
+ """
134
+ )
135
+
136
+ layout = QVBoxLayout(self)
137
+ layout.setContentsMargins(8, 8, 8, 8)
138
+ layout.setSpacing(6)
139
+
140
+ if title:
141
+ title_label = QLabel(title)
142
+ title_label.setStyleSheet(
143
+ """
144
+ QLabel {
145
+ color: #E0E0E0;
146
+ font-weight: bold;
147
+ font-size: 11px;
148
+ border: none;
149
+ background: transparent;
150
+ padding: 0px;
151
+ margin-bottom: 4px;
152
+ }
153
+ """
154
+ )
155
+ layout.addWidget(title_label)
156
+
157
+ self.content_layout = layout
158
+
159
+ def addWidget(self, widget):
160
+ """Add a widget to the card."""
161
+ self.content_layout.addWidget(widget)
162
+
163
+ def addLayout(self, layout):
164
+ """Add a layout to the card."""
165
+ self.content_layout.addLayout(layout)
166
+
167
+
168
+ class ControlPanel(QWidget):
169
+ """Left control panel with mode controls and settings."""
170
+
171
+ # Signals
172
+ sam_mode_requested = pyqtSignal()
173
+ polygon_mode_requested = pyqtSignal()
174
+ bbox_mode_requested = pyqtSignal()
175
+ selection_mode_requested = pyqtSignal()
176
+ edit_mode_requested = pyqtSignal()
177
+ clear_points_requested = pyqtSignal()
178
+ fit_view_requested = pyqtSignal()
179
+ browse_models_requested = pyqtSignal()
180
+ refresh_models_requested = pyqtSignal()
181
+ model_selected = pyqtSignal(str)
182
+ annotation_size_changed = pyqtSignal(int)
183
+ pan_speed_changed = pyqtSignal(int)
184
+ join_threshold_changed = pyqtSignal(int)
185
+ fragment_threshold_changed = pyqtSignal(int)
186
+ brightness_changed = pyqtSignal(int)
187
+ contrast_changed = pyqtSignal(int)
188
+ gamma_changed = pyqtSignal(int)
189
+ reset_adjustments_requested = pyqtSignal()
190
+ image_adjustment_changed = pyqtSignal()
191
+ hotkeys_requested = pyqtSignal()
192
+ pop_out_requested = pyqtSignal()
193
+ settings_changed = pyqtSignal()
194
+ # Border crop signals
195
+ crop_draw_requested = pyqtSignal()
196
+ crop_clear_requested = pyqtSignal()
197
+ crop_applied = pyqtSignal(int, int, int, int) # x1, y1, x2, y2
198
+ # Channel threshold signals
199
+ channel_threshold_changed = pyqtSignal()
200
+ # FFT threshold signals
201
+ fft_threshold_changed = pyqtSignal()
202
+
203
+ def __init__(self, parent=None):
204
+ super().__init__(parent)
205
+ self.setMinimumWidth(260) # Slightly wider for better layout
206
+ self.preferred_width = 280
207
+ self._setup_ui()
208
+ self._connect_signals()
209
+
210
+ def _setup_ui(self):
211
+ """Setup the professional UI layout."""
212
+ layout = QVBoxLayout(self)
213
+ layout.setContentsMargins(8, 8, 8, 8)
214
+ layout.setSpacing(8)
215
+
216
+ # Create widgets first
217
+ self.model_widget = ModelSelectionWidget()
218
+ self.crop_widget = BorderCropWidget()
219
+ self.channel_threshold_widget = ChannelThresholdWidget()
220
+ self.fft_threshold_widget = FFTThresholdWidget()
221
+ self.settings_widget = SettingsWidget()
222
+ self.adjustments_widget = AdjustmentsWidget()
223
+ self.fragment_widget = FragmentThresholdWidget()
224
+
225
+ # Top header with pop-out button
226
+ header_layout = QHBoxLayout()
227
+ header_layout.addStretch()
228
+
229
+ self.btn_popout = QPushButton("⋯")
230
+ self.btn_popout.setToolTip("Pop out panel to separate window")
231
+ self.btn_popout.setMaximumWidth(30)
232
+ self.btn_popout.setMaximumHeight(25)
233
+ self.btn_popout.setStyleSheet(
234
+ """
235
+ QPushButton {
236
+ background-color: rgba(60, 60, 60, 0.8);
237
+ border: 1px solid rgba(80, 80, 80, 0.6);
238
+ border-radius: 4px;
239
+ color: #E0E0E0;
240
+ }
241
+ QPushButton:hover {
242
+ background-color: rgba(80, 80, 80, 0.9);
243
+ }
244
+ QPushButton:pressed {
245
+ background-color: rgba(40, 40, 40, 0.9);
246
+ }
247
+ """
248
+ )
249
+ header_layout.addWidget(self.btn_popout)
250
+ layout.addLayout(header_layout)
251
+
252
+ # Fixed Mode Controls Section (Always Visible)
253
+ mode_card = self._create_mode_card()
254
+ layout.addWidget(mode_card)
255
+
256
+ # Tabbed Interface for Everything Else
257
+ self.tab_widget = QTabWidget()
258
+ self.tab_widget.setStyleSheet(
259
+ """
260
+ QTabWidget::pane {
261
+ border: 1px solid rgba(80, 80, 80, 0.6);
262
+ border-radius: 6px;
263
+ background-color: rgba(35, 35, 35, 0.9);
264
+ margin-top: 2px;
265
+ }
266
+ QTabWidget::tab-bar {
267
+ alignment: center;
268
+ }
269
+ QTabBar::tab {
270
+ background-color: rgba(50, 50, 50, 0.8);
271
+ border: 1px solid rgba(70, 70, 70, 0.6);
272
+ border-bottom: none;
273
+ border-radius: 4px 4px 0 0;
274
+ padding: 4px 8px;
275
+ margin-right: 1px;
276
+ color: #B0B0B0;
277
+ font-size: 9px;
278
+ min-width: 45px;
279
+ max-width: 80px;
280
+ }
281
+ QTabBar::tab:selected {
282
+ background-color: rgba(70, 70, 70, 0.9);
283
+ color: #E0E0E0;
284
+ font-weight: bold;
285
+ }
286
+ QTabBar::tab:hover:!selected {
287
+ background-color: rgba(60, 60, 60, 0.8);
288
+ color: #D0D0D0;
289
+ }
290
+ """
291
+ )
292
+
293
+ # AI & Settings Tab
294
+ ai_tab = self._create_ai_tab()
295
+ self.tab_widget.addTab(ai_tab, "🤖 AI")
296
+
297
+ # Processing & Adjustments Tab
298
+ processing_tab = self._create_processing_tab()
299
+ self.tab_widget.addTab(processing_tab, "🛠️ Tools")
300
+
301
+ layout.addWidget(self.tab_widget, 1)
302
+
303
+ # Status label at bottom
304
+ self.notification_label = QLabel("")
305
+ self.notification_label.setStyleSheet(
306
+ """
307
+ QLabel {
308
+ color: #FFA500;
309
+ font-style: italic;
310
+ font-size: 9px;
311
+ background: transparent;
312
+ border: none;
313
+ padding: 4px;
314
+ }
315
+ """
316
+ )
317
+ self.notification_label.setWordWrap(True)
318
+ layout.addWidget(self.notification_label)
319
+
320
+ def _create_mode_card(self):
321
+ """Create the fixed mode controls card."""
322
+ mode_card = ProfessionalCard("Mode Controls")
323
+
324
+ # Mode buttons in a clean grid
325
+ buttons_layout = QVBoxLayout()
326
+ buttons_layout.setSpacing(4)
327
+
328
+ # First row: AI and Polygon
329
+ row1_layout = QHBoxLayout()
330
+ row1_layout.setSpacing(4)
331
+
332
+ self.btn_sam_mode = self._create_mode_button(
333
+ "AI", "1", "Switch to AI Mode for AI segmentation"
334
+ )
335
+ self.btn_sam_mode.setCheckable(True)
336
+ self.btn_sam_mode.setChecked(True) # Default mode
337
+
338
+ self.btn_polygon_mode = self._create_mode_button(
339
+ "Poly", "2", "Switch to Polygon Drawing Mode"
340
+ )
341
+ self.btn_polygon_mode.setCheckable(True)
342
+
343
+ row1_layout.addWidget(self.btn_sam_mode)
344
+ row1_layout.addWidget(self.btn_polygon_mode)
345
+ buttons_layout.addLayout(row1_layout)
346
+
347
+ # Second row: BBox and Selection
348
+ row2_layout = QHBoxLayout()
349
+ row2_layout.setSpacing(4)
350
+
351
+ self.btn_bbox_mode = self._create_mode_button(
352
+ "Box", "3", "Switch to Bounding Box Drawing Mode"
353
+ )
354
+ self.btn_bbox_mode.setCheckable(True)
355
+
356
+ self.btn_selection_mode = self._create_mode_button(
357
+ "Select", "E", "Toggle segment selection"
358
+ )
359
+ self.btn_selection_mode.setCheckable(True)
360
+
361
+ row2_layout.addWidget(self.btn_bbox_mode)
362
+ row2_layout.addWidget(self.btn_selection_mode)
363
+ buttons_layout.addLayout(row2_layout)
364
+
365
+ mode_card.addLayout(buttons_layout)
366
+
367
+ # Bottom utility row: Edit and Hotkeys
368
+ utility_layout = QHBoxLayout()
369
+ utility_layout.setSpacing(4)
370
+
371
+ self.btn_edit_mode = self._create_utility_button(
372
+ "Edit", "R", "Edit segments and polygons"
373
+ )
374
+ self.btn_edit_mode.setCheckable(True) # Make edit button checkable
375
+
376
+ self.btn_hotkeys = self._create_utility_button(
377
+ "⌨️ Hotkeys", "", "Configure keyboard shortcuts"
378
+ )
379
+
380
+ utility_layout.addWidget(self.btn_edit_mode)
381
+ utility_layout.addWidget(self.btn_hotkeys)
382
+
383
+ mode_card.addLayout(utility_layout)
384
+
385
+ return mode_card
386
+
387
+ def _create_mode_button(self, text, key, tooltip):
388
+ """Create a professional mode button."""
389
+ button = QPushButton(f"{text} ({key})")
390
+ button.setToolTip(f"{tooltip} ({key})")
391
+ button.setFixedHeight(28)
392
+ button.setFixedWidth(75) # Fixed width for consistency
393
+ button.setStyleSheet(
394
+ """
395
+ QPushButton {
396
+ background-color: rgba(60, 90, 120, 0.8);
397
+ border: 1px solid rgba(80, 110, 140, 0.8);
398
+ border-radius: 6px;
399
+ color: #E0E0E0;
400
+ font-weight: bold;
401
+ font-size: 10px;
402
+ padding: 4px 8px;
403
+ }
404
+ QPushButton:hover {
405
+ background-color: rgba(80, 110, 140, 0.9);
406
+ border-color: rgba(100, 130, 160, 0.9);
407
+ }
408
+ QPushButton:pressed {
409
+ background-color: rgba(40, 70, 100, 0.9);
410
+ }
411
+ QPushButton:checked {
412
+ background-color: rgba(120, 170, 220, 1.0);
413
+ border: 2px solid rgba(150, 200, 250, 1.0);
414
+ color: #FFFFFF;
415
+ font-weight: bold;
416
+ }
417
+ QPushButton:checked:hover {
418
+ background-color: rgba(140, 190, 240, 1.0);
419
+ border: 2px solid rgba(170, 220, 255, 1.0);
420
+ }
421
+ """
422
+ )
423
+ return button
424
+
425
+ def _create_utility_button(self, text, key, tooltip):
426
+ """Create a utility button with consistent styling."""
427
+ if key:
428
+ button_text = f"{text} ({key})"
429
+ tooltip_text = f"{tooltip} ({key})"
430
+ else:
431
+ button_text = text
432
+ tooltip_text = tooltip
433
+
434
+ button = QPushButton(button_text)
435
+ button.setToolTip(tooltip_text)
436
+ button.setFixedHeight(28)
437
+ button.setFixedWidth(75) # Fixed width for consistency
438
+ button.setStyleSheet(
439
+ """
440
+ QPushButton {
441
+ background-color: rgba(70, 100, 130, 0.8);
442
+ border: 1px solid rgba(90, 120, 150, 0.8);
443
+ border-radius: 6px;
444
+ color: #E0E0E0;
445
+ font-weight: bold;
446
+ font-size: 10px;
447
+ padding: 4px 8px;
448
+ }
449
+ QPushButton:hover {
450
+ background-color: rgba(90, 120, 150, 0.9);
451
+ border-color: rgba(110, 140, 170, 0.9);
452
+ }
453
+ QPushButton:pressed {
454
+ background-color: rgba(50, 80, 110, 0.9);
455
+ }
456
+ QPushButton:checked {
457
+ background-color: rgba(120, 170, 220, 1.0);
458
+ border: 2px solid rgba(150, 200, 250, 1.0);
459
+ color: #FFFFFF;
460
+ font-weight: bold;
461
+ }
462
+ QPushButton:checked:hover {
463
+ background-color: rgba(140, 190, 240, 1.0);
464
+ border: 2px solid rgba(170, 220, 255, 1.0);
465
+ }
466
+ """
467
+ )
468
+ return button
469
+
470
+ def _get_mode_sized_button_style(self):
471
+ """Get styling for utility buttons that matches mode button size."""
472
+ return """
473
+ QPushButton {
474
+ background-color: rgba(80, 80, 80, 0.8);
475
+ border: 1px solid rgba(100, 100, 100, 0.6);
476
+ border-radius: 6px;
477
+ color: #E0E0E0;
478
+ font-weight: bold;
479
+ font-size: 10px;
480
+ padding: 4px 8px;
481
+ min-height: 22px;
482
+ }
483
+ QPushButton:hover {
484
+ background-color: rgba(100, 100, 100, 0.9);
485
+ border-color: rgba(120, 120, 120, 0.8);
486
+ }
487
+ QPushButton:pressed {
488
+ background-color: rgba(60, 60, 60, 0.9);
489
+ }
490
+ """
491
+
492
+ def _get_utility_button_style(self):
493
+ """Get styling for utility buttons."""
494
+ return """
495
+ QPushButton {
496
+ background-color: rgba(80, 80, 80, 0.8);
497
+ border: 1px solid rgba(100, 100, 100, 0.6);
498
+ border-radius: 5px;
499
+ color: #E0E0E0;
500
+ font-size: 10px;
501
+ padding: 4px 8px;
502
+ min-height: 22px;
503
+ }
504
+ QPushButton:hover {
505
+ background-color: rgba(100, 100, 100, 0.9);
506
+ }
507
+ QPushButton:pressed {
508
+ background-color: rgba(60, 60, 60, 0.9);
509
+ }
510
+ """
511
+
512
+ def _create_ai_tab(self):
513
+ """Create the AI & Settings tab."""
514
+ tab_widget = QWidget()
515
+
516
+ # Create scroll area for AI and settings
517
+ scroll = QScrollArea()
518
+ scroll.setWidgetResizable(True)
519
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
520
+ scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
521
+ scroll.setStyleSheet(
522
+ """
523
+ QScrollArea {
524
+ border: none;
525
+ background: transparent;
526
+ }
527
+ QScrollBar:vertical {
528
+ background-color: rgba(60, 60, 60, 0.5);
529
+ width: 8px;
530
+ border-radius: 4px;
531
+ }
532
+ QScrollBar::handle:vertical {
533
+ background-color: rgba(120, 120, 120, 0.7);
534
+ border-radius: 4px;
535
+ min-height: 20px;
536
+ }
537
+ QScrollBar::handle:vertical:hover {
538
+ background-color: rgba(140, 140, 140, 0.8);
539
+ }
540
+ """
541
+ )
542
+
543
+ scroll_content = QWidget()
544
+ layout = QVBoxLayout(scroll_content)
545
+ layout.setContentsMargins(6, 6, 6, 6)
546
+ layout.setSpacing(8)
547
+
548
+ # AI Model Selection - collapsible
549
+ model_collapsible = SimpleCollapsible("AI Model Selection", self.model_widget)
550
+ layout.addWidget(model_collapsible)
551
+
552
+ # AI Fragment Filter - collapsible
553
+ fragment_collapsible = SimpleCollapsible(
554
+ "AI Fragment Filter", self.fragment_widget
555
+ )
556
+ layout.addWidget(fragment_collapsible)
557
+
558
+ # Application Settings - collapsible
559
+ settings_collapsible = SimpleCollapsible(
560
+ "Application Settings", self.settings_widget
561
+ )
562
+ layout.addWidget(settings_collapsible)
563
+
564
+ layout.addStretch()
565
+
566
+ scroll.setWidget(scroll_content)
567
+
568
+ tab_layout = QVBoxLayout(tab_widget)
569
+ tab_layout.setContentsMargins(0, 0, 0, 0)
570
+ tab_layout.addWidget(scroll)
571
+
572
+ return tab_widget
573
+
574
+ def _create_processing_tab(self):
575
+ """Create the Processing & Tools tab."""
576
+ tab_widget = QWidget()
577
+
578
+ # Create scroll area for processing controls
579
+ scroll = QScrollArea()
580
+ scroll.setWidgetResizable(True)
581
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
582
+ scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
583
+ scroll.setStyleSheet(
584
+ """
585
+ QScrollArea {
586
+ border: none;
587
+ background: transparent;
588
+ }
589
+ QScrollBar:vertical {
590
+ background-color: rgba(60, 60, 60, 0.5);
591
+ width: 8px;
592
+ border-radius: 4px;
593
+ }
594
+ QScrollBar::handle:vertical {
595
+ background-color: rgba(120, 120, 120, 0.7);
596
+ border-radius: 4px;
597
+ min-height: 20px;
598
+ }
599
+ QScrollBar::handle:vertical:hover {
600
+ background-color: rgba(140, 140, 140, 0.8);
601
+ }
602
+ """
603
+ )
604
+
605
+ scroll_content = QWidget()
606
+ layout = QVBoxLayout(scroll_content)
607
+ layout.setContentsMargins(6, 6, 6, 6)
608
+ layout.setSpacing(8)
609
+
610
+ # Border Crop - collapsible
611
+ crop_collapsible = SimpleCollapsible("Border Crop", self.crop_widget)
612
+ layout.addWidget(crop_collapsible)
613
+
614
+ # Channel Threshold - collapsible
615
+ threshold_collapsible = SimpleCollapsible(
616
+ "Channel Threshold", self.channel_threshold_widget
617
+ )
618
+ layout.addWidget(threshold_collapsible)
619
+
620
+ # FFT Threshold - collapsible
621
+ self.fft_threshold_collapsible = SimpleCollapsible(
622
+ "FFT Threshold", self.fft_threshold_widget
623
+ )
624
+ layout.addWidget(self.fft_threshold_collapsible)
625
+
626
+ # Image Adjustments - collapsible
627
+ adjustments_collapsible = SimpleCollapsible(
628
+ "Image Adjustments", self.adjustments_widget
629
+ )
630
+ layout.addWidget(adjustments_collapsible)
631
+
632
+ layout.addStretch()
633
+
634
+ scroll.setWidget(scroll_content)
635
+
636
+ tab_layout = QVBoxLayout(tab_widget)
637
+ tab_layout.setContentsMargins(0, 0, 0, 0)
638
+ tab_layout.addWidget(scroll)
639
+
640
+ return tab_widget
641
+
642
+ def _connect_signals(self):
643
+ """Connect internal signals."""
644
+ self.btn_sam_mode.clicked.connect(self._on_sam_mode_clicked)
645
+ self.btn_polygon_mode.clicked.connect(self._on_polygon_mode_clicked)
646
+ self.btn_bbox_mode.clicked.connect(self._on_bbox_mode_clicked)
647
+ self.btn_selection_mode.clicked.connect(self._on_selection_mode_clicked)
648
+ self.btn_edit_mode.clicked.connect(self._on_edit_mode_clicked)
649
+ self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
650
+ self.btn_popout.clicked.connect(self.pop_out_requested)
651
+
652
+ # Model widget signals
653
+ self.model_widget.browse_requested.connect(self.browse_models_requested)
654
+ self.model_widget.refresh_requested.connect(self.refresh_models_requested)
655
+ self.model_widget.model_selected.connect(self.model_selected)
656
+
657
+ # Settings widget signals
658
+ self.settings_widget.settings_changed.connect(self.settings_changed)
659
+
660
+ # Adjustments widget signals
661
+ self.adjustments_widget.annotation_size_changed.connect(
662
+ self.annotation_size_changed
663
+ )
664
+ self.adjustments_widget.pan_speed_changed.connect(self.pan_speed_changed)
665
+ self.adjustments_widget.join_threshold_changed.connect(
666
+ self.join_threshold_changed
667
+ )
668
+ self.adjustments_widget.brightness_changed.connect(self.brightness_changed)
669
+ self.adjustments_widget.contrast_changed.connect(self.contrast_changed)
670
+ self.adjustments_widget.gamma_changed.connect(self.gamma_changed)
671
+ self.adjustments_widget.reset_requested.connect(
672
+ self.reset_adjustments_requested
673
+ )
674
+ self.adjustments_widget.image_adjustment_changed.connect(
675
+ self.image_adjustment_changed
676
+ )
677
+
678
+ # Fragment threshold widget signals
679
+ self.fragment_widget.fragment_threshold_changed.connect(
680
+ self.fragment_threshold_changed
681
+ )
682
+
683
+ # Border crop signals
684
+ self.crop_widget.crop_draw_requested.connect(self.crop_draw_requested)
685
+ self.crop_widget.crop_clear_requested.connect(self.crop_clear_requested)
686
+ self.crop_widget.crop_applied.connect(self.crop_applied)
687
+
688
+ # Channel threshold signals
689
+ self.channel_threshold_widget.thresholdChanged.connect(
690
+ self.channel_threshold_changed
691
+ )
692
+
693
+ # FFT threshold signals
694
+ self.fft_threshold_widget.fft_threshold_changed.connect(
695
+ self.fft_threshold_changed
696
+ )
697
+
698
+ def _on_sam_mode_clicked(self):
699
+ """Handle AI mode button click."""
700
+ self._set_active_mode_button(self.btn_sam_mode)
701
+ self.sam_mode_requested.emit()
702
+
703
+ def _on_polygon_mode_clicked(self):
704
+ """Handle polygon mode button click."""
705
+ self._set_active_mode_button(self.btn_polygon_mode)
706
+ self.polygon_mode_requested.emit()
707
+
708
+ def _on_bbox_mode_clicked(self):
709
+ """Handle bbox mode button click."""
710
+ self._set_active_mode_button(self.btn_bbox_mode)
711
+ self.bbox_mode_requested.emit()
712
+
713
+ def _on_selection_mode_clicked(self):
714
+ """Handle selection mode button click."""
715
+ self._set_active_mode_button(self.btn_selection_mode)
716
+ self.selection_mode_requested.emit()
717
+
718
+ def _on_edit_mode_clicked(self):
719
+ """Handle edit mode button click."""
720
+ # For now, emit the signal - the main window will handle polygon checking
721
+ self.edit_mode_requested.emit()
722
+
723
+ def _set_active_mode_button(self, active_button):
724
+ """Set the active mode button and deactivate others."""
725
+ mode_buttons = [
726
+ self.btn_sam_mode,
727
+ self.btn_polygon_mode,
728
+ self.btn_bbox_mode,
729
+ self.btn_selection_mode,
730
+ ]
731
+
732
+ # Clear all mode buttons
733
+ for button in mode_buttons:
734
+ button.setChecked(button == active_button if active_button else False)
735
+
736
+ # Clear edit button when setting mode buttons
737
+ if active_button and active_button != self.btn_edit_mode:
738
+ self.btn_edit_mode.setChecked(False)
739
+
740
+ def mouseDoubleClickEvent(self, event):
741
+ """Handle double-click to expand collapsed panel."""
742
+ if (
743
+ self.width() < 50
744
+ and self.parent()
745
+ and hasattr(self.parent(), "_expand_left_panel")
746
+ ):
747
+ self.parent()._expand_left_panel()
748
+ super().mouseDoubleClickEvent(event)
749
+
750
+ def show_notification(self, message: str, duration: int = 3000):
751
+ """Show a notification message."""
752
+ self.notification_label.setText(message)
753
+ # Note: Timer should be handled by the caller
754
+
755
+ def clear_notification(self):
756
+ """Clear the notification message."""
757
+ self.notification_label.clear()
758
+
759
+ def set_mode_text(self, mode: str):
760
+ """Set the active mode by highlighting the corresponding button."""
761
+ # Map internal mode names to buttons
762
+ mode_buttons = {
763
+ "sam_points": self.btn_sam_mode,
764
+ "polygon": self.btn_polygon_mode,
765
+ "bbox": self.btn_bbox_mode,
766
+ "selection": self.btn_selection_mode,
767
+ "edit": self.btn_edit_mode,
768
+ }
769
+
770
+ active_button = mode_buttons.get(mode)
771
+ if active_button:
772
+ # Clear all mode buttons first
773
+ self._set_active_mode_button(None)
774
+ # Set edit button separately if it's edit mode
775
+ if mode == "edit":
776
+ self.btn_edit_mode.setChecked(True)
777
+ else:
778
+ self._set_active_mode_button(active_button)
779
+
780
+ def set_edit_mode_active(self, active: bool):
781
+ """Set edit mode button as active or inactive."""
782
+ if active:
783
+ # Clear all mode buttons and set edit as active
784
+ self._set_active_mode_button(None)
785
+ self.btn_edit_mode.setChecked(True)
786
+ else:
787
+ self.btn_edit_mode.setChecked(False)
788
+
789
+ # Delegate methods for sub-widgets
790
+ def populate_models(self, models):
791
+ """Populate the models combo box."""
792
+ self.model_widget.populate_models(models)
793
+
794
+ def set_current_model(self, model_name):
795
+ """Set the current model display."""
796
+ self.model_widget.set_current_model(model_name)
797
+
798
+ def get_settings(self):
799
+ """Get current settings from the settings widget."""
800
+ return self.settings_widget.get_settings()
801
+
802
+ def set_settings(self, settings):
803
+ """Set settings in the settings widget."""
804
+ self.settings_widget.set_settings(settings)
805
+
806
+ def get_annotation_size(self):
807
+ """Get current annotation size."""
808
+ return self.adjustments_widget.get_annotation_size()
809
+
810
+ def set_annotation_size(self, value):
811
+ """Set annotation size."""
812
+ self.adjustments_widget.set_annotation_size(value)
813
+
814
+ def set_pan_speed(self, value):
815
+ """Set pan speed."""
816
+ self.adjustments_widget.set_pan_speed(value)
817
+
818
+ def set_join_threshold(self, value):
819
+ """Set join threshold."""
820
+ self.adjustments_widget.set_join_threshold(value)
821
+
822
+ def set_fragment_threshold(self, value):
823
+ """Set fragment threshold."""
824
+ self.fragment_widget.set_fragment_threshold(value)
825
+
826
+ def set_brightness(self, value):
827
+ """Set brightness."""
828
+ self.adjustments_widget.set_brightness(value)
829
+
830
+ def set_contrast(self, value):
831
+ """Set contrast."""
832
+ self.adjustments_widget.set_contrast(value)
833
+
834
+ def set_gamma(self, value):
835
+ """Set gamma."""
836
+ self.adjustments_widget.set_gamma(value)
837
+
838
+ def set_sam_mode_enabled(self, enabled: bool):
839
+ """Enable or disable the SAM mode button."""
840
+ self.btn_sam_mode.setEnabled(enabled)
841
+ if not enabled:
842
+ self.btn_sam_mode.setToolTip("AI Mode (SAM model not available)")
843
+ else:
844
+ self.btn_sam_mode.setToolTip("Switch to AI Mode for AI segmentation (1)")
845
+
846
+ def set_popout_mode(self, is_popped_out: bool):
847
+ """Update the pop-out button based on panel state."""
848
+ if is_popped_out:
849
+ self.btn_popout.setText("⇤")
850
+ self.btn_popout.setToolTip("Return panel to main window")
851
+ else:
852
+ self.btn_popout.setText("⋯")
853
+ self.btn_popout.setToolTip("Pop out panel to separate window")
854
+
855
+ # Border crop delegate methods
856
+ def set_crop_coordinates(self, x1, y1, x2, y2):
857
+ """Set crop coordinates in the crop widget."""
858
+ self.crop_widget.set_crop_coordinates(x1, y1, x2, y2)
859
+
860
+ def clear_crop_coordinates(self):
861
+ """Clear crop coordinates."""
862
+ self.crop_widget.clear_crop_coordinates()
863
+
864
+ def get_crop_coordinates(self):
865
+ """Get current crop coordinates."""
866
+ return self.crop_widget.get_crop_coordinates()
867
+
868
+ def has_crop(self):
869
+ """Check if crop coordinates are set."""
870
+ return self.crop_widget.has_crop()
871
+
872
+ def set_crop_status(self, message):
873
+ """Set crop status message."""
874
+ self.crop_widget.set_status(message)
875
+
876
+ # Channel threshold delegate methods
877
+ def update_channel_threshold_for_image(self, image_array):
878
+ """Update channel threshold widget for new image."""
879
+ self.channel_threshold_widget.update_for_image(image_array)
880
+
881
+ def get_channel_threshold_widget(self):
882
+ """Get the channel threshold widget."""
883
+ return self.channel_threshold_widget
884
+
885
+ # FFT threshold delegate methods
886
+ def update_fft_threshold_for_image(self, image_array):
887
+ """Update FFT threshold widget for new image."""
888
+ self.fft_threshold_widget.update_fft_threshold_for_image(image_array)
889
+
890
+ def get_fft_threshold_widget(self):
891
+ """Get the FFT threshold widget."""
892
+ return self.fft_threshold_widget
893
+
894
+ def auto_collapse_fft_threshold_for_image(self, image_array):
895
+ """Auto-collapse FFT threshold panel if image is not black and white."""
896
+ if not hasattr(self, "fft_threshold_collapsible"):
897
+ return
898
+
899
+ should_collapse = True # Default to collapsed
900
+
901
+ if image_array is not None:
902
+ # Check if image is grayscale (black and white)
903
+ if len(image_array.shape) == 2:
904
+ # True grayscale - keep expanded
905
+ should_collapse = False
906
+ elif len(image_array.shape) == 3 and image_array.shape[2] == 3:
907
+ # Check if all three channels are identical (grayscale stored as RGB)
908
+ import numpy as np
909
+
910
+ r_channel = image_array[:, :, 0]
911
+ g_channel = image_array[:, :, 1]
912
+ b_channel = image_array[:, :, 2]
913
+ if np.array_equal(r_channel, g_channel) and np.array_equal(
914
+ g_channel, b_channel
915
+ ):
916
+ # Grayscale stored as RGB - keep expanded
917
+ should_collapse = False
918
+
919
+ # Set collapsed state (collapse for non-BW images, expand for BW images)
920
+ self.fft_threshold_collapsible.set_collapsed(should_collapse)