Repository: RimSort/RimSort
Pull request #859: Add support for custom font settings
Author: login: oceanfrost
Labels: name: feature/improvement 🆕
description: Use this label to mark new feature or improvement requests
name: accessibility 👀 ℹ️🤓
description: user-friendly optimization, like notifications, warnings and action alerts
Created: 2025-05-13T14:30:16Z | Merged: 2025-05-13T17:58:40Z

This pull request introduces font customization capabilities to the application, allowing users to select a font family and size for the UI. The changes span multiple files and include updates to the settings model, theme controller, and settings dialog UI.

### Font Customization Feature:

* **Font Settings in Theme Controller**:
  - Added `font_family` and `font_size` attributes to `ThemeController` with default values. (`app/controllers/theme_controller.py`, [app/controllers/theme_controller.pyR30-R31](diffhunk://#diff-276c2c8ced8d6939f353484c4893972ea88d916114fec77c95579b054d797dbfR30-R31))
  - Introduced a `set_font` method to update font settings and modified `load_theme` to include font styles in the theme stylesheet. (`app/controllers/theme_controller.py`, [[1]](diffhunk://#diff-276c2c8ced8d6939f353484c4893972ea88d916114fec77c95579b054d797dbfR108-R112) [[2]](diffhunk://#diff-276c2c8ced8d6939f353484c4893972ea88d916114fec77c95579b054d797dbfL86-R96)

* **Settings Model Updates**:
  - Added `font_family` and `font_size` attributes to `Settings`, initialized with the application’s default font family and size. (`app/models/settings.py`, [app/models/settings.pyR91-R93](diffhunk://#diff-39a6811c59432dd1c6eeb1ecdb259948d255a8bb50f857b2286c44407c94dd09R91-R93))

* **Settings Dialog Enhancements**:
  - Updated the "Themes" tab in the settings dialog to include a new "Font Settings" group with a `QFontComboBox` for font family selection and a `QSpinBox` for font size. (`app/views/settings_dialog.py`, [app/views/settings_dialog.pyR777-R808](diffhunk://#diff-1c7472bd5026385f4d7b9a42956040a78e415814806dceaff0f69981bf3b33d1R777-R808))

* **Integration with Controllers**:
  - Updated `AppController` to apply font settings during initialization and when themes are applied. (`app/controllers/app_controller.py`, [app/controllers/app_controller.pyR36-R39](diffhunk://#diff-0c196b830c4e56ac10dfe132be0fa5e4080f11f336ef436f5f4b0dc63c9ece63R36-R39))
  - Updated `SettingsController` to sync font settings between the dialog and the model. (`app/controllers/settings_controller.py`, [[1]](diffhunk://#diff-0baa160fb2dacc80207043c85168d54627e517cbf0959bc7a65035c2c1be47beR857-R860) [[2]](diffhunk://#diff-0baa160fb2dacc80207043c85168d54627e517cbf0959bc7a65035c2c1be47beR930-R933)

These changes collectively enable users to customize the font appearance of the application, improving accessibility and personalization options.

![image](https://github.com/user-attachments/assets/628eda16-2322-4cd6-bb17-f8789fdd54bb)



Diff:
diff --git a/app/controllers/app_controller.py b/app/controllers/app_controller.py
index a4c9e76af..f67a47c31 100644
--- a/app/controllers/app_controller.py
+++ b/app/controllers/app_controller.py
@@ -32,6 +32,10 @@ def __init__(self) -> None:
         self.initialize_main_window()
 
         self.app.setStyle("Fusion")
+        self.theme_controller.set_font(
+            self.settings.font_family,
+            self.settings.font_size,
+        )
         self.theme_controller.apply_selected_theme(
             self.settings.enable_themes,
             self.settings.theme_name,
diff --git a/app/controllers/settings_controller.py b/app/controllers/settings_controller.py
index abd55084b..af5cff223 100644
--- a/app/controllers/settings_controller.py
+++ b/app/controllers/settings_controller.py
@@ -854,6 +854,10 @@ def _update_model_from_view(self) -> None:
         )
         self.settings.theme_name = self.settings_dialog.themes_combobox.currentText()
 
+        self.settings.font_family = self.settings_dialog.font_family_combobox.currentText()
+
+        self.settings.font_size = self.settings_dialog.font_size_spinbox.value()
+
         # Advanced tab
         self.settings.debug_logging_enabled = (
             self.settings_dialog.debug_logging_checkbox.isChecked()
@@ -923,6 +927,10 @@ def _on_global_ok_button_clicked(self) -> None:
         self.settings_dialog.close()
         self._update_model_from_view()
         self.settings.save()
+        self.theme_controller.set_font(
+            self.settings.font_family,
+            self.settings.font_size,
+        )
         self.theme_controller.apply_selected_theme(
             self.settings.enable_themes,
             self.settings.theme_name,
diff --git a/app/controllers/theme_controller.py b/app/controllers/theme_controller.py
index eb309f026..17a672aca 100644
--- a/app/controllers/theme_controller.py
+++ b/app/controllers/theme_controller.py
@@ -27,6 +27,8 @@ def __init__(self, default_theme: str = "RimPy") -> None:
             self.app_info.theme_storage_folder,
             self.app_info.theme_data_folder,
         ]
+        self.font_family = "Tahoma"
+        self.font_size = 12
 
     def _get_supported_themes(self) -> set[str]:
         """Retrieves a set of supported themes from the theme data and storage folders."""
@@ -83,7 +85,15 @@ def load_theme(self, theme_name: str) -> Optional[str]:
             if stylesheet_path:
                 try:
                     with open(stylesheet_path, "r") as f:
-                        return f.read()
+                        raw_stylesheet = f.read()
+                        font_style = f"""
+                        * {{
+                            font-family: "{self.font_family}";
+                            font-size: {self.font_size}px;
+                        }}
+                        """
+                        # return f.read()
+                        return font_style + raw_stylesheet
                 except Exception as e:
                     logger.error(f"Error loading theme: {e}")
         else:
@@ -95,6 +105,11 @@ def load_theme(self, theme_name: str) -> Optional[str]:
             return self.load_theme(self.default_theme)
         return None
 
+    def set_font(self, family: str, size: int) -> None:
+        self.font_family = family
+        self.font_size = size
+
+
     def get_theme_stylesheet_path(self, theme_name: str) -> Optional[Path]:
         """Returns the path to the stylesheet for the specified theme.
 
@@ -188,6 +203,14 @@ def setup_theme_dialog(
             # Set the current theme in the combobox
             current_theme_name = settings.theme_name
             current_index = settings_dialog.themes_combobox.findText(current_theme_name)
+            current_font_family = settings.font_family
+            current_font_size = settings.font_size
+            current_font_family_index = settings_dialog.font_family_combobox.findText(current_font_family)
+            settings_dialog.font_size_spinbox.setValue(current_font_size)
+            if current_font_family_index != -1:
+                settings_dialog.font_family_combobox.setCurrentIndex(current_font_family_index)
+            else:
+                settings_dialog.font_family_combobox.setCurrentIndex(-1)
             if current_index != -1:
                 settings_dialog.themes_combobox.setCurrentIndex(current_index)
             else:
diff --git a/app/models/settings.py b/app/models/settings.py
index ddb47a802..b4cec57de 100644
--- a/app/models/settings.py
+++ b/app/models/settings.py
@@ -9,6 +9,7 @@
 import msgspec
 from loguru import logger
 from PySide6.QtCore import QObject
+from PySide6.QtWidgets import QApplication
 
 from app.models.instance import Instance
 from app.utils.app_info import AppInfo
@@ -87,6 +88,9 @@ def __init__(self) -> None:
         self.enable_themes: bool = True
         self.theme_name: str = "RimPy"
 
+        self.font_family: str = QApplication.font().family()
+        self.font_size: int = 12
+
         # Advanced
         self.debug_logging_enabled: bool = False
         self.watchdog_toggle: bool = True
diff --git a/app/views/settings_dialog.py b/app/views/settings_dialog.py
index f545700be..8f0cf6553 100644
--- a/app/views/settings_dialog.py
+++ b/app/views/settings_dialog.py
@@ -1,10 +1,12 @@
 from PySide6.QtCore import Qt
 from PySide6.QtGui import QShowEvent
 from PySide6.QtWidgets import (
+    QApplication,
     QBoxLayout,
     QCheckBox,
     QComboBox,
     QDialog,
+    QFontComboBox,
     QGridLayout,
     QGroupBox,
     QHBoxLayout,
@@ -13,6 +15,7 @@
     QPushButton,
     QRadioButton,
     QSizePolicy,
+    QSpinBox,
     QTabWidget,
     QToolButton,
     QVBoxLayout,
@@ -774,6 +777,49 @@ def _do_themes_tab(self) -> None:
         self.theme_location_open_button.setText("Open Theme Location")
         group_layout.addWidget(self.theme_location_open_button)
 
+        font_group = QGroupBox("Font Settings")
+        font_group.setFont(GUIInfo().emphasis_font)
+        tab_layout.addWidget(font_group)
+
+        font_layout = QVBoxLayout(font_group)
+
+        font_family_layout = QHBoxLayout()
+        font_layout.addLayout(font_family_layout)
+
+        font_family_label = QLabel(self.tr("Font Family"))
+        font_family_layout.addWidget(font_family_label)
+
+        self.font_family_combobox = QFontComboBox()
+        self.font_family_combobox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_family_layout.addWidget(self.font_family_combobox)
+
+        font_size_layout = QHBoxLayout()
+        font_layout.addLayout(font_size_layout)
+
+        font_size_label = QLabel(self.tr("Font Size"))
+        font_size_layout.addWidget(font_size_label)
+
+        self.font_size_spinbox = QSpinBox()
+        self.font_size_spinbox.setRange(8,20)
+        self.font_size_spinbox.setValue(12)
+        self.font_size_spinbox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_size_layout.addWidget(self.font_size_spinbox)
+
+        reset_button = QPushButton(self.tr("Reset"))
+        reset_button.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+
+        reset_button_layout = QHBoxLayout()
+        reset_button_layout.addStretch(1)
+        reset_button_layout.addWidget(reset_button)
+        font_layout.addLayout(reset_button_layout)
+        reset_button.clicked.connect(self.reset_font_settings)
+
         if self.enable_themes_checkbox.isChecked():
             self.enable_themes_checkbox.stateChanged.connect(
                 self.connect_populate_themes_combobox
@@ -781,6 +827,11 @@ def _do_themes_tab(self) -> None:
         else:
             self.themes_combobox.clear()
 
+    def reset_font_settings(self) -> None:
+        default_font = QApplication.font()
+        self.font_family_combobox.setCurrentFont(default_font)
+        self.font_size_spinbox.setValue(12)
+
     def connect_populate_themes_combobox(self) -> None:
         """Populate the themes combobox with available themes."""
         from app.controllers.theme_controller import ThemeController


Reviews:
user: login: LionelColaso
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:17:50Z
user: login: LionelColaso
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:23:12Z
user: login: oceanfrost
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:25:22Z
user: login: LionelColaso
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:31:24Z
user: login: LionelColaso
body: Looks Good To Merge
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T17:37:14Z

Review comments:
diff_hunk: @@ -87,6 +88,9 @@ def __init__(self) -> None:
         self.enable_themes: bool = True
         self.theme_name: str = "RimPy"
 
+        self.font_family: str = QApplication.font().family()
path: app/models/settings.py
user: login: LionelColaso
body: use 
self.font_family: str = "Segoe UI"
Or font name you want to use as default
This way you won't need to import QApplication
created_at: 2025-05-13T17:17:50Z
author_association: MEMBER
diff_hunk: @@ -774,13 +777,61 @@ def _do_themes_tab(self) -> None:
         self.theme_location_open_button.setText("Open Theme Location")
         group_layout.addWidget(self.theme_location_open_button)
 
+        font_group = QGroupBox("Font Settings")
+        font_group.setFont(GUIInfo().emphasis_font)
+        tab_layout.addWidget(font_group)
+
+        font_layout = QVBoxLayout(font_group)
+
+        font_family_layout = QHBoxLayout()
+        font_layout.addLayout(font_family_layout)
+
+        font_family_label = QLabel(self.tr("Font Family"))
+        font_family_layout.addWidget(font_family_label)
+
+        self.font_family_combobox = QFontComboBox()
+        self.font_family_combobox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_family_layout.addWidget(self.font_family_combobox)
+
+        font_size_layout = QHBoxLayout()
+        font_layout.addLayout(font_size_layout)
+
+        font_size_label = QLabel(self.tr("Font Size"))
+        font_size_layout.addWidget(font_size_label)
+
+        self.font_size_spinbox = QSpinBox()
+        self.font_size_spinbox.setRange(8,20)
+        self.font_size_spinbox.setValue(12)
+        self.font_size_spinbox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_size_layout.addWidget(self.font_size_spinbox)
+
+        reset_button = QPushButton(self.tr("Reset"))
+        reset_button.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+
+        reset_button_layout = QHBoxLayout()
+        reset_button_layout.addStretch(1)
+        reset_button_layout.addWidget(reset_button)
+        font_layout.addLayout(reset_button_layout)
+        reset_button.clicked.connect(self.reset_font_settings)
+
         if self.enable_themes_checkbox.isChecked():
             self.enable_themes_checkbox.stateChanged.connect(
                 self.connect_populate_themes_combobox
             )
         else:
             self.themes_combobox.clear()
 
+    def reset_font_settings(self) -> None:
+        default_font = QApplication.font()
path: app/views/settings_dialog.py
user: login: LionelColaso
body: use
default_font = "Segoe UI"
Or font name you want to use as default
This way you won't need to import QApplication
created_at: 2025-05-13T17:23:12Z
author_association: MEMBER
diff_hunk: @@ -774,13 +777,61 @@ def _do_themes_tab(self) -> None:
         self.theme_location_open_button.setText("Open Theme Location")
         group_layout.addWidget(self.theme_location_open_button)
 
+        font_group = QGroupBox("Font Settings")
+        font_group.setFont(GUIInfo().emphasis_font)
+        tab_layout.addWidget(font_group)
+
+        font_layout = QVBoxLayout(font_group)
+
+        font_family_layout = QHBoxLayout()
+        font_layout.addLayout(font_family_layout)
+
+        font_family_label = QLabel(self.tr("Font Family"))
+        font_family_layout.addWidget(font_family_label)
+
+        self.font_family_combobox = QFontComboBox()
+        self.font_family_combobox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_family_layout.addWidget(self.font_family_combobox)
+
+        font_size_layout = QHBoxLayout()
+        font_layout.addLayout(font_size_layout)
+
+        font_size_label = QLabel(self.tr("Font Size"))
+        font_size_layout.addWidget(font_size_label)
+
+        self.font_size_spinbox = QSpinBox()
+        self.font_size_spinbox.setRange(8,20)
+        self.font_size_spinbox.setValue(12)
+        self.font_size_spinbox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_size_layout.addWidget(self.font_size_spinbox)
+
+        reset_button = QPushButton(self.tr("Reset"))
+        reset_button.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+
+        reset_button_layout = QHBoxLayout()
+        reset_button_layout.addStretch(1)
+        reset_button_layout.addWidget(reset_button)
+        font_layout.addLayout(reset_button_layout)
+        reset_button.clicked.connect(self.reset_font_settings)
+
         if self.enable_themes_checkbox.isChecked():
             self.enable_themes_checkbox.stateChanged.connect(
                 self.connect_populate_themes_combobox
             )
         else:
             self.themes_combobox.clear()
 
+    def reset_font_settings(self) -> None:
+        default_font = QApplication.font()
path: app/views/settings_dialog.py
user: login: oceanfrost
body: I've noticed that the font display may differ across platforms, do we need to standardize it?
created_at: 2025-05-13T17:25:21Z
author_association: COLLABORATOR
diff_hunk: @@ -774,13 +777,61 @@ def _do_themes_tab(self) -> None:
         self.theme_location_open_button.setText("Open Theme Location")
         group_layout.addWidget(self.theme_location_open_button)
 
+        font_group = QGroupBox("Font Settings")
+        font_group.setFont(GUIInfo().emphasis_font)
+        tab_layout.addWidget(font_group)
+
+        font_layout = QVBoxLayout(font_group)
+
+        font_family_layout = QHBoxLayout()
+        font_layout.addLayout(font_family_layout)
+
+        font_family_label = QLabel(self.tr("Font Family"))
+        font_family_layout.addWidget(font_family_label)
+
+        self.font_family_combobox = QFontComboBox()
+        self.font_family_combobox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_family_layout.addWidget(self.font_family_combobox)
+
+        font_size_layout = QHBoxLayout()
+        font_layout.addLayout(font_size_layout)
+
+        font_size_label = QLabel(self.tr("Font Size"))
+        font_size_layout.addWidget(font_size_label)
+
+        self.font_size_spinbox = QSpinBox()
+        self.font_size_spinbox.setRange(8,20)
+        self.font_size_spinbox.setValue(12)
+        self.font_size_spinbox.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+        font_size_layout.addWidget(self.font_size_spinbox)
+
+        reset_button = QPushButton(self.tr("Reset"))
+        reset_button.setSizePolicy(
+            QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
+        )
+
+        reset_button_layout = QHBoxLayout()
+        reset_button_layout.addStretch(1)
+        reset_button_layout.addWidget(reset_button)
+        font_layout.addLayout(reset_button_layout)
+        reset_button.clicked.connect(self.reset_font_settings)
+
         if self.enable_themes_checkbox.isChecked():
             self.enable_themes_checkbox.stateChanged.connect(
                 self.connect_populate_themes_combobox
             )
         else:
             self.themes_combobox.clear()
 
+    def reset_font_settings(self) -> None:
+        default_font = QApplication.font()
path: app/views/settings_dialog.py
user: login: LionelColaso
body: Yeah you are right we should leave it to the platform
created_at: 2025-05-13T17:31:24Z
author_association: MEMBER

PR comments:
user: login: oceanfrost
created_at: 2025-05-13T14:48:46Z
body: I saw that the other font setting was not continued, so I try this one.
author_association: COLLABORATOR
user: login: LionelColaso
created_at: 2025-05-13T17:47:19Z
body: if possible set limit to the minimum and maximum font size a user can set (setting unreasonable size will lead to unusable UI)
author_association: MEMBER
user: login: oceanfrost
created_at: 2025-05-13T17:51:18Z
body: > if possible set limit to the minimum and maximum font size a user can set (setting unreasonable size will lead to unusable UI)如果可能，请将 limit 设置为用户可以设置的最小和最大字体大小（设置不合理的大小将导致 UI 不可用）

`self.font_size_spinbox.setRange(8,20)`
It should be effective.

author_association: COLLABORATOR