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
*/
@ -392,9 +279,9 @@ var Display = {
init: function (options) {
// Set defaults for undefined values
options = options || {};
var isDisplay = options.isDisplay || false;
var doItemTransitions = options.doItemTransitions || false;
var hideMouse = options.hideMouse || false;
let isDisplay = options.isDisplay || false;
let doItemTransitions = options.doItemTransitions || false;
let hideMouse = options.hideMouse || false;
// Now continue to initialisation
if (!isDisplay) {
document.body.classList.add('checkerboard');
@ -1248,9 +1135,26 @@ var Display = {
*/
setScale: function(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) {
window.mediaWatcher = channel.objects.mediaWatcher;
window.displayWatcher = channel.objects.displayWatcher;
});

View File

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

View File

@ -25,9 +25,11 @@ import json
import logging
import os
import copy
import re
from PyQt5 import QtCore, QtWebChannel, QtWidgets
from openlp.core.common import is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import ServiceItemType
from openlp.core.common.i18n import translate
@ -39,70 +41,10 @@ from openlp.core.display.screens import ScreenList
from openlp.core.ui import HideMode
FONT_FOUNDRY = re.compile(r'(.*?) \[(.*?)\]')
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):
"""
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.checkerboard_path = display_base_path / 'checkerboard.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.media_watcher = MediaWatcher(self)
self.channel.registerObject('mediaWatcher', self.media_watcher)
self.display_watcher = DisplayWatcher(self)
self.channel.registerObject('displayWatcher', self.display_watcher)
self.webview.page().setWebChannel(self.channel)
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.scale = 1
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'):
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):
"""
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_main_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)
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
"""
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(lambda: False, timeout=1)
xml_file_paths = AppLocation.get_section_data_path('themes').glob('*/*.xml')
for xml_file_path in xml_file_paths:
theme_data = get_text_file_string(xml_file_path)
theme = self._create_theme_from_xml(theme_data, self.theme_path)

View File

@ -32,7 +32,7 @@ from PyQt5 import QtCore
# Mock QtWebEngineWidgets
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.lib.theme import Theme
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()
@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.webengine.WebEngineView')
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
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);
});
});
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("");
});
});