diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 8e28d8408..10992d6fe 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -146,7 +146,7 @@ class Registry(object): try: log.debug('Running function {} for {}'.format(function, event)) result = function(*args, **kwargs) - if result: + if result is not None: results.append(result) except TypeError: # Who has called me can help in debugging diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 220998c59..77c50e43c 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -984,8 +984,10 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi prev_item_last_slide = None service_iterator = QtWidgets.QTreeWidgetItemIterator(self.service_manager_list) while service_iterator.value(): + # Found the selected/current service item if service_iterator.value() == selected: if last_slide and prev_item_last_slide: + # Go to the last slide of the previous service item pos = prev_item.data(0, QtCore.Qt.UserRole) check_expanded = self.service_items[pos - 1]['expanded'] self.service_manager_list.setCurrentItem(prev_item_last_slide) @@ -994,13 +996,17 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi self.make_live() self.service_manager_list.setCurrentItem(prev_item) elif prev_item: + # Go to the first slide of the previous service item self.service_manager_list.setCurrentItem(prev_item) self.make_live() return + # Found the previous service item root if service_iterator.value().parent() is None: prev_item = service_iterator.value() + # Found the last slide of the previous item if service_iterator.value().parent() is prev_item: prev_item_last_slide = service_iterator.value() + # Go to next item in the tree service_iterator += 1 def on_set_item(self, message): diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 4f45f8ca6..4b40a01fb 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1261,9 +1261,18 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): if not self.service_item: return if self.service_item.is_command(): - Registry().execute('{text}_next'.format(text=self.service_item.name.lower()), - [self.service_item, self.is_live]) - if self.is_live: + past_end = Registry().execute('{text}_next'.format(text=self.service_item.name.lower()), + [self.service_item, self.is_live]) + # Check if we have gone past the end of the last slide + if self.is_live and past_end and past_end[0]: + if wrap is None: + if self.slide_limits == SlideLimits.Wrap: + self.on_slide_selected_index([0]) + elif self.is_live and self.slide_limits == SlideLimits.Next: + self.service_next() + elif wrap: + self.on_slide_selected_index([0]) + elif self.is_live: self.update_preview() else: row = self.preview_widget.current_slide_number() + 1 @@ -1290,9 +1299,16 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): if not self.service_item: return if self.service_item.is_command(): - Registry().execute('{text}_previous'.format(text=self.service_item.name.lower()), - [self.service_item, self.is_live]) - if self.is_live: + before_start = Registry().execute('{text}_previous'.format(text=self.service_item.name.lower()), + [self.service_item, self.is_live]) + # Check id we have tried to go before that start slide + if self.is_live and before_start and before_start[0]: + if self.slide_limits == SlideLimits.Wrap: + self.on_slide_selected_index([self.preview_widget.slide_count() - 1]) + elif self.is_live and self.slide_limits == SlideLimits.Next: + self.keypress_queue.append(ServiceItemAction.PreviousLastSlide) + self._process_queue() + elif self.is_live: self.update_preview() else: row = self.preview_widget.current_slide_number() - 1 diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index ebe491005..873e91b86 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -36,31 +36,49 @@ import time from PyQt5 import QtCore -from openlp.core.common import delete_file, get_uno_command, get_uno_instance, is_win +from openlp.core.common import delete_file, get_uno_command, get_uno_instance, is_win, trace_error_handler from openlp.core.common.registry import Registry from openlp.core.display.screens import ScreenList from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ TextType - +# Load the XSlideShowListener class so we can inherit from it if is_win(): from win32com.client import Dispatch import pywintypes uno_available = False - # Declare an empty exception to match the exception imported from UNO + try: + service_manager = Dispatch('com.sun.star.ServiceManager') + service_manager._FlagAsMethod('Bridge_GetStruct') + XSlideShowListenerObj = service_manager.Bridge_GetStruct('com.sun.star.presentation.XSlideShowListener') + class SlideShowListenerImport(XSlideShowListenerObj.__class__): + pass + except (AttributeError, pywintypes.com_error): + class SlideShowListenerImport(): + pass + + # Declare an empty exception to match the exception imported from UNO class ErrorCodeIOException(Exception): pass else: try: import uno + import unohelper from com.sun.star.beans import PropertyValue from com.sun.star.task import ErrorCodeIOException + from com.sun.star.presentation import XSlideShowListener + + class SlideShowListenerImport(unohelper.Base, XSlideShowListener): + pass uno_available = True except ImportError: uno_available = False + class SlideShowListenerImport(): + pass + log = logging.getLogger(__name__) @@ -82,6 +100,8 @@ class ImpressController(PresentationController): self.process = None self.desktop = None self.manager = None + self.conf_provider = None + self.presenter_screen_disabled_by_openlp = False def check_available(self): """ @@ -90,8 +110,7 @@ class ImpressController(PresentationController): log.debug('check_available') if is_win(): return self.get_com_servicemanager() is not None - else: - return uno_available + return uno_available def start_process(self): """ @@ -131,6 +150,7 @@ class ImpressController(PresentationController): self.manager = uno_instance.ServiceManager log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) + self.toggle_presentation_screen(False) return desktop except Exception: log.warning('Failed to get UNO desktop') @@ -148,6 +168,7 @@ class ImpressController(PresentationController): desktop = self.manager.createInstance('com.sun.star.frame.Desktop') except (AttributeError, pywintypes.com_error): log.warning('Failure to find desktop - Impress may have closed') + self.toggle_presentation_screen(False) return desktop if desktop else None def get_com_servicemanager(self): @@ -166,6 +187,8 @@ class ImpressController(PresentationController): Called at system exit to clean up any running presentations. """ log.debug('Kill OpenOffice') + if self.presenter_screen_disabled_by_openlp: + self.toggle_presentation_screen(True) while self.docs: self.docs[0].close_presentation() desktop = None @@ -195,6 +218,60 @@ class ImpressController(PresentationController): except Exception: log.warning('Failed to terminate OpenOffice') + def toggle_presentation_screen(self, set_visible): + """ + Enable or disable the Presentation Screen/Console + + :param bool set_visible: Should the presentation screen/console be set to be visible. + :rtype: None + """ + # Create Instance of ConfigurationProvider + if not self.conf_provider: + if is_win(): + self.conf_provider = self.manager.createInstance('com.sun.star.configuration.ConfigurationProvider') + else: + self.conf_provider = self.manager.createInstanceWithContext( + 'com.sun.star.configuration.ConfigurationProvider', uno.getComponentContext()) + # Setup lookup properties to get Impress settings + properties = [] + properties.append(self.create_property('nodepath', 'org.openoffice.Office.Impress')) + properties = tuple(properties) + try: + # Get an updateable configuration view + impress_conf_props = self.conf_provider.createInstanceWithArguments( + 'com.sun.star.configuration.ConfigurationUpdateAccess', properties) + # Get the specific setting for presentation screen + presenter_screen_enabled = impress_conf_props.getHierarchicalPropertyValue( + 'Misc/Start/EnablePresenterScreen') + # If the presentation screen is enabled we disable it + if presenter_screen_enabled != set_visible: + impress_conf_props.setHierarchicalPropertyValue('Misc/Start/EnablePresenterScreen', set_visible) + impress_conf_props.commitChanges() + # if set_visible is False this is an attempt to disable the Presenter Screen + # so we make a note that it has been disabled, so it can be enabled again on close. + if set_visible is False: + self.presenter_screen_disabled_by_openlp = True + except Exception as e: + log.exception(e) + trace_error_handler(log) + + def create_property(self, name, value): + """ + Create an OOo style property object which are passed into some Uno methods. + + :param str name: The name of the property + :param str value: The value of the property + :rtype: com.sun.star.beans.PropertyValue + """ + log.debug('create property OpenOffice') + if is_win(): + property_object = self.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue') + else: + property_object = PropertyValue() + property_object.Name = name + property_object.Value = value + return property_object + class ImpressDocument(PresentationDocument): """ @@ -213,6 +290,8 @@ class ImpressDocument(PresentationDocument): self.document = None self.presentation = None self.control = None + self.slide_ended = False + self.slide_ended_reverse = False def load_presentation(self): """ @@ -233,13 +312,16 @@ class ImpressDocument(PresentationDocument): return False self.desktop = desktop properties = [] - properties.append(self.create_property('Hidden', True)) + properties.append(self.controller.create_property('Hidden', True)) properties = tuple(properties) try: self.document = desktop.loadComponentFromURL(url, '_blank', 0, properties) except Exception: log.warning('Failed to load presentation {url}'.format(url=url)) return False + if self.document is None: + log.warning('Presentation {url} could not be loaded'.format(url=url)) + return False self.presentation = self.document.getPresentation() self.presentation.Display = ScreenList().current.number + 1 self.control = None @@ -257,7 +339,7 @@ class ImpressDocument(PresentationDocument): temp_folder_path = self.get_temp_folder() thumb_dir_url = temp_folder_path.as_uri() properties = [] - properties.append(self.create_property('FilterName', 'impress_png_Export')) + properties.append(self.controller.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) doc = self.document pages = doc.getDrawPages() @@ -279,19 +361,6 @@ class ImpressDocument(PresentationDocument): except Exception: log.exception('{path} - Unable to store openoffice preview'.format(path=path)) - def create_property(self, name, value): - """ - Create an OOo style property object which are passed into some Uno methods. - """ - log.debug('create property OpenOffice') - if is_win(): - property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue') - else: - property_object = PropertyValue() - property_object.Name = name - property_object.Value = value - return property_object - def close_presentation(self): """ Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being @@ -356,8 +425,7 @@ class ImpressDocument(PresentationDocument): log.debug('is blank OpenOffice') if self.control and self.control.isRunning(): return self.control.isPaused() - else: - return False + return False def stop_presentation(self): """ @@ -384,6 +452,8 @@ class ImpressDocument(PresentationDocument): sleep_count += 1 self.control = self.presentation.getController() window.setVisible(False) + listener = SlideShowListener(self) + self.control.getSlideShow().addSlideShowListener(listener) else: self.control.activate() self.goto_slide(1) @@ -415,17 +485,33 @@ class ImpressDocument(PresentationDocument): """ Triggers the next effect of slide on the running presentation. """ + # if we are at the presentations end don't go further, just return True + if self.slide_ended and self.get_slide_count() == self.get_slide_number(): + return True + self.slide_ended = False + self.slide_ended_reverse = False + past_end = False is_paused = self.control.isPaused() self.control.gotoNextEffect() time.sleep(0.1) + # If for some reason the presentation end was not detected above, this will catch it. + # The presentation is set to paused when going past the end. if not is_paused and self.control.isPaused(): self.control.gotoPreviousEffect() + past_end = True + return past_end def previous_step(self): """ Triggers the previous slide on the running presentation. """ + # if we are at the presentations start don't go further back, just return True + if self.slide_ended_reverse and self.get_slide_number() == 1: + return True + self.slide_ended = False + self.slide_ended_reverse = False self.control.gotoPreviousEffect() + return False def get_slide_text(self, slide_no): """ @@ -483,3 +569,100 @@ class ImpressDocument(PresentationDocument): note = ' ' notes.append(note) self.save_titles_and_notes(titles, notes) + + +class SlideShowListener(SlideShowListenerImport): + """ + Listener interface to receive global slide show events. + """ + + def __init__(self, document): + """ + Constructor + + :param document: The ImpressDocument being presented + """ + self.document = document + + def paused(self): + """ + Notify that the slide show is paused + """ + log.debug('LibreOffice SlideShowListener event: paused') + + def resumed(self): + """ + Notify that the slide show is resumed from a paused state + """ + log.debug('LibreOffice SlideShowListener event: resumed') + + def slideTransitionStarted(self): + """ + Notify that a new slide starts to become visible. + """ + log.debug('LibreOffice SlideShowListener event: slideTransitionStarted') + + def slideTransitionEnded(self): + """ + Notify that the slide transtion of the current slide ended. + """ + log.debug('LibreOffice SlideShowListener event: slideTransitionEnded') + + def slideAnimationsEnded(self): + """ + Notify that the last animation from the main sequence of the current slide has ended. + """ + log.debug('LibreOffice SlideShowListener event: slideAnimationsEnded') + if not Registry().get('main_window').isActiveWindow(): + log.debug('main window is not in focus - should update slidecontroller') + Registry().execute('slidecontroller_live_change', self.document.control.getCurrentSlideIndex() + 1) + + def slideEnded(self, reverse): + """ + Notify that the current slide has ended, e.g. the user has clicked on the slide. Calling displaySlide() + twice will not issue this event. + + :param bool reverse: Whether or not the direction of the "slide movement" is reversed/backwards. + :rtype: None + """ + log.debug('LibreOffice SlideShowListener event: slideEnded %d' % reverse) + if reverse: + self.document.slide_ended = False + self.document.slide_ended_reverse = True + else: + self.document.slide_ended = True + self.document.slide_ended_reverse = False + + def hyperLinkClicked(self, hyperLink): + """ + Notifies that a hyperlink has been clicked. + """ + log.debug('LibreOffice SlideShowListener event: hyperLinkClicked %s' % hyperLink) + + def disposing(self, source): + """ + gets called when the broadcaster is about to be disposed. + :param source: + """ + log.debug('LibreOffice SlideShowListener event: disposing') + + def beginEvent(self, node): + """ + This event is raised when the element local timeline begins to play. + :param node: + """ + log.debug('LibreOffice SlideShowListener event: beginEvent') + + def endEvent(self, node): + """ + This event is raised at the active end of the element. + :param node: + """ + log.debug('LibreOffice SlideShowListener event: endEvent') + + def repeat(self, node): + """ + This event is raised when the element local timeline repeats. + :param node: + """ + log.debug('LibreOffice SlideShowListener event: repeat') diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 55f472399..090299fa5 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -169,24 +169,21 @@ class Controller(object): """ log.debug('Live = {live}, next'.format(live=self.is_live)) if not self.doc: - return + return False if not self.is_live: - return + return False if self.hide_mode: if not self.doc.is_active(): - return + return False if self.doc.slidenumber < self.doc.get_slide_count(): self.doc.slidenumber += 1 self.poll() - return + return False if not self.activate(): - return - # The "End of slideshow" screen is after the last slide. Note, we can't just stop on the last slide, since it - # may contain animations that need to be stepped through. - if self.doc.slidenumber > self.doc.get_slide_count(): - return - self.doc.next_step() + return False + ret = self.doc.next_step() self.poll() + return ret def previous(self): """ @@ -194,20 +191,21 @@ class Controller(object): """ log.debug('Live = {live}, previous'.format(live=self.is_live)) if not self.doc: - return + return False if not self.is_live: - return + return False if self.hide_mode: if not self.doc.is_active(): - return + return False if self.doc.slidenumber > 1: self.doc.slidenumber -= 1 self.poll() - return + return False if not self.activate(): - return - self.doc.previous_step() + return False + ret = self.doc.previous_step() self.poll() + return ret def shutdown(self): """ @@ -418,11 +416,12 @@ class MessageListener(object): """ is_live = message[1] if is_live: - self.live_handler.next() + ret = self.live_handler.next() if Settings().value('core/click live slide to unblank'): Registry().execute('slidecontroller_live_unblank') + return ret else: - self.preview_handler.next() + return self.preview_handler.next() def previous(self, message): """ @@ -432,11 +431,12 @@ class MessageListener(object): """ is_live = message[1] if is_live: - self.live_handler.previous() + ret = self.live_handler.previous() if Settings().value('core/click live slide to unblank'): Registry().execute('slidecontroller_live_unblank') + return ret else: - self.preview_handler.previous() + return self.preview_handler.previous() def shutdown(self, message): """ diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index acac15f79..3ea7e6d41 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -145,8 +145,8 @@ class PowerpointDocument(PresentationDocument): try: if not self.controller.process: self.controller.start_process() - self.controller.process.Presentations.Open(str(self.file_path), False, False, False) - self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) + self.presentation = self.controller.process.Presentations.Open(str(self.file_path), False, False, False) + log.debug('Loaded presentation %s' % self.presentation.FullName) self.create_thumbnails() self.create_titles_and_notes() # Make sure powerpoint doesn't steal focus, unless we're on a single screen setup @@ -170,14 +170,17 @@ class PowerpointDocument(PresentationDocument): However, for the moment, we want a physical file since it makes life easier elsewhere. """ log.debug('create_thumbnails') + generate_thumbs = True if self.check_thumbnails(): - return + # No need for thumbnails but we still need the index + generate_thumbs = False key = 1 for num in range(self.presentation.Slides.Count): if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden: self.index_map[key] = num + 1 - self.presentation.Slides(num + 1).Export( - str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) + if generate_thumbs: + self.presentation.Slides(num + 1).Export( + str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) key += 1 self.slide_count = key - 1 @@ -318,6 +321,9 @@ class PowerpointDocument(PresentationDocument): size = ScreenList().current.display_geometry ppt_window = None try: + # Disable the presentation console + self.presentation.SlideShowSettings.ShowPresenterView = 0 + # Start the presentation ppt_window = self.presentation.SlideShowSettings.Run() except (AttributeError, pywintypes.com_error): log.exception('Caught exception while in start_presentation') @@ -437,6 +443,12 @@ class PowerpointDocument(PresentationDocument): Triggers the next effect of slide on the running presentation. """ log.debug('next_step') + # if we are at the presentations end don't go further, just return True + if self.presentation.SlideShowWindow.View.GetClickCount() == \ + self.presentation.SlideShowWindow.View.GetClickIndex() \ + and self.get_slide_number() == self.get_slide_count(): + return True + past_end = False try: self.presentation.SlideShowWindow.Activate() self.presentation.SlideShowWindow.View.Next() @@ -444,28 +456,35 @@ class PowerpointDocument(PresentationDocument): log.exception('Caught exception while in next_step') trace_error_handler(log) self.show_error_msg() - return + return past_end + # If for some reason the presentation end was not detected above, this will catch it. if self.get_slide_number() > self.get_slide_count(): log.debug('past end, stepping back to previous') self.previous_step() + past_end = True # Stop powerpoint from flashing in the taskbar if self.presentation_hwnd: win32gui.FlashWindowEx(self.presentation_hwnd, win32con.FLASHW_STOP, 0, 0) # Make sure powerpoint doesn't steal focus, unless we're on a single screen setup if len(ScreenList()) > 1: Registry().get('main_window').activateWindow() + return past_end def previous_step(self): """ Triggers the previous slide on the running presentation. """ log.debug('previous_step') + # if we are at the presentations start we can't go further back, just return True + if self.presentation.SlideShowWindow.View.GetClickIndex() == 0 and self.get_slide_number() == 1: + return True try: self.presentation.SlideShowWindow.View.Previous() except (AttributeError, pywintypes.com_error): log.exception('Caught exception while in previous_step') trace_error_handler(log) self.show_error_msg() + return False def get_slide_text(self, slide_no): """ diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 7d8d99518..ac25fa9a4 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -248,15 +248,17 @@ class PresentationDocument(object): def next_step(self): """ Triggers the next effect of slide on the running presentation. This might be the next animation on the current - slide, or the next slide + slide, or the next slide. + :rtype bool: True if we stepped beyond the slides of the presentation """ - pass + return False def previous_step(self): """ Triggers the previous slide on the running presentation + :rtype bool: True if we stepped beyond the slides of the presentation """ - pass + return False def convert_thumbnail(self, image_path, index): """