← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/presentation-beyond-last into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/presentation-beyond-last into lp:openlp.

Commit message:
Make it possible to go to next or previous service item when stepping through a presentation.
Disables impress and powerpoint presentation console.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #1165855 in OpenLP: "Presentations do not advance correctly despite settings"
  https://bugs.launchpad.net/openlp/+bug/1165855
  Bug #1798651 in OpenLP: "Impress Presentation Console should be disabled by OpenLP"
  https://bugs.launchpad.net/openlp/+bug/1798651

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/presentation-beyond-last/+merge/367932
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/presentation-beyond-last into lp:openlp.
=== modified file 'openlp/core/common/registry.py'
--- openlp/core/common/registry.py	2019-04-13 13:00:22 +0000
+++ openlp/core/common/registry.py	2019-05-25 19:23:30 +0000
@@ -146,7 +146,7 @@
                 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

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2019-05-24 18:50:51 +0000
+++ openlp/core/ui/servicemanager.py	2019-05-25 19:23:30 +0000
@@ -976,8 +976,10 @@
         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)
@@ -986,13 +988,17 @@
                     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):

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2019-05-22 06:47:00 +0000
+++ openlp/core/ui/slidecontroller.py	2019-05-25 19:23:30 +0000
@@ -1261,9 +1261,18 @@
         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 @@
         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

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2019-05-25 19:23:30 +0000
@@ -36,7 +36,7 @@
 
 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, \
@@ -47,15 +47,30 @@
     from win32com.client import Dispatch
     import pywintypes
     uno_available = False
+    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:
@@ -82,6 +97,8 @@
         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 +107,7 @@
         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 +147,7 @@
             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 +165,7 @@
             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 +184,8 @@
         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 +215,54 @@
             except Exception:
                 log.warning('Failed to terminate OpenOffice')
 
+    def toggle_presentation_screen(self, target_value):
+        """
+        Enable or disable the Presentation Screen/Console
+        """
+        # 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 != target_value:
+                impress_conf_props.setHierarchicalPropertyValue('Misc/Start/EnablePresenterScreen', target_value)
+                impress_conf_props.commitChanges()
+                # if target_value 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 target_value is False:
+                    self.presenter_screen_disabled_by_openlp = True
+        except Exception as e:
+            log.exception(e)
+            trace_error_handler(log)
+            return
+
+    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.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 +281,8 @@
         self.document = None
         self.presentation = None
         self.control = None
+        self.slide_ended = False
+        self.slide_ended_reverse = False
 
     def load_presentation(self):
         """
@@ -233,13 +303,16 @@
             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 +330,7 @@
         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 +352,6 @@
             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 +416,7 @@
         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 +443,8 @@
                 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 +476,33 @@
         """
         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 +560,97 @@
                 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.
+        """
+        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')

=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
--- openlp/plugins/presentations/lib/messagelistener.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/messagelistener.py	2019-05-25 19:23:30 +0000
@@ -169,24 +169,21 @@
         """
         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 @@
         """
         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 @@
         """
         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 @@
         """
         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):
         """

=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2019-05-25 19:23:30 +0000
@@ -170,14 +170,17 @@
         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 @@
             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 @@
         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 @@
             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):
         """

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2019-05-22 06:47:00 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2019-05-25 19:23:30 +0000
@@ -248,15 +248,17 @@
     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.
+        Returns 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
+        Returns True if we stepped beyond the slides of the presentation
         """
-        pass
+        return False
 
     def convert_thumbnail(self, image_path, index):
         """


Follow ups