Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Fix changing color scheme and UI theme (Appearance) #16991

Merged
merged 6 commits into from
Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import shutil
import subprocess
import sys
import warnings


# ---- To activate/deactivate certain things for pytest's only
Expand Down
1 change: 0 additions & 1 deletion spyder/api/plugin_registration/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from spyder.config.manager import CONF
from spyder.api.config.mixins import SpyderConfigurationAccessor
from spyder.api.plugin_registration._confpage import PluginsConfigPage
from spyder.api.plugins.enum import Plugins
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import (
Plugins, SpyderPluginV2, SpyderDockablePlugin, SpyderPluginWidget,
Expand Down
2 changes: 1 addition & 1 deletion spyder/app/tests/test_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4332,7 +4332,7 @@ def foo(x):

# Change focus_to_editor option
main_window.editor.set_option('focus_to_editor', focus_to_editor)
main_window.editor.apply_plugin_settings({(None, 'focus_to_editor')})
main_window.editor.apply_plugin_settings({'focus_to_editor'})
code_editor = main_window.editor.get_current_editor()

# Wait for the console to be up
Expand Down
93 changes: 48 additions & 45 deletions spyder/plugins/appearance/confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@

from qtconsole.styles import dark_color
from qtpy.QtCore import Slot
from qtpy.QtWidgets import (QApplication, QDialog, QFontComboBox,
QGridLayout, QGroupBox, QMessageBox,
QPushButton, QStackedWidget, QStyleFactory,
QVBoxLayout)
from qtpy.QtWidgets import (QFontComboBox, QGridLayout, QGroupBox, QMessageBox,
QPushButton, QStackedWidget, QVBoxLayout)

from spyder.api.preferences import PluginConfigPage
from spyder.api.translations import get_translation
from spyder.config.gui import get_font, is_dark_font_color, set_font
from spyder.config.manager import CONF
from spyder.config.utils import is_gtk_desktop
from spyder.plugins.appearance.widgets import SchemeEditor
from spyder.utils import syntaxhighlighters
from spyder.utils.palette import QStylePalette
Expand Down Expand Up @@ -48,6 +45,7 @@ def setup_page(self):
ui_theme_choices,
'ui_theme',
restart=True)
self.ui_combobox = ui_theme_combo.combobox

themes = ['Spyder 2', 'Spyder 3']
icon_choices = list(zip(themes, [theme.lower() for theme in themes]))
Expand Down Expand Up @@ -172,55 +170,31 @@ def set_font(self, font, option):
plugin.update_font()

def apply_settings(self):
self.set_option('selected', self.current_scheme)
color_scheme = self.get_option('selected')

# A dark color scheme is characterized by a light font and viceversa
is_dark_color_scheme = not is_dark_font_color(color_scheme)
ui_theme = self.get_option('ui_theme')
mismatch = self.color_scheme_and_ui_theme_mismatch(
self.current_scheme, ui_theme)

if ui_theme == 'automatic':
if ((self.is_dark_interface() and not is_dark_color_scheme) or
(not self.is_dark_interface() and is_dark_color_scheme)):
if mismatch:
# Ask for a restart
self.changed_options.add('ui_theme')
elif 'ui_theme' in self.changed_options:
self.changed_options.remove('ui_theme')

if 'ui_theme' not in self.changed_options:
self.main.editor.apply_plugin_settings(['color_scheme_name'])

for plugin in self.main.thirdparty_plugins:
try:
# New API
plugin.apply_conf(['color_scheme_name'])
except AttributeError:
# Old API
plugin.apply_plugin_settings(['color_scheme_name'])

self.update_combobox()
self.update_preview()
else:
# Don't ask for a restart
if 'ui_theme' in self.changed_options:
self.changed_options.remove('ui_theme')
else:
if 'ui_theme' in self.changed_options:
if ((self.is_dark_interface() and ui_theme == 'dark') or
(not self.is_dark_interface() and ui_theme == 'light')):
if not mismatch:
# Don't ask for a restart
self.changed_options.remove('ui_theme')
else:
if mismatch:
# Ask for a restart
self.changed_options.add('ui_theme')

if 'ui_theme' not in self.changed_options:
self.main.editor.apply_plugin_settings(['color_scheme_name'])

for plugin in self.main.thirdparty_plugins:
try:
# New API
plugin.apply_conf(['color_scheme_name'])
except AttributeError:
# Old API
plugin.apply_plugin_settings(['color_scheme_name'])

self.update_combobox()
self.update_preview()
self.update_combobox()
self.update_preview()

if self.main.historylog is not None:
self.main.historylog.apply_conf(['color_scheme_name'])
return set(self.changed_options)

# Helpers
Expand Down Expand Up @@ -434,3 +408,32 @@ def is_dark_interface(self):
detect correctly the current theme.
"""
return dark_color(QStylePalette.COLOR_BACKGROUND_1)

def color_scheme_and_ui_theme_mismatch(self, color_scheme, ui_theme):
"""
Detect if there is a mismatch between the current color scheme and
UI theme.

Parameters
----------
color_scheme: str
Name of one of Spyder's color schemes. For instance: 'Zenburn' or
'Monokai'.
ui_theme: str
Name of the one of Spyder's interface themes. This can 'automatic',
'dark' or 'light'.
"""
# A dark color scheme is characterized by a light font and viceversa
is_dark_color_scheme = not is_dark_font_color(color_scheme)
if ui_theme == 'automatic':
mismatch = (
(self.is_dark_interface() and not is_dark_color_scheme) or
(not self.is_dark_interface() and is_dark_color_scheme)
)
else:
mismatch = (
(self.is_dark_interface() and ui_theme == 'light') or
(not self.is_dark_interface() and ui_theme == 'dark')
)

return mismatch
4 changes: 2 additions & 2 deletions spyder/plugins/appearance/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class Appearance(SpyderPluginV2):
CONF_FILE = False
CAN_BE_DISABLED = False

# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
# ---- SpyderPluginV2 API
# -------------------------------------------------------------------------
@staticmethod
def get_name():
return _("Appearance")
Expand Down
9 changes: 9 additions & 0 deletions spyder/plugins/appearance/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) Spyder Project Contributors
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
# -----------------------------------------------------------------------------

"""Appearance tests."""
76 changes: 76 additions & 0 deletions spyder/plugins/appearance/tests/test_confpage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) Spyder Project Contributors
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
# -----------------------------------------------------------------------------

# Third-party imports
import pytest

# Local imports
from spyder.plugins.appearance.plugin import Appearance
from spyder.plugins.preferences.api import SpyderConfigPage
from spyder.plugins.preferences.tests.conftest import (
config_dialog, MainWindowMock)


@pytest.mark.parametrize(
'config_dialog',
[[MainWindowMock, [], [Appearance]]],
indirect=True)
def test_change_ui_theme_and_color_scheme(config_dialog, mocker, qtbot):
"""Test that changing color scheme or UI theme works as expected."""
# Patch methods whose calls we want to check
mocker.patch.object(SpyderConfigPage, "prompt_restart_required")

# Get reference to Preferences dialog and widget page to interact with
dlg = config_dialog
widget = config_dialog.get_page()

# List of color schemes
names = widget.get_option('names')

# Assert no restarts have been requested so far.
assert SpyderConfigPage.prompt_restart_required.call_count == 0

# Assert default UI theme is 'automatic' and interface is dark. The other
# tests below depend on this.
assert widget.get_option('ui_theme') == 'automatic'
assert widget.is_dark_interface()

# Change to another dark color scheme
widget.schemes_combobox.setCurrentIndex(names.index('monokai'))
dlg.apply_btn.click()
assert SpyderConfigPage.prompt_restart_required.call_count == 0

# Change to a light color scheme
widget.schemes_combobox.setCurrentIndex(names.index('pydev'))
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 1

# Change to the 'dark' ui theme
widget.ui_combobox.setCurrentIndex(2)
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 1

# Change to the 'automatic' ui theme
widget.ui_combobox.setCurrentIndex(0)
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 2

# Change to the 'light' ui theme
widget.ui_combobox.setCurrentIndex(1)
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 3

# Change to another dark color scheme
widget.schemes_combobox.setCurrentIndex(names.index('solarized/dark'))
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 4

# Change to the 'automatic' ui theme again
widget.ui_combobox.setCurrentIndex(0)
dlg.apply_btn.clicked.emit()
assert SpyderConfigPage.prompt_restart_required.call_count == 4
5 changes: 0 additions & 5 deletions spyder/plugins/completion/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,6 @@ def after_configuration_update(self, options: List[Union[tuple, str]]):
provider tabs.
"""
providers_to_update = set({})
options = [
x[1] if isinstance(x, tuple) and
len(x) == 2 and x[0] is None or 'editor'
else x for x in options
]
for option in options:
if option == 'completions_wait_for_ms':
self.wait_for_ms = self.get_conf(
Expand Down
14 changes: 7 additions & 7 deletions spyder/plugins/editor/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3019,12 +3019,7 @@ def zoom(self, factor):
def apply_plugin_settings(self, options):
"""Apply configuration file's plugin settings"""
if self.editorstacks is not None:
# Get option names from the tuples sent by Preferences
options = list({option[1] for option in options})

# --- syntax highlight and text rendering settings
color_scheme_n = 'color_scheme_name'
color_scheme_o = self.get_color_scheme()
currentline_n = 'highlight_current_line'
currentline_o = self.get_option(currentline_n)
currentcell_n = 'highlight_current_cell'
Expand All @@ -3037,8 +3032,6 @@ def apply_plugin_settings(self, options):
focus_to_editor_o = self.get_option(focus_to_editor_n)

for editorstack in self.editorstacks:
if color_scheme_n in options:
editorstack.set_color_scheme(color_scheme_o)
if currentline_n in options:
editorstack.set_highlight_current_line_enabled(
currentline_o)
Expand Down Expand Up @@ -3273,6 +3266,13 @@ def set_underline_errors_enabled(self, value):
for editorstack in self.editorstacks:
editorstack.set_underline_errors_enabled(value)

@on_conf_change(option='selected', section='appearance')
def set_color_scheme(self, value):
if self.editorstacks is not None:
logger.debug(f"Set color scheme to {value}")
for editorstack in self.editorstacks:
editorstack.set_color_scheme(value)

# --- Open files
def get_open_filenames(self):
"""Get the list of open files in the current stack"""
Expand Down
4 changes: 0 additions & 4 deletions spyder/plugins/help/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,6 @@ def on_close(self, cancelable=False):

def apply_conf(self, options_set, notify=False):
super().apply_conf(options_set)
widget = self.get_widget()

if 'color_scheme_name' in options_set:
widget.set_plain_text_color_scheme(self.get_color_scheme())

# To make auto-connection changes take place instantly
try:
Expand Down
4 changes: 4 additions & 0 deletions spyder/plugins/help/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,10 @@ def on_show_source_update(self, value):
else:
self.force_refresh()

@on_conf_change(section='appearance', option='selected')
def change_color_scheme(self, value):
self.set_plain_text_color_scheme(value)

def update_actions(self):
for __, action in self.get_actions().items():
# IMPORTANT: Since we are defining the main actions in here
Expand Down
2 changes: 1 addition & 1 deletion spyder/plugins/history/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def on_line_numbers_update(self, value):
@on_conf_change(option='selected', section='appearance')
def on_color_scheme_change(self, value):
for editor in self.editors:
editor.set_font(self.font)
editor.set_color_scheme(value)

# --- Public API
# ------------------------------------------------------------------------
Expand Down
9 changes: 2 additions & 7 deletions spyder/plugins/maininterpreter/tests/test_confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
# Local imports
from spyder.api.plugins import Plugins
from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY
from spyder.config.manager import CONF
from spyder.plugins.preferences.api import PreferencePages
from spyder.plugins.maininterpreter.plugin import MainInterpreter
from spyder.plugins.preferences.tests.conftest import MainWindowMock
from spyder.utils.conda import get_list_conda_envs
Expand All @@ -22,7 +20,7 @@

# Get envs to show them in the Main interpreter page. This is actually
# done in a thread in the InterpreterStatus widget.
# We also recording the time needed to get them to compare it with the
# We're also recording the time needed to get them to compare it with the
# loading time of that config page.
t0 = time.time()
get_list_conda_envs()
Expand All @@ -31,14 +29,11 @@


def test_load_time(qtbot):
from spyder.plugins.maininterpreter.confpage import (
MainInterpreterConfigPage)

# Create Preferences dialog
main = MainWindowMock()
preferences = main.get_plugin(Plugins.Preferences)

main_interpreter = PLUGIN_REGISTRY.register_plugin(main, MainInterpreter)
PLUGIN_REGISTRY.register_plugin(main, MainInterpreter)

# Create page and measure time to do it
t0 = time.time()
Expand Down
Loading