handling screen display change corner cases

This commit is contained in:
Mateus Meyer Jiacomelli 2022-11-19 17:12:30 -03:00
parent 0ecc316bce
commit 3067175ff5
6 changed files with 188 additions and 28 deletions

View File

@ -163,7 +163,7 @@ class Screen(object):
Callback function for when the screens geometry changes
"""
self.geometry = geometry
Registry().execute('config_screen_changed')
emit_config_screen_changed()
class ScreenList(metaclass=Singleton):
@ -396,7 +396,7 @@ class ScreenList(metaclass=Singleton):
is_primary=self.application.primaryScreen() == changed_screen))
self.find_new_display_screen()
changed_screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
Registry().execute('config_screen_changed')
emit_config_screen_changed()
def on_screen_removed(self, removed_screen):
"""
@ -417,7 +417,7 @@ class ScreenList(metaclass=Singleton):
self.screens.pop(removed_screen_number)
if removed_screen_is_display:
self.find_new_display_screen()
Registry().execute('config_screen_changed')
emit_config_screen_changed()
def on_primary_screen_changed(self):
"""
@ -426,5 +426,21 @@ class ScreenList(metaclass=Singleton):
for screen in self.screens:
screen.is_primary = self.application.primaryScreen().geometry() == screen.geometry
self.find_new_display_screen()
emit_config_screen_changed()
Registry().execute('config_screen_changed')
SCREEN_CHANGED_DEBOUNCE_TIMEOUT = 350
def emit_config_screen_changed():
screen_changed_debounce.start()
def __do_emit_config_screen_changed():
Registry().execute('config_screen_changed')
screen_changed_debounce = QtCore.QTimer(None)
screen_changed_debounce.setInterval(SCREEN_CHANGED_DEBOUNCE_TIMEOUT)
screen_changed_debounce.setSingleShot(True)
screen_changed_debounce.timeout.connect(__do_emit_config_screen_changed)

View File

@ -130,11 +130,13 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
"""
This is a window to show the output
"""
def __init__(self, parent=None, screen=None, can_show_startup_screen=True):
def __init__(self, parent=None, screen=None, can_show_startup_screen=True, start_hidden=False,
after_loaded_callback=None):
"""
Create the display window
"""
super(DisplayWindow, self).__init__(parent)
self.after_loaded_callback = after_loaded_callback
# Gather all flags for the display window
flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint
if self.settings.value('advanced/x11 bypass wm'):
@ -183,7 +185,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.update_from_screen(screen)
self.is_display = True
# Only make visible on single monitor setup if setting enabled.
if len(ScreenList()) > 1 or self.settings.value('core/display on monitor'):
if not start_hidden and (len(ScreenList()) > 1 or self.settings.value('core/display on monitor')):
self.show()
def closeEvent(self, event):
@ -303,6 +305,8 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.set_scale(self.scale)
if self._can_show_startup_screen:
self.set_startup_screen()
if self.after_loaded_callback:
self.after_loaded_callback()
def run_javascript(self, script, is_sync=False):
"""

View File

@ -25,6 +25,7 @@ import shutil
from datetime import datetime, date
from pathlib import Path
from tempfile import gettempdir
from threading import Lock
from PyQt5 import QtCore, QtGui, QtWidgets
@ -526,6 +527,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# Starting up web services
self.http_server = HttpServer(self)
self.ws_server = WebSocketServer()
self.screen_updating_lock = Lock()
def _wait_for_threads(self):
"""
@ -1015,25 +1017,33 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
The screen has changed so we have to update components such as the renderer.
"""
self.application.set_busy_cursor()
self.renderer.resize(self.live_controller.screens.current.display_geometry.size())
self.preview_controller.screen_size_changed()
self.live_controller.setup_displays()
self.live_controller.screen_size_changed()
self.setFocus()
self.activateWindow()
self.application.set_normal_cursor()
# if a warning has been shown within the last 5 seconds, skip showing again to avoid spamming user,
# also do not show if the settings window is visible
if not self.settings_form.isVisible() and not self.screen_change_timestamp or \
self.screen_change_timestamp and (datetime.now() - self.screen_change_timestamp).seconds > 5:
self.screen_change_timestamp = datetime.now()
QtWidgets.QMessageBox.warning(self, translate('OpenLP.MainWindow', 'Screen setup has changed'),
translate('OpenLP.MainWindow',
'The screen setup has changed. '
'OpenLP will try to automatically select a display screen, but '
'you should consider updating the screen settings.'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
try:
self.screen_updating_lock.acquire()
# if a warning has been shown within the last 5 seconds, skip showing again to avoid spamming user,
# also do not show if the settings window is visible
if not self.settings_form.isVisible() and not self.screen_change_timestamp or \
self.screen_change_timestamp and (datetime.now() - self.screen_change_timestamp).seconds > 5:
self.screen_change_timestamp = datetime.now()
QtWidgets.QMessageBox.warning(self, translate('OpenLP.MainWindow', 'Screen setup has changed'),
translate('OpenLP.MainWindow',
'The screen setup has changed. '
'OpenLP will try to automatically select a display screen, but '
'you should consider updating the screen settings.'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
self.application.set_busy_cursor()
self.renderer.resize(self.live_controller.screens.current.display_geometry.size())
self.preview_controller.screen_size_changed()
self.live_controller.setup_displays()
self.live_controller.screen_size_changed()
self.setFocus()
self.activateWindow()
self.application.set_normal_cursor()
# Forcing application to process events to trigger display update
# We need to wait a little of time as it would otherwise need a mouse move
# to process the screen change, for example
QtCore.QTimer.singleShot(150, lambda: self.application.process_events())
finally:
self.screen_updating_lock.release()
def closeEvent(self, event):
"""

View File

@ -181,12 +181,23 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.close_displays()
for screen in self.screens:
if screen.is_display:
display = DisplayWindow(self, screen)
will_start_hidden = self._current_hide_mode == HideMode.Screen
display = DisplayWindow(self, screen, start_hidden=will_start_hidden,
after_loaded_callback=self._display_after_loaded_callback)
self.displays.append(display)
self._reset_blank(False)
if self.display:
self.__add_actions_to_widget(self.display)
def _display_after_loaded_callback(self):
# As the display was reloaded, we'll need to process current item again
if self.service_item:
self._process_item(self.service_item, self.selected_row, is_reloading=True)
if self._current_hide_mode == HideMode.Screen:
# Forcing screen to be on transparent mode if already hidden, otherwise the 'show' animation would not
# be performed.
self.display.hide_display(HideMode.Screen)
def close_displays(self):
"""
Close all open displays
@ -929,12 +940,13 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
for display in self.displays:
display.set_theme(theme_data, service_item_type=service_item.service_item_type)
def _process_item(self, service_item, slide_no):
def _process_item(self, service_item, slide_no, is_reloading=False):
"""
Loads a ServiceItem into the system from ServiceManager. Display the slide number passed.
:param service_item: The current service item
:param slide_no: The slide number to select
:param is_reloading: If the controller is reloading the current item, due to a display update (for example).
"""
self.log_debug('_process_item start')
self.on_stop_loop()
@ -1044,7 +1056,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.application.process_events()
self.ignore_toolbar_resize_events = False
self.on_controller_size_changed()
if self.settings.value('core/auto unblank'):
if not is_reloading and self.settings.value('core/auto unblank'):
self.set_hide_mode(None)
self.log_debug('_process_item end')

View File

@ -29,6 +29,7 @@ from pathlib import Path
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore
from openlp.core.display.screens import Screen
# Mock QtWebEngineWidgets
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
@ -123,6 +124,48 @@ def test_not_macos_toolwindow_attribute_set(mocked_is_macosx, mock_settings, dis
assert display_window.testAttribute(QtCore.Qt.WA_MacAlwaysShowToolWindow) is False
@patch.object(DisplayWindow, 'show')
def test_not_shown_if_start_hidden_is_set(mocked_show, display_window_env, mock_settings):
"""
Tests if DisplayWindow's .show() method is not called on constructor if constructed with start_hidden=True
"""
# GIVEN: A mocked DisplayWindow's show method, a fake screen and relevant settings
settings = {
'advanced/x11 bypass wm': False,
'core/display on monitor': True
}
mock_settings.value.side_effect = lambda key: settings[key]
screen = Screen(1, QtCore.QRect(0, 0, 800, 600), is_display=True)
# WHEN: A DisplayWindow is created with start_hidden=True
DisplayWindow(screen=screen, start_hidden=True)
# THEN: Window is not shown
mocked_show.assert_not_called()
@patch.object(DisplayWindow, 'show')
def test_shown_if_start_hidden_is_not_set(mocked_show, display_window_env, mock_settings):
"""
Tests if DisplayWindow's .show() method is called on constructor if constructed with start_hidden=False
"""
# GIVEN: A mocked DisplayWindow's show method, a fake screen and relevant settings
settings = {
'advanced/x11 bypass wm': False,
'core/display on monitor': True
}
mock_settings.value.side_effect = lambda key: settings[key]
screen = Screen(1, QtCore.QRect(0, 0, 800, 600), is_display=True)
# WHEN: A DisplayWindow is created with start_hidden=True
DisplayWindow(screen=screen, start_hidden=False)
# THEN: Window is shown
mocked_show.assert_called()
def test_set_scale_not_initialised(display_window_env, mock_settings):
"""
Test that the scale js is not run if the page is not initialised
@ -318,6 +361,26 @@ def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
'});')
def test_after_loaded_callback(display_window_env, mock_settings):
"""
Test if the __ is loaded on after_loaded() method correctly
"""
# GIVEN: An initialised display window and settings for item transitions and hide mouse returns true
mocked_after_loaded_callback = MagicMock()
display_window = DisplayWindow(after_loaded_callback=mocked_after_loaded_callback)
display_window.is_display = True
mock_settings.value.return_value = True
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.set_scale = MagicMock()
display_window.set_startup_screen = MagicMock()
# WHEN: after_loaded is run
display_window.after_loaded()
# THEN: The after_loaded_callback should be called
mocked_after_loaded_callback.assert_called_once()
@patch.object(time, 'time')
def test_run_javascript_no_sync_no_wait(mock_time, display_window_env, mock_settings):
"""

View File

@ -1134,6 +1134,61 @@ def test_process_item_song_no_vlc(mocked_execute, registry, state_media):
assert 2 == slide_controller.preview_display.load_verses.call_count, 'Execute should have been called 2 times'
@patch.object(Registry, 'execute')
def test_process_item_is_reloading_wont_change_display_hide_mode(mocked_execute, registry, state_media):
"""
Test if the display's hide mode is not changed when using is_reloading parameter
"""
# GIVEN: A mocked presentation service item, a mocked media service item, a mocked Registry.execute
# and a slide controller with many mocks.
# and the setting 'themes/item transitions' = True
mocked_media_item = MagicMock()
mocked_media_item.name = 'mocked_media_item'
mocked_media_item.get_transition_delay.return_value = 0
mocked_media_item.is_text.return_value = True
mocked_media_item.is_command.return_value = False
mocked_media_item.is_media.return_value = False
mocked_media_item.requires_media.return_value = False
mocked_media_item.is_image.return_value = False
mocked_media_item.from_service = False
mocked_media_item.get_frames.return_value = []
mocked_media_item.display_slides = [{'verse': 'Verse name'}]
mocked_settings = MagicMock()
mocked_settings.value.return_value = True
mocked_main_window = MagicMock()
Registry().register('main_window', mocked_main_window)
Registry().register('media_controller', MagicMock())
Registry().register('application', MagicMock())
Registry().register('settings', mocked_settings)
slide_controller = SlideController(None)
slide_controller.service_item = None
slide_controller.is_live = True
slide_controller._reset_blank = MagicMock()
slide_controller.preview_widget = MagicMock()
slide_controller.preview_display = MagicMock()
slide_controller.enable_tool_bar = MagicMock()
slide_controller.on_controller_size_changed = MagicMock()
slide_controller.on_media_start = MagicMock()
slide_controller.on_media_close = MagicMock()
slide_controller.slide_selected = MagicMock()
slide_controller.set_hide_mode = MagicMock()
slide_controller.new_song_menu = MagicMock()
slide_controller.on_stop_loop = MagicMock()
slide_controller.info_label = MagicMock()
slide_controller.song_menu = MagicMock()
slide_controller.displays = [MagicMock()]
slide_controller.toolbar = MagicMock()
slide_controller.split = 0
slide_controller.type_prefix = 'test'
slide_controller.screen_capture = 'old_capture'
# WHEN: _process_item is called with is_reloading=True
slide_controller._process_item(mocked_media_item, 0, is_reloading=True)
# THEN: set_hide_mode should not be called
slide_controller.set_hide_mode.assert_not_called()
def test_live_stolen_focus_shortcuts(settings):
"""
Test that all the needed shortcuts are available in scenarios where Live has stolen focus.