diff --git a/src/bedit_gui/application.py b/src/bedit_gui/application.py
index 9158891..2866083 100644
--- a/src/bedit_gui/application.py
+++ b/src/bedit_gui/application.py
@@ -6,10 +6,12 @@ from PySide6.QtWidgets import QApplication
from bedit_gui.controllers.document_controller import DocumentController
from bedit_gui.controllers.log_controller import LogController
+from bedit_gui.controllers.settings_controller import SettingsController
from bedit_gui.controllers.undo_controller import UndoController
from bedit_gui.controllers.view_menu_controller import ViewMenuController
from bedit_gui.controllers.window_state_controller import WindowStateController
from bedit_gui.documents import Document
+from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.views.main_window import MainWindow
@@ -19,10 +21,12 @@ def main() -> int:
app.setOrganizationName("BEdit")
app.setApplicationName("BEdit")
+ settings = ApplicationSettings()
document = Document(app)
window = MainWindow()
- LogController(window)
+ LogController(window, settings.log_level)
DocumentController(document, window)
+ SettingsController(window, settings)
UndoController(document, window)
ViewMenuController(window)
window_state_controller = WindowStateController(app, window)
diff --git a/src/bedit_gui/controllers/settings_controller.py b/src/bedit_gui/controllers/settings_controller.py
new file mode 100644
index 0000000..637cea6
--- /dev/null
+++ b/src/bedit_gui/controllers/settings_controller.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from collections.abc import Callable
+from typing import Protocol
+
+from PySide6.QtCore import QObject
+from PySide6.QtWidgets import QDialog
+
+from bedit_gui.services.application_logging import get_logger, set_log_level
+from bedit_gui.services.application_settings import ApplicationSettings
+from bedit_gui.views.dialogs.settings_dialog import SettingsDialog
+from bedit_gui.views.main_window import MainWindow
+
+logger = get_logger(__name__)
+
+
+class SettingsDialogLike(Protocol):
+ @property
+ def log_level(self) -> int: ...
+
+ def exec(self) -> int: ...
+
+
+SettingsDialogFactory = Callable[[int, MainWindow], SettingsDialogLike]
+
+
+class SettingsController(QObject):
+ """Opens the settings dialog and applies accepted preferences."""
+
+ def __init__(
+ self,
+ window: MainWindow,
+ settings: ApplicationSettings,
+ dialog_factory: SettingsDialogFactory = SettingsDialog,
+ ) -> None:
+ super().__init__(window)
+ self.window = window
+ self.settings = settings
+ self.dialog_factory = dialog_factory
+
+ window.ui.actionSettings.triggered.connect(self.open_settings)
+
+ def open_settings(self) -> None:
+ dialog = self.dialog_factory(self.settings.log_level, self.window)
+ if dialog.exec() != QDialog.DialogCode.Accepted:
+ return
+
+ self.settings.log_level = dialog.log_level
+ set_log_level(dialog.log_level)
+ logger.info("Application settings updated")
diff --git a/src/bedit_gui/services/application_settings.py b/src/bedit_gui/services/application_settings.py
new file mode 100644
index 0000000..fe823f3
--- /dev/null
+++ b/src/bedit_gui/services/application_settings.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+import logging
+
+from PySide6.QtCore import QSettings
+
+
+class ApplicationSettings:
+ """Typed access to persistent BEdit application settings."""
+
+ LOG_LEVEL_KEY = "logging/level"
+ DEFAULT_LOG_LEVEL = logging.INFO
+
+ def __init__(self, settings: QSettings | None = None) -> None:
+ self._settings = settings if settings is not None else QSettings()
+
+ @property
+ def log_level(self) -> int:
+ return self._settings.value(
+ self.LOG_LEVEL_KEY,
+ self.DEFAULT_LOG_LEVEL,
+ type=int,
+ )
+
+ @log_level.setter
+ def log_level(self, level: int) -> None:
+ self._settings.setValue(self.LOG_LEVEL_KEY, level)
diff --git a/src/bedit_gui/ui/forms/main_window.ui b/src/bedit_gui/ui/forms/main_window.ui
index 6f08e37..9b7f3e7 100644
--- a/src/bedit_gui/ui/forms/main_window.ui
+++ b/src/bedit_gui/ui/forms/main_window.ui
@@ -51,6 +51,8 @@
+
+
diff --git a/src/bedit_gui/ui/forms/settings_dialog.ui b/src/bedit_gui/ui/forms/settings_dialog.ui
new file mode 100644
index 0000000..8b54520
--- /dev/null
+++ b/src/bedit_gui/ui/forms/settings_dialog.ui
@@ -0,0 +1,122 @@
+
+
+ Settings
+
+
+
+ 0
+ 0
+ 400
+ 230
+
+
+
+ Settings
+
+
+ -
+
+
+ 0
+
+
+
+ General
+
+
+
-
+
+
-
+
+ Debug
+
+
+ -
+
+ Info
+
+
+ -
+
+ Warning
+
+
+ -
+
+ Error
+
+
+
+
+ -
+
+
+ Log level:
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Settings
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Settings
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/bedit_gui/views/dialogs/settings_dialog.py b/src/bedit_gui/views/dialogs/settings_dialog.py
new file mode 100644
index 0000000..33fd9df
--- /dev/null
+++ b/src/bedit_gui/views/dialogs/settings_dialog.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+import logging
+
+from PySide6.QtWidgets import QDialog, QWidget
+
+from bedit_gui.ui.generated.ui_settings_dialog import Ui_Settings
+
+
+class SettingsDialog(QDialog):
+ """Handwritten behavior for the generated settings form."""
+
+ LOG_LEVELS = (
+ logging.DEBUG,
+ logging.INFO,
+ logging.WARNING,
+ logging.ERROR,
+ )
+
+ def __init__(
+ self,
+ log_level: int,
+ parent: QWidget | None = None,
+ ) -> None:
+ super().__init__(parent)
+
+ self.ui = Ui_Settings()
+ self.ui.setupUi(self)
+ self.setWindowTitle("Settings")
+
+ for index, level in enumerate(self.LOG_LEVELS):
+ self.ui.logLevel.setItemData(index, level)
+
+ selected = self.ui.logLevel.findData(log_level)
+ self.ui.logLevel.setCurrentIndex(
+ selected if selected >= 0 else self.LOG_LEVELS.index(logging.INFO)
+ )
+
+ @property
+ def log_level(self) -> int:
+ return int(self.ui.logLevel.currentData())
diff --git a/tests/unit/test_settings_controller.py b/tests/unit/test_settings_controller.py
new file mode 100644
index 0000000..bf5a479
--- /dev/null
+++ b/tests/unit/test_settings_controller.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+import pytest
+from PySide6.QtCore import QSettings
+from PySide6.QtWidgets import QApplication, QDialog
+
+from bedit_gui.controllers.settings_controller import SettingsController
+from bedit_gui.services.application_logging import get_logger
+from bedit_gui.services.application_settings import ApplicationSettings
+from bedit_gui.views.main_window import MainWindow
+
+
+class FakeSettingsDialog:
+ def __init__(self, log_level: int) -> None:
+ self.initial_log_level = log_level
+ self.log_level = logging.ERROR
+
+ def exec(self) -> QDialog.DialogCode:
+ return QDialog.DialogCode.Accepted
+
+
+@pytest.fixture(scope="module")
+def qt_app() -> QApplication:
+ return QApplication.instance() or QApplication([])
+
+
+@pytest.mark.unit
+def test_log_level_is_persisted(tmp_path: Path) -> None:
+ path = tmp_path / "settings.ini"
+ backend = QSettings(str(path), QSettings.Format.IniFormat)
+ settings = ApplicationSettings(backend)
+
+ settings.log_level = logging.DEBUG
+ backend.sync()
+
+ reloaded = ApplicationSettings(
+ QSettings(str(path), QSettings.Format.IniFormat)
+ )
+ assert reloaded.log_level == logging.DEBUG
+
+
+@pytest.mark.unit
+def test_settings_action_applies_log_level(
+ qt_app: QApplication,
+ tmp_path: Path,
+) -> None:
+ backend = QSettings(
+ str(tmp_path / "settings.ini"),
+ QSettings.Format.IniFormat,
+ )
+ settings = ApplicationSettings(backend)
+ window = MainWindow()
+ dialogs: list[FakeSettingsDialog] = []
+
+ def make_dialog(
+ log_level: int,
+ _window: MainWindow,
+ ) -> FakeSettingsDialog:
+ dialog = FakeSettingsDialog(log_level)
+ dialogs.append(dialog)
+ return dialog
+
+ SettingsController(window, settings, make_dialog)
+ window.ui.actionSettings.trigger()
+
+ assert dialogs[0].initial_log_level == logging.INFO
+ assert settings.log_level == logging.ERROR
+ assert get_logger("tests").isEnabledFor(logging.ERROR)
+ assert not get_logger("tests").isEnabledFor(logging.WARNING)