From 368c1aa69adfd8e4a865e33cac2b8d391fb3e8c9 Mon Sep 17 00:00:00 2001 From: Mateus Meyer Jiacomelli Date: Wed, 1 Sep 2021 18:34:06 +0000 Subject: [PATCH] Better Dark Mode and UI Theme Support --- openlp/core/app.py | 7 +- openlp/core/common/settings.py | 16 +- openlp/core/ui/generaltab.py | 78 +++++-- openlp/core/ui/icons.py | 5 +- openlp/core/ui/style.py | 183 +++++++++++++--- setup.py | 1 + tests/openlp_core/ui/test_style.py | 324 +++++++++++++++++++++++++---- 7 files changed, 519 insertions(+), 95 deletions(-) diff --git a/openlp/core/app.py b/openlp/core/app.py index c2903b9ea..492d417c7 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -55,7 +55,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm from openlp.core.ui.mainwindow import MainWindow 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 @@ -117,9 +117,8 @@ class OpenLP(QtCore.QObject, LogMixin): self.backup_on_upgrade(has_run_wizard, can_show_splash) # start the main app window loader() - # Set the darkmode for windows is enabled - if is_win(): - set_windows_darkmode(app) + # Set the darkmode based on theme + set_default_theme(app) self.main_window = MainWindow() self.main_window.installEventFilter(self.main_window) # Correct stylesheet bugs diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index a12a49ee1..4532f15b9 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -24,6 +24,7 @@ This class contains the core default settings. import datetime import json import logging +from openlp.core.ui.style import UiThemes import os from enum import IntEnum 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 to wrap QSettings. @@ -174,7 +185,7 @@ class Settings(QtCore.QSettings): 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, 'advanced/search as type': True, - 'advanced/use_dark_style': False, + 'advanced/ui_theme_name': UiThemes.Automatic, 'alerts/font face': QtGui.QFont().family(), 'alerts/font size': 40, 'alerts/db type': 'sqlite', @@ -446,7 +457,8 @@ class Settings(QtCore.QSettings): ('media/override player', '', []), ('core/audio start paused', '', []), ('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 diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 49bb989c7..24db3342b 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -26,10 +26,10 @@ from pathlib import Path 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.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.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.setObjectName('new_service_message_check_box') self.ui_layout.addRow(self.new_service_message_check_box) - if not is_win() and HAS_DARK_STYLE: - self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box) - self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox') - self.ui_layout.addRow(self.use_dark_style_checkbox) + self.ui_theme_style_label = QtWidgets.QLabel(self.ui_group_box) + self.ui_theme_style_label.setObjectName('theme_style_label') + self.ui_theme_style_combo_box = QtWidgets.QComboBox(self.ui_group_box) + 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) # Push everything in both columns to the top self.left_layout.addStretch() @@ -220,8 +226,7 @@ class GeneralTab(SettingsTab): '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(1, translate('OpenLP.AdvancedTab', 'Automatic')) - self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', - 'When changing slides:')) + self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', 'When changing slides:')) self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll')) self.autoscroll_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', 'Auto-scroll the previous slide into view')) @@ -251,8 +256,12 @@ class GeneralTab(SettingsTab): self.new_service_message_check_box.setText(translate('OpenLP.AdvancedTab', 'Alert if New clicked on blank service')) self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type')) - if not is_win() and HAS_DARK_STYLE: - self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) + self.ui_theme_style_label.setText(translate('OpenLP.AdvancedTab', 'Interface Theme (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')) def load(self): @@ -289,14 +298,55 @@ class GeneralTab(SettingsTab): if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count(): self.autoscroll_combo_box.setCurrentIndex(i) 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.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.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) + @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): """ 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/hide mouse', self.hide_mouse_check_box.isChecked()) self.settings.setValue('advanced/search as type', self.is_search_as_you_type_enabled) - if not is_win() and HAS_DARK_STYLE: - self.settings.setValue('advanced/use_dark_style', self.use_dark_style_checkbox.isChecked()) + theme_name = GeneralTab.get_ui_theme_name(self.ui_theme_style_combo_box.currentIndex()) + self.settings.setValue('advanced/ui_theme_name', theme_name) self.post_set_up() def post_set_up(self): diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py index c40485bd7..70f1cadce 100644 --- a/openlp/core/ui/icons.py +++ b/openlp/core/ui/icons.py @@ -28,9 +28,8 @@ from PyQt5 import QtGui, QtWidgets from openlp.core.common import Singleton from openlp.core.common.applocation import AppLocation -from openlp.core.common.registry import Registry 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__) @@ -180,7 +179,7 @@ class UiIcons(metaclass=Singleton): """ 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: try: icon = icon_list[key]['icon'] diff --git a/openlp/core/ui/style.py b/openlp/core/ui/style.py index 758a51948..0d75c1c56 100644 --- a/openlp/core/ui/style.py +++ b/openlp/core/ui/style.py @@ -21,17 +21,18 @@ """ 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 openlp.core.common import is_win +from openlp.core.common import is_macosx, is_win from openlp.core.common.registry import Registry - try: import qdarkstyle - HAS_DARK_STYLE = True + HAS_DARK_THEME = True except ImportError: - HAS_DARK_STYLE = False + HAS_DARK_THEME = False WIN_REPAIR_STYLESHEET = """ 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 - Source: https://github.com/worstje/manuskript/blob/develop/manuskript/main.py (GPL3) + An enumeration for themes. + """ + 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' '\\Personalize', QtCore.QSettings.NativeFormat) - if theme_settings.value('AppsUseLightTheme') == 0: - 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.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) - app.setPalette(dark_palette) + return theme_settings.value('AppsUseLightTheme') == 0 + + +def is_macosx_darkmode(): + """ + Detects if Mac OS X is using dark mode system theme. + + Source: https://stackoverflow.com/a/65357166 (CC BY-SA 4.0) + + Changes: + * Using OpenLP formatting rules + * Handling exceptions + """ + try: + command = 'defaults read -g AppleInterfaceStyle' + process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True) + stdin = process.communicate()[0] + return bool(stdin) + except Exception: + return False + + +def set_default_theme(app): + """ + 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(): @@ -118,7 +233,7 @@ def get_application_stylesheet(): :return str: The correct stylesheet as a string """ 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() else: 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 """ - 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 else: return '' diff --git a/setup.py b/setup.py index 14765851c..6dee09f58 100644 --- a/setup.py +++ b/setup.py @@ -102,6 +102,7 @@ using a computer and a data projector.""", 'chardet', 'dbus-python; platform_system=="Linux"', 'distro; platform_system=="Linux"', + 'darkdetect', 'flask', 'flask-cors', 'lxml', diff --git a/tests/openlp_core/ui/test_style.py b/tests/openlp_core/ui/test_style.py index 509226e50..71ab7bb6b 100644 --- a/tests/openlp_core/ui/test_style.py +++ b/tests/openlp_core/ui/test_style.py @@ -22,88 +22,336 @@ Package to test the :mod:`~openlp.core.ui.style` module. """ 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 -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') -@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) -@patch('openlp.core.ui.style.is_win') +@patch('openlp.core.ui.style.HAS_DARK_THEME', True) @patch('openlp.core.ui.style.qdarkstyle') -def test_get_application_stylesheet_dark(mocked_qdarkstyle, mocked_is_win, mock_settings): - """Test that the dark stylesheet is returned when available and enabled""" - # GIVEN: We're not on Windows and dark style is set - mocked_is_win.return_value = False - mocked_settings = MagicMock() - mocked_settings.value.return_value = True - mock_settings.return_value = mocked_settings +def test_get_application_stylesheet_qdarkstyle(mocked_qdarkstyle, mock_settings): + """Test that the QDarkStyle stylesheet is returned when available and enabled""" + # GIVEN: Theme is QDarkStyle + mock_settings.value.return_value = UiThemes.QDarkStyle 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() - # THEN: the result should be false + # THEN: the result should be QDarkStyle stylesheet 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.app.QtWidgets.QApplication.palette') 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""" - # 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 - 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' - # WHEN: can_show_icon() is called + # WHEN: get_application_stylesheet() is called result = get_application_stylesheet() - # THEN: the result should be false - mock_settings.value.assert_called_once_with('advanced/alternate rows') + # THEN: result should match non-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 -@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') def test_get_application_stylesheet_win_repair(mocked_is_win, mock_settings): """Test that the Windows repair stylesheet is returned when on Windows""" - # GIVEN: We're on Windows and no dark style is set - mocked_is_win.return_value = True - mock_settings.value.return_value = True + def settings_values(key): + if key == 'advanced/ui_theme_name': + 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() - # THEN: the result should be false - mock_settings.value.assert_called_once_with('advanced/alternate rows') + # THEN: result should 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_STYLE', False) -def test_get_library_stylesheet_no_dark_style(mock_settings): - """Test that the media manager stylesheet is returned when there's no dark theme available""" - # GIVEN: No dark style - mock_settings.value.return_value = False +@patch('openlp.core.ui.style.HAS_DARK_THEME', False) +@patch('openlp.core.ui.style.is_win') +def test_get_application_stylesheet_not_win_repair(mocked_is_win, mock_settings): + """Test that the Windows repair stylesheet is not returned when not in Windows""" + 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 result = get_library_stylesheet() - # THEN: The correct stylesheet should be returned + # THEN: the correct stylesheet should be returned assert result == MEDIA_MANAGER_STYLE -@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) -def test_get_library_stylesheet_dark_style(mock_settings): - """Test that no stylesheet is returned when the dark theme is enabled""" - # GIVEN: No dark style - mock_settings.value.return_value = True +@patch('openlp.core.ui.style.HAS_DARK_THEME', False) +def test_get_library_stylesheet_defaultlight_ui_theme(mock_settings): + """Test that the media manager stylesheet is returned for Default Light UI theme""" + # GIVEN: UI theme is DefaultLight + 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 result = get_library_stylesheet() # THEN: The correct stylesheet should be returned 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()