Coverage for src / ezqt_app / widgets / core / settings_panel.py: 49.67%

363 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 07:07 +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 

15 

16# Third-party imports 

17from PySide6.QtCore import QEvent, QSize, Qt, Signal 

18from PySide6.QtWidgets import ( 

19 QFrame, 

20 QLabel, 

21 QScrollArea, 

22 QSizePolicy, 

23 QVBoxLayout, 

24 QWidget, 

25) 

26 

27# Local imports 

28from ...services.settings import get_settings_service 

29from ...services.translation import get_translation_service 

30from ...services.ui import Fonts 

31from ...utils.diagnostics import warn_tech 

32 

33 

34# /////////////////////////////////////////////////////////////// 

35# CLASSES 

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

37class SettingsPanel(QFrame): 

38 """ 

39 This class is used to create a settings panel. 

40 It contains a top border, a content settings frame and a theme settings container. 

41 The settings panel is used to display the settings. 

42 """ 

43 

44 # Signal emitted when a setting changes 

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

46 # Signal emitted when language changes 

47 languageChanged = Signal() 

48 

49 # /////////////////////////////////////////////////////////////// 

50 

51 def _settings_storage_prefix(self) -> list[str]: 

52 """Return config keys (file name + nested path) used to persist setting values.""" 

53 config_name = "app" 

54 try: 

55 from ...services.config import get_config_service 

56 

57 app_config = get_config_service().load_config(config_name) 

58 app_section = app_config.get("app", {}) 

59 root = str(app_section.get("settings_storage_root", "settings_panel")) 

60 parts = [part for part in root.split(".") if part] 

61 

62 def _exists(path_parts: list[str]) -> bool: 

63 current: object = app_config 

64 for part in path_parts: 

65 if not isinstance(current, dict) or part not in current: 

66 return False 

67 current = current[part] 

68 return isinstance(current, dict) 

69 

70 if parts and _exists(parts): 

71 return [config_name, *parts] 

72 except Exception as e: 

73 warn_tech( 

74 code="widgets.settings_panel.storage_prefix_resolution_failed", 

75 message="Could not resolve settings storage prefix from app config", 

76 error=e, 

77 ) 

78 return [config_name, "settings_panel"] 

79 

80 def _sync_theme_selector_with_settings(self) -> None: 

81 """Align theme selector UI with the currently active settings theme.""" 

82 try: 

83 if not hasattr(self, "_theme_selector"): 

84 return 

85 

86 theme_selector = self._theme_selector 

87 gui = get_settings_service().gui 

88 current_internal = f"{gui.THEME_PRESET}:{gui.THEME}" 

89 current_display = self._theme_value_to_display.get(current_internal, "") 

90 

91 if current_display and hasattr(theme_selector, "set_value"): 

92 theme_selector.set_value(current_display) 

93 except Exception as e: 

94 warn_tech( 

95 code="widgets.settings_panel.theme_selector_sync_failed", 

96 message="Could not synchronize theme selector with active settings", 

97 error=e, 

98 ) 

99 

100 def __init__( 

101 self, 

102 parent: QWidget | None = None, 

103 width: int = 240, 

104 load_from_yaml: bool = True, 

105 ) -> None: 

106 super().__init__(parent) 

107 self._widgets: list[QWidget] = [] 

108 self._settings: dict[str, Any] = {} 

109 

110 # Store configuration 

111 self._width = width 

112 

113 self.setObjectName("settings_panel") 

114 self.setMinimumSize(QSize(0, 0)) 

115 self.setMaximumSize(QSize(0, 16777215)) 

116 self.setFrameShape(QFrame.Shape.NoFrame) 

117 self.setFrameShadow(QFrame.Shadow.Raised) 

118 

119 # ////// SETUP MAIN LAYOUT 

120 self._layout = QVBoxLayout(self) 

121 self._layout.setSpacing(0) 

122 self._layout.setObjectName("settings_layout") 

123 self._layout.setContentsMargins(0, 0, 0, 0) 

124 

125 # ////// SETUP TOP BORDER 

126 self._top_border = QFrame(self) 

127 self._top_border.setObjectName("settings_top_border") 

128 self._top_border.setMaximumSize(QSize(16777215, 3)) 

129 self._top_border.setFrameShape(QFrame.Shape.NoFrame) 

130 self._top_border.setFrameShadow(QFrame.Shadow.Raised) 

131 self._layout.addWidget(self._top_border) 

132 

133 # ////// SETUP SCROLL AREA 

134 self._scroll_area = QScrollArea(self) 

135 self._scroll_area.setObjectName("settings_scroll_area") 

136 self._scroll_area.setWidgetResizable(True) 

137 self._scroll_area.setHorizontalScrollBarPolicy( 

138 Qt.ScrollBarPolicy.ScrollBarAlwaysOff 

139 ) 

140 self._scroll_area.setVerticalScrollBarPolicy( 

141 Qt.ScrollBarPolicy.ScrollBarAsNeeded 

142 ) 

143 self._scroll_area.setFrameShape(QFrame.Shape.NoFrame) 

144 self._scroll_area.setFrameShadow(QFrame.Shadow.Raised) 

145 self._layout.addWidget(self._scroll_area) 

146 

147 # ////// SETUP CONTENT WIDGET 

148 self._content_widget = QFrame() 

149 self._content_widget.setObjectName("content_widget") 

150 self._content_widget.setFrameShape(QFrame.Shape.NoFrame) 

151 self._content_widget.setFrameShadow(QFrame.Shadow.Raised) 

152 self._content_widget.setSizePolicy( 

153 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred 

154 ) 

155 self._scroll_area.setWidget(self._content_widget) 

156 

157 self._content_layout = QVBoxLayout(self._content_widget) 

158 self._content_layout.setObjectName("content_layout") 

159 self._content_layout.setSpacing(0) 

160 self._content_layout.setContentsMargins(0, 0, 0, 0) 

161 self._content_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 

162 

163 # ////// SETUP THEME SECTION 

164 self._theme_section_frame = QFrame(self._content_widget) 

165 self._theme_section_frame.setObjectName("theme_section_frame") 

166 self._theme_section_frame.setFrameShape(QFrame.Shape.NoFrame) 

167 self._theme_section_frame.setFrameShadow(QFrame.Shadow.Raised) 

168 self._content_layout.addWidget( 

169 self._theme_section_frame, 0, Qt.AlignmentFlag.AlignTop 

170 ) 

171 

172 self._theme_section_layout = QVBoxLayout(self._theme_section_frame) 

173 self._theme_section_layout.setSpacing(8) 

174 self._theme_section_layout.setObjectName("theme_section_layout") 

175 self._theme_section_layout.setContentsMargins(10, 10, 10, 10) 

176 

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

178 from ...services.ui.theme_service import ThemeService 

179 

180 _theme_options_data = ThemeService.get_available_themes() 

181 self._theme_options_map: dict[str, str] = dict(_theme_options_data) 

182 self._theme_value_to_display: dict[str, str] = { 

183 v: d for d, v in _theme_options_data 

184 } 

185 

186 _gui = get_settings_service().gui 

187 _current_internal = f"{_gui.THEME_PRESET}:{_gui.THEME}" 

188 _current_display = self._theme_value_to_display.get( 

189 _current_internal, 

190 _theme_options_data[0][0] if _theme_options_data else "", 

191 ) 

192 

193 from ...widgets.extended.setting_widgets import SettingSelect 

194 

195 self._theme_selector = SettingSelect( 

196 label="Active Theme", 

197 description="", 

198 options=[d for d, _ in _theme_options_data], 

199 default=_current_display, 

200 ) 

201 self._theme_selector.setObjectName("theme_selector") 

202 self._theme_selector.setSizePolicy( 

203 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred 

204 ) 

205 self._widgets.append(self._theme_selector) 

206 self._theme_selector.valueChanged.connect( 

207 lambda _key, display_val: self._on_theme_selector_changed(display_val) 

208 ) 

209 self._theme_section_layout.addWidget(self._theme_selector) 

210 

211 if load_from_yaml: 

212 self.load_settings_from_yaml() 

213 

214 self.settingChanged.connect(self._on_setting_changed) 

215 

216 # /////////////////////////////////////////////////////////////// 

217 

218 def load_settings_from_yaml(self) -> None: 

219 """Load settings from YAML file.""" 

220 try: 

221 from pathlib import Path 

222 

223 import yaml 

224 

225 possible_paths = [ 

226 Path.cwd() / "bin" / "config" / "app.config.yaml", 

227 Path(__file__).parent.parent.parent 

228 / "resources" 

229 / "config" 

230 / "app.config.yaml", 

231 ] 

232 

233 app_config = None 

234 for path in possible_paths: 234 ↛ 240line 234 didn't jump to line 240 because the loop on line 234 didn't complete

235 if path.exists(): 

236 with open(path, encoding="utf-8") as f: 

237 app_config = yaml.safe_load(f) 

238 break 

239 

240 if app_config is None: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

241 warn_tech( 

242 code="widgets.settings_panel.app_config_yaml_not_found", 

243 message="Could not find app.config.yaml file", 

244 ) 

245 return 

246 

247 settings_config = app_config.get("settings_panel", {}) 

248 

249 for key, config in settings_config.items(): 

250 if key == "theme": 

251 continue 

252 

253 if config.get("enabled", True): 

254 widget = self.add_setting_from_config(key, config) 

255 default_value = config.get("default") 

256 if default_value is not None and hasattr(widget, "set_value"): 256 ↛ 249line 256 didn't jump to line 249 because the condition on line 256 was always true

257 widget.set_value(default_value) # type: ignore[union-attr] 

258 

259 except Exception as e: 

260 warn_tech( 

261 code="widgets.settings_panel.load_yaml_failed", 

262 message="Error loading settings from YAML", 

263 error=e, 

264 ) 

265 

266 def add_setting_from_config(self, key: str, config: dict) -> QWidget: 

267 """Add a setting based on its YAML configuration.""" 

268 setting_type = config.get("type", "text") 

269 label = config.get("label", key) 

270 description = config.get("description", "") 

271 default_value = config.get("default") 

272 

273 setting_container = QFrame(self._content_widget) 

274 setting_container.setObjectName(f"setting_container_{key}") 

275 setting_container.setFrameShape(QFrame.Shape.NoFrame) 

276 setting_container.setFrameShadow(QFrame.Shadow.Raised) 

277 

278 container_layout = QVBoxLayout(setting_container) 

279 container_layout.setSpacing(8) 

280 container_layout.setObjectName(f"setting_container_layout_{key}") 

281 container_layout.setContentsMargins(10, 10, 10, 10) 

282 

283 if setting_type == "toggle": 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true

284 widget = self._create_toggle_widget( 

285 label, 

286 description, 

287 bool(default_value) if default_value is not None else False, 

288 key, 

289 ) 

290 elif setting_type == "select": 290 ↛ 299line 290 didn't jump to line 299 because the condition on line 290 was always true

291 options = config.get("options", []) 

292 widget = self._create_select_widget( 

293 label, 

294 description, 

295 options, 

296 str(default_value) if default_value is not None else "", 

297 key, 

298 ) 

299 elif setting_type == "slider": 

300 min_val = config.get("min", 0) 

301 max_val = config.get("max", 100) 

302 unit = config.get("unit", "") 

303 widget = self._create_slider_widget( 

304 label, 

305 description, 

306 min_val, 

307 max_val, 

308 int(default_value) if default_value is not None else 0, 

309 unit, 

310 key, 

311 ) 

312 elif setting_type == "checkbox": 

313 widget = self._create_checkbox_widget( 

314 label, 

315 description, 

316 bool(default_value) if default_value is not None else False, 

317 key, 

318 ) 

319 else: 

320 widget = self._create_text_widget( 

321 label, 

322 description, 

323 str(default_value) if default_value is not None else "", 

324 key, 

325 ) 

326 

327 container_layout.addWidget(widget) 

328 self._content_layout.addWidget(setting_container) 

329 self._settings[key] = widget 

330 

331 return widget 

332 

333 def _create_toggle_widget( 

334 self, label: str, description: str, default: bool, key: str | None = None 

335 ) -> QWidget: 

336 """Create a toggle widget with label and description.""" 

337 from ...widgets.extended.setting_widgets import SettingToggle 

338 

339 widget = SettingToggle(label, description, default) 

340 if key: 

341 widget.set_key(key) 

342 widget.valueChanged.connect(self._on_setting_changed) 

343 return widget 

344 

345 def _create_select_widget( 

346 self, 

347 label: str, 

348 description: str, 

349 options: list[str], 

350 default: str, 

351 key: str | None = None, 

352 ) -> QWidget: 

353 """Create a select widget with label and description.""" 

354 from ...widgets.extended.setting_widgets import SettingSelect 

355 

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

357 if key: 357 ↛ 359line 357 didn't jump to line 359 because the condition on line 357 was always true

358 widget.set_key(key) 

359 widget.valueChanged.connect(self._on_setting_changed) 

360 return widget 

361 

362 def _create_slider_widget( 

363 self, 

364 label: str, 

365 description: str, 

366 min_val: int, 

367 max_val: int, 

368 default: int, 

369 unit: str, 

370 key: str | None = None, 

371 ) -> QWidget: 

372 """Create a slider widget with label and description.""" 

373 from ...widgets.extended.setting_widgets import SettingSlider 

374 

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

376 if key: 

377 widget.set_key(key) 

378 widget.valueChanged.connect(self._on_setting_changed) 

379 return widget 

380 

381 def _create_checkbox_widget( 

382 self, label: str, description: str, default: bool, key: str | None = None 

383 ) -> QWidget: 

384 """Create a checkbox widget with label and description.""" 

385 from ...widgets.extended.setting_widgets import SettingCheckbox 

386 

387 widget = SettingCheckbox(label, description, default) 

388 if key: 

389 widget.set_key(key) 

390 widget.valueChanged.connect(self._on_setting_changed) 

391 return widget 

392 

393 def _create_text_widget( 

394 self, label: str, description: str, default: str, key: str | None = None 

395 ) -> QWidget: 

396 """Create a text widget with label and description.""" 

397 from ...widgets.extended.setting_widgets import SettingText 

398 

399 widget = SettingText(label, description, default) 

400 if key: 

401 widget.set_key(key) 

402 widget.valueChanged.connect(self._on_setting_changed) 

403 return widget 

404 

405 # /////////////////////////////////////////////////////////////// 

406 

407 def add_toggle_setting( 

408 self, 

409 key: str, 

410 label: str, 

411 default: bool = False, 

412 description: str = "", 

413 enabled: bool = True, # noqa: ARG002 

414 ): 

415 """Add a toggle setting.""" 

416 from ...widgets.extended.setting_widgets import SettingToggle 

417 

418 widget = SettingToggle(label, description, default) 

419 widget.set_key(key) 

420 widget.valueChanged.connect(self._on_setting_changed) 

421 

422 self._settings[key] = widget 

423 self.add_setting_widget(widget) 

424 return widget 

425 

426 def add_select_setting( 

427 self, 

428 key: str, 

429 label: str, 

430 options: list[str], 

431 default: str | None = None, 

432 description: str = "", 

433 enabled: bool = True, # noqa: ARG002 

434 ): 

435 """Add a selection setting.""" 

436 from ...widgets.extended.setting_widgets import SettingSelect 

437 

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

439 widget.set_key(key) 

440 widget.valueChanged.connect(self._on_setting_changed) 

441 

442 self._settings[key] = widget 

443 self.add_setting_widget(widget) 

444 return widget 

445 

446 def add_slider_setting( 

447 self, 

448 key: str, 

449 label: str, 

450 min_val: int, 

451 max_val: int, 

452 default: int, 

453 unit: str = "", 

454 description: str = "", 

455 enabled: bool = True, # noqa: ARG002 

456 ): 

457 """Add a slider setting.""" 

458 from ...widgets.extended.setting_widgets import SettingSlider 

459 

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

461 widget.set_key(key) 

462 widget.valueChanged.connect(self._on_setting_changed) 

463 

464 self._settings[key] = widget 

465 self.add_setting_widget(widget) 

466 return widget 

467 

468 def add_text_setting( 

469 self, 

470 key: str, 

471 label: str, 

472 default: str = "", 

473 description: str = "", 

474 enabled: bool = True, # noqa: ARG002 

475 ): 

476 """Add a text setting.""" 

477 from ...widgets.extended.setting_widgets import SettingText 

478 

479 widget = SettingText(label, description, default) 

480 widget.set_key(key) 

481 widget.valueChanged.connect(self._on_setting_changed) 

482 

483 self._settings[key] = widget 

484 self.add_setting_widget(widget) 

485 return widget 

486 

487 def add_checkbox_setting( 

488 self, 

489 key: str, 

490 label: str, 

491 default: bool = False, 

492 description: str = "", 

493 enabled: bool = True, # noqa: ARG002 

494 ): 

495 """Add a checkbox setting.""" 

496 from ...widgets.extended.setting_widgets import SettingCheckbox 

497 

498 widget = SettingCheckbox(label, description, default) 

499 widget.set_key(key) 

500 widget.valueChanged.connect(self._on_setting_changed) 

501 

502 self._settings[key] = widget 

503 self.add_setting_widget(widget) 

504 return widget 

505 

506 def _on_setting_changed(self, key: str, value): 

507 """Called when a setting changes.""" 

508 if not hasattr(self, "_processing_setting_change"): 

509 self._processing_setting_change = False 

510 

511 if self._processing_setting_change: 

512 return 

513 

514 self._processing_setting_change = True 

515 

516 try: 

517 try: 

518 from ...services.application.app_service import AppService 

519 

520 AppService.stage_config_value( 

521 [*self._settings_storage_prefix(), key, "default"], value 

522 ) 

523 except Exception as e: 

524 warn_tech( 

525 code="widgets.settings_panel.save_setting_failed", 

526 message=f"Could not save setting '{key}' to YAML", 

527 error=e, 

528 ) 

529 

530 if key == "language": 530 ↛ 545line 530 didn't jump to line 545 because the condition on line 530 was always true

531 try: 

532 translation_service = get_translation_service() 

533 current_lang = translation_service.get_current_language_name() 

534 if current_lang != str(value): 534 ↛ 545line 534 didn't jump to line 545 because the condition on line 534 was always true

535 translation_service.change_language(str(value)) 

536 self._sync_theme_selector_with_settings() 

537 self.languageChanged.emit() 

538 except Exception as e: 

539 warn_tech( 

540 code="widgets.settings_panel.change_language_failed", 

541 message="Could not change language", 

542 error=e, 

543 ) 

544 

545 self.settingChanged.emit(key, value) 

546 finally: 

547 self._processing_setting_change = False 

548 

549 # /////////////////////////////////////////////////////////////// 

550 

551 def get_setting_value(self, key: str) -> Any: 

552 """Get the value of a setting.""" 

553 if key in self._settings: 

554 return self._settings[key].get_value() 

555 return None 

556 

557 def set_setting_value(self, key: str, value: Any) -> None: 

558 """Set the value of a setting.""" 

559 if key in self._settings: 

560 self._settings[key].set_value(value) 

561 

562 def get_all_settings(self) -> dict[str, Any]: 

563 """Get all settings and their values.""" 

564 return {key: widget.get_value() for key, widget in self._settings.items()} 

565 

566 def save_all_settings_to_yaml(self) -> None: 

567 """Stage all current setting values.""" 

568 from ...services.application.app_service import AppService 

569 

570 for key, widget in self._settings.items(): 

571 try: 

572 AppService.stage_config_value( 

573 [*self._settings_storage_prefix(), key, "default"], 

574 widget.get_value(), 

575 ) 

576 except Exception as e: 

577 warn_tech( 

578 code="widgets.settings_panel.save_all_settings_failed", 

579 message=f"Could not save setting '{key}' to YAML", 

580 error=e, 

581 ) 

582 

583 def retranslate_ui(self) -> None: 

584 """Apply current translations to all owned text labels.""" 

585 if hasattr(self, "_theme_selector") and hasattr( 

586 self._theme_selector, "retranslate_ui" 

587 ): 

588 self._theme_selector.retranslate_ui() 

589 for widget in self._settings.values(): 

590 if hasattr(widget, "retranslate_ui"): 

591 widget.retranslate_ui() 

592 

593 def changeEvent(self, event: QEvent) -> None: 

594 """Handle Qt change events.""" 

595 if event.type() == QEvent.Type.LanguageChange: 

596 self.retranslate_ui() 

597 super().changeEvent(event) 

598 

599 # /////////////////////////////////////////////////////////////// 

600 

601 def get_width(self) -> int: 

602 """Get panel width.""" 

603 return self._width 

604 

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

606 """Set panel width.""" 

607 self._width = width 

608 

609 def get_theme_selector(self): 

610 """Get the theme selector if available.""" 

611 if hasattr(self, "_theme_selector"): 611 ↛ 613line 611 didn't jump to line 613 because the condition on line 611 was always true

612 return self._theme_selector 

613 return None 

614 

615 def update_all_theme_icons(self) -> None: 

616 """Update theme icons for all widgets that support it.""" 

617 current_theme = get_settings_service().gui.THEME 

618 for widget in self._widgets: 

619 # New pattern: widgets exposing setTheme(theme) directly 

620 setter = getattr(widget, "setTheme", None) 

621 if callable(setter): 

622 setter(current_theme) 

623 continue 

624 # Legacy pattern: widgets exposing update_theme_icon() 

625 updater = getattr(widget, "update_theme_icon", None) 

626 if callable(updater): 

627 updater() 

628 

629 self.style().unpolish(self) 

630 self.style().polish(self) 

631 

632 for child in self.findChildren(QWidget): 

633 child.style().unpolish(child) 

634 child.style().polish(child) 

635 

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

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

638 

639 ``display_value`` is the human-readable label emitted by the select 

640 widget (e.g. ``"Blue Gray - Dark"``). It is converted to the internal 

641 ``"preset:variant"`` format before being persisted and broadcast. 

642 """ 

643 try: 

644 internal_value = self._theme_options_map.get(display_value, display_value) 

645 from ...services.application.app_service import AppService 

646 from ...services.settings import get_settings_service 

647 

648 get_settings_service().set_theme(internal_value) 

649 AppService.stage_config_value( 

650 [*self._settings_storage_prefix(), "theme", "default"], 

651 internal_value, 

652 ) 

653 self.settingChanged.emit("theme", internal_value) 

654 except Exception as e: 

655 warn_tech( 

656 code="widgets.settings_panel.theme_selector_change_failed", 

657 message="Could not handle theme selector change", 

658 error=e, 

659 ) 

660 

661 def add_setting_widget(self, widget: QWidget) -> None: 

662 """Add a new setting widget to the settings panel.""" 

663 setting_container = QFrame(self._content_widget) 

664 setting_container.setObjectName(f"setting_container_{widget.objectName()}") 

665 setting_container.setFrameShape(QFrame.Shape.NoFrame) 

666 setting_container.setFrameShadow(QFrame.Shadow.Raised) 

667 

668 container_layout = QVBoxLayout(setting_container) 

669 container_layout.setSpacing(8) 

670 container_layout.setObjectName( 

671 f"setting_container_layout_{widget.objectName()}" 

672 ) 

673 container_layout.setContentsMargins(10, 10, 10, 10) 

674 

675 container_layout.addWidget(widget) 

676 self._content_layout.addWidget(setting_container) 

677 self._widgets.append(widget) 

678 

679 def add_setting_section(self, title: str = "") -> QFrame: 

680 """Add a new settings section with optional title.""" 

681 section = QFrame(self._content_widget) 

682 section.setObjectName(f"settings_section_{title.replace(' ', '_').lower()}") 

683 section.setFrameShape(QFrame.Shape.NoFrame) 

684 section.setFrameShadow(QFrame.Shadow.Raised) 

685 

686 section_layout = QVBoxLayout(section) 

687 section_layout.setSpacing(8) 

688 section_layout.setObjectName( 

689 f"settings_section_layout_{title.replace(' ', '_').lower()}" 

690 ) 

691 section_layout.setContentsMargins(10, 10, 10, 10) 

692 

693 if title: 

694 title_label = QLabel(title, section) 

695 title_label.setObjectName( 

696 f"settings_section_title_{title.replace(' ', '_').lower()}" 

697 ) 

698 if Fonts.SEGOE_UI_10_REG is not None: 

699 title_label.setFont(Fonts.SEGOE_UI_10_REG) 

700 title_label.setAlignment( 

701 Qt.AlignmentFlag.AlignLeading 

702 | Qt.AlignmentFlag.AlignLeft 

703 | Qt.AlignmentFlag.AlignVCenter 

704 ) 

705 section_layout.addWidget(title_label) 

706 

707 self._content_layout.addWidget(section) 

708 return section 

709 

710 def scroll_to_top(self) -> None: 

711 """Scroll to top of settings panel.""" 

712 if hasattr(self, "_scroll_area"): 

713 self._scroll_area.verticalScrollBar().setValue(0) 

714 

715 def scroll_to_bottom(self) -> None: 

716 """Scroll to bottom of settings panel.""" 

717 if hasattr(self, "_scroll_area"): 

718 scrollbar = self._scroll_area.verticalScrollBar() 

719 scrollbar.setValue(scrollbar.maximum()) 

720 

721 def scroll_to_widget(self, widget: QWidget) -> None: 

722 """Scroll to a specific widget in the settings panel.""" 

723 if hasattr(self, "_scroll_area") and widget: 

724 widget_pos = widget.mapTo(self._content_widget, widget.rect().topLeft()) 

725 self._scroll_area.verticalScrollBar().setValue(widget_pos.y()) 

726 

727 

728# /////////////////////////////////////////////////////////////// 

729# PUBLIC API 

730# /////////////////////////////////////////////////////////////// 

731__all__ = ["SettingsPanel"]