lazylabel-gui 1.1.6__py3-none-any.whl → 1.1.8__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.
@@ -6,11 +6,155 @@ from PyQt6.QtWidgets import (
6
6
  QHBoxLayout,
7
7
  QLabel,
8
8
  QPushButton,
9
+ QScrollArea,
10
+ QTabWidget,
9
11
  QVBoxLayout,
10
12
  QWidget,
11
13
  )
12
14
 
13
- from .widgets import AdjustmentsWidget, ModelSelectionWidget, SettingsWidget
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)
14
158
 
15
159
 
16
160
  class ControlPanel(QWidget):
@@ -19,8 +163,9 @@ class ControlPanel(QWidget):
19
163
  # Signals
20
164
  sam_mode_requested = pyqtSignal()
21
165
  polygon_mode_requested = pyqtSignal()
22
- bbox_mode_requested = pyqtSignal() # New signal for bounding box mode
166
+ bbox_mode_requested = pyqtSignal()
23
167
  selection_mode_requested = pyqtSignal()
168
+ edit_mode_requested = pyqtSignal()
24
169
  clear_points_requested = pyqtSignal()
25
170
  fit_view_requested = pyqtSignal()
26
171
  browse_models_requested = pyqtSignal()
@@ -37,133 +182,453 @@ class ControlPanel(QWidget):
37
182
  image_adjustment_changed = pyqtSignal()
38
183
  hotkeys_requested = pyqtSignal()
39
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()
40
192
 
41
193
  def __init__(self, parent=None):
42
194
  super().__init__(parent)
43
- self.setMinimumWidth(50) # Allow collapsing but maintain minimum
44
- self.preferred_width = 250 # Store preferred width for expansion
195
+ self.setMinimumWidth(260) # Slightly wider for better layout
196
+ self.preferred_width = 280
45
197
  self._setup_ui()
46
198
  self._connect_signals()
47
199
 
48
200
  def _setup_ui(self):
49
- """Setup the UI layout."""
201
+ """Setup the professional UI layout."""
50
202
  layout = QVBoxLayout(self)
51
- layout.setAlignment(Qt.AlignmentFlag.AlignTop)
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()
52
213
 
53
- # Top button row
54
- toggle_layout = QHBoxLayout()
55
- toggle_layout.addStretch()
214
+ # Top header with pop-out button
215
+ header_layout = QHBoxLayout()
216
+ header_layout.addStretch()
56
217
 
57
218
  self.btn_popout = QPushButton("⋯")
58
219
  self.btn_popout.setToolTip("Pop out panel to separate window")
59
220
  self.btn_popout.setMaximumWidth(30)
60
- toggle_layout.addWidget(self.btn_popout)
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
+ )
61
281
 
62
- layout.addLayout(toggle_layout)
282
+ # AI & Settings Tab
283
+ ai_tab = self._create_ai_tab()
284
+ self.tab_widget.addTab(ai_tab, "🤖 AI")
63
285
 
64
- # Main controls widget
65
- self.main_controls_widget = QWidget()
66
- main_layout = QVBoxLayout(self.main_controls_widget)
67
- main_layout.setContentsMargins(0, 0, 0, 0)
286
+ # Processing & Adjustments Tab
287
+ processing_tab = self._create_processing_tab()
288
+ self.tab_widget.addTab(processing_tab, "🛠️ Tools")
68
289
 
69
- # Mode label
70
- self.mode_label = QLabel("Mode: Points")
71
- font = self.mode_label.font()
72
- font.setPointSize(14)
73
- font.setBold(True)
74
- self.mode_label.setFont(font)
75
- main_layout.addWidget(self.mode_label)
290
+ layout.addWidget(self.tab_widget, 1)
76
291
 
77
- # Mode buttons
78
- self._add_mode_buttons(main_layout)
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)
79
308
 
80
- # Separator
81
- main_layout.addSpacing(20)
82
- main_layout.addWidget(self._create_separator())
83
- main_layout.addSpacing(10)
309
+ def _create_mode_card(self):
310
+ """Create the fixed mode controls card."""
311
+ mode_card = ProfessionalCard("Mode Controls")
84
312
 
85
- # Model selection
86
- self.model_widget = ModelSelectionWidget()
87
- main_layout.addWidget(self.model_widget)
313
+ # Mode buttons in a clean grid
314
+ buttons_layout = QVBoxLayout()
315
+ buttons_layout.setSpacing(4)
88
316
 
89
- # Separator
90
- main_layout.addSpacing(10)
91
- main_layout.addWidget(self._create_separator())
92
- main_layout.addSpacing(10)
317
+ # First row: AI and Polygon
318
+ row1_layout = QHBoxLayout()
319
+ row1_layout.setSpacing(4)
93
320
 
94
- # Action buttons
95
- self._add_action_buttons(main_layout)
96
- main_layout.addSpacing(10)
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
97
326
 
98
- # Settings
99
- self.settings_widget = SettingsWidget()
100
- main_layout.addWidget(self.settings_widget)
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)
101
331
 
102
- # Adjustments
103
- self.adjustments_widget = AdjustmentsWidget()
104
- main_layout.addWidget(self.adjustments_widget)
332
+ row1_layout.addWidget(self.btn_sam_mode)
333
+ row1_layout.addWidget(self.btn_polygon_mode)
334
+ buttons_layout.addLayout(row1_layout)
105
335
 
106
- main_layout.addStretch()
336
+ # Second row: BBox and Selection
337
+ row2_layout = QHBoxLayout()
338
+ row2_layout.setSpacing(4)
107
339
 
108
- # Status labels
109
- self.notification_label = QLabel("")
110
- font = self.notification_label.font()
111
- font.setItalic(True)
112
- self.notification_label.setFont(font)
113
- self.notification_label.setStyleSheet("color: #ffa500;")
114
- self.notification_label.setWordWrap(True)
115
- main_layout.addWidget(self.notification_label)
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)
116
344
 
117
- layout.addWidget(self.main_controls_widget)
345
+ self.btn_selection_mode = self._create_mode_button(
346
+ "Select", "E", "Toggle segment selection"
347
+ )
348
+ self.btn_selection_mode.setCheckable(True)
118
349
 
119
- def _add_mode_buttons(self, layout):
120
- """Add mode control buttons."""
121
- self.btn_sam_mode = QPushButton("Point Mode (1)")
122
- self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
350
+ row2_layout.addWidget(self.btn_bbox_mode)
351
+ row2_layout.addWidget(self.btn_selection_mode)
352
+ buttons_layout.addLayout(row2_layout)
123
353
 
124
- self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
125
- self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
354
+ mode_card.addLayout(buttons_layout)
126
355
 
127
- self.btn_bbox_mode = QPushButton("BBox Mode (3)")
128
- self.btn_bbox_mode.setToolTip("Switch to Bounding Box Drawing Mode (3)")
356
+ # Bottom utility row: Edit and Hotkeys
357
+ utility_layout = QHBoxLayout()
358
+ utility_layout.setSpacing(4)
129
359
 
130
- self.btn_selection_mode = QPushButton("Selection Mode (E)")
131
- self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
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)
132
536
 
133
- layout.addWidget(self.btn_sam_mode)
134
- layout.addWidget(self.btn_polygon_mode)
135
- layout.addWidget(self.btn_bbox_mode)
136
- layout.addWidget(self.btn_selection_mode)
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)
137
608
 
138
- def _add_action_buttons(self, layout):
139
- """Add action buttons."""
140
- self.btn_fit_view = QPushButton("Fit View (.)")
141
- self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
609
+ # Image Adjustments - collapsible
610
+ adjustments_collapsible = SimpleCollapsible(
611
+ "Image Adjustments", self.adjustments_widget
612
+ )
613
+ layout.addWidget(adjustments_collapsible)
142
614
 
143
- self.btn_clear_points = QPushButton("Clear Clicks (C)")
144
- self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
615
+ layout.addStretch()
145
616
 
146
- self.btn_hotkeys = QPushButton("Hotkeys")
147
- self.btn_hotkeys.setToolTip("Configure keyboard shortcuts")
617
+ scroll.setWidget(scroll_content)
148
618
 
149
- layout.addWidget(self.btn_fit_view)
150
- layout.addWidget(self.btn_clear_points)
151
- layout.addWidget(self.btn_hotkeys)
619
+ tab_layout = QVBoxLayout(tab_widget)
620
+ tab_layout.setContentsMargins(0, 0, 0, 0)
621
+ tab_layout.addWidget(scroll)
152
622
 
153
- def _create_separator(self):
154
- """Create a horizontal separator line."""
155
- line = QFrame()
156
- line.setFrameShape(QFrame.Shape.HLine)
157
- return line
623
+ return tab_widget
158
624
 
159
625
  def _connect_signals(self):
160
626
  """Connect internal signals."""
161
- self.btn_sam_mode.clicked.connect(self.sam_mode_requested)
162
- self.btn_polygon_mode.clicked.connect(self.polygon_mode_requested)
163
- self.btn_bbox_mode.clicked.connect(self.bbox_mode_requested)
164
- self.btn_selection_mode.clicked.connect(self.selection_mode_requested)
165
- self.btn_clear_points.clicked.connect(self.clear_points_requested)
166
- self.btn_fit_view.clicked.connect(self.fit_view_requested)
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)
167
632
  self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
168
633
  self.btn_popout.clicked.connect(self.pop_out_requested)
169
634
 
@@ -172,6 +637,9 @@ class ControlPanel(QWidget):
172
637
  self.model_widget.refresh_requested.connect(self.refresh_models_requested)
173
638
  self.model_widget.model_selected.connect(self.model_selected)
174
639
 
640
+ # Settings widget signals
641
+ self.settings_widget.settings_changed.connect(self.settings_changed)
642
+
175
643
  # Adjustments widget signals
176
644
  self.adjustments_widget.annotation_size_changed.connect(
177
645
  self.annotation_size_changed
@@ -180,9 +648,6 @@ class ControlPanel(QWidget):
180
648
  self.adjustments_widget.join_threshold_changed.connect(
181
649
  self.join_threshold_changed
182
650
  )
183
- self.adjustments_widget.fragment_threshold_changed.connect(
184
- self.fragment_threshold_changed
185
- )
186
651
  self.adjustments_widget.brightness_changed.connect(self.brightness_changed)
187
652
  self.adjustments_widget.contrast_changed.connect(self.contrast_changed)
188
653
  self.adjustments_widget.gamma_changed.connect(self.gamma_changed)
@@ -193,6 +658,63 @@ class ControlPanel(QWidget):
193
658
  self.image_adjustment_changed
194
659
  )
195
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
+
196
718
  def mouseDoubleClickEvent(self, event):
197
719
  """Handle double-click to expand collapsed panel."""
198
720
  if (
@@ -213,8 +735,34 @@ class ControlPanel(QWidget):
213
735
  self.notification_label.clear()
214
736
 
215
737
  def set_mode_text(self, mode: str):
216
- """Set the mode label text."""
217
- self.mode_label.setText(f"Mode: {mode.replace('_', ' ').title()}")
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)
218
766
 
219
767
  # Delegate methods for sub-widgets
220
768
  def populate_models(self, models):
@@ -251,7 +799,7 @@ class ControlPanel(QWidget):
251
799
 
252
800
  def set_fragment_threshold(self, value):
253
801
  """Set fragment threshold."""
254
- self.adjustments_widget.set_fragment_threshold(value)
802
+ self.fragment_widget.set_fragment_threshold(value)
255
803
 
256
804
  def set_brightness(self, value):
257
805
  """Set brightness."""
@@ -269,9 +817,9 @@ class ControlPanel(QWidget):
269
817
  """Enable or disable the SAM mode button."""
270
818
  self.btn_sam_mode.setEnabled(enabled)
271
819
  if not enabled:
272
- self.btn_sam_mode.setToolTip("Point Mode (SAM model not available)")
820
+ self.btn_sam_mode.setToolTip("AI Mode (SAM model not available)")
273
821
  else:
274
- self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
822
+ self.btn_sam_mode.setToolTip("Switch to AI Mode for AI segmentation (1)")
275
823
 
276
824
  def set_popout_mode(self, is_popped_out: bool):
277
825
  """Update the pop-out button based on panel state."""
@@ -281,3 +829,33 @@ class ControlPanel(QWidget):
281
829
  else:
282
830
  self.btn_popout.setText("⋯")
283
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