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()
return result
def run(self, args):
def run(self, args, app):
"""
Run the OpenLP application.
@ -97,7 +97,7 @@ class OpenLP(QtCore.QObject, LogMixin):
args.remove('OpenLP')
self.args.extend(args)
# Decide how many screens we have and their size
screens = ScreenList.create(QtWidgets.QApplication.desktop())
screens = ScreenList.create(app)
# First time checks in settings
has_run_wizard = self.settings.value('core/has run wizard')
if not has_run_wizard:
@ -435,4 +435,4 @@ def main():
log.debug('Could not find translators.')
if args and not args.no_error_form:
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.
"""
import logging
import copy
from functools import cmp_to_key
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
"""
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
def from_dict(cls, screen_dict):
@ -147,6 +155,13 @@ class Screen(object):
screen_dict['custom_geometry']['width'],
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):
"""
@ -202,37 +217,66 @@ class ScreenList(metaclass=Singleton):
return None
@classmethod
def create(cls, desktop):
def create(cls, application):
"""
Initialise the screen list.
:param desktop: A QDesktopWidget object.
:param desktop: A QApplication object.
"""
screen_list = cls()
screen_list.desktop = desktop
screen_list.desktop.resized.connect(screen_list.on_screen_resolution_changed)
screen_list.desktop.screenCountChanged.connect(screen_list.on_screen_count_changed)
screen_list.desktop.primaryScreenChanged.connect(screen_list.on_primary_screen_changed)
screen_list.application = application
screen_list.application.primaryScreenChanged.connect(screen_list.on_primary_screen_changed)
screen_list.application.screenAdded.connect(screen_list.on_screen_added)
screen_list.application.screenRemoved.connect(screen_list.on_screen_removed)
screen_list.update_screens()
cls.settings = Registry().get('settings')
screen_list.load_screen_settings()
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):
"""
Loads the screen size and the screen number from the settings.
"""
screen_settings = self.settings.value('core/screens')
if screen_settings:
need_new_display_screen = False
for number, screen_dict in screen_settings.items():
# Sometimes this loads as a string instead of an int
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)
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:
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):
"""
@ -285,14 +329,15 @@ class ScreenList(metaclass=Singleton):
if can_save:
self.save_screen_settings()
def has_screen(self, number):
def has_screen(self, screen_dict):
"""
Confirms a screen is known.
:param number: The screen number (int).
:param screen_dict: The dict descrebing the screen.
"""
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 False
@ -316,39 +361,44 @@ class ScreenList(metaclass=Singleton):
else:
return 0
self.screens = []
os_screens = QtWidgets.QApplication.screens()
os_screens = self.application.screens()
os_screens.sort(key=cmp_to_key(_screen_compare))
for number, screen in enumerate(os_screens):
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``
The number of the screen, which size has changed.
:param changed_screen: The screen which has been plugged.
"""
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:
if number == screen.number:
screen.geometry = self.desktop.screenGeometry(number)
screen.is_primary = self.desktop.primaryScreen() == number
Registry().execute('config_screen_changed')
break
def on_screen_count_changed(self, changed_screen=None):
"""
Called when a screen has been added or removed.
``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.
# once the screen that must be removed has been found, update numbering
if removed_screen_number >= 0:
screen.number -= 1
# find the screen that is removed
if removed_screen.geometry() == screen.geometry:
removed_screen_number = screen.number
removed_screen_is_display = self.screens[removed_screen_number].is_display
self.screens.pop(removed_screen_number)
if removed_screen_is_display:
self.find_new_display_screen()
Registry().execute('config_screen_changed')
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
"""
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')

View File

@ -521,6 +521,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# Media Manager
self.media_tool_box.currentChanged.connect(self.on_media_tool_box_changed)
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
Registry().register_function('theme_update_global', self.default_theme_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.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 (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):
"""

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.
"""
size = self.screens.current.display_geometry.size()
if self.is_live and 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):
"""

View File

@ -60,8 +60,10 @@ class AspectRatioLayout(QtWidgets.QLayout):
:param float aspect_ratio: The aspect ratio to set
"""
# TODO: Update the layout/widget if this changes
self._aspect_ratio = aspect_ratio
# Update the layout/widget
geo = self.geometry()
self.setGeometry(geo)
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
"""
# GIVEN: Mocked desktop
mocked_desktop = MagicMock()
mocked_desktop.screenCount.return_value = 2
mocked_desktop.primaryScreen.return_value = 0
mocked_screens.return_value = [
MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)}),
MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
]
# GIVEN: Mocked application
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
mocked_application.primaryScreen.return_value = mocked_screen1
# WHEN: create() is called
screen_list = ScreenList.create(mocked_desktop)
screen_list = ScreenList.create(mocked_application)
# THEN: The correct screens have been set up
assert screen_list.screens[0].number == 0

View File

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

View File

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