From fe0f2e5a20345b1a9fd91a9bb93276f4198afa81 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 1 Mar 2022 07:49:44 +0000 Subject: [PATCH] Switch to using the old way of making text outlines --- openlp/core/display/html/display.js | 68 +++++++-- openlp/core/ui/themeform.py | 2 +- tests/js/test_display.js | 65 ++++++++- tests/openlp_core/ui/test_themeform.py | 193 +++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 16 deletions(-) diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index c19a177bb..3b849b17b 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -158,6 +158,45 @@ function _buildRadialGradient(width, startColor, endColor) { return "radial-gradient(" + startColor + ", " + endColor + ") fixed"; } +/** + * Build a set of text shadows to form an outline + * @private + * @param {Number} size - The desired width of the outline + * @param {string} color - The color of the outline + * @returns {array} A list of shadows to be given to "text-shadow" + */ +function _buildTextOutline(size, color) { + let shadows = []; + // Outlines work from -(size) to +(size) + let from = size * -1; + // Loop through all the possible size iterations and add them to the array + for (let i = from; i <= size; i++) { + for (let j = from; j <= size; j++) { + shadows.push(color + " " + i + "pt " + j + "pt 0pt"); + } + } + return shadows; +} + +/** + * Build a text shadow + * @private + * @param {Number} offset - The offset of the shadow + * @param {string} color - The color that the shadow should be + * @returns {string} The text-shadow rule + */ +function _buildTextShadow(offset, size, color) { + let shadows = []; + let from = (size * -1) + offset; + let to = size + offset; + for (let i = from; i <= to; i++) { + for (let j = from; j <= to; j++) { + shadows.push(color + " " + i + "pt " + j + "pt 0pt"); + } + } + return shadows.join(", "); +} + /** * Get a style value from an element (computed or manual) * @private @@ -464,11 +503,9 @@ var Display = { return null; } if (Display._alertState === AlertState.Displaying) { - console.debug("Adding to queue"); Display.addAlertToQueue(text, settings); } else { - console.debug("Displaying immediately"); Display.showAlert(text, settings); } }, @@ -541,7 +578,6 @@ var Display = { */ alertTransitionEndEvent: function (e) { e.stopPropagation(); - console.debug("Transition end event reached: " + Display._transitionState); if (Display._transitionState === TransitionState.EntranceTransition) { Display._transitionState = TransitionState.NoTransition; } @@ -1018,13 +1054,11 @@ var Display = { break; case BackgroundType.Image: backgroundContent = "url('" + Display._theme.background_filename + "')"; - console.warn(backgroundContent); break; case BackgroundType.Video: // never actually used since background type is overridden from video to transparent in window.py backgroundContent = Display._theme.background_border_color; backgroundHtml = ""; - console.warn(backgroundHtml); break; default: backgroundContent = "#000"; @@ -1039,11 +1073,6 @@ var Display = { return; } mainStyle = {}; - if (!!Display._theme.font_main_outline) { - mainStyle["-webkit-text-stroke"] = "" + Display._theme.font_main_outline_size + "pt " + - Display._theme.font_main_outline_color; - mainStyle["-webkit-text-fill-color"] = Display._theme.font_main_color; - } // These need to be fixed, in the Python they use a width passed in as a parameter mainStyle.width = Display._theme.font_main_width + "px"; mainStyle.height = Display._theme.font_main_height + "px"; @@ -1092,9 +1121,22 @@ var Display = { default: mainStyle["justify-content"] = "center"; } - if (Display._theme.hasOwnProperty('font_main_shadow_size') && !!Display._theme.font_main_shadow) { - mainStyle["text-shadow"] = Display._theme.font_main_shadow_color + " " + Display._theme.font_main_shadow_size + "pt " + - Display._theme.font_main_shadow_size + "pt"; + /** + * This section draws the font outline. Previously we used the proprietary -webkit-text-stroke property + * but it draws the outline INSIDE the text, instead of OUTSIDE, so we had to go back to the old way + * of using multiple text-shadow rules to fake an outline. + */ + if (!!Display._theme.font_main_outline && Display._theme.hasOwnProperty('font_main_shadow_size') && !!Display._theme.font_main_shadow) { + let outlineShadows = _buildTextOutline(Display._theme.font_main_outline_size, Display._theme.font_main_outline_color); + let textShadow = _buildTextShadow(Display._theme.font_main_shadow_size, Display._theme.font_main_outline_size, Display._theme.font_main_shadow_color); + mainStyle["text-shadow"] = outlineShadows.join(", ") + ", " + textShadow; + } + else if (!!Display._theme.font_main_outline) { + let outlineShadows = _buildTextOutline(Display._theme.font_main_outline_size, Display._theme.font_main_outline_color); + mainStyle["text-shadow"] = outlineShadows.join(", "); + } + else if (Display._theme.hasOwnProperty('font_main_shadow_size') && !!Display._theme.font_main_shadow) { + mainStyle["text-shadow"] = _buildTextShadow(Display._theme.font_main_shadow_size, 0, Display._theme.font_main_shadow_color); } targetElement.style.cssText = ""; for (var mainKey in mainStyle) { diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index c96818665..0bcae9d61 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -377,7 +377,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme.font_main_outline_size = self.main_area_page.outline_size self.theme.font_main_shadow = self.main_area_page.is_shadow_enabled self.theme.font_main_shadow_size = self.main_area_page.shadow_size - self.main_area_page.shadow_color = self.theme.font_main_shadow_color + self.theme.font_main_shadow_color = self.main_area_page.shadow_color self.theme.font_main_bold = self.main_area_page.is_bold self.theme.font_main_italics = self.main_area_page.is_italic # footer page diff --git a/tests/js/test_display.js b/tests/js/test_display.js index c6c73f4bc..f4fa8525f 100644 --- a/tests/js/test_display.js +++ b/tests/js/test_display.js @@ -53,6 +53,68 @@ describe("The function", function () { expect(gradient).toBe("radial-gradient(#000, #fff) fixed"); }); + it("_buildTextOutline should return an array of text-shadow values", function () { + let shadows = _buildTextOutline(2, "#fff"); + expect(shadows).toEqual([ + "#fff -2pt -2pt 0pt", + "#fff -2pt -1pt 0pt", + "#fff -2pt 0pt 0pt", + "#fff -2pt 1pt 0pt", + "#fff -2pt 2pt 0pt", + "#fff -1pt -2pt 0pt", + "#fff -1pt -1pt 0pt", + "#fff -1pt 0pt 0pt", + "#fff -1pt 1pt 0pt", + "#fff -1pt 2pt 0pt", + "#fff 0pt -2pt 0pt", + "#fff 0pt -1pt 0pt", + "#fff 0pt 0pt 0pt", + "#fff 0pt 1pt 0pt", + "#fff 0pt 2pt 0pt", + "#fff 1pt -2pt 0pt", + "#fff 1pt -1pt 0pt", + "#fff 1pt 0pt 0pt", + "#fff 1pt 1pt 0pt", + "#fff 1pt 2pt 0pt", + "#fff 2pt -2pt 0pt", + "#fff 2pt -1pt 0pt", + "#fff 2pt 0pt 0pt", + "#fff 2pt 1pt 0pt", + "#fff 2pt 2pt 0pt" + ]); + }); + + it("_buildTextShadow should return a string of text-shadow", function () { + let shadow = _buildTextShadow(2, 2, "#acf"); + expect(shadow).toEqual([ + "#acf 0pt 0pt 0pt", + "#acf 0pt 1pt 0pt", + "#acf 0pt 2pt 0pt", + "#acf 0pt 3pt 0pt", + "#acf 0pt 4pt 0pt", + "#acf 1pt 0pt 0pt", + "#acf 1pt 1pt 0pt", + "#acf 1pt 2pt 0pt", + "#acf 1pt 3pt 0pt", + "#acf 1pt 4pt 0pt", + "#acf 2pt 0pt 0pt", + "#acf 2pt 1pt 0pt", + "#acf 2pt 2pt 0pt", + "#acf 2pt 3pt 0pt", + "#acf 2pt 4pt 0pt", + "#acf 3pt 0pt 0pt", + "#acf 3pt 1pt 0pt", + "#acf 3pt 2pt 0pt", + "#acf 3pt 3pt 0pt", + "#acf 3pt 4pt 0pt", + "#acf 4pt 0pt 0pt", + "#acf 4pt 1pt 0pt", + "#acf 4pt 2pt 0pt", + "#acf 4pt 3pt 0pt", + "#acf 4pt 4pt 0pt" + ].join(", ")); + }); + it("_getStyle should return the correct style on an element", function () { var div = _createDiv({"id": "style-test"}); div.style.setProperty("width", "100px"); @@ -682,8 +744,7 @@ describe("Display.setTextSlides", function () { Display.setTextSlides(slides); const slidesDiv = $(".text-slides")[0]; - expect(slidesDiv.style['-webkit-text-stroke']).toEqual('42pt red'); - expect(slidesDiv.style['-webkit-text-fill-color']).toEqual('yellow'); + expect(slidesDiv.style['text-shadow']).toEqual(_buildTextOutline(42, 'red').join(', ')); }) it("should correctly set text alignment,\ diff --git a/tests/openlp_core/ui/test_themeform.py b/tests/openlp_core/ui/test_themeform.py index ea1b5bf4d..32dd46480 100644 --- a/tests/openlp_core/ui/test_themeform.py +++ b/tests/openlp_core/ui/test_themeform.py @@ -24,12 +24,45 @@ Test the ThemeForm class and related methods. from pathlib import Path from unittest.mock import patch, MagicMock +import pytest + from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundType from openlp.core.ui.themeform import ThemeForm from openlp.core.ui.themelayoutform import ThemeLayoutForm +def _make_path(s): + return MagicMock(**{'__str__.return_value': s, 'exists.return_value': True}) + + +THEME_BACKGROUNDS = { + 'solid': [ + ('color', 'background_color', '#ff0') + ], + 'gradient': [ + ('gradient_type', 'background_direction', 'horizontal'), + ('gradient_start', 'background_start_color', '#fff'), + ('gradient_end', 'background_end_color', '#000') + ], + 'image': [ + ('image_color', 'background_border_color', '#f0f0f0'), + ('image_path', 'background_source', '/path/to/image.png'), + ('image_path', 'background_filename', '/path/to/image.png') + ], + 'video': [ + ('video_color', 'background_border_color', '#222'), + ('video_path', 'background_source', '/path/to/video.mkv'), + ('video_path', 'background_filename', '/path/to/video.mkv') + ], + 'stream': [ + ('stream_color', 'background_border_color', '#222'), + ('stream_mrl', 'background_source', 'http:/127.0.0.1/stream.mkv'), + ('stream_mrl', 'background_filename', 'http:/127.0.0.1/stream.mkv') + ] +} + + @patch('openlp.core.ui.themeform.ThemeForm._setup') def test_create_theme_wizard(mocked_setup, settings): """ @@ -407,3 +440,163 @@ def test_initialise_page_area_position(mocked_setup, settings): # THEN: Everything is working right theme_form.set_position_page_values.assert_called_once() + + +@patch('openlp.core.ui.themeform.ThemeForm._setup') +def test_update_theme_static(mocked_setup, settings): + """ + Test that the update_theme() method correctly sets all the "static" theme variables + """ + # GIVEN: An instance of a ThemeForm with some mocked out pages which return certain values + theme_form = ThemeForm(None) + theme_form.can_update_theme = True + theme_form.theme = MagicMock() + theme_form.background_page = MagicMock() + theme_form.main_area_page = MagicMock(font_name='Montserrat', font_color='#f00', font_size=50, line_spacing=12, + is_outline_enabled=True, outline_color='#00f', outline_size=3, + is_shadow_enabled=True, shadow_color='#111', shadow_size=5, is_bold=True, + is_italic=False) + theme_form.footer_area_page = MagicMock(font_name='Oxygen', font_color='#fff', font_size=20, is_bold=False, + is_italic=True) + theme_form.alignment_page = MagicMock(horizontal_align='left', vertical_align='top', is_transition_enabled=True, + transition_type='fade', transition_speed='normal', + transition_direction='horizontal', is_transition_reverse_enabled=False) + theme_form.area_position_page = MagicMock(use_main_default_location=True, use_footer_default_location=True) + + # WHEN: ThemeForm.update_theme() is called + theme_form.update_theme() + + # THEN: The theme should be correct + # Main area + assert theme_form.theme.font_main_name == 'Montserrat' + assert theme_form.theme.font_main_color == '#f00' + assert theme_form.theme.font_main_size == 50 + assert theme_form.theme.font_main_line_adjustment == 12 + assert theme_form.theme.font_main_outline is True + assert theme_form.theme.font_main_outline_color == '#00f' + assert theme_form.theme.font_main_outline_size == 3 + assert theme_form.theme.font_main_shadow is True + assert theme_form.theme.font_main_shadow_color == '#111' + assert theme_form.theme.font_main_shadow_size == 5 + assert theme_form.theme.font_main_bold is True + assert theme_form.theme.font_main_italics is False + assert theme_form.theme.font_main_override is False + theme_form.theme.set_default_header.assert_called_once_with() + # Footer + assert theme_form.theme.font_footer_name == 'Oxygen' + assert theme_form.theme.font_footer_color == '#fff' + assert theme_form.theme.font_footer_size == 20 + assert theme_form.theme.font_footer_bold is False + assert theme_form.theme.font_footer_italics is True + assert theme_form.theme.font_footer_override is False + theme_form.theme.set_default_footer.assert_called_once_with() + # Alignment + assert theme_form.theme.display_horizontal_align == 'left' + assert theme_form.theme.display_vertical_align == 'top' + # Transitions + assert theme_form.theme.display_slide_transition is True + assert theme_form.theme.display_slide_transition_type == 'fade' + assert theme_form.theme.display_slide_transition_direction == 'horizontal' + assert theme_form.theme.display_slide_transition_speed == 'normal' + assert theme_form.theme.display_slide_transition_reverse is False + + +@patch('openlp.core.ui.themeform.ThemeForm._setup') +def test_update_theme_overridden_areas(mocked_setup, settings): + """ + Test that the update_theme() method correctly sets all the positioning information for a custom position + """ + # GIVEN: An instance of a ThemeForm with some mocked out pages which return certain values + theme_form = ThemeForm(None) + theme_form.can_update_theme = True + theme_form.theme = MagicMock() + theme_form.background_page = MagicMock() + theme_form.main_area_page = MagicMock() + theme_form.footer_area_page = MagicMock() + theme_form.alignment_page = MagicMock() + theme_form.area_position_page = MagicMock(use_main_default_location=False, use_footer_default_location=False, + main_x=20, main_y=50, main_height=900, main_width=1880, + footer_x=20, footer_y=910, footer_height=70, footer_width=1880) + + # WHEN: ThemeForm.update_theme() is called + theme_form.update_theme() + + # THEN: The theme should be correct + assert theme_form.theme.font_main_override is True + assert theme_form.theme.font_main_x == 20 + assert theme_form.theme.font_main_y == 50 + assert theme_form.theme.font_main_height == 900 + assert theme_form.theme.font_main_width == 1880 + assert theme_form.theme.font_footer_override is True + assert theme_form.theme.font_footer_x == 20 + assert theme_form.theme.font_footer_y == 910 + assert theme_form.theme.font_footer_height == 70 + assert theme_form.theme.font_footer_width == 1880 + + +@pytest.mark.parametrize('background_type', THEME_BACKGROUNDS.keys()) +@patch('openlp.core.ui.themeform.ThemeForm._setup') +def test_update_theme_background(mocked_setup, background_type, settings): + """ + Test that the update_theme() method correctly sets all the theme background variables for each background type + """ + # GIVEN: An instance of a ThemeForm with some mocked out pages which return certain values + page_props = {page_prop: value for page_prop, _, value in THEME_BACKGROUNDS[background_type]} + theme_form = ThemeForm(None) + theme_form.can_update_theme = True + theme_form.theme = MagicMock() + theme_form.background_page = MagicMock(background_type=background_type, **page_props) + theme_form.main_area_page = MagicMock() + theme_form.footer_area_page = MagicMock() + theme_form.alignment_page = MagicMock() + theme_form.area_position_page = MagicMock() + + # WHEN: ThemeForm.update_theme() is called + theme_form.update_theme() + + # THEN: The theme should be correct + for _, theme_prop, value in THEME_BACKGROUNDS[background_type]: + assert getattr(theme_form.theme, theme_prop) == value, f'{theme_prop} should have been {value}' + + +@pytest.mark.skip('Being a bit problematic right now') +@pytest.mark.parametrize('background_type', THEME_BACKGROUNDS.keys()) +@patch('openlp.core.ui.themeform.ThemeForm._setup') +def test_set_background_page_values(mocked_setup, background_type, settings): + """ + Test that the set_background_page_values() method sets the background page values correctly + """ + # GIVEN: An instance of a ThemeForm with some mocked out pages and a mocked theme with values + theme_props = {theme_prop: value for _, theme_prop, value in THEME_BACKGROUNDS[background_type]} + theme_form = ThemeForm(None) + theme_form.theme = MagicMock(background_type=background_type, **theme_props) + theme_form.background_page = MagicMock() + theme_form.main_area_page = MagicMock() + theme_form.footer_area_page = MagicMock() + theme_form.alignment_page = MagicMock() + theme_form.area_position_page = MagicMock() + + # WHEN: set_background_page_values() is called + theme_form.set_background_page_values() + + # THEN: The correct values are set on the page + for page_prop, _, value in THEME_BACKGROUNDS[background_type]: + assert getattr(theme_form.background_page, page_prop) == value, ( + f'{page_prop} should have been {value} but was {getattr(theme_form.background_page, page_prop)}' + ) + + +@patch('openlp.core.ui.themeform.ThemeForm._setup') +def test_update_theme_cannot_update(mocked_setup, settings): + """ + Test that the update_theme() method skips out early when the theme cannot be updated + """ + # GIVEN: An instance of a ThemeForm with some mocked out pages which return certain values + theme_form = ThemeForm(None) + theme_form.can_update_theme = False + + # WHEN: ThemeForm.update_theme() is called + theme_form.update_theme() + + # THEN: The theme should be correct + # TODO: Figure out a way to check this