Coverage for src / ezqt_app / widgets / core / settings_panel.py: 69.52%
374 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 13:12 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 13:12 +0000
1# ///////////////////////////////////////////////////////////////
2# WIDGETS.CORE.SETTINGS_PANEL - Settings panel widget
3# Project: ezqt_app
4# ///////////////////////////////////////////////////////////////
6"""SettingsPanel widget with scrollable settings and YAML persistence."""
8from __future__ import annotations
10# ///////////////////////////////////////////////////////////////
11# IMPORTS
12# ///////////////////////////////////////////////////////////////
13# Standard library imports
14from typing import Any, cast
16from pydantic import BaseModel, ConfigDict, ValidationError
18# Third-party imports
19from PySide6.QtCore import QEvent, QSize, Qt, Signal
20from PySide6.QtWidgets import (
21 QFrame,
22 QLabel,
23 QScrollArea,
24 QSizePolicy,
25 QVBoxLayout,
26 QWidget,
27)
29# Local imports
30from ...services.settings import get_settings_service
31from ...services.translation import get_translation_service
32from ...services.ui import Fonts
33from ...utils.diagnostics import warn_tech
36# ///////////////////////////////////////////////////////////////
37# PYDANTIC SCHEMAS
38# ///////////////////////////////////////////////////////////////
39class _SettingsPanelOptionSchema(BaseModel):
40 """Schema for one settings panel option."""
42 model_config = ConfigDict(extra="forbid")
44 type: str = "text"
45 label: str | None = None
46 description: str = ""
47 default: Any = None
48 enabled: bool = True
49 options: list[str] = []
50 min: int | None = None
51 max: int | None = None
52 unit: str | None = None
55class _SettingsPanelConfigSchema(BaseModel):
56 """Schema for the settings_panel section in app config."""
58 model_config = ConfigDict(extra="forbid")
60 app: dict[str, Any] | None = None
61 settings_panel: dict[str, _SettingsPanelOptionSchema] = {}
64# ///////////////////////////////////////////////////////////////
65# CLASSES
66# ///////////////////////////////////////////////////////////////
67class SettingsPanel(QFrame):
68 """
69 This class is used to create a settings panel.
70 It contains a top border, a content settings frame and a theme settings container.
71 The settings panel is used to display the settings.
72 """
74 # Signal emitted when a setting changes
75 settingChanged = Signal(str, object) # key, value
76 # Signal emitted when language changes
77 languageChanged = Signal()
79 # ///////////////////////////////////////////////////////////////
81 def _settings_storage_prefix(self) -> list[str]:
82 """Return config keys (file name + nested path) used to persist setting values."""
83 config_name = "app"
84 try:
85 from ...services.config import get_config_service
87 app_config = get_config_service().load_config(config_name)
88 app_section = app_config.get("app", {})
89 root = str(app_section.get("settings_storage_root", "settings_panel"))
90 parts = [part for part in root.split(".") if part]
92 def _exists(path_parts: list[str]) -> bool:
93 current: object = app_config
94 for part in path_parts:
95 if not isinstance(current, dict) or part not in current:
96 return False
97 current = current[part]
98 return isinstance(current, dict)
100 if parts and _exists(parts):
101 return [config_name, *parts]
102 except Exception as e:
103 warn_tech(
104 code="widgets.settings_panel.storage_prefix_resolution_failed",
105 message="Could not resolve settings storage prefix from app config",
106 error=e,
107 )
108 return [config_name, "settings_panel"]
110 def _sync_theme_selector_with_settings(self) -> None:
111 """Align theme selector UI with the currently active settings theme."""
112 try:
113 if not hasattr(self, "_theme_selector"):
114 return
116 theme_selector = self._theme_selector
117 gui = get_settings_service().gui
118 current_internal = f"{gui.THEME_PRESET}:{gui.THEME}"
119 current_display = self._theme_value_to_display.get(current_internal, "")
121 if current_display and hasattr(theme_selector, "set_value"):
122 theme_selector.set_value(current_display)
123 except Exception as e:
124 warn_tech(
125 code="widgets.settings_panel.theme_selector_sync_failed",
126 message="Could not synchronize theme selector with active settings",
127 error=e,
128 )
130 def __init__(
131 self,
132 parent: QWidget | None = None,
133 width: int = 240,
134 load_from_yaml: bool = True,
135 ) -> None:
136 super().__init__(parent)
137 self._widgets: list[QWidget] = []
138 self._settings: dict[str, Any] = {}
140 # Store configuration
141 self._width = width
143 self.setObjectName("settings_panel")
144 self.setMinimumSize(QSize(0, 0))
145 self.setMaximumSize(QSize(0, 16777215))
146 self.setFrameShape(QFrame.Shape.NoFrame)
147 self.setFrameShadow(QFrame.Shadow.Raised)
149 # ////// SETUP MAIN LAYOUT
150 self._layout = QVBoxLayout(self)
151 self._layout.setSpacing(0)
152 self._layout.setObjectName("settings_layout")
153 self._layout.setContentsMargins(0, 0, 0, 0)
155 # ////// SETUP TOP BORDER
156 self._top_border = QFrame(self)
157 self._top_border.setObjectName("settings_top_border")
158 self._top_border.setMaximumSize(QSize(16777215, 3))
159 self._top_border.setFrameShape(QFrame.Shape.NoFrame)
160 self._top_border.setFrameShadow(QFrame.Shadow.Raised)
161 self._layout.addWidget(self._top_border)
163 # ////// SETUP SCROLL AREA
164 self._scroll_area = QScrollArea(self)
165 self._scroll_area.setObjectName("settings_scroll_area")
166 self._scroll_area.setWidgetResizable(True)
167 self._scroll_area.setHorizontalScrollBarPolicy(
168 Qt.ScrollBarPolicy.ScrollBarAlwaysOff
169 )
170 self._scroll_area.setVerticalScrollBarPolicy(
171 Qt.ScrollBarPolicy.ScrollBarAsNeeded
172 )
173 self._scroll_area.setFrameShape(QFrame.Shape.NoFrame)
174 self._scroll_area.setFrameShadow(QFrame.Shadow.Raised)
175 self._layout.addWidget(self._scroll_area)
177 # ////// SETUP CONTENT WIDGET
178 self._content_widget = QFrame()
179 self._content_widget.setObjectName("content_widget")
180 self._content_widget.setFrameShape(QFrame.Shape.NoFrame)
181 self._content_widget.setFrameShadow(QFrame.Shadow.Raised)
182 self._content_widget.setSizePolicy(
183 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
184 )
185 self._scroll_area.setWidget(self._content_widget)
187 self._content_layout = QVBoxLayout(self._content_widget)
188 self._content_layout.setObjectName("content_layout")
189 self._content_layout.setSpacing(0)
190 self._content_layout.setContentsMargins(0, 0, 0, 0)
191 self._content_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
193 # ////// SETUP THEME SECTION
194 self._theme_section_frame = QFrame(self._content_widget)
195 self._theme_section_frame.setObjectName("theme_section_frame")
196 self._theme_section_frame.setFrameShape(QFrame.Shape.NoFrame)
197 self._theme_section_frame.setFrameShadow(QFrame.Shadow.Raised)
198 self._content_layout.addWidget(
199 self._theme_section_frame, 0, Qt.AlignmentFlag.AlignTop
200 )
202 self._theme_section_layout = QVBoxLayout(self._theme_section_frame)
203 self._theme_section_layout.setSpacing(8)
204 self._theme_section_layout.setObjectName("theme_section_layout")
205 self._theme_section_layout.setContentsMargins(10, 10, 10, 10)
207 # Build theme options from palette (display label → internal value mapping).
208 from ...services.ui.theme_service import ThemeService
210 _theme_options_data = ThemeService.get_available_themes()
211 self._theme_options_map: dict[str, str] = dict(_theme_options_data)
212 self._theme_value_to_display: dict[str, str] = {
213 v: d for d, v in _theme_options_data
214 }
216 _gui = get_settings_service().gui
217 _current_internal = f"{_gui.THEME_PRESET}:{_gui.THEME}"
218 _current_display = self._theme_value_to_display.get(
219 _current_internal,
220 _theme_options_data[0][0] if _theme_options_data else "",
221 )
223 from ...widgets.extended.setting_widgets import SettingSelect
225 self._theme_selector = SettingSelect(
226 label="Active Theme",
227 description="",
228 options=[d for d, _ in _theme_options_data],
229 default=_current_display,
230 )
231 self._theme_selector.setObjectName("theme_selector")
232 self._theme_selector.setSizePolicy(
233 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
234 )
235 self._widgets.append(self._theme_selector)
236 self._theme_selector.valueChanged.connect(
237 lambda _key, display_val: self._on_theme_selector_changed(display_val)
238 )
239 self._theme_section_layout.addWidget(self._theme_selector)
241 if load_from_yaml:
242 self.load_settings_from_yaml()
244 self.settingChanged.connect(self._on_setting_changed)
246 # ///////////////////////////////////////////////////////////////
248 def load_settings_from_yaml(self) -> None:
249 """Load settings from YAML file."""
250 try:
251 from ...services.config import get_config_service
253 app_config = get_config_service().load_config("app", force_reload=True)
254 if not isinstance(app_config, dict): 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true
255 app_config = {}
257 validated = _SettingsPanelConfigSchema.model_validate(app_config)
259 for key, config_model in validated.settings_panel.items():
260 if key == "theme":
261 continue
263 if config_model.enabled: 263 ↛ 259line 263 didn't jump to line 259 because the condition on line 263 was always true
264 config = config_model.model_dump(mode="python", exclude_none=True)
265 widget = self.add_setting_from_config(key, config)
266 default_value = config_model.default
267 if default_value is not None and hasattr(widget, "set_value"): 267 ↛ 259line 267 didn't jump to line 259 because the condition on line 267 was always true
268 cast(Any, widget).set_value(default_value)
270 except ValidationError as e:
271 warn_tech(
272 code="widgets.settings_panel.load_yaml_validation_failed",
273 message="Invalid settings panel configuration payload",
274 error=e,
275 )
276 except Exception as e:
277 warn_tech(
278 code="widgets.settings_panel.load_yaml_failed",
279 message="Error loading settings from YAML",
280 error=e,
281 )
283 def add_setting_from_config(self, key: str, config: dict) -> QWidget:
284 """Add a setting based on its YAML configuration."""
285 setting_type = config.get("type", "text")
286 label = config.get("label", key)
287 description = config.get("description", "")
288 default_value = config.get("default")
290 setting_container = QFrame(self._content_widget)
291 setting_container.setObjectName(f"setting_container_{key}")
292 setting_container.setFrameShape(QFrame.Shape.NoFrame)
293 setting_container.setFrameShadow(QFrame.Shadow.Raised)
295 container_layout = QVBoxLayout(setting_container)
296 container_layout.setSpacing(8)
297 container_layout.setObjectName(f"setting_container_layout_{key}")
298 container_layout.setContentsMargins(10, 10, 10, 10)
300 if setting_type == "toggle": 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true
301 widget = self._create_toggle_widget(
302 label,
303 description,
304 bool(default_value) if default_value is not None else False,
305 key,
306 )
307 elif setting_type == "select": 307 ↛ 316line 307 didn't jump to line 316 because the condition on line 307 was always true
308 options = config.get("options", [])
309 widget = self._create_select_widget(
310 label,
311 description,
312 options,
313 str(default_value) if default_value is not None else "",
314 key,
315 )
316 elif setting_type == "slider":
317 min_val = config.get("min", 0)
318 max_val = config.get("max", 100)
319 unit = config.get("unit", "")
320 widget = self._create_slider_widget(
321 label,
322 description,
323 min_val,
324 max_val,
325 int(default_value) if default_value is not None else 0,
326 unit,
327 key,
328 )
329 elif setting_type == "checkbox":
330 widget = self._create_checkbox_widget(
331 label,
332 description,
333 bool(default_value) if default_value is not None else False,
334 key,
335 )
336 else:
337 widget = self._create_text_widget(
338 label,
339 description,
340 str(default_value) if default_value is not None else "",
341 key,
342 )
344 container_layout.addWidget(widget)
345 self._content_layout.addWidget(setting_container)
346 self._settings[key] = widget
348 return widget
350 def _create_toggle_widget(
351 self, label: str, description: str, default: bool, key: str | None = None
352 ) -> QWidget:
353 """Create a toggle widget with label and description."""
354 from ...widgets.extended.setting_widgets import SettingToggle
356 widget = SettingToggle(label, description, default)
357 if key:
358 widget.set_key(key)
359 widget.valueChanged.connect(self._on_setting_changed)
360 return widget
362 def _create_select_widget(
363 self,
364 label: str,
365 description: str,
366 options: list[str],
367 default: str,
368 key: str | None = None,
369 ) -> QWidget:
370 """Create a select widget with label and description."""
371 from ...widgets.extended.setting_widgets import SettingSelect
373 widget = SettingSelect(label, description, options, default)
374 if key: 374 ↛ 376line 374 didn't jump to line 376 because the condition on line 374 was always true
375 widget.set_key(key)
376 widget.valueChanged.connect(self._on_setting_changed)
377 return widget
379 def _create_slider_widget(
380 self,
381 label: str,
382 description: str,
383 min_val: int,
384 max_val: int,
385 default: int,
386 unit: str,
387 key: str | None = None,
388 ) -> QWidget:
389 """Create a slider widget with label and description."""
390 from ...widgets.extended.setting_widgets import SettingSlider
392 widget = SettingSlider(label, description, min_val, max_val, default, unit)
393 if key:
394 widget.set_key(key)
395 widget.valueChanged.connect(self._on_setting_changed)
396 return widget
398 def _create_checkbox_widget(
399 self, label: str, description: str, default: bool, key: str | None = None
400 ) -> QWidget:
401 """Create a checkbox widget with label and description."""
402 from ...widgets.extended.setting_widgets import SettingCheckbox
404 widget = SettingCheckbox(label, description, default)
405 if key:
406 widget.set_key(key)
407 widget.valueChanged.connect(self._on_setting_changed)
408 return widget
410 def _create_text_widget(
411 self, label: str, description: str, default: str, key: str | None = None
412 ) -> QWidget:
413 """Create a text widget with label and description."""
414 from ...widgets.extended.setting_widgets import SettingText
416 widget = SettingText(label, description, default)
417 if key:
418 widget.set_key(key)
419 widget.valueChanged.connect(self._on_setting_changed)
420 return widget
422 # ///////////////////////////////////////////////////////////////
424 def add_toggle_setting(
425 self,
426 key: str,
427 label: str,
428 default: bool = False,
429 description: str = "",
430 enabled: bool = True, # noqa: ARG002
431 ):
432 """Add a toggle setting."""
433 from ...widgets.extended.setting_widgets import SettingToggle
435 widget = SettingToggle(label, description, default)
436 widget.set_key(key)
437 widget.valueChanged.connect(self._on_setting_changed)
439 self._settings[key] = widget
440 self.add_setting_widget(widget)
441 return widget
443 def add_select_setting(
444 self,
445 key: str,
446 label: str,
447 options: list[str],
448 default: str | None = None,
449 description: str = "",
450 enabled: bool = True, # noqa: ARG002
451 ):
452 """Add a selection setting."""
453 from ...widgets.extended.setting_widgets import SettingSelect
455 widget = SettingSelect(label, description, options, default)
456 widget.set_key(key)
457 widget.valueChanged.connect(self._on_setting_changed)
459 self._settings[key] = widget
460 self.add_setting_widget(widget)
461 return widget
463 def add_slider_setting(
464 self,
465 key: str,
466 label: str,
467 min_val: int,
468 max_val: int,
469 default: int,
470 unit: str = "",
471 description: str = "",
472 enabled: bool = True, # noqa: ARG002
473 ):
474 """Add a slider setting."""
475 from ...widgets.extended.setting_widgets import SettingSlider
477 widget = SettingSlider(label, description, min_val, max_val, default, unit)
478 widget.set_key(key)
479 widget.valueChanged.connect(self._on_setting_changed)
481 self._settings[key] = widget
482 self.add_setting_widget(widget)
483 return widget
485 def add_text_setting(
486 self,
487 key: str,
488 label: str,
489 default: str = "",
490 description: str = "",
491 enabled: bool = True, # noqa: ARG002
492 ):
493 """Add a text setting."""
494 from ...widgets.extended.setting_widgets import SettingText
496 widget = SettingText(label, description, default)
497 widget.set_key(key)
498 widget.valueChanged.connect(self._on_setting_changed)
500 self._settings[key] = widget
501 self.add_setting_widget(widget)
502 return widget
504 def add_checkbox_setting(
505 self,
506 key: str,
507 label: str,
508 default: bool = False,
509 description: str = "",
510 enabled: bool = True, # noqa: ARG002
511 ):
512 """Add a checkbox setting."""
513 from ...widgets.extended.setting_widgets import SettingCheckbox
515 widget = SettingCheckbox(label, description, default)
516 widget.set_key(key)
517 widget.valueChanged.connect(self._on_setting_changed)
519 self._settings[key] = widget
520 self.add_setting_widget(widget)
521 return widget
523 def _on_setting_changed(self, key: str, value):
524 """Called when a setting changes."""
525 if not hasattr(self, "_processing_setting_change"):
526 self._processing_setting_change = False
528 if self._processing_setting_change:
529 return
531 self._processing_setting_change = True
533 try:
534 try:
535 from ...services.application.app_service import AppService
537 AppService.stage_config_value(
538 [*self._settings_storage_prefix(), key, "default"], value
539 )
540 except Exception as e:
541 warn_tech(
542 code="widgets.settings_panel.save_setting_failed",
543 message=f"Could not save setting '{key}' to YAML",
544 error=e,
545 )
547 if key == "language":
548 try:
549 translation_service = get_translation_service()
550 current_lang = translation_service.get_current_language_name()
551 if current_lang != str(value): 551 ↛ 562line 551 didn't jump to line 562 because the condition on line 551 was always true
552 translation_service.change_language(str(value))
553 self._sync_theme_selector_with_settings()
554 self.languageChanged.emit()
555 except Exception as e:
556 warn_tech(
557 code="widgets.settings_panel.change_language_failed",
558 message="Could not change language",
559 error=e,
560 )
562 self.settingChanged.emit(key, value)
563 finally:
564 self._processing_setting_change = False
566 # ///////////////////////////////////////////////////////////////
568 def get_setting_value(self, key: str) -> Any:
569 """Get the value of a setting."""
570 if key in self._settings:
571 return self._settings[key].get_value()
572 return None
574 def set_setting_value(self, key: str, value: Any) -> None:
575 """Set the value of a setting."""
576 if key in self._settings: 576 ↛ exitline 576 didn't return from function 'set_setting_value' because the condition on line 576 was always true
577 self._settings[key].set_value(value)
579 def get_all_settings(self) -> dict[str, Any]:
580 """Get all settings and their values."""
581 return {key: widget.get_value() for key, widget in self._settings.items()}
583 def save_all_settings_to_yaml(self) -> None:
584 """Stage all current setting values."""
585 from ...services.application.app_service import AppService
587 for key, widget in self._settings.items():
588 try:
589 AppService.stage_config_value(
590 [*self._settings_storage_prefix(), key, "default"],
591 widget.get_value(),
592 )
593 except Exception as e:
594 warn_tech(
595 code="widgets.settings_panel.save_all_settings_failed",
596 message=f"Could not save setting '{key}' to YAML",
597 error=e,
598 )
600 def retranslate_ui(self) -> None:
601 """Apply current translations to all owned text labels."""
602 if hasattr(self, "_theme_selector") and hasattr( 602 ↛ 606line 602 didn't jump to line 606 because the condition on line 602 was always true
603 self._theme_selector, "retranslate_ui"
604 ):
605 self._theme_selector.retranslate_ui()
606 for widget in self._settings.values():
607 if hasattr(widget, "retranslate_ui"):
608 widget.retranslate_ui()
610 def changeEvent(self, event: QEvent) -> None:
611 """Handle Qt change events."""
612 if event.type() == QEvent.Type.LanguageChange:
613 self.retranslate_ui()
614 super().changeEvent(event)
616 # ///////////////////////////////////////////////////////////////
618 def get_width(self) -> int:
619 """Get panel width."""
620 return self._width
622 def set_width(self, width: int) -> None:
623 """Set panel width."""
624 self._width = width
626 def get_theme_selector(self):
627 """Get the theme selector if available."""
628 if hasattr(self, "_theme_selector"): 628 ↛ 630line 628 didn't jump to line 630 because the condition on line 628 was always true
629 return self._theme_selector
630 return None
632 def update_all_theme_icons(self) -> None:
633 """Update theme icons for all widgets that support it."""
634 current_theme = get_settings_service().gui.THEME
635 for widget in self._widgets:
636 # New pattern: widgets exposing setTheme(theme) directly
637 setter = getattr(widget, "setTheme", None)
638 if callable(setter):
639 setter(current_theme)
640 continue
641 # Legacy pattern: widgets exposing update_theme_icon()
642 updater = getattr(widget, "update_theme_icon", None)
643 if callable(updater): 643 ↛ 635line 643 didn't jump to line 635 because the condition on line 643 was always true
644 updater()
646 self.style().unpolish(self)
647 self.style().polish(self)
649 for child in self.findChildren(QWidget):
650 child.style().unpolish(child)
651 child.style().polish(child)
653 def _on_theme_selector_changed(self, display_value: str) -> None:
654 """Called when the theme selector value changes.
656 ``display_value`` is the human-readable label emitted by the select
657 widget (e.g. ``"Blue Gray - Dark"``). It is converted to the internal
658 ``"preset:variant"`` format before being persisted and broadcast.
659 """
660 try:
661 internal_value = self._theme_options_map.get(display_value, display_value)
662 from ...services.application.app_service import AppService
663 from ...services.settings import get_settings_service
665 get_settings_service().set_theme(internal_value)
666 AppService.stage_config_value(
667 [*self._settings_storage_prefix(), "theme", "default"],
668 internal_value,
669 )
670 self.settingChanged.emit("theme", internal_value)
671 except Exception as e:
672 warn_tech(
673 code="widgets.settings_panel.theme_selector_change_failed",
674 message="Could not handle theme selector change",
675 error=e,
676 )
678 def add_setting_widget(self, widget: QWidget) -> None:
679 """Add a new setting widget to the settings panel."""
680 setting_container = QFrame(self._content_widget)
681 setting_container.setObjectName(f"setting_container_{widget.objectName()}")
682 setting_container.setFrameShape(QFrame.Shape.NoFrame)
683 setting_container.setFrameShadow(QFrame.Shadow.Raised)
685 container_layout = QVBoxLayout(setting_container)
686 container_layout.setSpacing(8)
687 container_layout.setObjectName(
688 f"setting_container_layout_{widget.objectName()}"
689 )
690 container_layout.setContentsMargins(10, 10, 10, 10)
692 container_layout.addWidget(widget)
693 self._content_layout.addWidget(setting_container)
694 self._widgets.append(widget)
696 def add_setting_section(self, title: str = "") -> QFrame:
697 """Add a new settings section with optional title."""
698 section = QFrame(self._content_widget)
699 section.setObjectName(f"settings_section_{title.replace(' ', '_').lower()}")
700 section.setFrameShape(QFrame.Shape.NoFrame)
701 section.setFrameShadow(QFrame.Shadow.Raised)
703 section_layout = QVBoxLayout(section)
704 section_layout.setSpacing(8)
705 section_layout.setObjectName(
706 f"settings_section_layout_{title.replace(' ', '_').lower()}"
707 )
708 section_layout.setContentsMargins(10, 10, 10, 10)
710 if title: 710 ↛ 724line 710 didn't jump to line 724 because the condition on line 710 was always true
711 title_label = QLabel(title, section)
712 title_label.setObjectName(
713 f"settings_section_title_{title.replace(' ', '_').lower()}"
714 )
715 if Fonts.SEGOE_UI_10_REG is not None: 715 ↛ 717line 715 didn't jump to line 717 because the condition on line 715 was always true
716 title_label.setFont(Fonts.SEGOE_UI_10_REG)
717 title_label.setAlignment(
718 Qt.AlignmentFlag.AlignLeading
719 | Qt.AlignmentFlag.AlignLeft
720 | Qt.AlignmentFlag.AlignVCenter
721 )
722 section_layout.addWidget(title_label)
724 self._content_layout.addWidget(section)
725 return section
727 def scroll_to_top(self) -> None:
728 """Scroll to top of settings panel."""
729 if hasattr(self, "_scroll_area"): 729 ↛ exitline 729 didn't return from function 'scroll_to_top' because the condition on line 729 was always true
730 self._scroll_area.verticalScrollBar().setValue(0)
732 def scroll_to_bottom(self) -> None:
733 """Scroll to bottom of settings panel."""
734 if hasattr(self, "_scroll_area"): 734 ↛ exitline 734 didn't return from function 'scroll_to_bottom' because the condition on line 734 was always true
735 scrollbar = self._scroll_area.verticalScrollBar()
736 scrollbar.setValue(scrollbar.maximum())
738 def scroll_to_widget(self, widget: QWidget) -> None:
739 """Scroll to a specific widget in the settings panel."""
740 if hasattr(self, "_scroll_area") and widget: 740 ↛ exitline 740 didn't return from function 'scroll_to_widget' because the condition on line 740 was always true
741 widget_pos = widget.mapTo(self._content_widget, widget.rect().topLeft())
742 self._scroll_area.verticalScrollBar().setValue(widget_pos.y())
745# ///////////////////////////////////////////////////////////////
746# PUBLIC API
747# ///////////////////////////////////////////////////////////////
748__all__ = ["SettingsPanel"]