Remove and add screens without reloading screens. Do not show screen-change-popup when the settings form is visible.

This commit is contained in:
Tomas Groth 2020-07-21 20:05:59 +00:00 committed by Tim Bentley
parent eac44f4f24
commit 73221cef9b
8 changed files with 138 additions and 65 deletions

View File

@ -83,7 +83,7 @@ class OpenLP(QtCore.QObject, LogMixin):
self.server.close_server() self.server.close_server()
return result return result
def run(self, args): def run(self, args, app):
""" """
Run the OpenLP application. Run the OpenLP application.
@ -97,7 +97,7 @@ class OpenLP(QtCore.QObject, LogMixin):
args.remove('OpenLP') args.remove('OpenLP')
self.args.extend(args) self.args.extend(args)
# Decide how many screens we have and their size # Decide how many screens we have and their size
screens = ScreenList.create(QtWidgets.QApplication.desktop()) screens = ScreenList.create(app)
# First time checks in settings # First time checks in settings
has_run_wizard = self.settings.value('core/has run wizard') has_run_wizard = self.settings.value('core/has run wizard')
if not has_run_wizard: if not has_run_wizard:
@ -435,4 +435,4 @@ def main():
log.debug('Could not find translators.') log.debug('Could not find translators.')
if args and not args.no_error_form: if args and not args.no_error_form:
sys.excepthook = app.hook_exception sys.excepthook = app.hook_exception
sys.exit(app.run(qt_args)) sys.exit(app.run(qt_args, application))

View File

@ -23,6 +23,7 @@ The :mod:`screen` module provides management functionality for a machines'
displays. displays.
""" """
import logging import logging
import copy
from functools import cmp_to_key from functools import cmp_to_key
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
@ -77,7 +78,14 @@ class Screen(object):
""" """
Returns the geometry to use when displaying. This property decides between the native and custom geometries Returns the geometry to use when displaying. This property decides between the native and custom geometries
""" """
return self.custom_geometry or self.geometry # If custom geometry is used, convert to absolute position
if self.custom_geometry:
adjusted_custom_geometry = copy.deepcopy(self.custom_geometry)
adjusted_custom_geometry.moveTo(self.geometry.x() + adjusted_custom_geometry.x(),
self.geometry.y() + adjusted_custom_geometry.y())
return adjusted_custom_geometry
else:
return self.geometry
@classmethod @classmethod
def from_dict(cls, screen_dict): def from_dict(cls, screen_dict):
@ -147,6 +155,13 @@ class Screen(object):
screen_dict['custom_geometry']['width'], screen_dict['custom_geometry']['width'],
screen_dict['custom_geometry']['height']) screen_dict['custom_geometry']['height'])
def on_geometry_changed(self, geometry):
"""
Callback function for when the screens geometry changes
"""
self.geometry = geometry
Registry().execute('config_screen_changed')
class ScreenList(metaclass=Singleton): class ScreenList(metaclass=Singleton):
""" """
@ -202,37 +217,66 @@ class ScreenList(metaclass=Singleton):
return None return None
@classmethod @classmethod
def create(cls, desktop): def create(cls, application):
""" """
Initialise the screen list. Initialise the screen list.
:param desktop: A QDesktopWidget object. :param desktop: A QApplication object.
""" """
screen_list = cls() screen_list = cls()
screen_list.desktop = desktop screen_list.application = application
screen_list.desktop.resized.connect(screen_list.on_screen_resolution_changed) screen_list.application.primaryScreenChanged.connect(screen_list.on_primary_screen_changed)
screen_list.desktop.screenCountChanged.connect(screen_list.on_screen_count_changed) screen_list.application.screenAdded.connect(screen_list.on_screen_added)
screen_list.desktop.primaryScreenChanged.connect(screen_list.on_primary_screen_changed) screen_list.application.screenRemoved.connect(screen_list.on_screen_removed)
screen_list.update_screens() screen_list.update_screens()
cls.settings = Registry().get('settings') cls.settings = Registry().get('settings')
screen_list.load_screen_settings() screen_list.load_screen_settings()
return screen_list return screen_list
def find_new_display_screen(self):
"""
If more than 1 screen, set first non-primary screen to display, otherwise just set the available screen as
display.
"""
if len(self) > 1:
for screen in self:
if not screen.is_primary:
screen.is_display = True
break
else:
self[0].is_display = True
def load_screen_settings(self): def load_screen_settings(self):
""" """
Loads the screen size and the screen number from the settings. Loads the screen size and the screen number from the settings.
""" """
screen_settings = self.settings.value('core/screens') screen_settings = self.settings.value('core/screens')
if screen_settings: if screen_settings:
need_new_display_screen = False
for number, screen_dict in screen_settings.items(): for number, screen_dict in screen_settings.items():
# Sometimes this loads as a string instead of an int # Sometimes this loads as a string instead of an int
number = int(number) number = int(number)
if self.has_screen(number): # Compare geometry, primarity of screen from settings with avilable screens
if self.has_screen(screen_dict):
# If match was found, we're all happy, update with custom geometry, display info, if available
self[number].update(screen_dict) self[number].update(screen_dict)
else: else:
self.screens.append(Screen.from_dict(screen_dict)) # If no match, ignore this screen, also need to find new display screen if the discarded screen was
# marked as such.
if screen_dict['is_display']:
need_new_display_screen = True
if need_new_display_screen:
QtWidgets.QMessageBox.warning(None, translate('OpenLP.Screen',
'Screen settings and screen setup is not the same'),
translate('OpenLP.Screen',
'There is a mismatch between screens and screen settings. '
'OpenLP will try to automatically select a display screen, but '
'you should consider updating the screen settings.'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
self.find_new_display_screen()
else: else:
self[len(self) - 1].is_display = True # if no settings we need to set a display
self.find_new_display_screen()
def save_screen_settings(self): def save_screen_settings(self):
""" """
@ -285,14 +329,15 @@ class ScreenList(metaclass=Singleton):
if can_save: if can_save:
self.save_screen_settings() self.save_screen_settings()
def has_screen(self, number): def has_screen(self, screen_dict):
""" """
Confirms a screen is known. Confirms a screen is known.
:param number: The screen number (int). :param screen_dict: The dict descrebing the screen.
""" """
for screen in self.screens: for screen in self.screens:
if screen.number == number: if screen.to_dict()['geometry'] == screen_dict['geometry'] \
and screen.is_primary == screen_dict['is_primary']:
return True return True
return False return False
@ -316,39 +361,44 @@ class ScreenList(metaclass=Singleton):
else: else:
return 0 return 0
self.screens = [] self.screens = []
os_screens = QtWidgets.QApplication.screens() os_screens = self.application.screens()
os_screens.sort(key=cmp_to_key(_screen_compare)) os_screens.sort(key=cmp_to_key(_screen_compare))
for number, screen in enumerate(os_screens): for number, screen in enumerate(os_screens):
self.screens.append( self.screens.append(
Screen(number, screen.geometry(), is_primary=self.desktop.primaryScreen() == number)) Screen(number, screen.geometry(), is_primary=self.application.primaryScreen() == screen))
screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
def on_screen_resolution_changed(self, number): def on_screen_added(self, changed_screen):
""" """
Called when the resolution of a screen has changed. Called when a screen has been added
``number`` :param changed_screen: The screen which has been plugged.
The number of the screen, which size has changed.
""" """
log.info('screen_resolution_changed {number:d}'.format(number=number)) number = len(self.screens)
self.screens.append(Screen(number, changed_screen.geometry(),
is_primary=self.application.primaryScreen() == changed_screen))
changed_screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
Registry().execute('config_screen_changed')
def on_screen_removed(self, removed_screen):
"""
Called when a screen has been removed.
:param changed_screen: The screen which has been unplugged.
"""
# Remove screens
removed_screen_number = -1
for screen in self.screens: for screen in self.screens:
if number == screen.number: # once the screen that must be removed has been found, update numbering
screen.geometry = self.desktop.screenGeometry(number) if removed_screen_number >= 0:
screen.is_primary = self.desktop.primaryScreen() == number screen.number -= 1
Registry().execute('config_screen_changed') # find the screen that is removed
break if removed_screen.geometry() == screen.geometry:
removed_screen_number = screen.number
def on_screen_count_changed(self, changed_screen=None): removed_screen_is_display = self.screens[removed_screen_number].is_display
""" self.screens.pop(removed_screen_number)
Called when a screen has been added or removed. if removed_screen_is_display:
self.find_new_display_screen()
``changed_screen``
The screen's number which has been (un)plugged.
"""
screen_count = self.desktop.screenCount()
log.info('screen_count_changed {count:d}'.format(count=screen_count))
# Update the list of screens
self.update_screens()
# Reload setting tabs to apply possible changes.
Registry().execute('config_screen_changed') Registry().execute('config_screen_changed')
def on_primary_screen_changed(self): def on_primary_screen_changed(self):
@ -356,5 +406,5 @@ class ScreenList(metaclass=Singleton):
The primary screen has changed, let's sort it out and then notify everyone The primary screen has changed, let's sort it out and then notify everyone
""" """
for screen in self.screens: for screen in self.screens:
screen.is_primary = self.desktop.primaryScreen() == screen.number screen.is_primary = self.desktop.primaryScreen().geometry() == screen.geometry
Registry().execute('config_screen_changed') Registry().execute('config_screen_changed')

View File

@ -521,6 +521,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# Media Manager # Media Manager
self.media_tool_box.currentChanged.connect(self.on_media_tool_box_changed) self.media_tool_box.currentChanged.connect(self.on_media_tool_box_changed)
self.application.set_busy_cursor() self.application.set_busy_cursor()
# Timestamp for latest screen-change-popup. Used to prevent spamming the user with popups
self.screen_change_timestamp = None
# Simple message boxes # Simple message boxes
Registry().register_function('theme_update_global', self.default_theme_changed) Registry().register_function('theme_update_global', self.default_theme_changed)
Registry().register_function('config_screen_changed', self.screen_changed) Registry().register_function('config_screen_changed', self.screen_changed)
@ -999,6 +1001,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.setFocus() self.setFocus()
self.activateWindow() self.activateWindow()
self.application.set_normal_cursor() 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 (datetime.now() - self.screen_change_timestamp).seconds > 5:
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.screen_change_timestamp = datetime.now()
def closeEvent(self, event): def closeEvent(self, event):
""" """

View File

@ -661,9 +661,16 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
""" """
Settings dialog has changed the screen size of adjust output and screen previews. Settings dialog has changed the screen size of adjust output and screen previews.
""" """
size = self.screens.current.display_geometry.size()
if self.is_live and self.displays: if self.is_live and self.displays:
for display in self.displays: for display in self.displays:
display.resize(self.screens.current.display_geometry.size()) display.resize(size)
old_preview_width = self.preview_display.size().width()
scale = old_preview_width / size.width()
new_preview_size = size * scale
self.ratio = self.screens.current.display_geometry.width() / self.screens.current.display_geometry.height()
self.preview_display.resize(new_preview_size)
self.slide_layout.set_aspect_ratio(self.ratio)
def __add_actions_to_widget(self, widget): def __add_actions_to_widget(self, widget):
""" """

View File

@ -60,8 +60,10 @@ class AspectRatioLayout(QtWidgets.QLayout):
:param float aspect_ratio: The aspect ratio to set :param float aspect_ratio: The aspect ratio to set
""" """
# TODO: Update the layout/widget if this changes
self._aspect_ratio = aspect_ratio self._aspect_ratio = aspect_ratio
# Update the layout/widget
geo = self.geometry()
self.setGeometry(geo)
aspect_ratio = property(get_aspect_ratio, set_aspect_ratio) aspect_ratio = property(get_aspect_ratio, set_aspect_ratio)

View File

@ -74,17 +74,15 @@ def test_create_screen_list(mocked_screens, settings):
""" """
Create the screen list Create the screen list
""" """
# GIVEN: Mocked desktop # GIVEN: Mocked application
mocked_desktop = MagicMock() mocked_application = MagicMock()
mocked_desktop.screenCount.return_value = 2 mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_desktop.primaryScreen.return_value = 0 mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_screens.return_value = [ mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)}), mocked_application.primaryScreen.return_value = mocked_screen1
MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
]
# WHEN: create() is called # WHEN: create() is called
screen_list = ScreenList.create(mocked_desktop) screen_list = ScreenList.create(mocked_application)
# THEN: The correct screens have been set up # THEN: The correct screens have been set up
assert screen_list.screens[0].number == 0 assert screen_list.screens[0].number == 0

View File

@ -47,7 +47,7 @@ def _create_mock_action(parent, name, **kwargs):
@pytest.yield_fixture() @pytest.yield_fixture()
def main_window(state, settings): def main_window(state, settings, mocked_qapp):
app = Registry().get('application') app = Registry().get('application')
app.set_busy_cursor = MagicMock() app.set_busy_cursor = MagicMock()
app.set_normal_cursor = MagicMock() app.set_normal_cursor = MagicMock()
@ -59,11 +59,13 @@ def main_window(state, settings):
mocked_add_toolbar_action.side_effect = _create_mock_action mocked_add_toolbar_action.side_effect = _create_mock_action
renderer_patcher = patch('openlp.core.display.render.Renderer') renderer_patcher = patch('openlp.core.display.render.Renderer')
renderer_patcher.start() renderer_patcher.start()
mocked_desktop = MagicMock() mocked_screen = MagicMock()
mocked_desktop.screenCount.return_value = 1 mocked_screen.geometry.return_value = QtCore.QRect(0, 0, 1024, 768)
mocked_desktop.screenGeometry.return_value = QtCore.QRect(0, 0, 1024, 768) mocked_qapp.screens = MagicMock()
mocked_desktop.primaryScreen.return_value = 1 mocked_qapp.screens.return_value = [mocked_screen]
ScreenList.create(mocked_desktop) mocked_qapp.primaryScreen = MagicMock()
mocked_qapp.primaryScreen.return_value = mocked_screen
ScreenList.create(mocked_qapp)
mainwindow = MainWindow() mainwindow = MainWindow()
yield mainwindow yield mainwindow
del mainwindow del mainwindow

View File

@ -38,14 +38,15 @@ from tests.utils.constants import RESOURCE_PATH
@pytest.yield_fixture() @pytest.yield_fixture()
def pdf_env(settings, mock_plugin): def pdf_env(settings, mock_plugin, mocked_qapp):
temp_folder_path = Path(mkdtemp()) temp_folder_path = Path(mkdtemp())
thumbnail_folder_path = Path(mkdtemp()) thumbnail_folder_path = Path(mkdtemp())
desktop = MagicMock() mocked_screen = MagicMock()
desktop.primaryScreen.return_value = SCREEN['primary'] mocked_screen.geometry.return_value = QtCore.QRect(0, 0, 1024, 768)
desktop.screenCount.return_value = SCREEN['number'] mocked_qapp.screens.return_value = [mocked_screen]
desktop.screenGeometry.return_value = SCREEN['size'] mocked_qapp.primaryScreen = MagicMock()
ScreenList.create(desktop) mocked_qapp.primaryScreen.return_value = mocked_screen
ScreenList.create(mocked_qapp)
yield settings, mock_plugin, temp_folder_path, thumbnail_folder_path yield settings, mock_plugin, temp_folder_path, thumbnail_folder_path
rmtree(thumbnail_folder_path) rmtree(thumbnail_folder_path)
rmtree(temp_folder_path) rmtree(temp_folder_path)