Repository: spyder-ide/spyder

Issue:
Crash after setting empty high DPI scaling factor
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

Spyder crashed after I did the following:
1. Set the custom high DPI scaling to 2
1. Restarted Spyder 
1. After realizing that was too large, I deleted the custom DPI scaling value
1. I then selected "Enable auto high DPI scaling"
1. Click apply

Not sure if this is repeatable, this is the first time it's crashed.

### Traceback
```python-traceback
Traceback (most recent call last):
  File "C:\Users\jerem\anaconda3\envs\shadow\lib\site-packages\spyder\plugins\preferences\widgets\configdialog.py", line 142, in button_clicked
    configpage.apply_changes()
  File "C:\Users\jerem\anaconda3\envs\shadow\lib\site-packages\spyder\plugins\preferences\api.py", line 118, in apply_changes
    self.apply_callback()
  File "C:\Users\jerem\anaconda3\envs\shadow\lib\site-packages\spyder\plugins\preferences\api.py", line 150, in <lambda>
    self._apply_settings_tabs(self.changed_options))
  File "C:\Users\jerem\anaconda3\envs\shadow\lib\site-packages\spyder\plugins\preferences\api.py", line 178, in _apply_settings_tabs
    self.apply_settings(options)
  File "C:\Users\jerem\anaconda3\envs\shadow\lib\site-packages\spyder\plugins\application\confpage.py", line 249, in apply_settings
    scale_factor = float(scale_factor)
ValueError: could not convert string to float: ''
```

## Versions

* Spyder version: 5.4.3  (conda)
* Python version: 3.10.12 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Windows 10

### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0                             :  1.4.0 (OK)
chardet >=2.0.0                                  :  4.0.0 (OK)
cloudpickle >=0.5.0                              :  2.2.1 (OK)
cookiecutter >=1.6.0                             :  1.7.3 (OK)
diff_match_patch >=20181111                      :  20200713 (OK)
intervaltree >=3.0.2                             :  3.1.0 (OK)
IPython >=7.31.1,<9.0.0,!=8.8.0,!=8.9.0,!=8.10.0 :  8.12.2 (OK)
jedi >=0.17.2,<0.19.0                            :  0.18.1 (OK)
jellyfish >=0.7                                  :  0.9.0 (OK)
jsonschema >=3.2.0                               :  4.17.3 (OK)
keyring >=17.0.0                                 :  23.13.1 (OK)
nbconvert >=4.0                                  :  6.5.4 (OK)
numpydoc >=0.6.0                                 :  1.5.0 (OK)
paramiko >=2.4.0                                 :  2.8.1 (OK)
parso >=0.7.0,<0.9.0                             :  0.8.3 (OK)
pexpect >=4.4.0                                  :  4.8.0 (OK)
pickleshare >=0.4                                :  0.7.5 (OK)
psutil >=5.3                                     :  5.9.0 (OK)
pygments >=2.0                                   :  2.15.1 (OK)
pylint >=2.5.0,<3.0                              :  2.16.2 (OK)
pylint_venv >=2.1.1                              :  2.3.0 (OK)
pyls_spyder >=0.4.0                              :  0.4.0 (OK)
pylsp >=1.7.2,<1.8.0                             :  1.7.2 (OK)
pylsp_black >=1.2.0                              :  1.2.1 (OK)
qdarkstyle >=3.0.2,<3.2.0                        :  3.0.2 (OK)
qstylizer >=0.2.2                                :  0.2.2 (OK)
qtawesome >=1.2.1                                :  1.2.2 (OK)
qtconsole >=5.4.2,<5.5.0                         :  5.4.2 (OK)
qtpy >=2.1.0                                     :  2.2.0 (OK)
rtree >=0.9.7                                    :  1.0.1 (OK)
setuptools >=49.6.0                              :  68.0.0 (OK)
sphinx >=0.6.6                                   :  5.0.2 (OK)
spyder_kernels >=2.4.3,<2.5.0                    :  2.4.4 (OK)
textdistance >=4.2.0                             :  4.2.1 (OK)
three_merge >=0.1.1                              :  0.1.1 (OK)
watchdog >=0.10.3                                :  2.1.6 (OK)
zmq >=22.1.0                                     :  25.1.0 (OK)

# Optional:
cython >=0.21                                    :  None (NOK)
matplotlib >=3.0.0                               :  3.7.2 (OK)
numpy >=1.7                                      :  1.25.2 (OK)
pandas >=1.1.1                                   :  2.0.3 (OK)
scipy >=0.17.0                                   :  1.11.1 (OK)
sympy >=0.7.3                                    :  None (NOK)
```



Discussion:
After some testing, it does appear that the "Spyder encountered an internal error" warning does appear when switching from custom to auto scaling if the custom scaling field is empty.  I haven't checked to see if this is a known issue, so my apologies if it's a duplicate.
Could you tell me how much DPI, mine has 7200 dpi and it's flawless.

Thanks
The spyder version is 5.4.3.  I'm not sure of the DPI, but its 1920x1200 on a 17-inch screen.  I'm not sure if it's a matter of screen resolution though, as it only seems to happen when the "custom scaling" field is blank.
Hey @jeremymatt, thanks for reporting. I was able to reproduce this error and it'll be fixed in our next version (5.5.1), to be released at the end of the week.
@ccordoba12 Thanks!  Your work on Spyder is appreciated!

Resolving patch:
diff --git a/spyder/plugins/application/confpage.py b/spyder/plugins/application/confpage.py
--- a/spyder/plugins/application/confpage.py
+++ b/spyder/plugins/application/confpage.py
@@ -30,7 +30,10 @@
 
 
 class ApplicationConfigPage(PluginConfigPage):
-    APPLY_CONF_PAGE_SETTINGS = True
+
+    def __init__(self, plugin, parent):
+        super().__init__(plugin, parent)
+        self.pre_apply_callback = self.perform_checks
 
     def setup_page(self):
         newcb = self.create_checkbox
@@ -177,48 +180,53 @@ def set_open_file(state):
         screen_resolution_label.setWordWrap(True)
         screen_resolution_label.setOpenExternalLinks(True)
 
-        normal_radio = self.create_radiobutton(
-                                _("Normal"),
-                                'normal_screen_resolution',
-                                button_group=screen_resolution_bg)
+        self.normal_radio = self.create_radiobutton(
+            _("Normal"),
+            'normal_screen_resolution',
+            button_group=screen_resolution_bg
+        )
         auto_scale_radio = self.create_radiobutton(
-                                _("Enable auto high DPI scaling"),
-                                'high_dpi_scaling',
-                                button_group=screen_resolution_bg,
-                                tip=_("Set this for high DPI displays"),
-                                restart=True)
-
-        custom_scaling_radio = self.create_radiobutton(
-                                _("Set a custom high DPI scaling"),
-                                'high_dpi_custom_scale_factor',
-                                button_group=screen_resolution_bg,
-                                tip=_("Set this for high DPI displays when "
-                                      "auto scaling does not work"),
-                                restart=True)
+            _("Enable auto high DPI scaling"),
+            'high_dpi_scaling',
+            button_group=screen_resolution_bg,
+            tip=_("Set this for high DPI displays"),
+            restart=True
+        )
+
+        self.custom_scaling_radio = self.create_radiobutton(
+            _("Set a custom high DPI scaling"),
+            'high_dpi_custom_scale_factor',
+            button_group=screen_resolution_bg,
+            tip=_("Set this for high DPI displays when auto scaling does not "
+                  "work"),
+            restart=True
+        )
 
         self.custom_scaling_edit = self.create_lineedit(
             "",
             'high_dpi_custom_scale_factors',
-            tip=_("Enter values for different screens "
-                  "separated by semicolons ';'.\n"
-                  "Float values are supported"),
+            tip=_("Enter values for different screens separated by semicolons "
+                  "';'.\n Float values are supported"),
             alignment=Qt.Horizontal,
-            regex=r"[0-9]+(?:\.[0-9]*)(;[0-9]+(?:\.[0-9]*))*",
-            restart=True)
+            regex=r"[1-9]+(?:\.[0-9]*)(;[1-9]+(?:\.[0-9]*))*",
+            restart=True
+        )
 
-        normal_radio.toggled.connect(self.custom_scaling_edit.setDisabled)
+        self.normal_radio.toggled.connect(self.custom_scaling_edit.setDisabled)
         auto_scale_radio.toggled.connect(self.custom_scaling_edit.setDisabled)
-        custom_scaling_radio.toggled.connect(
-            self.custom_scaling_edit.setEnabled)
+        self.custom_scaling_radio.toggled.connect(
+            self.custom_scaling_edit.setEnabled
+        )
 
         # Layout Screen resolution
         screen_resolution_layout = QVBoxLayout()
         screen_resolution_layout.addWidget(screen_resolution_label)
 
         screen_resolution_inner_layout = QGridLayout()
-        screen_resolution_inner_layout.addWidget(normal_radio, 0, 0)
+        screen_resolution_inner_layout.addWidget(self.normal_radio, 0, 0)
         screen_resolution_inner_layout.addWidget(auto_scale_radio, 1, 0)
-        screen_resolution_inner_layout.addWidget(custom_scaling_radio, 2, 0)
+        screen_resolution_inner_layout.addWidget(
+            self.custom_scaling_radio, 2, 0)
         screen_resolution_inner_layout.addWidget(
             self.custom_scaling_edit, 2, 1)
 
@@ -240,29 +248,14 @@ def set_open_file(state):
         vlayout.addWidget(self.tabs)
         self.setLayout(vlayout)
 
-    def apply_settings(self, options):
-        if 'high_dpi_custom_scale_factors' in options:
-            scale_factors = self.get_option(
-                'high_dpi_custom_scale_factors').split(';')
-            change_min_scale_factor = False
-            for idx, scale_factor in enumerate(scale_factors[:]):
-                scale_factor = float(scale_factor)
-                if scale_factor < 1.0:
-                    change_min_scale_factor = True
-                    scale_factors[idx] = "1.0"
-            if change_min_scale_factor:
-                scale_factors_text = ";".join(scale_factors)
-                QMessageBox.critical(
-                    self, _("Error"),
-                    _("We're sorry but setting a scale factor bellow 1.0 "
-                      "isn't possible. Any scale factor bellow 1.0 will "
-                      "be set to 1.0"),
-                    QMessageBox.Ok)
-                self.custom_scaling_edit.textbox.setText(scale_factors_text)
-                self.set_option(
-                    'high_dpi_custom_scale_factors', scale_factors_text)
+    def perform_checks(self):
+        # Prevent setting an empty scale factor in case users try to do it.
+        # See spyder-ide/spyder#21733 for the details.
+        if self.custom_scaling_radio.isChecked():
+            scale_factor = self.custom_scaling_edit.textbox.text()
+            if scale_factor == "":
+                self.normal_radio.setChecked(True)
                 self.changed_options.add('high_dpi_custom_scale_factors')
-        self.plugin.apply_settings()
 
     def _save_lang(self):
         """
@@ -278,9 +271,11 @@ def _save_lang(self):
             self.set_option('interface_language', value)
         except Exception:
             QMessageBox.critical(
-                self, _("Error"),
+                self,
+                _("Error"),
                 _("We're sorry but the following error occurred while trying "
                   "to set your selected language:<br><br>"
                   "<tt>{}</tt>").format(traceback.format_exc()),
-                QMessageBox.Ok)
+                QMessageBox.Ok
+            )
             return


Test patch: