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

1# /////////////////////////////////////////////////////////////// 

2# WIDGETS.CORE.SETTINGS_PANEL - Settings panel widget 

3# Project: ezqt_app 

4# /////////////////////////////////////////////////////////////// 

5 

6"""SettingsPanel widget with scrollable settings and YAML persistence.""" 

7 

8from __future__ import annotations 

9 

10# /////////////////////////////////////////////////////////////// 

11# IMPORTS 

12# /////////////////////////////////////////////////////////////// 

13# Standard library imports 

14from typing import Any, cast 

15 

16from pydantic import BaseModel, ConfigDict, ValidationError 

17 

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) 

28 

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 

34 

35 

36# /////////////////////////////////////////////////////////////// 

37# PYDANTIC SCHEMAS 

38# /////////////////////////////////////////////////////////////// 

39class _SettingsPanelOptionSchema(BaseModel): 

40 """Schema for one settings panel option.""" 

41 

42 model_config = ConfigDict(extra="forbid") 

43 

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 

53 

54 

55class _SettingsPanelConfigSchema(BaseModel): 

56 """Schema for the settings_panel section in app config.""" 

57 

58 model_config = ConfigDict(extra="forbid") 

59 

60 app: dict[str, Any] | None = None 

61 settings_panel: dict[str, _SettingsPanelOptionSchema] = {} 

62 

63 

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 """ 

73 

74 # Signal emitted when a setting changes 

75 settingChanged = Signal(str, object) # key, value 

76 # Signal emitted when language changes 

77 languageChanged = Signal() 

78 

79 # /////////////////////////////////////////////////////////////// 

80 

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 

86 

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] 

91 

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) 

99 

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"] 

109 

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 

115 

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, "") 

120 

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 ) 

129 

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] = {} 

139 

140 # Store configuration 

141 self._width = width 

142 

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) 

148 

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) 

154 

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) 

162 

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) 

176 

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) 

186 

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) 

192 

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 ) 

201 

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) 

206 

207 # Build theme options from palette (display label → internal value mapping). 

208 from ...services.ui.theme_service import ThemeService 

209 

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 } 

215 

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 ) 

222 

223 from ...widgets.extended.setting_widgets import SettingSelect 

224 

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) 

240 

241 if load_from_yaml: 

242 self.load_settings_from_yaml() 

243 

244 self.settingChanged.connect(self._on_setting_changed) 

245 

246 # /////////////////////////////////////////////////////////////// 

247 

248 def load_settings_from_yaml(self) -> None: 

249 """Load settings from YAML file.""" 

250 try: 

251 from ...services.config import get_config_service 

252 

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 = {} 

256 

257 validated = _SettingsPanelConfigSchema.model_validate(app_config) 

258 

259 for key, config_model in validated.settings_panel.items(): 

260 if key == "theme": 

261 continue 

262 

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) 

269 

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 ) 

282 

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") 

289 

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) 

294 

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) 

299 

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 ) 

343 

344 container_layout.addWidget(widget) 

345 self._content_layout.addWidget(setting_container) 

346 self._settings[key] = widget 

347 

348 return widget 

349 

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 

355 

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 

361 

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 

372 

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 

378 

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 

391 

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 

397 

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 

403 

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 

409 

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 

415 

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 

421 

422 # /////////////////////////////////////////////////////////////// 

423 

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 

434 

435 widget = SettingToggle(label, description, default) 

436 widget.set_key(key) 

437 widget.valueChanged.connect(self._on_setting_changed) 

438 

439 self._settings[key] = widget 

440 self.add_setting_widget(widget) 

441 return widget 

442 

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 

454 

455 widget = SettingSelect(label, description, options, default) 

456 widget.set_key(key) 

457 widget.valueChanged.connect(self._on_setting_changed) 

458 

459 self._settings[key] = widget 

460 self.add_setting_widget(widget) 

461 return widget 

462 

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 

476 

477 widget = SettingSlider(label, description, min_val, max_val, default, unit) 

478 widget.set_key(key) 

479 widget.valueChanged.connect(self._on_setting_changed) 

480 

481 self._settings[key] = widget 

482 self.add_setting_widget(widget) 

483 return widget 

484 

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 

495 

496 widget = SettingText(label, description, default) 

497 widget.set_key(key) 

498 widget.valueChanged.connect(self._on_setting_changed) 

499 

500 self._settings[key] = widget 

501 self.add_setting_widget(widget) 

502 return widget 

503 

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 

514 

515 widget = SettingCheckbox(label, description, default) 

516 widget.set_key(key) 

517 widget.valueChanged.connect(self._on_setting_changed) 

518 

519 self._settings[key] = widget 

520 self.add_setting_widget(widget) 

521 return widget 

522 

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 

527 

528 if self._processing_setting_change: 

529 return 

530 

531 self._processing_setting_change = True 

532 

533 try: 

534 try: 

535 from ...services.application.app_service import AppService 

536 

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 ) 

546 

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 ) 

561 

562 self.settingChanged.emit(key, value) 

563 finally: 

564 self._processing_setting_change = False 

565 

566 # /////////////////////////////////////////////////////////////// 

567 

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 

573 

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) 

578 

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()} 

582 

583 def save_all_settings_to_yaml(self) -> None: 

584 """Stage all current setting values.""" 

585 from ...services.application.app_service import AppService 

586 

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 ) 

599 

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() 

609 

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) 

615 

616 # /////////////////////////////////////////////////////////////// 

617 

618 def get_width(self) -> int: 

619 """Get panel width.""" 

620 return self._width 

621 

622 def set_width(self, width: int) -> None: 

623 """Set panel width.""" 

624 self._width = width 

625 

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 

631 

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() 

645 

646 self.style().unpolish(self) 

647 self.style().polish(self) 

648 

649 for child in self.findChildren(QWidget): 

650 child.style().unpolish(child) 

651 child.style().polish(child) 

652 

653 def _on_theme_selector_changed(self, display_value: str) -> None: 

654 """Called when the theme selector value changes. 

655 

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 

664 

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 ) 

677 

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) 

684 

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) 

691 

692 container_layout.addWidget(widget) 

693 self._content_layout.addWidget(setting_container) 

694 self._widgets.append(widget) 

695 

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) 

702 

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) 

709 

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) 

723 

724 self._content_layout.addWidget(section) 

725 return section 

726 

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) 

731 

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()) 

737 

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()) 

743 

744 

745# /////////////////////////////////////////////////////////////// 

746# PUBLIC API 

747# /////////////////////////////////////////////////////////////// 

748__all__ = ["SettingsPanel"]