From 860d630360fffa71c42644060585f16e2a6f7572 Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 24 Dec 2015 14:26:41 -0500 Subject: [PATCH 1/3] Make sure the main display stays above the menu bar and dock but still allow the main window to be focused --- openlp/core/ui/generaltab.py | 2 + openlp/core/ui/maindisplay.py | 70 ++++++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 47a6a9594..c0e18cc27 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -317,6 +317,8 @@ class GeneralTab(SettingsTab): self.custom_Y_value_edit.value(), self.custom_width_value_edit.value(), self.custom_height_value_edit.value()) + self.screens.override['number'] = self.screens.which_screen(self.screens.override['size']) + self.screens.override['primary'] = (self.screens.desktop.primaryScreen() == self.screens.override['number']) if self.override_radio_button.isChecked(): self.screens.set_override_display() else: diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 05ab222b9..3ba979280 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -39,6 +39,13 @@ from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, ex from openlp.core.lib.theme import BackgroundType from openlp.core.ui import HideMode, AlertLocation +if is_macosx(): + from ctypes import pythonapi, c_void_p, c_char_p, py_object + + from sip import voidptr + from objc import objc_object + from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged + log = logging.getLogger(__name__) OPAQUE_STYLESHEET = """ @@ -154,15 +161,30 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): # regressions on other platforms. if is_macosx(): window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window - # For primary screen ensure it stays above the OS X dock - # and menu bar - if self.screens.current['primary']: - self.setWindowState(QtCore.Qt.WindowFullScreen) - else: - window_flags |= QtCore.Qt.WindowStaysOnTopHint self.setWindowFlags(window_flags) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.set_transparency(False) + if is_macosx(): + if self.is_live: + # Get a pointer to the underlying NSView + try: + nsview_pointer = self.winId().ascapsule() + except: + nsview_pointer = voidptr(self.winId()).ascapsule() + # Set PyCapsule name so pyobjc will accept it + pythonapi.PyCapsule_SetName.restype = c_void_p + pythonapi.PyCapsule_SetName.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_SetName(nsview_pointer, c_char_p(b"objc.__object__")) + # Covert the NSView pointer into a pyobjc NSView object + self.pyobjc_nsview = objc_object(cobject=nsview_pointer) + # Set the window level so that the MainDisplay is above the menu bar and dock + self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2) + # Set the collection behavior so the window is visible when Mission Control is activated + self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged) + if self.screens.current['primary']: + # Connect focusWindowChanged signal so we can change the window level when the display is not in + # focus on the primary screen + self.application.focusWindowChanged.connect(self.change_window_level) if self.is_live: Registry().register_function('live_display_hide', self.hide_display) Registry().register_function('live_display_show', self.show_display) @@ -186,6 +208,12 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): Remove registered function on close. """ if self.is_live: + if is_macosx(): + # Block signals so signal we are disconnecting can't get called while we disconnect it + self.blockSignals(True) + if self.screens.current['primary']: + self.application.focusWindowChanged.disconnect() + self.blockSignals(False) Registry().remove_function('live_display_hide', self.hide_display) Registry().remove_function('live_display_show', self.show_display) Registry().remove_function('update_display_css', self.css_changed) @@ -500,6 +528,36 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.setCursor(QtCore.Qt.ArrowCursor) self.frame.evaluateJavaScript('document.body.style.cursor = "auto"') + def change_window_level(self, window): + """ + Changes the display window level on Mac OS X so that the main window can be brought into focus but still allow + the main display to be above the menu bar and dock when it in focus. + + :param window: Window from our application that focus changed to or None if outside our application + """ + if is_macosx(): + if window: + # Get different window ids' as int's + try: + window_id = window.winId().__int__() + main_window_id = self.main_window.winId().__int__() + self_id = self.winId().__int__() + except: + return + # If the passed window has the same id as our window make sure the display has the proper level and + # collection behavior. + if window_id == self_id: + self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2) + self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged) + # Else set the displays window level back to normal since we are trying to focus a window other than + # the display. + else: + self.pyobjc_nsview.window().setLevel_(0) + self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged) + # If we are trying to focus the main window raise it now to complete the focus change. + if window_id == main_window_id: + self.main_window.raise_() + class AudioPlayer(OpenLPMixin, QtCore.QObject): """ From 65b8b12590f4c59e805604287cf745876c33cc87 Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 24 Dec 2015 14:27:44 -0500 Subject: [PATCH 2/3] Add tests --- .../openlp_core_ui/test_maindisplay.py | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index afdd7416f..48b646b78 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -26,7 +26,7 @@ from unittest import TestCase from PyQt5 import QtCore -from openlp.core.common import Registry +from openlp.core.common import Registry, is_macosx from openlp.core.lib import ScreenList from openlp.core.ui import MainDisplay from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET @@ -34,6 +34,13 @@ from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET from tests.helpers.testmixin import TestMixin from tests.functional import MagicMock, patch +if is_macosx(): + from ctypes import pythonapi, c_void_p, c_char_p, py_object + + from sip import voidptr + from objc import objc_object + from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged + class TestMainDisplay(TestCase, TestMixin): @@ -135,31 +142,11 @@ class TestMainDisplay(TestCase, TestMixin): mocked_bibles_plugin.refresh_css.assert_called_with(main_display.frame) @patch('openlp.core.ui.maindisplay.is_macosx') - def macosx_non_primary_screen_window_flags_state_test(self, is_macosx): + def macosx_display_window_flags_state_test(self, is_macosx): """ - Test that on Mac OS X when the current screen isn't primary we set the proper window flags and window state + Test that on Mac OS X we set the proper window flags """ - # GIVEN: A new SlideController instance on Mac OS X with the current display not being primary. - is_macosx.return_value = True - self.screens.set_current_display(1) - display = MagicMock() - - # WHEN: The default controller is built. - main_display = MainDisplay(display) - - # THEN: The window flags and state should be the same as those needed on Mac OS X for the non primary display. - self.assertEqual(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint, - main_display.windowFlags(), - 'The window flags should be Qt.WindowStaysOnTop, Qt.Window, and Qt.FramelessWindowHint.') - self.assertNotEqual(QtCore.Qt.WindowFullScreen, main_display.windowState(), - 'The window state should not be full screen.') - - @patch('openlp.core.ui.maindisplay.is_macosx') - def macosx_primary_screen_window_flags_state_test(self, is_macosx): - """ - Test that on Mac OS X when the current screen is primary we set the proper window flags and window state - """ - # GIVEN: A new SlideController instance on Mac OS X with the current display being primary. + # GIVEN: A new SlideController instance on Mac OS X. is_macosx.return_value = True self.screens.set_current_display(0) display = MagicMock() @@ -167,8 +154,34 @@ class TestMainDisplay(TestCase, TestMixin): # WHEN: The default controller is built. main_display = MainDisplay(display) - # THEN: The window flags and state should be the same as those needed on Mac OS X for the primary display. - self.assertEqual(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint, main_display.windowFlags(), - 'The window flags should be Qt.Window and Qt.FramelessWindowHint.') - self.assertEqual(QtCore.Qt.WindowFullScreen, main_display.windowState(), - 'The window state should be full screen.') + # THEN: The window flags should be the same as those needed on Mac OS X. + self.assertEqual(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint, + main_display.windowFlags(), + 'The window flags should be Qt.Window, and Qt.FramelessWindowHint.') + + def macosx_display_test(self): + """ + Test display on Mac OS X + """ + if not is_macosx(): + self.skipTest('Can only run test on Mac OS X due to pyobjc dependency.') + # GIVEN: A new SlideController instance on Mac OS X. + self.screens.set_current_display(0) + display = MagicMock() + + # WHEN: The default controller is built and a reference to the underlying NSView is stored. + main_display = MainDisplay(display) + try: + nsview_pointer = main_display.winId().ascapsule() + except: + nsview_pointer = voidptr(main_display.winId()).ascapsule() + pythonapi.PyCapsule_SetName.restype = c_void_p + pythonapi.PyCapsule_SetName.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_SetName(nsview_pointer, c_char_p(b"objc.__object__")) + pyobjc_nsview = objc_object(cobject=nsview_pointer) + + # THEN: The window level and collection behavior should be the same as those needed for Mac OS X. + self.assertEqual(pyobjc_nsview.window().level(), NSMainMenuWindowLevel + 2, + 'Window level should be NSMainMenuWindowLevel + 2') + self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged, + 'Window collection behavior should be NSWindowCollectionBehaviorManaged') From e3329fc8c4b12453d16dc8d392d91e7c567a9640 Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 24 Dec 2015 14:41:47 -0500 Subject: [PATCH 3/3] Add pyobjc to check_dependencies script --- scripts/check_dependencies.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index f4c54f856..de8fd0203 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -41,6 +41,7 @@ except ImportError: IS_WIN = sys.platform.startswith('win') IS_LIN = sys.platform.startswith('lin') +IS_MAC = sys.platform.startswith('dar') VERS = { @@ -66,6 +67,11 @@ LINUX_MODULES = [ 'dbus', ] +MACOSX_MODULES = [ + 'objc', + 'AppKit' +] + MODULES = [ 'PyQt5', @@ -234,6 +240,10 @@ def main(): print('Checking for Linux specific modules...') for m in LINUX_MODULES: check_module(m) + elif IS_MAC: + print('Checking for Mac OS X specific modules...') + for m in MACOSX_MODULES: + check_module(m) verify_versions() print_qt_image_formats() print_enchant_backends_and_languages()