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