Try to fix some font issues

- Closes #39
This commit is contained in:
Raoul Snyman 2020-10-04 13:59:26 -07:00
parent c8b328c4f5
commit 47261d206b
Signed by: raoul
GPG Key ID: F55BCED79626AE9C
6 changed files with 126 additions and 372 deletions

View File

@ -235,119 +235,6 @@ function _createStyle(selector, rules) {
} }
} }
/**
* An audio player with a play list
*/
var AudioPlayer = function (audioElement) {
this._audioElement = null;
this._eventListeners = {};
this._playlist = [];
this._currentTrack = null;
this._canRepeat = false;
this._state = AudioState.Stopped;
this.createAudioElement();
};
/**
* Call all listeners associated with this event
* @private
* @param {object} event - The event that was emitted
*/
AudioPlayer.prototype._callListener = function (event) {
if (this._eventListeners.hasOwnProperty(event.type)) {
this._eventListeners[event.type].forEach(function (listener) {
listener(event);
});
}
else {
console.warn("Received unknown event \"" + event.type + "\", doing nothing.");
}
};
/**
* Create the <audio> element that is used to play the audio
*/
AudioPlayer.prototype.createAudioElement = function () {
this._audioElement = document.createElement("audio");
this._audioElement.addEventListener("ended", this.onEnded);
this._audioElement.addEventListener("ended", this._callListener);
this._audioElement.addEventListener("timeupdate", this._callListener);
this._audioElement.addEventListener("volumechange", this._callListener);
this._audioElement.addEventListener("durationchange", this._callListener);
this._audioElement.addEventListener("loadeddata", this._callListener);
document.addEventListener("complete", function(event) {
document.body.appendChild(this._audioElement);
});
};
AudioPlayer.prototype.addEventListener = function (eventType, listener) {
this._eventListeners[eventType] = this._eventListeners[eventType] || [];
this._eventListeners[eventType].push(listener);
};
AudioPlayer.prototype.onEnded = function (event) {
this.nextTrack();
};
AudioPlayer.prototype.setCanRepeat = function (canRepeat) {
this._canRepeat = canRepeat;
};
AudioPlayer.prototype.clearTracks = function () {
this._playlist = [];
};
AudioPlayer.prototype.addTrack = function (track) {
this._playlist.push(track);
};
AudioPlayer.prototype.nextTrack = function () {
if (!!this._currentTrack) {
var trackIndex = this._playlist.indexOf(this._currentTrack);
if ((trackIndex + 1 >= this._playlist.length) && this._canRepeat) {
this.play(this._playlist[0]);
}
else if (trackIndex + 1 < this._playlist.length) {
this.play(this._playlist[trackIndex + 1]);
}
else {
this.stop();
}
}
else if (this._playlist.length > 0) {
this.play(this._playlist[0]);
}
else {
console.warn("No tracks in playlist, doing nothing.");
}
};
AudioPlayer.prototype.play = function () {
if (arguments.length > 0) {
this._currentTrack = arguments[0];
this._audioElement.src = this._currentTrack;
this._audioElement.play();
this._state = AudioState.Playing;
}
else if (this._state == AudioState.Paused) {
this._audioElement.play();
this._state = AudioState.Playing;
}
else {
console.warn("No track currently paused and no track specified, doing nothing.");
}
};
/**
* Pause
*/
AudioPlayer.prototype.pause = function () {
this._audioElement.pause();
this._state = AudioState.Paused;
};
/**
* Stop playing
*/
AudioPlayer.prototype.stop = function () {
this._audioElement.pause();
this._audioElement.src = "";
this._state = AudioState.Stopped;
};
/** /**
* The Display object is what we use from OpenLP * The Display object is what we use from OpenLP
*/ */
@ -392,9 +279,9 @@ var Display = {
init: function (options) { init: function (options) {
// Set defaults for undefined values // Set defaults for undefined values
options = options || {}; options = options || {};
var isDisplay = options.isDisplay || false; let isDisplay = options.isDisplay || false;
var doItemTransitions = options.doItemTransitions || false; let doItemTransitions = options.doItemTransitions || false;
var hideMouse = options.hideMouse || false; let hideMouse = options.hideMouse || false;
// Now continue to initialisation // Now continue to initialisation
if (!isDisplay) { if (!isDisplay) {
document.body.classList.add('checkerboard'); document.body.classList.add('checkerboard');
@ -1248,9 +1135,26 @@ var Display = {
*/ */
setScale: function(scale) { setScale: function(scale) {
document.body.style.zoom = scale+"%"; document.body.style.zoom = scale+"%";
},
/**
* In order to check if a font exists, we need a container to do
* calculations on. This method creates that container and caches
* some width values so that we don't have to do this step every
* time we check if a font exists.
*/
_prepareFontContainer: function() {
Display._fontContainer = document.createElement("span");
Display._fontContainer.id = "does-font-exist";
Display._fontContainer.innerHTML = Array(100).join("wi");
Display._fontContainer.style.cssText = [
"position: absolute",
"width: auto",
"font-size: 128px",
"left: -999999px"
].join(" !important;");
document.body.appendChild(Display._fontContainer);
} }
}; };
new QWebChannel(qt.webChannelTransport, function (channel) { new QWebChannel(qt.webChannelTransport, function (channel) {
window.mediaWatcher = channel.objects.mediaWatcher;
window.displayWatcher = channel.objects.displayWatcher; window.displayWatcher = channel.objects.displayWatcher;
}); });

View File

@ -49,6 +49,7 @@ class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
""" """
# The JS log has the entire file location, which we don't really care about # The JS log has the entire file location, which we don't really care about
app_dir = AppLocation.get_directory(AppLocation.AppDir).parent app_dir = AppLocation.get_directory(AppLocation.AppDir).parent
if str(app_dir) in source_id:
source_id = source_id.replace('file://{app_dir}/'.format(app_dir=app_dir), '') source_id = source_id.replace('file://{app_dir}/'.format(app_dir=app_dir), '')
# Log the JS messages to the Python logger # Log the JS messages to the Python logger
log.log(LOG_LEVELS[level], '{source_id}:{line_number} {message}'.format(source_id=source_id, log.log(LOG_LEVELS[level], '{source_id}:{line_number} {message}'.format(source_id=source_id,

View File

@ -25,9 +25,11 @@ import json
import logging import logging
import os import os
import copy import copy
import re
from PyQt5 import QtCore, QtWebChannel, QtWidgets from PyQt5 import QtCore, QtWebChannel, QtWidgets
from openlp.core.common import is_win
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import ServiceItemType from openlp.core.common.enum import ServiceItemType
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
@ -39,70 +41,10 @@ from openlp.core.display.screens import ScreenList
from openlp.core.ui import HideMode from openlp.core.ui import HideMode
FONT_FOUNDRY = re.compile(r'(.*?) \[(.*?)\]')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MediaWatcher(QtCore.QObject):
"""
A class to watch media events in the display and emit signals for OpenLP
"""
progress = QtCore.pyqtSignal(float)
duration = QtCore.pyqtSignal(float)
volume = QtCore.pyqtSignal(float)
playback_rate = QtCore.pyqtSignal(float)
ended = QtCore.pyqtSignal(bool)
muted = QtCore.pyqtSignal(bool)
@QtCore.pyqtSlot(float)
def update_progress(self, time):
"""
Notify about the current position of the media
"""
log.warning(time)
self.progress.emit(time)
@QtCore.pyqtSlot(float)
def update_duration(self, time):
"""
Notify about the duration of the media
"""
log.warning(time)
self.duration.emit(time)
@QtCore.pyqtSlot(float)
def update_volume(self, level):
"""
Notify about the volume of the media
"""
log.warning(level)
level = level * 100
self.volume.emit(level)
@QtCore.pyqtSlot(float)
def update_playback_rate(self, rate):
"""
Notify about the playback rate of the media
"""
log.warning(rate)
self.playback_rate.emit(rate)
@QtCore.pyqtSlot(bool)
def has_ended(self, is_ended):
"""
Notify that the media has ended playing
"""
log.warning(is_ended)
self.ended.emit(is_ended)
@QtCore.pyqtSlot(bool)
def has_muted(self, is_muted):
"""
Notify that the media has been muted
"""
log.warning(is_muted)
self.muted.emit(is_muted)
class DisplayWatcher(QtCore.QObject): class DisplayWatcher(QtCore.QObject):
""" """
This facilitates communication from the Display object in the browser back to the Python This facilitates communication from the Display object in the browser back to the Python
@ -153,14 +95,12 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.display_path = display_base_path / 'display.html' self.display_path = display_base_path / 'display.html'
self.checkerboard_path = display_base_path / 'checkerboard.png' self.checkerboard_path = display_base_path / 'checkerboard.png'
self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png' self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png'
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
self.channel = QtWebChannel.QWebChannel(self) self.channel = QtWebChannel.QWebChannel(self)
self.media_watcher = MediaWatcher(self)
self.channel.registerObject('mediaWatcher', self.media_watcher)
self.display_watcher = DisplayWatcher(self) self.display_watcher = DisplayWatcher(self)
self.channel.registerObject('displayWatcher', self.display_watcher) self.channel.registerObject('displayWatcher', self.display_watcher)
self.webview.page().setWebChannel(self.channel) self.webview.page().setWebChannel(self.channel)
self.display_watcher.initialised.connect(self.on_initialised) self.display_watcher.initialised.connect(self.on_initialised)
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
self.is_display = False self.is_display = False
self.scale = 1 self.scale = 1
self.hide_mode = None self.hide_mode = None
@ -175,6 +115,19 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
if len(ScreenList()) > 1 or self.settings.value('core/display on monitor'): if len(ScreenList()) > 1 or self.settings.value('core/display on monitor'):
self.show() self.show()
def _fix_font_name(self, font_name):
"""
Do some font machinations to see if we can fix the font name
"""
# Some fonts on Windows that end in "Bold" are made into a base font that is bold
if is_win() and font_name.endswith(' Bold'):
font_name = font_name.split(' Bold')[0]
# Some fonts may have the Foundry name in their name. Remove the foundry name
match = FONT_FOUNDRY.match(font_name)
if match:
font_name = match.group(1)
return font_name
def deregister_display(self): def deregister_display(self):
""" """
De-register this displays callbacks in the registry to be able to remove it De-register this displays callbacks in the registry to be able to remove it
@ -424,6 +377,9 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
theme_copy.background_end_color = '#590909' theme_copy.background_end_color = '#590909'
theme_copy.background_main_color = '#090909' theme_copy.background_main_color = '#090909'
theme_copy.background_footer_color = '#090909' theme_copy.background_footer_color = '#090909'
# Do some font-checking, see https://gitlab.com/openlp/openlp/-/issues/39
theme_copy.font_main_name = self._fix_font_name(theme.font_main_name)
theme_copy.font_footer_name = self._fix_font_name(theme.font_footer_name)
exported_theme = theme_copy.export_theme(is_js=True) exported_theme = theme_copy.export_theme(is_js=True)
self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme), is_sync=is_sync) self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme), is_sync=is_sync)

View File

@ -199,9 +199,12 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
:rtype: None :rtype: None
""" """
xml_file_paths = [p for p in AppLocation.get_section_data_path('themes').glob('*/*.xml')]
# Exit early if there are no themes to upgrade
if not xml_file_paths:
return
# Wait for 2 seconds to allow some other things to start processing first # Wait for 2 seconds to allow some other things to start processing first
wait_for(lambda: False, timeout=1) wait_for(lambda: False, timeout=1)
xml_file_paths = AppLocation.get_section_data_path('themes').glob('*/*.xml')
for xml_file_path in xml_file_paths: for xml_file_path in xml_file_paths:
theme_data = get_text_file_string(xml_file_path) theme_data = get_text_file_string(xml_file_path)
theme = self._create_theme_from_xml(theme_data, self.theme_path) theme = self._create_theme_from_xml(theme_data, self.theme_path)

View File

@ -32,7 +32,7 @@ from PyQt5 import QtCore
# Mock QtWebEngineWidgets # Mock QtWebEngineWidgets
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock() sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
from openlp.core.display.window import DisplayWindow from openlp.core.display.window import DisplayWindow, DisplayWatcher
from openlp.core.common.enum import ServiceItemType from openlp.core.common.enum import ServiceItemType
from openlp.core.lib.theme import Theme from openlp.core.lib.theme import Theme
from openlp.core.ui import HideMode from openlp.core.ui import HideMode
@ -220,6 +220,69 @@ def test_run_javascript_sync_no_wait(mock_time, mocked_webengine, mocked_addWidg
mock_time.sleep.assert_not_called() mock_time.sleep.assert_not_called()
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
@patch('openlp.core.display.webengine.WebEngineView')
@patch('openlp.core.display.window.is_win')
def test_fix_font_bold_windows(mocked_is_win, mocked_webengine, mocked_layout, mock_settings):
"""
Test that on Windows, fonts that end with "Bold" are handled
"""
# GIVEN: A display window and a font name
mocked_is_win.return_value = True
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
font_name = 'Arial Rounded MT Bold'
# WHEN: The font is processed
result = display_window._fix_font_name(font_name)
# Then the font name should be fixed
assert result == 'Arial Rounded MT'
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
@patch('openlp.core.display.webengine.WebEngineView')
@patch('openlp.core.display.window.is_win')
def test_fix_font_bold_not_windows(mocked_is_win, mocked_webengine, mocked_layout, mock_settings):
"""
Test that on NOT Windows, fonts that end with "Bold" are ignored
"""
# GIVEN: A display window and a font name
mocked_is_win.return_value = False
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
font_name = 'Arial Rounded MT Bold'
# WHEN: The font is processed
result = display_window._fix_font_name(font_name)
# Then the font name should be fixed
assert result == 'Arial Rounded MT Bold'
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
@patch('openlp.core.display.webengine.WebEngineView')
@patch('openlp.core.display.window.is_win')
def test_fix_font_foundry(mocked_is_win, mocked_webengine, mocked_layout, mock_settings):
"""
Test that a font with a foundry name in it has the foundry removed
"""
# GIVEN: A display window and a font name
mocked_is_win.return_value = False
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
font_name = 'CMG Sans [Foundry]'
# WHEN: The font is processed
result = display_window._fix_font_name(font_name)
# Then the font name should be fixed
assert result == 'CMG Sans'
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout') @patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
@patch('openlp.core.display.webengine.WebEngineView') @patch('openlp.core.display.webengine.WebEngineView')
def test_set_theme_is_display_video(mocked_webengine, mocked_addWidget, mock_settings, mock_geometry): def test_set_theme_is_display_video(mocked_webengine, mocked_addWidget, mock_settings, mock_geometry):
@ -463,3 +526,18 @@ def test_hide_display_no_display(mocked_screenlist, mocked_webengine, mocked_add
# THEN: Hide mode should still be none # THEN: Hide mode should still be none
assert display_window.hide_mode is None assert display_window.hide_mode is None
def test_display_watcher_set_initialised():
"""
Test that the initialised signal is emitted
"""
# GIVEN: A DisplayWatcher instance
display_watcher = DisplayWatcher(None)
# WHEN: setInitialised is called
with patch.object(display_watcher, 'initialised') as mocked_initialised:
display_watcher.setInitialised(True)
# THEN: initialised should have been emitted
mocked_initialised.emit.assert_called_once_with(True)

View File

@ -960,191 +960,3 @@ describe("Display.toggleVideoMute", function () {
expect(mockVideo.muted).toEqual(false); expect(mockVideo.muted).toEqual(false);
}); });
}); });
describe("AudioPlayer", function () {
var audioPlayer, audioElement;
beforeEach(function () {
audioElement = {
_eventListeners: {},
_playing: false,
_paused: false,
_stopped: false,
src: "",
addEventListener: function (eventType, listener) {
this._eventListeners[eventType] = this._eventListeners[eventType] || [];
this._eventListeners[eventType].push(listener);
},
play: function () {
this._playing = true;
this._paused = false;
this._stopped = false;
},
pause: function () {
this._playing = false;
this._paused = true;
this._stopped = false;
}
};
spyOn(document, "createElement").and.returnValue(audioElement);
audioPlayer = new AudioPlayer();
});
it("should create an object", function () {
expect(audioPlayer).toBeDefined();
expect(audioPlayer._audioElement).not.toBeNull();
expect(audioPlayer._eventListeners).toEqual({});
expect(audioPlayer._playlist).toEqual([]);
expect(audioPlayer._currentTrack).toEqual(null);
expect(audioPlayer._canRepeat).toEqual(false);
expect(audioPlayer._state).toEqual(AudioState.Stopped);
});
it("should call the correct method when _callListener is called", function () {
var testCalled = false;
function test(event) {
testCalled = true;
}
audioPlayer._eventListeners["test"] = [test];
audioPlayer._callListener({"type": "test"});
expect(testCalled).toEqual(true);
});
it("should log a warning when _callListener is called for an unknown event", function () {
spyOn(console, "warn");
audioPlayer._callListener({"type": "test"});
expect(console.warn).toHaveBeenCalledWith("Received unknown event \"test\", doing nothing.");
});
it("should add all the correct event listeners", function () {
expectedListeners = {
"ended": [audioPlayer.onEnded, audioPlayer._callListener],
"timeupdate": [audioPlayer._callListener],
"volumechange": [audioPlayer._callListener],
"durationchange": [audioPlayer._callListener],
"loadeddata": [audioPlayer._callListener]
};
expect(audioElement._eventListeners).toEqual(expectedListeners);
});
it("should add the correct event listener when calling addEventListener", function () {
function dummy () {};
var expectedListeners = {
"test": [dummy]
};
audioPlayer.addEventListener("test", dummy);
expect(audioPlayer._eventListeners).toEqual(expectedListeners);
});
it("should set call nextTrack when the onEnded listener is called", function () {
spyOn(audioPlayer, "nextTrack");
audioPlayer.onEnded({});
expect(audioPlayer.nextTrack).toHaveBeenCalled();
});
it("should set the _canRepeat property when calling setCanRepeat", function () {
audioPlayer.setCanRepeat(true);
expect(audioPlayer._canRepeat).toEqual(true);
});
it("should clear the playlist when clearTracks is called", function () {
audioPlayer._playlist = ["one", "two", "three"];
audioPlayer.clearTracks();
expect(audioPlayer._playlist).toEqual([]);
});
it("should add a track to the playlist when addTrack is called", function () {
audioPlayer._playlist = [];
audioPlayer.addTrack("one");
expect(audioPlayer._playlist).toEqual(["one"]);
});
it("should move to the first track when canRepeat is true and nextTrack is called", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer.setCanRepeat(true);
audioPlayer._currentTrack = "two";
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("one");
});
it("should move to the next track when nextTrack is called", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer._currentTrack = "one";
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("two");
});
it("should stop when canRepeat is false and nextTrack is called on the last track in the list", function () {
spyOn(audioPlayer, "play");
spyOn(audioPlayer, "stop");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer.setCanRepeat(false);
audioPlayer._currentTrack = "two";
audioPlayer.nextTrack();
expect(audioPlayer.play).not.toHaveBeenCalled();
expect(audioPlayer.stop).toHaveBeenCalled();
});
it("should play the first track when nextTrack is called when no songs are playing", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("one");
});
it("should log a warning when nextTrack is called when no songs are in the playlist", function () {
spyOn(console, "warn");
audioPlayer.nextTrack();
expect(console.warn).toHaveBeenCalledWith("No tracks in playlist, doing nothing.");
});
it("should play the track specified when play is called with a filename", function () {
audioPlayer.addTrack("one");
audioPlayer.play("one");
expect(audioPlayer._currentTrack).toEqual("one");
expect(audioElement._playing).toEqual(true);
expect(audioElement.src).toEqual("one");
expect(audioPlayer._state).toEqual(AudioState.Playing);
});
it("should continue playing when play is called without a filename and the player is paused", function () {
audioPlayer._state = AudioState.Paused;
audioPlayer.play();
expect(audioElement._playing).toEqual(true);
expect(audioPlayer._state).toEqual(AudioState.Playing);
});
it("should do nothing when play is called without a filename and the player is not paused", function () {
spyOn(console, "warn");
audioPlayer._state = AudioState.Playing;
audioPlayer.play();
expect(console.warn).toHaveBeenCalledWith("No track currently paused and no track specified, doing nothing.");
});
it("should pause the current track when pause is called", function () {
audioPlayer.pause();
expect(audioPlayer._state).toEqual(AudioState.Paused);
expect(audioElement._paused).toEqual(true);
});
it("should stop the current track when stop is called", function () {
audioPlayer.stop();
expect(audioPlayer._state).toEqual(AudioState.Stopped);
expect(audioElement._paused).toEqual(true);
expect(audioElement.src).toEqual("");
});
});