mirror of https://gitlab.com/openlp/openlp.git
Merge branch 'wayland-display-window-screenshot' into 'master'
RFC/Proposal: Fallback code for display screenshot code (used on '/main' Web Remote) Closes #1284 and #1405 See merge request openlp/openlp!582
This commit is contained in:
commit
4f7a2338e6
|
@ -75,10 +75,50 @@ def is_64bit_instance():
|
|||
return (sys.maxsize > 2**32)
|
||||
|
||||
|
||||
def is_xorg_server():
|
||||
__IS_WAYLAND_COMPOSITOR = None
|
||||
|
||||
|
||||
def is_wayland_compositor():
|
||||
"""
|
||||
Returns true if the Qt is running on X.org/XWayland display server (Linux/*nix)
|
||||
Returns true if the OpenLP/Qt instance is running in a Wayland compositor.
|
||||
NOTE: This checks is only about compositor. Returns True if the application is running in a Wayland compositor.
|
||||
Will also return True if application is running on X11/X.org mode in a Wayland compositor/desktop environment
|
||||
|
||||
:return: True if the OpenLP/Qt instance is running in a Wayland compositor, otherwise False
|
||||
"""
|
||||
global __IS_WAYLAND_COMPOSITOR
|
||||
if __IS_WAYLAND_COMPOSITOR is None:
|
||||
__IS_WAYLAND_COMPOSITOR = bool(os.getenv('WAYLAND_DISPLAY', False))
|
||||
return __IS_WAYLAND_COMPOSITOR
|
||||
|
||||
|
||||
def is_xwayland_server():
|
||||
"""
|
||||
Returns true if the OpenLP/Qt instance is running in a XWayland X11 server.
|
||||
NOTE: This will be True only if OpenLP is running on X11 mode on a XWayland server
|
||||
|
||||
:return: True if the OpenLP/Qt instance is running in a XWayland X11 server, otherwise False
|
||||
"""
|
||||
return is_wayland_compositor() and is_xorg_platform()
|
||||
|
||||
|
||||
def is_xorg_platform():
|
||||
"""
|
||||
Returns True if the Qt is running on X.org/XWayland platform (Linux/*nix).
|
||||
NOTE: This will return True if user is running the OpenLP as X.org application, but on a Wayland/XWayland
|
||||
environment.
|
||||
|
||||
:return: True if the Qt is running on X.org/XWayland display server (Linux/*nix), otherwise False.
|
||||
"""
|
||||
from PyQt5 import QtGui
|
||||
return QtGui.QGuiApplication.platformName() == 'xcb'
|
||||
|
||||
|
||||
def is_wayland_platform():
|
||||
"""
|
||||
Returns true if the OpenLP/Qt instance is running using Qt's Wayland platform and running in a Wayland compositor
|
||||
|
||||
:return: True if the OpenLP/Qt instance is running in a Wayland compositor, otherwise False
|
||||
"""
|
||||
from PyQt5 import QtGui
|
||||
return QtGui.QGuiApplication.platformName() == 'wayland'
|
||||
|
|
|
@ -112,6 +112,14 @@ class Registry(metaclass=Singleton):
|
|||
if event in self.functions_list and function in self.functions_list[event]:
|
||||
self.functions_list[event].remove(function)
|
||||
|
||||
def has_function(self, event):
|
||||
"""
|
||||
Returns whether there's any hander associated with the event.
|
||||
|
||||
:param event: The function to be checked
|
||||
"""
|
||||
return event in self.functions_list
|
||||
|
||||
def execute(self, event, *args, **kwargs):
|
||||
"""
|
||||
Execute all the handlers associated with the event and return an array of results.
|
||||
|
@ -122,7 +130,7 @@ class Registry(metaclass=Singleton):
|
|||
"""
|
||||
log.debug(f'Running function {event}')
|
||||
results = []
|
||||
if event in self.functions_list:
|
||||
if self.has_function(event):
|
||||
for function in self.functions_list[event]:
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
|
|
|
@ -195,6 +195,7 @@ class Settings(QtCore.QSettings):
|
|||
'advanced/single click preview': False,
|
||||
'advanced/single click service preview': False,
|
||||
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
'advanced/prefer windowed screen capture': False,
|
||||
'advanced/search as type': True,
|
||||
'advanced/ui_theme_name': UiThemes.Automatic,
|
||||
'advanced/delete service item confirmation': False,
|
||||
|
|
|
@ -40,7 +40,8 @@ class Screen(object):
|
|||
A Python representation of a screen
|
||||
"""
|
||||
|
||||
def __init__(self, number=None, geometry=None, custom_geometry=None, is_primary=False, is_display=False):
|
||||
def __init__(self, number=None, geometry=None, custom_geometry=None, is_primary=False, is_display=False,
|
||||
device_pixel_ratio=1.0, raw_screen=None):
|
||||
"""
|
||||
Set up the screen object
|
||||
|
||||
|
@ -53,12 +54,16 @@ class Screen(object):
|
|||
so left = 0, top = 0 refers to the top left of that screen.
|
||||
:param bool is_primary: Whether or not this screen is the primary screen
|
||||
:param bool is_display: Whether or not this screen should be used to display lyrics
|
||||
:param float device_pixel_ratio: Device pixel ratio of screen
|
||||
:param raw_screen: Raw screen (Qt/QScreen) object
|
||||
"""
|
||||
self.number = int(number)
|
||||
self.geometry = geometry
|
||||
self.custom_geometry = custom_geometry
|
||||
self.is_primary = is_primary
|
||||
self.is_display = is_display
|
||||
self.device_pixel_ratio = device_pixel_ratio
|
||||
self.raw_screen = raw_screen
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
|
@ -158,13 +163,29 @@ class Screen(object):
|
|||
screen_dict['custom_geometry']['width'],
|
||||
screen_dict['custom_geometry']['height'])
|
||||
|
||||
def on_geometry_changed(self, geometry):
|
||||
def on_geometry_changed(self, geometry, device_pixel_ratio):
|
||||
"""
|
||||
Callback function for when the screens geometry changes
|
||||
"""
|
||||
self.geometry = geometry
|
||||
# Device pixel ratio is implicity changed due to geometry change
|
||||
self.device_pixel_ratio = device_pixel_ratio
|
||||
ConfigScreenChangedEmitter().emit()
|
||||
|
||||
def try_grab_screen_part(self, x, y, width, height):
|
||||
"""
|
||||
Tries to grab a screenshot using the underlying display object
|
||||
"""
|
||||
try:
|
||||
if self.raw_screen:
|
||||
# windowId = 0 means to grab entire screen. See: https://doc.qt.io/qt-6/qscreen.html#grabWindow
|
||||
return self.raw_screen.grabWindow(0, x, y, width, height)
|
||||
else:
|
||||
return None
|
||||
except BaseException as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
|
||||
class ScreenList(metaclass=Singleton):
|
||||
"""
|
||||
|
@ -199,7 +220,7 @@ class ScreenList(metaclass=Singleton):
|
|||
return len(self.screens)
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
def current(self) -> Screen:
|
||||
"""
|
||||
Return the first "current" desktop
|
||||
|
||||
|
@ -374,9 +395,12 @@ class ScreenList(metaclass=Singleton):
|
|||
os_screens = self.application.screens()
|
||||
os_screens.sort(key=cmp_to_key(_screen_compare))
|
||||
for number, screen in enumerate(os_screens):
|
||||
device_pixel_ratio = screen.devicePixelRatio()
|
||||
self.screens.append(
|
||||
Screen(number, screen.geometry(), is_primary=self.application.primaryScreen() == screen))
|
||||
screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
|
||||
Screen(number, screen.geometry(), is_primary=self.application.primaryScreen() == screen,
|
||||
device_pixel_ratio=device_pixel_ratio, raw_screen=screen))
|
||||
screen.geometryChanged.connect(lambda geometry: self.screens[-1]
|
||||
.on_geometry_changed(geometry, screen.devicePixelRatio()))
|
||||
|
||||
def on_screen_added(self, changed_screen):
|
||||
"""
|
||||
|
@ -391,11 +415,13 @@ class ScreenList(metaclass=Singleton):
|
|||
if is_primary:
|
||||
for screen in self.screens:
|
||||
screen.is_primary = False
|
||||
|
||||
device_pixel_ratio = changed_screen.devicePixelRatio()
|
||||
self.screens.append(Screen(number, changed_screen.geometry(),
|
||||
is_primary=self.application.primaryScreen() == changed_screen))
|
||||
is_primary=self.application.primaryScreen() == changed_screen,
|
||||
device_pixel_ratio=device_pixel_ratio, raw_screen=changed_screen))
|
||||
self.find_new_display_screen()
|
||||
changed_screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
|
||||
changed_screen.geometryChanged.connect(lambda geometry: self.screens[-1]
|
||||
.on_geometry_changed(geometry, changed_screen.devicePixelRatio()))
|
||||
ConfigScreenChangedEmitter().emit()
|
||||
|
||||
def on_screen_removed(self, removed_screen):
|
||||
|
|
|
@ -571,3 +571,15 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
|
|||
Set an alert
|
||||
"""
|
||||
self.run_javascript('Display.alert("{text}", {settings});'.format(text=text, settings=settings))
|
||||
|
||||
@QtCore.pyqtSlot(result='QPixmap')
|
||||
def _grab_screenshot_safe_signal(self):
|
||||
return self.save_screenshot()
|
||||
|
||||
def grab_screenshot_safe(self):
|
||||
# Using internal Qt's messaging/event system to invoke the function.
|
||||
# Usually we would need to use PyQt's signals, but they aren't blocking. So we had to resort to this solution,
|
||||
# which use a less-documented Qt mechanism to invoke the signal in a blocking way.
|
||||
return QtCore.QMetaObject.invokeMethod(self, '_grab_screenshot_safe_signal',
|
||||
QtCore.Qt.ConnectionType.BlockingQueuedConnection,
|
||||
QtCore.Q_RETURN_ARG('QPixmap'))
|
||||
|
|
|
@ -100,6 +100,9 @@ class AdvancedTab(SettingsTab):
|
|||
self.allow_transparent_display_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.allow_transparent_display_check_box.setObjectName('allow_transparent_display_check_box')
|
||||
self.display_workaround_layout.addWidget(self.allow_transparent_display_check_box)
|
||||
self.prefer_windowed_capture_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.prefer_windowed_capture_check_box.setObjectName('prefer_windowed_capture_check_box')
|
||||
self.display_workaround_layout.addWidget(self.prefer_windowed_capture_check_box)
|
||||
self.left_layout.addWidget(self.display_workaround_group_box)
|
||||
# Proxies
|
||||
self.proxy_widget = ProxyWidget(self.right_column)
|
||||
|
@ -135,6 +138,8 @@ class AdvancedTab(SettingsTab):
|
|||
self.alternate_rows_check_box.setText(translate('OpenLP.AdvancedTab', 'Use alternating row colours in lists'))
|
||||
self.allow_transparent_display_check_box.setText(
|
||||
translate('OpenLP.AdvancedTab', 'Disable display transparency'))
|
||||
self.prefer_windowed_capture_check_box.setText(
|
||||
translate('OpenLP.AdvancedTab', 'Prefer window capture instead of screen capture'))
|
||||
self.proxy_widget.retranslate_ui()
|
||||
|
||||
def load(self):
|
||||
|
@ -148,6 +153,8 @@ class AdvancedTab(SettingsTab):
|
|||
self.alternate_rows_check_box.setChecked(self.settings.value('advanced/alternate rows'))
|
||||
self.alternate_rows_check_box.blockSignals(False)
|
||||
self.allow_transparent_display_check_box.setChecked(self.settings.value('advanced/disable transparent display'))
|
||||
self.prefer_windowed_capture_check_box.setChecked(
|
||||
self.settings.value('advanced/prefer windowed screen capture'))
|
||||
self.data_directory_copy_check_box.hide()
|
||||
self.new_data_directory_has_files_label.hide()
|
||||
self.data_directory_cancel_button.hide()
|
||||
|
@ -163,6 +170,8 @@ class AdvancedTab(SettingsTab):
|
|||
"""
|
||||
self.settings.setValue('advanced/disable transparent display',
|
||||
self.allow_transparent_display_check_box.isChecked())
|
||||
self.settings.setValue('advanced/prefer windowed screen capture',
|
||||
self.prefer_windowed_capture_check_box.isChecked())
|
||||
self.settings.setValue('advanced/ignore aspect ratio', self.ignore_aspect_ratio_check_box.isChecked())
|
||||
if self.x11_bypass_check_box.isChecked() != self.settings.value('advanced/x11 bypass wm'):
|
||||
self.settings.setValue('advanced/x11 bypass wm', self.x11_bypass_check_box.isChecked())
|
||||
|
|
|
@ -33,6 +33,7 @@ from openlp.core.common import SlideLimits
|
|||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.platform import is_macosx, is_wayland_compositor
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.common.utils import wait_for
|
||||
from openlp.core.display.screens import ScreenList
|
||||
|
@ -1234,6 +1235,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
|||
Gets an image of the display screen and updates the preview frame.
|
||||
"""
|
||||
display_image = self._capture_maindisplay()
|
||||
# display_image.setDevicePixelRatio(self.preview_display.devicePixelRatio())
|
||||
base64_image = image_to_byte(display_image)
|
||||
self.screen_capture = base64_image
|
||||
self.preview_display.set_single_image_data('#000', base64_image)
|
||||
|
@ -1243,11 +1245,95 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
|||
Creates an image of the current screen.
|
||||
"""
|
||||
self.log_debug('_capture_maindisplay {text}'.format(text=self.screens.current))
|
||||
win_id = QtWidgets.QApplication.desktop().winId()
|
||||
screen = QtWidgets.QApplication.primaryScreen()
|
||||
rect = ScreenList().current.display_geometry
|
||||
win_image = screen.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height())
|
||||
win_image.setDevicePixelRatio(self.preview_display.devicePixelRatio())
|
||||
# Wayland needs screenshot fallback even when OpenLP is running on X11/xcb mode.
|
||||
fallback_to_windowed = is_wayland_compositor() or \
|
||||
self.settings.value('advanced/prefer windowed screen capture')
|
||||
if not fallback_to_windowed:
|
||||
# Check if display screen is outside real screen bounds
|
||||
# OpenLP can't take reliable screenshots when any of these conditions happens
|
||||
# See last warning in grabWindow at https://doc.qt.io/qt-6/qscreen.html#grabWindow
|
||||
display_rect = ScreenList().current.display_geometry
|
||||
screen_rect = ScreenList().current.geometry
|
||||
display_above_horizontal = display_rect.left() < screen_rect.left()
|
||||
display_above_vertical = display_rect.top() < screen_rect.top()
|
||||
display_beyond_horizontal = (display_rect.left() + display_rect.width() >
|
||||
screen_rect.left() + screen_rect.width())
|
||||
display_beyond_vertical = (display_rect.top() + display_rect.height() >
|
||||
screen_rect.top() + screen_rect.height())
|
||||
fallback_to_windowed = display_above_horizontal or display_above_vertical \
|
||||
or display_beyond_horizontal or display_beyond_vertical
|
||||
if fallback_to_windowed:
|
||||
if self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay) or self.service_item.is_media() or \
|
||||
self.service_item.is_command():
|
||||
if self.service_item.is_command():
|
||||
# Attempting to get screenshot from command handler
|
||||
service_item_name = self.service_item.name.lower()
|
||||
event_name = '{text}_attempt_screenshot'.format(text=service_item_name)
|
||||
if Registry().has_function(event_name):
|
||||
results = Registry().execute(event_name, [self.service_item, self.selected_row])
|
||||
if len(results):
|
||||
succedded, binary_data = results[0]
|
||||
if succedded and binary_data:
|
||||
self.log_debug('_capture_maindisplay using window capture from {name}'
|
||||
.format(name=service_item_name))
|
||||
return binary_data
|
||||
# Falling back to desktop capture, maybe it works
|
||||
self.log_debug('_capture_maindisplay cannot do window capture, trying to get screenshot from screen'
|
||||
' anyway')
|
||||
return self._capture_maindisplay_desktop()
|
||||
else:
|
||||
self.log_debug('_capture_maindisplay falling back to window capture')
|
||||
return self._capture_maindisplay_window()
|
||||
else:
|
||||
return self._capture_maindisplay_desktop()
|
||||
|
||||
def _capture_maindisplay_desktop(self):
|
||||
# At least on macOS, there's a crash when opening /main Remote API endpoint,
|
||||
# due to macOS requiring the screenshot code to be called on the main thread.
|
||||
if is_macosx():
|
||||
self.log_debug('_capture_maindisplay_desktop macos thread-safe call')
|
||||
return self._capture_maindisplay_desktop_mainthread_safe()
|
||||
else:
|
||||
self.log_debug('_capture_maindisplay_desktop default call')
|
||||
return self._capture_maindisplay_desktop_signal()
|
||||
|
||||
@QtCore.pyqtSlot(result='QPixmap')
|
||||
def _capture_maindisplay_desktop_signal(self):
|
||||
current_screen = ScreenList().current
|
||||
display_rect = current_screen.display_geometry
|
||||
screen_rect = current_screen.geometry
|
||||
# As we capture using current screen object, we need relative coordinates.
|
||||
# (OpenLP is storing screen positions like a big one-screen, and we can't change this without
|
||||
# disrupting existing users)
|
||||
relative_x = display_rect.x() - screen_rect.x()
|
||||
relative_y = display_rect.y() - screen_rect.y()
|
||||
width = display_rect.width()
|
||||
height = display_rect.height()
|
||||
win_image = current_screen.try_grab_screen_part(relative_x, relative_y, width, height)
|
||||
return win_image
|
||||
|
||||
def _capture_maindisplay_desktop_mainthread_safe(self):
|
||||
# Using internal Qt's messaging/event system to invoke the function.
|
||||
# Usually we would need to use PyQt's signals, but they aren't blocking. So we had to resort to this solution,
|
||||
# which use a less-documented Qt mechanism to invoke the signal in a blocking way.
|
||||
return QtCore.QMetaObject.invokeMethod(self, '_capture_maindisplay_desktop_signal',
|
||||
QtCore.Qt.ConnectionType.BlockingQueuedConnection,
|
||||
QtCore.Q_RETURN_ARG('QPixmap'))
|
||||
|
||||
def _capture_maindisplay_window(self):
|
||||
win_image = None
|
||||
for display in self.displays:
|
||||
if display.is_display:
|
||||
if display.hide_mode == HideMode.Screen or display.hide_mode == HideMode.Blank:
|
||||
# Sending a black image to avoid artifacts
|
||||
size = display.size()
|
||||
win_image = QtGui.QPixmap(size)
|
||||
win_image.fill(QtGui.QColorConstants.Black)
|
||||
else:
|
||||
win_image = display.grab_screenshot_safe()
|
||||
if win_image:
|
||||
win_image.setDevicePixelRatio(self.preview_display.devicePixelRatio())
|
||||
break
|
||||
return win_image
|
||||
|
||||
def is_slide_loaded(self):
|
||||
|
|
|
@ -279,6 +279,16 @@ class Controller(object):
|
|||
return
|
||||
self.doc.poll_slidenumber(self.is_live, self.hide_mode)
|
||||
|
||||
def attempt_screenshot(self, index):
|
||||
"""
|
||||
Tries to perform a live screenshot when visible service item uses ProvidesOwnDisplay flag.
|
||||
|
||||
:returns: A tuple: whether the request succedded, and then the image.
|
||||
"""
|
||||
if self.is_live:
|
||||
return self.doc.attempt_screenshot(index)
|
||||
return (False, None)
|
||||
|
||||
|
||||
class MessageListener(object):
|
||||
"""
|
||||
|
@ -310,6 +320,7 @@ class MessageListener(object):
|
|||
Registry().register_function('presentations_slide', self.slide)
|
||||
Registry().register_function('presentations_blank', self.blank)
|
||||
Registry().register_function('presentations_unblank', self.unblank)
|
||||
Registry().register_function('presentations_attempt_screenshot', self.attempt_live_screenshot)
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.setInterval(500)
|
||||
self.timer.timeout.connect(self.timeout)
|
||||
|
@ -487,3 +498,13 @@ class MessageListener(object):
|
|||
Poll occasionally to check which slide is currently displayed so the slidecontroller view can be updated.
|
||||
"""
|
||||
self.live_handler.poll()
|
||||
|
||||
def attempt_live_screenshot(self, message):
|
||||
"""
|
||||
Tries to perform a live screenshot when visible service item uses ProvidesOwnDisplay flag.
|
||||
|
||||
:returns: A tuple: whether the request succedded, and then the image.
|
||||
"""
|
||||
current_row = message[1]
|
||||
result = self.live_handler.attempt_screenshot(current_row)
|
||||
return result
|
||||
|
|
|
@ -86,6 +86,10 @@ class PresentationDocument(object):
|
|||
``get_thumbnail_path(slide_no, check_exists)``
|
||||
Returns a path to an image containing a preview for the requested slide
|
||||
|
||||
``attempt_screenshot()``
|
||||
Attemps to take a screenshot from the presentation window. Returns a tuple with whether it succedded and
|
||||
the result image.
|
||||
|
||||
"""
|
||||
def __init__(self, controller, document_path):
|
||||
"""
|
||||
|
@ -386,6 +390,9 @@ class PresentationDocument(object):
|
|||
self._sha256_file_hash = sha256_file_hash(self.file_path)
|
||||
return self._sha256_file_hash
|
||||
|
||||
def attempt_screenshot(self, index):
|
||||
return (False, None)
|
||||
|
||||
|
||||
class PresentationList(metaclass=Singleton):
|
||||
"""
|
||||
|
|
|
@ -21,16 +21,19 @@
|
|||
"""
|
||||
Package to test the openlp.core.ui.slidecontroller package.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import datetime
|
||||
|
||||
from unittest.mock import MagicMock, patch, sentinel
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from openlp.core.lib.serviceitem import ServiceItem
|
||||
from pytest import mark
|
||||
|
||||
from openlp.core.state import State
|
||||
from openlp.core.common.enum import ServiceItemType
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib import ItemCapabilities, ServiceItemAction
|
||||
from openlp.core.lib.serviceitem import ServiceItem
|
||||
from openlp.core.ui import HideMode
|
||||
from openlp.core.ui.slidecontroller import NON_TEXT_MENU, WIDE_MENU, NARROW_MENU, InfoLabel, LiveController, \
|
||||
PreviewController, SlideController
|
||||
|
@ -1432,36 +1435,200 @@ def test_display_maindisplay(mocked_image_to_byte, registry):
|
|||
slide_controller.preview_display.set_single_image_data.assert_called_once_with('#000', 'placeholder bytified')
|
||||
|
||||
|
||||
CaptureMainDisplayMockReturn = namedtuple('CaptureMainDisplayMockReturn', ['slide_controller', 'mocked_primary_screen',
|
||||
'windowed_screenshot_mock',
|
||||
'mocked_screenlist_instance'])
|
||||
|
||||
|
||||
def _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application, mocked_is_wayland_compositor):
|
||||
mocked_is_wayland_compositor.return_value = False
|
||||
slide_controller = SlideController(None)
|
||||
windowed_screenshot_mock = QtGui.QPixmap(64, 33)
|
||||
display_mock = MagicMock(grab_screenshot_safe=MagicMock(return_value=windowed_screenshot_mock), is_display=True)
|
||||
slide_controller.displays = [display_mock]
|
||||
slide_controller.service_item = ServiceItem(None)
|
||||
mocked_geometry = MagicMock(
|
||||
x=MagicMock(return_value=geometry[1][0]),
|
||||
y=MagicMock(return_value=geometry[1][1]),
|
||||
left=MagicMock(return_value=geometry[1][0]),
|
||||
top=MagicMock(return_value=geometry[1][1]),
|
||||
width=MagicMock(return_value=geometry[1][2]),
|
||||
height=MagicMock(return_value=geometry[1][3])
|
||||
)
|
||||
mocked_display_geometry = MagicMock(
|
||||
x=MagicMock(return_value=geometry[0][0]),
|
||||
y=MagicMock(return_value=geometry[0][1]),
|
||||
left=MagicMock(return_value=geometry[0][0]),
|
||||
top=MagicMock(return_value=geometry[0][1]),
|
||||
width=MagicMock(return_value=geometry[0][2]),
|
||||
height=MagicMock(return_value=geometry[0][3])
|
||||
)
|
||||
mocked_screenlist_instance = MagicMock()
|
||||
mocked_screenlist.return_value = mocked_screenlist_instance
|
||||
mocked_screenlist_instance.current = MagicMock(display_geometry=mocked_display_geometry, geometry=mocked_geometry)
|
||||
mocked_primary_screen = MagicMock()
|
||||
mocked_application.primaryScreen = MagicMock(return_value=mocked_primary_screen)
|
||||
slide_controller.preview_display = MagicMock()
|
||||
return CaptureMainDisplayMockReturn(mocked_primary_screen=mocked_primary_screen, slide_controller=slide_controller,
|
||||
windowed_screenshot_mock=windowed_screenshot_mock,
|
||||
mocked_screenlist_instance=mocked_screenlist_instance)
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')
|
||||
@patch(u'openlp.core.ui.slidecontroller.ScreenList')
|
||||
@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication')
|
||||
def test__capture_maindisplay(mocked_application, mocked_screenlist, mocked_image_to_byte, registry):
|
||||
@patch(u'openlp.core.ui.slidecontroller.is_wayland_compositor')
|
||||
@mark.parametrize('geometry', [[[34, 67, 77, 42], [0, 0, 800, 600]]])
|
||||
def test__capture_maindisplay(mocked_is_wayland_compositor, mocked_application, mocked_screenlist,
|
||||
mocked_image_to_byte, geometry, registry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay method
|
||||
"""
|
||||
# GIVEN: A mocked slide controller, with mocked functions
|
||||
slide_controller = SlideController(None)
|
||||
mocked_display_geometry = MagicMock(
|
||||
x=MagicMock(return_value=34),
|
||||
y=MagicMock(return_value=67),
|
||||
width=MagicMock(return_value=77),
|
||||
height=MagicMock(return_value=42)
|
||||
)
|
||||
mocked_screenlist_instance = MagicMock()
|
||||
mocked_screenlist.return_value = mocked_screenlist_instance
|
||||
mocked_screenlist_instance.current = MagicMock(display_geometry=mocked_display_geometry)
|
||||
mocked_primary_screen = MagicMock()
|
||||
mocked_application.primaryScreen = MagicMock(return_value=mocked_primary_screen)
|
||||
mocked_application.desktop = MagicMock(return_value=MagicMock(
|
||||
winId=MagicMock(return_value=23)
|
||||
))
|
||||
slide_controller.preview_display = MagicMock()
|
||||
mocks = _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application,
|
||||
mocked_is_wayland_compositor)
|
||||
|
||||
# WHEN: _capture_maindisplay is called
|
||||
slide_controller._capture_maindisplay()
|
||||
mocks.slide_controller._capture_maindisplay()
|
||||
|
||||
# THEN: Screenshot should have been taken with correct winId and dimensions
|
||||
mocked_primary_screen.grabWindow.assert_called_once_with(23, 34, 67, 77, 42)
|
||||
mocks.mocked_screenlist_instance.current.try_grab_screen_part.assert_called_once()
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')
|
||||
@patch(u'openlp.core.ui.slidecontroller.ScreenList')
|
||||
@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication')
|
||||
@patch(u'openlp.core.ui.slidecontroller.is_wayland_compositor')
|
||||
@mark.parametrize('geometry', [[[34, 67, 77, 42], [0, 0, 800, 600]]])
|
||||
def test__capture_maindisplay_wayland_fallbacks_to_windowed(mocked_is_wayland_compositor, mocked_application,
|
||||
mocked_screenlist, mocked_image_to_byte, registry,
|
||||
geometry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay method fallbacks to windowed capture mode if user is running on Wayland compositor
|
||||
"""
|
||||
# GIVEN: A mocked slide controller, with mocked functions
|
||||
mocks = _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application,
|
||||
mocked_is_wayland_compositor)
|
||||
mocked_is_wayland_compositor.return_value = True
|
||||
|
||||
# WHEN: _capture_maindisplay is called
|
||||
photo = mocks.slide_controller._capture_maindisplay()
|
||||
|
||||
# THEN: Screenshot should have been taken from DisplayWindow and Screen should not be touched
|
||||
assert photo == mocks.windowed_screenshot_mock
|
||||
mocks.mocked_primary_screen.grabWindow.assert_not_called()
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')
|
||||
@patch(u'openlp.core.ui.slidecontroller.ScreenList')
|
||||
@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication')
|
||||
@patch(u'openlp.core.ui.slidecontroller.is_wayland_compositor')
|
||||
@mark.parametrize('geometry', [[[400, 400, 800, 600], [0, 0, 800, 600]], [[510, 0, 800, 600], [0, 0, 800, 600]],
|
||||
[[0, 320, 800, 600], [0, 0, 800, 600]], [[-200, -100, 800, 600], [0, 0, 800, 600]],
|
||||
[[-120, 0, 800, 600], [0, 0, 800, 600]], [[0, -140, 800, 600], [0, 0, 800, 600]],
|
||||
[[-150, 0, 800, 600], [-152, 0, 800, 600]], [[0, -210, 800, 600], [0, -200, 800, 600]],
|
||||
[[200, 0, 800, 600], [120, 0, 800, 600]], [[0, 230, 800, 600], [0, 110, 800, 600]]])
|
||||
def test__capture_maindisplay_offscreen_fallbacks_to_windowed(mocked_is_wayland_compositor, mocked_application,
|
||||
mocked_screenlist, mocked_image_to_byte, geometry,
|
||||
registry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay method fallbacks to windowed capture mode if user have a display
|
||||
above/beyond screen boundaries.
|
||||
"""
|
||||
# GIVEN: A mocked slide controller, with mocked functions and offscreen geometry
|
||||
mocks = _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application,
|
||||
mocked_is_wayland_compositor)
|
||||
mocked_is_wayland_compositor.return_value = False
|
||||
|
||||
# WHEN: _capture_maindisplay is called
|
||||
photo = mocks.slide_controller._capture_maindisplay()
|
||||
|
||||
# THEN: Screenshot should have been taken from DisplayWindow and Screen should not be touched
|
||||
assert photo == mocks.windowed_screenshot_mock
|
||||
mocks.mocked_primary_screen.grabWindow.assert_not_called()
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.ScreenList')
|
||||
@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication')
|
||||
@patch(u'openlp.core.ui.slidecontroller.is_wayland_compositor')
|
||||
@mark.parametrize('geometry', [[[34, 67, 77, 42], [0, 0, 800, 600]]])
|
||||
def test__capture_maindisplay_offscreen_command_screenshot(mocked_is_wayland_compositor, mocked_application,
|
||||
mocked_screenlist, geometry, registry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay method invoke '{text}_attempt_screenshot' event on command-based service items.
|
||||
"""
|
||||
# GIVEN: A mocked slide controller, with mocked functions and offscreen geometry
|
||||
mocks = _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application,
|
||||
mocked_is_wayland_compositor)
|
||||
mocked_is_wayland_compositor.return_value = True
|
||||
mocks.slide_controller.service_item.capabilities = [ItemCapabilities.ProvidesOwnDisplay]
|
||||
mocks.slide_controller.service_item.name = 'screenshottable'
|
||||
mocks.slide_controller.service_item.service_item_type = ServiceItemType.Command
|
||||
mocks.slide_controller.selected_row = 0
|
||||
pixmap = QtGui.QPixmap()
|
||||
|
||||
def attempt_screenshot(params):
|
||||
nonlocal pixmap
|
||||
return True, pixmap
|
||||
|
||||
registry.register_function('screenshottable_attempt_screenshot', attempt_screenshot)
|
||||
|
||||
# WHEN: _capture_maindisplay is called
|
||||
photo = mocks.slide_controller._capture_maindisplay()
|
||||
|
||||
# THEN: Screenshot should have been taken from DisplayWindow and Screen should not be touched
|
||||
assert photo == pixmap
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.ScreenList')
|
||||
@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication')
|
||||
@patch(u'openlp.core.ui.slidecontroller.is_wayland_compositor')
|
||||
@mark.parametrize('geometry', [[[-800, -600, 800, 600], [0, 0, 800, 600]]])
|
||||
@mark.parametrize('hide_mode', [HideMode.Screen, HideMode.Blank])
|
||||
def test__capture_maindisplay_window_fakes_black_screen(mocked_is_wayland_compositor, mocked_application,
|
||||
mocked_screenlist, geometry, hide_mode, registry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay_window method fakes a black screen if display mode is Screen or Blank.
|
||||
"""
|
||||
# GIVEN: A mocked slide controller, with mocked functions and offscreen geometry
|
||||
mocks = _init__capture_maindisplay_mocks(geometry, mocked_screenlist, mocked_application,
|
||||
mocked_is_wayland_compositor)
|
||||
screen_size = QtCore.QSize(geometry[1][2], geometry[1][3])
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.is_display = True
|
||||
mocked_display.hide_mode = hide_mode
|
||||
mocked_display.size = MagicMock(return_value=screen_size)
|
||||
mocks.slide_controller.displays = [mocked_display]
|
||||
|
||||
# WHEN: _capture_maindisplay is called
|
||||
pixmap = mocks.slide_controller._capture_maindisplay()
|
||||
|
||||
# THEN: A fake black screen should be returned
|
||||
photo_size = pixmap.size()
|
||||
image = pixmap.toImage()
|
||||
assert photo_size.width() == screen_size.width()
|
||||
assert photo_size.height() == screen_size.height()
|
||||
assert image.pixelColor(int(geometry[1][2] / 2), int(geometry[1][3] / 2)).isValid()
|
||||
assert image.pixelColor(int(geometry[1][2] / 2), int(geometry[1][3] / 2)) == QtGui.QColorConstants.Black
|
||||
|
||||
|
||||
@patch('openlp.core.ui.slidecontroller.is_macosx')
|
||||
def test__capture_maindisplay_desktop_calls_safe_on_macos(mocked_is_macosx, registry, settings):
|
||||
"""
|
||||
Test the _capture_maindisplay_desktop method fallbacks to calling thread-safe code on macOS
|
||||
(avoids a hard crash due to Cocoa/macOS internal details)
|
||||
"""
|
||||
# GIVEN: A mocked system check (running macOS) and mocked maindisplay call
|
||||
mocked_is_macosx.return_value = True
|
||||
slide_controller = SlideController()
|
||||
# slidecontroller._capture_maindisplay_desktop_signal = MagicMock(return_value=QtGui.QPixmap())
|
||||
slide_controller._capture_maindisplay_desktop_mainthread_safe = MagicMock()
|
||||
|
||||
# WHEN: trying to grab desktop screenshot
|
||||
slide_controller._capture_maindisplay_desktop()
|
||||
|
||||
# THEN: Screenshot should have been taken through thread-safe call
|
||||
slide_controller._capture_maindisplay_desktop_mainthread_safe.assert_called_once()
|
||||
|
||||
|
||||
@patch(u'openlp.core.ui.slidecontroller.image_to_byte')
|
||||
|
|
Loading…
Reference in New Issue