Better Dark Mode and UI Theme Support

This commit is contained in:
Mateus Meyer Jiacomelli 2021-09-01 18:34:06 +00:00 committed by Tim Bentley
parent 54ad7496cd
commit 368c1aa69a
7 changed files with 519 additions and 95 deletions

View File

@ -55,7 +55,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
from openlp.core.ui.mainwindow import MainWindow from openlp.core.ui.mainwindow import MainWindow
from openlp.core.ui.splashscreen import SplashScreen from openlp.core.ui.splashscreen import SplashScreen
from openlp.core.ui.style import get_application_stylesheet, set_windows_darkmode from openlp.core.ui.style import get_application_stylesheet, set_default_theme
from openlp.core.version import check_for_update, get_version from openlp.core.version import check_for_update, get_version
@ -117,9 +117,8 @@ class OpenLP(QtCore.QObject, LogMixin):
self.backup_on_upgrade(has_run_wizard, can_show_splash) self.backup_on_upgrade(has_run_wizard, can_show_splash)
# start the main app window # start the main app window
loader() loader()
# Set the darkmode for windows is enabled # Set the darkmode based on theme
if is_win(): set_default_theme(app)
set_windows_darkmode(app)
self.main_window = MainWindow() self.main_window = MainWindow()
self.main_window.installEventFilter(self.main_window) self.main_window.installEventFilter(self.main_window)
# Correct stylesheet bugs # Correct stylesheet bugs

View File

@ -24,6 +24,7 @@ This class contains the core default settings.
import datetime import datetime
import json import json
import logging import logging
from openlp.core.ui.style import UiThemes
import os import os
from enum import IntEnum from enum import IntEnum
from pathlib import Path from pathlib import Path
@ -104,6 +105,16 @@ def upgrade_screens(number, x_position, y_position, height, width, can_override,
} }
def upgrade_dark_theme_to_ui_theme(value):
"""
Upgrade the dark theme setting to use the new UiThemes setting.
:param bool value: The old use_dark_style setting
:returns UiThemes: New UiThemes value
"""
return UiThemes.QDarkStyle if value else UiThemes.Automatic
class Settings(QtCore.QSettings): class Settings(QtCore.QSettings):
""" """
Class to wrap QSettings. Class to wrap QSettings.
@ -174,7 +185,7 @@ class Settings(QtCore.QSettings):
'advanced/single click service preview': False, 'advanced/single click service preview': False,
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
'advanced/search as type': True, 'advanced/search as type': True,
'advanced/use_dark_style': False, 'advanced/ui_theme_name': UiThemes.Automatic,
'alerts/font face': QtGui.QFont().family(), 'alerts/font face': QtGui.QFont().family(),
'alerts/font size': 40, 'alerts/font size': 40,
'alerts/db type': 'sqlite', 'alerts/db type': 'sqlite',
@ -446,7 +457,8 @@ class Settings(QtCore.QSettings):
('media/override player', '', []), ('media/override player', '', []),
('core/audio start paused', '', []), ('core/audio start paused', '', []),
('core/audio repeat list', '', []), ('core/audio repeat list', '', []),
('core/save prompt', '', []) ('core/save prompt', '', []),
('advanced/use_dark_style', 'advanced/ui_theme_name', [(upgrade_dark_theme_to_ui_theme, [False])])
] ]
@staticmethod @staticmethod

View File

@ -26,10 +26,10 @@ from pathlib import Path
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import get_images_filter, is_win from openlp.core.common import get_images_filter
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.lib.settingstab import SettingsTab from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.style import HAS_DARK_STYLE from openlp.core.ui.style import UiThemes, has_ui_theme
from openlp.core.widgets.buttons import ColorButton from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit from openlp.core.widgets.edits import PathEdit
@ -169,10 +169,16 @@ class GeneralTab(SettingsTab):
self.new_service_message_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.new_service_message_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.new_service_message_check_box.setObjectName('new_service_message_check_box') self.new_service_message_check_box.setObjectName('new_service_message_check_box')
self.ui_layout.addRow(self.new_service_message_check_box) self.ui_layout.addRow(self.new_service_message_check_box)
if not is_win() and HAS_DARK_STYLE: self.ui_theme_style_label = QtWidgets.QLabel(self.ui_group_box)
self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box) self.ui_theme_style_label.setObjectName('theme_style_label')
self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox') self.ui_theme_style_combo_box = QtWidgets.QComboBox(self.ui_group_box)
self.ui_layout.addRow(self.use_dark_style_checkbox) if has_ui_theme(UiThemes.QDarkStyle):
self.ui_theme_style_combo_box.addItems(['', '', '', ''])
else:
self.ui_theme_style_combo_box.addItems(['', '', ''])
self.ui_theme_style_combo_box.setObjectName('theme_style_combo_box')
self.ui_layout.addRow(self.ui_theme_style_label)
self.ui_layout.addRow(self.ui_theme_style_combo_box)
self.right_layout.addWidget(self.ui_group_box) self.right_layout.addWidget(self.ui_group_box)
# Push everything in both columns to the top # Push everything in both columns to the top
self.left_layout.addStretch() self.left_layout.addStretch()
@ -220,8 +226,7 @@ class GeneralTab(SettingsTab):
'Max height for non-text slides\nin slide controller:')) 'Max height for non-text slides\nin slide controller:'))
self.slide_max_height_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Disabled')) self.slide_max_height_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Disabled'))
self.slide_max_height_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', 'Automatic')) self.slide_max_height_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', 'Automatic'))
self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', 'When changing slides:'))
'When changing slides:'))
self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll')) self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll'))
self.autoscroll_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', self.autoscroll_combo_box.setItemText(1, translate('OpenLP.AdvancedTab',
'Auto-scroll the previous slide into view')) 'Auto-scroll the previous slide into view'))
@ -251,8 +256,12 @@ class GeneralTab(SettingsTab):
self.new_service_message_check_box.setText(translate('OpenLP.AdvancedTab', self.new_service_message_check_box.setText(translate('OpenLP.AdvancedTab',
'Alert if New clicked on blank service')) 'Alert if New clicked on blank service'))
self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type')) self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
if not is_win() and HAS_DARK_STYLE: self.ui_theme_style_label.setText(translate('OpenLP.AdvancedTab', 'Interface Theme (needs restart):'))
self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) self.ui_theme_style_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Use system theme'))
self.ui_theme_style_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', 'Default Light'))
self.ui_theme_style_combo_box.setItemText(2, translate('OpenLP.AdvancedTab', 'Default Dark'))
if has_ui_theme(UiThemes.QDarkStyle):
self.ui_theme_style_combo_box.setItemText(3, translate('OpenLP.AdvancedTab', 'QDarkStyle'))
self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window')) self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window'))
def load(self): def load(self):
@ -289,14 +298,55 @@ class GeneralTab(SettingsTab):
if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count(): if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count():
self.autoscroll_combo_box.setCurrentIndex(i) self.autoscroll_combo_box.setCurrentIndex(i)
self.enable_auto_close_check_box.setChecked(self.settings.value('advanced/enable exit confirmation')) self.enable_auto_close_check_box.setChecked(self.settings.value('advanced/enable exit confirmation'))
ui_theme_index = GeneralTab.get_ui_theme_index(self.settings.value('advanced/ui_theme_name'))
self.ui_theme_style_combo_box.setCurrentIndex(ui_theme_index)
self.slide_no_in_footer_checkbox.setChecked(self.settings.value('advanced/slide numbers in footer')) self.slide_no_in_footer_checkbox.setChecked(self.settings.value('advanced/slide numbers in footer'))
self.new_service_message_check_box.setChecked(self.settings.value('advanced/new service message')) self.new_service_message_check_box.setChecked(self.settings.value('advanced/new service message'))
if not is_win() and HAS_DARK_STYLE:
self.use_dark_style_checkbox.setChecked(self.settings.value('advanced/use_dark_style'))
self.hide_mouse_check_box.setChecked(self.settings.value('advanced/hide mouse')) self.hide_mouse_check_box.setChecked(self.settings.value('advanced/hide mouse'))
self.is_search_as_you_type_enabled = self.settings.value('advanced/search as type') self.is_search_as_you_type_enabled = self.settings.value('advanced/search as type')
self.search_as_type_check_box.setChecked(self.is_search_as_you_type_enabled) self.search_as_type_check_box.setChecked(self.is_search_as_you_type_enabled)
@staticmethod
def get_ui_theme_index(ui_theme):
"""
Converts :class:`~openlp.core.ui.dark.UiThemes` item to Interface Theme ComboBox
:param ui_theme UIThemes enum item
:return ComboBox index
"""
if ui_theme == UiThemes.Automatic:
return 0
if ui_theme == UiThemes.DefaultLight:
return 1
if ui_theme == UiThemes.DefaultDark:
return 2
if ui_theme == UiThemes.QDarkStyle:
return 3 if has_ui_theme(UiThemes.QDarkStyle) else 2
return 0
@staticmethod
def get_ui_theme_name(index):
"""
Converts "Interface Theme" ComboBox index to :class:`~openlp.core.ui.dark.UiThemes` item
:param index "Interface Theme" ComboBox current index
:return UiThemes enum item
"""
if not has_ui_theme(UiThemes.QDarkStyle) and index == 3:
index = 2
if index == 0:
return UiThemes.Automatic
if index == 1:
return UiThemes.DefaultLight
if index == 2:
return UiThemes.DefaultDark
if index == 3:
return UiThemes.QDarkStyle
return UiThemes.Automatic
def save(self): def save(self):
""" """
Save the settings from the form Save the settings from the form
@ -326,8 +376,8 @@ class GeneralTab(SettingsTab):
self.settings.setValue('advanced/new service message', self.new_service_message_check_box.isChecked()) self.settings.setValue('advanced/new service message', self.new_service_message_check_box.isChecked())
self.settings.setValue('advanced/hide mouse', self.hide_mouse_check_box.isChecked()) self.settings.setValue('advanced/hide mouse', self.hide_mouse_check_box.isChecked())
self.settings.setValue('advanced/search as type', self.is_search_as_you_type_enabled) self.settings.setValue('advanced/search as type', self.is_search_as_you_type_enabled)
if not is_win() and HAS_DARK_STYLE: theme_name = GeneralTab.get_ui_theme_name(self.ui_theme_style_combo_box.currentIndex())
self.settings.setValue('advanced/use_dark_style', self.use_dark_style_checkbox.isChecked()) self.settings.setValue('advanced/ui_theme_name', theme_name)
self.post_set_up() self.post_set_up()
def post_set_up(self): def post_set_up(self):

View File

@ -28,9 +28,8 @@ from PyQt5 import QtGui, QtWidgets
from openlp.core.common import Singleton from openlp.core.common import Singleton
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.registry import Registry
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.ui.style import HAS_DARK_STYLE from openlp.core.ui.style import is_ui_theme_dark
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -180,7 +179,7 @@ class UiIcons(metaclass=Singleton):
""" """
Load the list of icons to be processed Load the list of icons to be processed
""" """
is_dark = (HAS_DARK_STYLE and Registry().get('settings').value('advanced/use_dark_style')) is_dark = is_ui_theme_dark()
for key in icon_list: for key in icon_list:
try: try:
icon = icon_list[key]['icon'] icon = icon_list[key]['icon']

View File

@ -21,17 +21,18 @@
""" """
The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme
""" """
from subprocess import Popen, PIPE
from enum import Enum
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import is_win from openlp.core.common import is_macosx, is_win
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
try: try:
import qdarkstyle import qdarkstyle
HAS_DARK_STYLE = True HAS_DARK_THEME = True
except ImportError: except ImportError:
HAS_DARK_STYLE = False HAS_DARK_THEME = False
WIN_REPAIR_STYLESHEET = """ WIN_REPAIR_STYLESHEET = """
QMainWindow::separator QMainWindow::separator
@ -76,39 +77,153 @@ QProgressBar{
""" """
def set_windows_darkmode(app): class UiThemes(Enum):
""" """
Setup darkmode on the application if enabled in the OS (windows) settings An enumeration for themes.
Source: https://github.com/worstje/manuskript/blob/develop/manuskript/main.py (GPL3) """
Automatic = 'automatic'
DefaultLight = 'light:default'
DefaultDark = 'dark:default'
QDarkStyle = 'dark:qdarkstyle'
def is_ui_theme_dark():
ui_theme_name = Registry().get('settings').value('advanced/ui_theme_name')
if ui_theme_name == UiThemes.Automatic:
return is_system_darkmode()
else:
return ui_theme_name.value.startswith('dark:')
def is_ui_theme(ui_theme: UiThemes):
ui_theme_name = Registry().get('settings').value('advanced/ui_theme_name')
return ui_theme_name == ui_theme
def init_ui_theme_if_needed(ui_theme_name):
return not isinstance(ui_theme_name, UiThemes)
def has_ui_theme(ui_theme: UiThemes):
if ui_theme == UiThemes.QDarkStyle:
return HAS_DARK_THEME
return True
IS_SYSTEM_DARKMODE = None
def is_system_darkmode():
global IS_SYSTEM_DARKMODE
if IS_SYSTEM_DARKMODE is None:
try:
if is_win():
IS_SYSTEM_DARKMODE = is_windows_darkmode()
elif is_macosx():
IS_SYSTEM_DARKMODE = is_macosx_darkmode()
else:
IS_SYSTEM_DARKMODE = False
except Exception:
IS_SYSTEM_DARKMODE = False
return IS_SYSTEM_DARKMODE
def is_windows_darkmode():
"""
Detects if Windows is using dark mode system theme.
Source: https://github.com/olivierkes/manuskript/blob/731e017e9e0dd7e4062f1af419705c11b2825515/manuskript/main.py
(GPL3)
Changes:
* Allowed palette to be set on any operating system;
* Split Windows Dark Mode detection to another function.
""" """
theme_settings = QtCore.QSettings('HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes' theme_settings = QtCore.QSettings('HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes'
'\\Personalize', '\\Personalize',
QtCore.QSettings.NativeFormat) QtCore.QSettings.NativeFormat)
if theme_settings.value('AppsUseLightTheme') == 0: return theme_settings.value('AppsUseLightTheme') == 0
app.setStyle('Fusion')
dark_palette = QtGui.QPalette()
dark_color = QtGui.QColor(45, 45, 45) def is_macosx_darkmode():
disabled_color = QtGui.QColor(127, 127, 127) """
dark_palette.setColor(QtGui.QPalette.Window, dark_color) Detects if Mac OS X is using dark mode system theme.
dark_palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.Base, QtGui.QColor(18, 18, 18)) Source: https://stackoverflow.com/a/65357166 (CC BY-SA 4.0)
dark_palette.setColor(QtGui.QPalette.AlternateBase, dark_color)
dark_palette.setColor(QtGui.QPalette.ToolTipBase, QtCore.Qt.white) Changes:
dark_palette.setColor(QtGui.QPalette.ToolTipText, QtCore.Qt.white) * Using OpenLP formatting rules
dark_palette.setColor(QtGui.QPalette.Text, QtCore.Qt.white) * Handling exceptions
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, disabled_color) """
dark_palette.setColor(QtGui.QPalette.Button, dark_color) try:
dark_palette.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.white) command = 'defaults read -g AppleInterfaceStyle'
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, disabled_color) process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True)
dark_palette.setColor(QtGui.QPalette.BrightText, QtCore.Qt.red) stdin = process.communicate()[0]
dark_palette.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218)) return bool(stdin)
dark_palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218)) except Exception:
dark_palette.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.black) return False
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, disabled_color)
# Fixes ugly (not to mention hard to read) disabled menu items.
# Source: https://bugreports.qt.io/browse/QTBUG-10322?focusedCommentId=371060#comment-371060 def set_default_theme(app):
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtCore.Qt.transparent) """
app.setPalette(dark_palette) Setup theme
"""
if is_ui_theme(UiThemes.DefaultDark) or (is_ui_theme(UiThemes.Automatic) and is_ui_theme_dark()):
set_default_darkmode(app)
elif is_ui_theme(UiThemes.DefaultLight):
set_default_lightmode(app)
def set_default_lightmode(app):
"""
Setup lightmode on the application if Default Lightt theme is enabled in the OpenLP Settings.
"""
app.setStyle('Fusion')
app.setPalette(app.style().standardPalette())
def set_default_darkmode(app):
"""
Setup darkmode on the application if enabled in the OpenLP Settings or using a dark mode system theme.
Source:
https://github.com/olivierkes/manuskript/blob/731e017e9e0dd7e4062f1af419705c11b2825515/manuskript/main.py
(GPL3)
Changes:
* Allowed palette to be set on any operating system;
* Split Windows Dark Mode detection to another function.
"""
app.setStyle('Fusion')
dark_palette = QtGui.QPalette()
dark_color = QtGui.QColor(45, 45, 45)
disabled_color = QtGui.QColor(127, 127, 127)
dark_palette.setColor(QtGui.QPalette.Window, dark_color)
dark_palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, disabled_color)
dark_palette.setColor(QtGui.QPalette.Base, QtGui.QColor(18, 18, 18))
dark_palette.setColor(QtGui.QPalette.AlternateBase, dark_color)
dark_palette.setColor(QtGui.QPalette.ToolTipBase, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.ToolTipText, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.Text, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, disabled_color)
dark_palette.setColor(QtGui.QPalette.Button, dark_color)
dark_palette.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.white)
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, disabled_color)
dark_palette.setColor(QtGui.QPalette.BrightText, QtCore.Qt.red)
dark_palette.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218))
dark_palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218))
dark_palette.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.black)
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, disabled_color)
# Fixes ugly (not to mention hard to read) disabled menu items.
# Source: https://bugreports.qt.io/browse/QTBUG-10322?focusedCommentId=371060#comment-371060
dark_palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtCore.Qt.transparent)
# Fixes ugly media manager headers.
dark_palette.setColor(QtGui.QPalette.Mid, QtGui.QColor(64, 64, 64))
app.setPalette(dark_palette)
def get_application_stylesheet(): def get_application_stylesheet():
@ -118,7 +233,7 @@ def get_application_stylesheet():
:return str: The correct stylesheet as a string :return str: The correct stylesheet as a string
""" """
stylesheet = '' stylesheet = ''
if not is_win() and HAS_DARK_STYLE and Registry().get('settings').value('advanced/use_dark_style'): if is_ui_theme(UiThemes.QDarkStyle):
stylesheet = qdarkstyle.load_stylesheet_pyqt5() stylesheet = qdarkstyle.load_stylesheet_pyqt5()
else: else:
if not Registry().get('settings').value('advanced/alternate rows'): if not Registry().get('settings').value('advanced/alternate rows'):
@ -137,7 +252,7 @@ def get_library_stylesheet():
:return str: The correct stylesheet as a string :return str: The correct stylesheet as a string
""" """
if not HAS_DARK_STYLE or not Registry().get('settings').value('advanced/use_dark_style'): if not is_ui_theme(UiThemes.QDarkStyle):
return MEDIA_MANAGER_STYLE return MEDIA_MANAGER_STYLE
else: else:
return '' return ''

View File

@ -102,6 +102,7 @@ using a computer and a data projector.""",
'chardet', 'chardet',
'dbus-python; platform_system=="Linux"', 'dbus-python; platform_system=="Linux"',
'distro; platform_system=="Linux"', 'distro; platform_system=="Linux"',
'darkdetect',
'flask', 'flask',
'flask-cors', 'flask-cors',
'lxml', 'lxml',

View File

@ -22,88 +22,336 @@
Package to test the :mod:`~openlp.core.ui.style` module. Package to test the :mod:`~openlp.core.ui.style` module.
""" """
from unittest import skipIf from unittest import skipIf
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch, call
from openlp.core.ui.style import MEDIA_MANAGER_STYLE, UiThemes, WIN_REPAIR_STYLESHEET, get_application_stylesheet, \
get_library_stylesheet, has_ui_theme, is_ui_theme_dark, set_default_theme
import openlp.core.ui.style import openlp.core.ui.style
from openlp.core.ui.style import MEDIA_MANAGER_STYLE, WIN_REPAIR_STYLESHEET, get_application_stylesheet, \
get_library_stylesheet
@skipIf(not hasattr(openlp.core.ui.style, 'qdarkstyle'), 'qdarkstyle is not installed') @skipIf(not hasattr(openlp.core.ui.style, 'qdarkstyle'), 'qdarkstyle is not installed')
@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) @patch('openlp.core.ui.style.HAS_DARK_THEME', True)
@patch('openlp.core.ui.style.is_win')
@patch('openlp.core.ui.style.qdarkstyle') @patch('openlp.core.ui.style.qdarkstyle')
def test_get_application_stylesheet_dark(mocked_qdarkstyle, mocked_is_win, mock_settings): def test_get_application_stylesheet_qdarkstyle(mocked_qdarkstyle, mock_settings):
"""Test that the dark stylesheet is returned when available and enabled""" """Test that the QDarkStyle stylesheet is returned when available and enabled"""
# GIVEN: We're not on Windows and dark style is set # GIVEN: Theme is QDarkStyle
mocked_is_win.return_value = False mock_settings.value.return_value = UiThemes.QDarkStyle
mocked_settings = MagicMock()
mocked_settings.value.return_value = True
mock_settings.return_value = mocked_settings
mocked_qdarkstyle.load_stylesheet_pyqt5.return_value = 'dark_style' mocked_qdarkstyle.load_stylesheet_pyqt5.return_value = 'dark_style'
# WHEN: can_show_icon() is called # WHEN: get_application_stylesheet() is called
result = get_application_stylesheet() result = get_application_stylesheet()
# THEN: the result should be false # THEN: the result should be QDarkStyle stylesheet
assert result == 'dark_style' assert result == 'dark_style'
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) @skipIf(not hasattr(openlp.core.ui.style, 'qdarkstyle'), 'qdarkstyle is not installed')
@patch('openlp.core.ui.style.HAS_DARK_THEME', True)
def test_has_ui_theme_qdarkstyle_true_when_available(mock_settings):
"""Test that the QDarkStyle UI theme exists when qdarkstyle is available """
# GIVEN: Theme is QDarkStyle
mock_settings.value.return_value = UiThemes.QDarkStyle
# WHEN: has_ui_theme() is called
result = has_ui_theme(UiThemes.QDarkStyle)
# THEN: the result should be true
assert result is True
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_has_ui_theme_qdarkstyle_false_when_unavailable(mock_settings):
"""Test that the QDarkStyle UI theme not exists when qdarkstyle is not available """
# GIVEN: Theme is QDarkStyle
mock_settings.value.return_value = UiThemes.QDarkStyle
# WHEN: has_ui_theme() is called
result = has_ui_theme(UiThemes.QDarkStyle)
# THEN: the result should be false
assert result is False
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_win') @patch('openlp.core.ui.style.is_win')
@patch('openlp.core.app.QtWidgets.QApplication.palette') @patch('openlp.core.app.QtWidgets.QApplication.palette')
def test_get_application_stylesheet_not_alternate_rows(mocked_palette, mocked_is_win, mock_settings): def test_get_application_stylesheet_not_alternate_rows(mocked_palette, mocked_is_win, mock_settings):
"""Test that the alternate rows stylesheet is returned when enabled in settings""" """Test that the alternate rows stylesheet is returned when enabled in settings"""
# GIVEN: We're not on Windows and no dark style is set def settings_values(key):
if key == 'advanced/ui_theme_name':
return UiThemes.DefaultLight
else:
return False
# GIVEN: We're not on Windows and UI theme is not QDarkStyle
mocked_is_win.return_value = False mocked_is_win.return_value = False
mock_settings.value.return_value = False mock_settings.value = MagicMock(side_effect=settings_values)
mocked_palette.return_value.color.return_value.name.return_value = 'color' mocked_palette.return_value.color.return_value.name.return_value = 'color'
# WHEN: can_show_icon() is called # WHEN: get_application_stylesheet() is called
result = get_application_stylesheet() result = get_application_stylesheet()
# THEN: the result should be false # THEN: result should match non-alternate-rows
mock_settings.value.assert_called_once_with('advanced/alternate rows') mock_settings.value.assert_has_calls([call('advanced/ui_theme_name'), call('advanced/alternate rows')])
assert result == 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: color;}\n', result assert result == 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: color;}\n', result
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) @patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_win') @patch('openlp.core.ui.style.is_win')
def test_get_application_stylesheet_win_repair(mocked_is_win, mock_settings): def test_get_application_stylesheet_win_repair(mocked_is_win, mock_settings):
"""Test that the Windows repair stylesheet is returned when on Windows""" """Test that the Windows repair stylesheet is returned when on Windows"""
# GIVEN: We're on Windows and no dark style is set def settings_values(key):
mocked_is_win.return_value = True if key == 'advanced/ui_theme_name':
mock_settings.value.return_value = True return UiThemes.DefaultLight
else:
return True
# WHEN: can_show_icon() is called # GIVEN: We're on Windows and Theme is not QDarkStyle
mocked_is_win.return_value = True
mock_settings.value = MagicMock(side_effect=settings_values)
# WHEN: get_application_stylesheet() is called
result = get_application_stylesheet() result = get_application_stylesheet()
# THEN: the result should be false # THEN: result should return Windows repair stylesheet
mock_settings.value.assert_called_once_with('advanced/alternate rows') mock_settings.value.assert_has_calls([call('advanced/ui_theme_name'), call('advanced/alternate rows')])
assert result == WIN_REPAIR_STYLESHEET assert result == WIN_REPAIR_STYLESHEET
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) @patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_get_library_stylesheet_no_dark_style(mock_settings): @patch('openlp.core.ui.style.is_win')
"""Test that the media manager stylesheet is returned when there's no dark theme available""" def test_get_application_stylesheet_not_win_repair(mocked_is_win, mock_settings):
# GIVEN: No dark style """Test that the Windows repair stylesheet is not returned when not in Windows"""
mock_settings.value.return_value = False def settings_values(key):
if key == 'advanced/ui_theme_name':
return UiThemes.DefaultLight
else:
return True
# GIVEN: We're on Windows and Theme is not QDarkStyle
mocked_is_win.return_value = False
mock_settings.value = MagicMock(side_effect=settings_values)
# WHEN: get_application_stylesheet() is called
result = get_application_stylesheet()
# THEN: result should not return Windows repair stylesheet
mock_settings.value.assert_has_calls([call('advanced/ui_theme_name'), call('advanced/alternate rows')])
assert result != WIN_REPAIR_STYLESHEET
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_get_library_stylesheet_automatic_ui_theme(mock_settings):
"""Test that the media manager stylesheet is returned for Automatic UI theme"""
# GIVEN: UI theme is Automatic
mock_settings.value.return_value = UiThemes.Automatic
# WHEN: get_library_stylesheet() is called # WHEN: get_library_stylesheet() is called
result = get_library_stylesheet() result = get_library_stylesheet()
# THEN: The correct stylesheet should be returned # THEN: the correct stylesheet should be returned
assert result == MEDIA_MANAGER_STYLE assert result == MEDIA_MANAGER_STYLE
@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) @patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_get_library_stylesheet_dark_style(mock_settings): def test_get_library_stylesheet_defaultlight_ui_theme(mock_settings):
"""Test that no stylesheet is returned when the dark theme is enabled""" """Test that the media manager stylesheet is returned for Default Light UI theme"""
# GIVEN: No dark style # GIVEN: UI theme is DefaultLight
mock_settings.value.return_value = True mock_settings.value.return_value = UiThemes.DefaultLight
# WHEN: get_library_stylesheet() is called
result = get_library_stylesheet()
# THEN: the correct stylesheet should be returned
assert result == MEDIA_MANAGER_STYLE
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_get_library_stylesheet_defaultdark_ui_theme(mock_settings):
"""Test that the media manager stylesheet is returned for Default Dark UI theme"""
# GIVEN: UI theme is DefaultDark
mock_settings.value.return_value = UiThemes.DefaultDark
# WHEN: get_library_stylesheet() is called
result = get_library_stylesheet()
# THEN: the correct stylesheet should be returned
assert result == MEDIA_MANAGER_STYLE
@skipIf(not hasattr(openlp.core.ui.style, 'qdarkstyle'), 'qdarkstyle is not installed')
@patch('openlp.core.ui.style.HAS_DARK_THEME', True)
def test_get_library_stylesheet_qdarktheme_ui_theme(mock_settings):
"""Test that the media manager stylesheet is not returned for QDarkStyle UI theme"""
# GIVEN: UI theme is QDarkStyle
mock_settings.value.return_value = UiThemes.QDarkStyle
# WHEN: get_library_stylesheet() is called # WHEN: get_library_stylesheet() is called
result = get_library_stylesheet() result = get_library_stylesheet()
# THEN: The correct stylesheet should be returned # THEN: The correct stylesheet should be returned
assert result == '' assert result == ''
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_system_darkmode')
def test_is_ui_theme_automatic_dark_when_system_dark(mocked_is_system_darkmode, mock_settings):
"""Test that the Automatic UI Theme is Dark on System Dark Theme"""
# GIVEN: UI theme is Automatic and System Theme is Dark
mock_settings.value.return_value = UiThemes.Automatic
mocked_is_system_darkmode.return_value = True
# WHEN: is_ui_theme_dark() is called
result = is_ui_theme_dark()
# THEN: the result should be true
assert result is True
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_system_darkmode')
def test_is_ui_theme_dark_automatic_light_when_system_light(mocked_is_system_darkmode, mock_settings):
"""Test that the Automatic UI Theme is not Dark on System Light Theme"""
# GIVEN: UI theme is Automatic and System Theme is Light
mocked_is_system_darkmode.return_value = False
mock_settings.value.return_value = UiThemes.Automatic
# WHEN: is_ui_theme_dark() is called
result = is_ui_theme_dark()
# THEN: the result should be false
assert result is False
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_is_ui_theme_dark_defaultlight_not_dark(mock_settings):
"""Test that the DefaultLight UI Theme is not Dark"""
# GIVEN: UI theme is DefaultLight
mock_settings.value.return_value = UiThemes.DefaultLight
# WHEN: is_ui_theme_dark() is called
result = is_ui_theme_dark()
# THEN: the result should be false
assert result is False
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_is_ui_theme_dark_defaultdark_dark(mock_settings):
"""Test that the DefaultDark UI Theme is Dark"""
# GIVEN: UI theme is DefaultDark
mock_settings.value.return_value = UiThemes.DefaultDark
# WHEN: is_ui_theme_dark() is called
result = is_ui_theme_dark()
# THEN: the result should be true
assert result is True
@skipIf(not hasattr(openlp.core.ui.style, 'qdarkstyle'), 'qdarkstyle is not installed')
@patch('openlp.core.ui.style.HAS_DARK_THEME', True)
def test_is_ui_theme_dark_qdarkstyle_dark(mock_settings):
"""Test that the QDarkStyle UI Theme is Dark"""
# GIVEN: UI theme is DefaultDark
mock_settings.value.return_value = UiThemes.QDarkStyle
# WHEN: is_ui_theme_dark() is called
result = is_ui_theme_dark()
# THEN: the result should be true
assert result is True
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
def test_set_default_theme_defaultdark_theme_sets_palette(mock_settings):
"""Test that the set_default_theme sets App Palette for DefaultDark UI theme"""
# GIVEN: UI theme is DefaultDark
mock_settings.value.return_value = UiThemes.DefaultDark
mock_app = MagicMock()
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: app palette should be changed
mock_app.setPalette.assert_called_once()
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_system_darkmode')
def test_set_default_theme_automatic_theme_system_dark_sets_palette(mocked_is_system_darkmode, mock_settings):
"""Test that the set_default_theme sets App Palette for Automatic UI theme on System with Dark Theme"""
# GIVEN: UI theme is Automatic on System with Dark Theme
mock_settings.value.return_value = UiThemes.Automatic
mocked_is_system_darkmode.return_value = True
mock_app = MagicMock()
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: app palette should be changed
mock_app.setPalette.assert_called_once()
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.set_default_darkmode')
def test_set_default_theme_defaultdark_theme_calls_set_default_darkmode(mock_set_default_darkmode, mock_settings):
"""Test that the set_default_theme calls set_default_darkmode for DefaultDark UI theme"""
# GIVEN: UI theme is DefaultDark
mock_settings.value.return_value = UiThemes.DefaultDark
mock_app = MagicMock()
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: set_default_darkmode should be changed
mock_set_default_darkmode.assert_called_once()
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.set_default_darkmode')
@patch('openlp.core.ui.style.is_system_darkmode')
def test_set_default_theme_automatic_theme_calls_set_default_darkmode(mock_is_system_darkmode,
mock_set_default_darkmode, mock_settings):
"""Test that the set_default_theme calls set_default_darkmode for Automatic UI theme on system dark theme"""
# GIVEN: UI theme is Automatic and System is using Dark Theme
mock_settings.value.return_value = UiThemes.Automatic
mock_app = MagicMock()
mock_is_system_darkmode.return_value = True
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: set_default_darkmode should be changed
mock_set_default_darkmode.assert_called_once()
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.is_system_darkmode')
def test_set_default_theme_automatic_theme_system_light_not_sets_palette(mocked_is_system_darkmode, mock_settings):
"""Test that the set_default_theme doesnt't set App Palette for Automatic UI theme on System Light Theme"""
# GIVEN: UI theme is Automatic with System Light Theme
mock_settings.value.return_value = UiThemes.Automatic
mocked_is_system_darkmode.return_value = False
mock_app = MagicMock()
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: app palette should not be changed
mock_app.setPalette.assert_not_called()
@patch('openlp.core.ui.style.HAS_DARK_THEME', False)
@patch('openlp.core.ui.style.set_default_lightmode')
def test_set_default_theme_defaultlight_theme_calls_set_default_lightmode(mock_set_default_lightmode, mock_settings):
"""Test that the set_default_theme calls set_default_darkmode for DefaultLight UI theme"""
# GIVEN: UI theme is DefaultLight
mock_settings.value.return_value = UiThemes.DefaultLight
mock_app = MagicMock()
# WHEN: set_default_theme() is called
set_default_theme(mock_app)
# THEN: set_default_darkmode should be changed
mock_set_default_lightmode.assert_called_once()