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

bzr-revno: 2875
Fixes: https://launchpad.net/bugs/1165855, https://launchpad.net/bugs/1798651
This commit is contained in:
second@tgc.dk 2019-06-05 21:25:35 +02:00 committed by Tomas Groth
commit f2779edcd4
7 changed files with 284 additions and 58 deletions

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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')

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -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):
"""