Added settings dialog with loglevel
This commit is contained in:
@@ -6,10 +6,12 @@ from PySide6.QtWidgets import QApplication
|
|||||||
|
|
||||||
from bedit_gui.controllers.document_controller import DocumentController
|
from bedit_gui.controllers.document_controller import DocumentController
|
||||||
from bedit_gui.controllers.log_controller import LogController
|
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.undo_controller import UndoController
|
||||||
from bedit_gui.controllers.view_menu_controller import ViewMenuController
|
from bedit_gui.controllers.view_menu_controller import ViewMenuController
|
||||||
from bedit_gui.controllers.window_state_controller import WindowStateController
|
from bedit_gui.controllers.window_state_controller import WindowStateController
|
||||||
from bedit_gui.documents import Document
|
from bedit_gui.documents import Document
|
||||||
|
from bedit_gui.services.application_settings import ApplicationSettings
|
||||||
from bedit_gui.views.main_window import MainWindow
|
from bedit_gui.views.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
@@ -19,10 +21,12 @@ def main() -> int:
|
|||||||
app.setOrganizationName("BEdit")
|
app.setOrganizationName("BEdit")
|
||||||
app.setApplicationName("BEdit")
|
app.setApplicationName("BEdit")
|
||||||
|
|
||||||
|
settings = ApplicationSettings()
|
||||||
document = Document(app)
|
document = Document(app)
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
LogController(window)
|
LogController(window, settings.log_level)
|
||||||
DocumentController(document, window)
|
DocumentController(document, window)
|
||||||
|
SettingsController(window, settings)
|
||||||
UndoController(document, window)
|
UndoController(document, window)
|
||||||
ViewMenuController(window)
|
ViewMenuController(window)
|
||||||
window_state_controller = WindowStateController(app, window)
|
window_state_controller = WindowStateController(app, window)
|
||||||
|
|||||||
50
src/bedit_gui/controllers/settings_controller.py
Normal file
50
src/bedit_gui/controllers/settings_controller.py
Normal file
@@ -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")
|
||||||
27
src/bedit_gui/services/application_settings.py
Normal file
27
src/bedit_gui/services/application_settings.py
Normal file
@@ -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)
|
||||||
@@ -51,6 +51,8 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="actionUndo"/>
|
<addaction name="actionUndo"/>
|
||||||
<addaction name="actionRedo"/>
|
<addaction name="actionRedo"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionSettings"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuView">
|
<widget class="QMenu" name="menuView">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@@ -287,6 +289,11 @@
|
|||||||
<string>Toolbars</string>
|
<string>Toolbars</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionSettings">
|
||||||
|
<property name="text">
|
||||||
|
<string>Settings</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../resources/resources.qrc"/>
|
<include location="../../resources/resources.qrc"/>
|
||||||
|
|||||||
122
src/bedit_gui/ui/forms/settings_dialog.ui
Normal file
122
src/bedit_gui/ui/forms/settings_dialog.ui
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Settings</class>
|
||||||
|
<widget class="QDialog" name="Settings">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>230</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Settings</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QTabWidget" name="tabWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="General">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>General</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QFormLayout" name="formLayout">
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="logLevel">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Debug</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Info</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Warning</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Error</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="lableLogLevel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Log level:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Settings</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Settings</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
41
src/bedit_gui/views/dialogs/settings_dialog.py
Normal file
41
src/bedit_gui/views/dialogs/settings_dialog.py
Normal file
@@ -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())
|
||||||
72
tests/unit/test_settings_controller.py
Normal file
72
tests/unit/test_settings_controller.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user