Remove Powerpoint Viewer

Clean up the logging framework and add to MainWindow
Fix bug in remote API
various minor text fixes. 


lp:~trb143/openlp/cleanups2018 (revision 2827)
https://ci.openlp.io/job/Branch-01-Pull/2441/                          [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2342/                  [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/136/                   [SUCCESS]
https://ci.openlp.io/job/Branch-03a-Build-Source/58/                   [SUCCES...

bzr-revno: 2812
This commit is contained in:
Tim Bentley 2018-02-03 20:41:24 +00:00
commit 0d2bf9926d
23 changed files with 148 additions and 2159 deletions

View File

@ -69,6 +69,7 @@ def _make_response(view_result):
"""
Create a Response object from response
"""
log.debug("in Make response")
if isinstance(view_result, Response):
return view_result
elif isinstance(view_result, tuple):
@ -88,6 +89,9 @@ def _make_response(view_result):
elif isinstance(view_result, str):
return Response(body=view_result, status=200,
content_type='text/html', charset='utf8')
else:
return Response(body=view_result, status=200,
content_type='text/plain', charset='utf8')
def _handle_exception(error):

View File

@ -120,7 +120,8 @@ class OpenLP(QtWidgets.QApplication, LogMixin):
self.main_window.show()
if can_show_splash:
# now kill the splashscreen
self.splash.finish(self.main_window)
log.debug('Splashscreen closing')
self.splash.close()
log.debug('Splashscreen closed')
# make sure Qt really display the splash screen
self.processEvents()

View File

@ -56,7 +56,10 @@ class LogMixin(object):
def wrapped(*args, **kwargs):
parent.logger.debug("Entering {function}".format(function=func.__name__))
try:
return func(*args, **kwargs)
if len(inspect.signature(func).parameters.values()):
return func(*args, **kwargs)
else:
return func(*args)
except Exception as e:
if parent.logger.getEffectiveLevel() <= logging.ERROR:
parent.logger.error('Exception in {function} : {error}'.format(function=func.__name__,
@ -89,6 +92,13 @@ class LogMixin(object):
trace_error_handler(self.logger)
self.logger.error(message)
def log_critical(self, message):
"""
Common log critical handler which prints the calling path
"""
trace_error_handler(self.logger)
self.logger.critical(message)
def log_exception(self, message):
"""
Common log exception handler which prints the calling path

View File

@ -259,7 +259,8 @@ class Settings(QtCore.QSettings):
('media/last directory', 'media/last directory', [(str_to_path, None)]),
('songuasge/db password', 'songusage/db password', []),
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', [])
('songuasge/db database', 'songusage/db database', []),
('presentations / Powerpoint Viewer', '', [])
]
@staticmethod

View File

@ -98,7 +98,7 @@ class UiAboutDialog(object):
'OpenLP is free church presentation software, or lyrics '
'projection software, used to display slides of songs, Bible '
'verses, videos, images, and even presentations (if '
'Impress, PowerPoint or PowerPoint Viewer is installed) '
'Impress or PowerPoint is installed) '
'for church worship using a computer and a data projector.\n'
'\n'
'Find out more about OpenLP: http://openlp.org/\n'

View File

@ -37,7 +37,7 @@ from openlp.core.common import is_win, is_macosx, add_actions
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
@ -56,8 +56,6 @@ from openlp.core.version import get_version
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager
log = logging.getLogger(__name__)
class Ui_MainWindow(object):
"""
@ -465,12 +463,10 @@ class Ui_MainWindow(object):
self.mode_live_item.setStatusTip(translate('OpenLP.MainWindow', 'Use layout that focuses on Live.'))
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryProperties):
"""
The main window.
"""
log.info('MainWindow loaded')
def __init__(self):
"""
This constructor sets up the interface, the various managers, and the plugins.
@ -557,7 +553,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
wait_dialog.setCancelButton(None)
wait_dialog.show()
for thread_name in self.application.worker_threads.keys():
log.debug('Waiting for thread %s', thread_name)
self.log_debug('Waiting for thread %s' % thread_name)
self.application.processEvents()
thread = self.application.worker_threads[thread_name]['thread']
worker = self.application.worker_threads[thread_name]['worker']
@ -595,7 +591,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
Called on start up to restore the last active media plugin.
"""
log.info('Load data from Settings')
self.log_info('Load data from Settings')
if Settings().value('advanced/save current plugin'):
saved_plugin_id = Settings().value('advanced/current media plugin')
if saved_plugin_id != -1:
@ -627,7 +623,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
:param version: The Version to be displayed.
"""
log.debug('version_notice')
version_text = translate('OpenLP.MainWindow', 'Version {new} of OpenLP is now available for download (you are '
'currently running version {current}). \n\nYou can download the latest version from '
'http://openlp.org/.').format(new=version, current=get_version()[u'full'])
@ -774,7 +769,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.application.splash.close()
QtWidgets.QMessageBox.information(self, title, message)
def on_help_web_site_clicked(self):
@staticmethod
def on_help_web_site_clicked():
"""
Load the OpenLP website
"""
@ -891,7 +887,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings = Settings()
import_settings = Settings(str(temp_config_path), Settings.IniFormat)
log.info('hook upgrade_plugin_settings')
self.log_info('hook upgrade_plugin_settings')
self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
# Upgrade settings to prepare the import.
if import_settings.can_upgrade():
@ -929,7 +925,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
value = import_settings.value(section_key)
except KeyError:
value = None
log.warning('The key "{key}" does not exist (anymore), so it will be skipped.'.format(key=section_key))
self.log_warning('The key "{key}" does not exist (anymore), so it will be skipped.'.
format(key=section_key))
if value is not None:
settings.setValue('{key}'.format(key=section_key), value)
now = datetime.now()
@ -1013,7 +1010,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
The screen has changed so we have to update components such as the renderer.
"""
log.debug('screen_changed')
self.application.set_busy_cursor()
self.image_manager.update_display()
self.renderer.update_display()
@ -1076,7 +1072,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if Settings().value('advanced/save current plugin'):
Settings().setValue('advanced/current media plugin', self.media_tool_box.currentIndex())
# Call the cleanup method to shutdown plugins.
log.info('cleanup plugins')
self.log_info('cleanup plugins')
self.plugin_manager.finalise_plugins()
if save_settings:
# Save settings
@ -1216,7 +1212,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
Load the main window settings.
"""
log.debug('Loading QSettings')
settings = Settings()
# Remove obsolete entries.
settings.remove('custom slide')
@ -1243,7 +1238,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Exit if we just did a settings import.
if self.settings_imported:
return
log.debug('Saving QSettings')
settings = Settings()
settings.beginGroup(self.general_settings_section)
settings.setValue('recent files', self.recent_files)
@ -1268,7 +1262,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if not recent_path.is_file():
continue
count += 1
log.debug('Recent file name: {name}'.format(name=recent_path))
self.log_debug('Recent file name: {name}'.format(name=recent_path))
action = create_action(self, '',
text='&{n} {name}'.format(n=count, name=recent_path.name),
data=recent_path, triggers=self.service_manager_contents.on_recent_service_clicked)
@ -1350,21 +1344,21 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
Change the data directory.
"""
log.info('Changing data path to {newpath}'.format(newpath=self.new_data_path))
self.log_info('Changing data path to {newpath}'.format(newpath=self.new_data_path))
old_data_path = AppLocation.get_data_path()
# Copy OpenLP data to new location if requested.
self.application.set_busy_cursor()
if self.copy_data:
log.info('Copying data to new path')
self.log_info('Copying data to new path')
try:
self.show_status_message(
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
'- Please wait for copy to finish').format(path=self.new_data_path))
dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
log.info('Copy successful')
self.log_info('Copy successful')
except (OSError, DistutilsFileError) as why:
self.application.set_normal_cursor()
log.exception('Data copy failed {err}'.format(err=str(why)))
self.log_exception('Data copy failed {err}'.format(err=str(why)))
err_text = translate('OpenLP.MainWindow',
'OpenLP Data directory copy failed\n\n{err}').format(err=str(why)),
QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'New Data Directory Error'),
@ -1372,7 +1366,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
return False
else:
log.info('No data copy requested')
self.log_info('No data copy requested')
# Change the location of data directory in config file.
settings = QtCore.QSettings()
settings.setValue('advanced/data path', self.new_data_path)

View File

@ -302,7 +302,7 @@ class Ui_ServiceManager(object):
class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixin, RegistryProperties):
"""
Manages the services. This involves taking text strings from plugins and adding them to the service. This service
can then be zipped up with all the resources used into one OSZ or oszl file for use on any OpenLP v2 installation.
can then be zipped up with all the resources used into one OSZ or OSZL file for use on any OpenLP installation.
Also handles the UI tasks of moving things up and down etc.
"""
servicemanager_set_item = QtCore.pyqtSignal(int)
@ -415,10 +415,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if suffix not in self.suffixes:
self.suffixes.append(suffix)
def on_new_service_clicked(self, field=None):
def on_new_service_clicked(self):
"""
Create a new service.
:param field:
"""
if self.is_modified():
result = self.save_modified_service()
@ -466,10 +465,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard |
QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save)
def on_recent_service_clicked(self, field=None):
def on_recent_service_clicked(self):
"""
Load a recent file as the service triggered by mainwindow recent service list.
:param field:
"""
if self.is_modified():
result = self.save_modified_service()
@ -608,7 +606,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.set_modified(False)
return True
def save_file_as(self, field=None):
def save_file_as(self):
"""
Get a file name and then call :func:`ServiceManager.save_file` to save the file.
"""
@ -660,10 +658,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.set_file_name(file_path)
self.decide_save_method()
def decide_save_method(self, field=None):
def decide_save_method(self):
"""
Determine which type of save method to use.
:param field:
"""
if not self.file_name():
return self.save_file_as()
@ -824,10 +821,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
theme_action.setChecked(True)
self.menu.exec(self.service_manager_list.mapToGlobal(point))
def on_service_item_note_form(self, field=None):
def on_service_item_note_form(self):
"""
Allow the service note to be edited
:param field:
"""
item = self.find_service_item()[0]
self.service_note_form.text_edit.setPlainText(self.service_items[item]['service_item'].notes)
@ -836,21 +832,18 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(item, -1)
self.set_modified()
def on_start_time_form(self, field=None):
def on_start_time_form(self):
"""
Opens a dialog to type in service item notes.
:param field:
"""
item = self.find_service_item()[0]
self.start_time_form.item = self.service_items[item]
if self.start_time_form.exec():
self.repaint_service_list(item, -1)
def toggle_auto_play_slides_once(self, field=None):
def toggle_auto_play_slides_once(self):
"""
Toggle Auto play slide once. Inverts auto play once option for the item
:param field:
"""
item = self.find_service_item()[0]
service_item = self.service_items[item]['service_item']
@ -863,11 +856,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.main_window.general_settings_section + '/loop delay')
self.set_modified()
def toggle_auto_play_slides_loop(self, field=None):
def toggle_auto_play_slides_loop(self):
"""
Toggle Auto play slide loop.
:param field:
"""
item = self.find_service_item()[0]
service_item = self.service_items[item]['service_item']
@ -880,10 +871,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.main_window.general_settings_section + '/loop delay')
self.set_modified()
def on_timed_slide_interval(self, field=None):
def on_timed_slide_interval(self):
"""
Shows input dialog for enter interval in seconds for delay
:param field:
"""
item = self.find_service_item()[0]
service_item = self.service_items[item]['service_item']
@ -906,7 +896,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
service_item.auto_play_slides_once = False
self.set_modified()
def on_auto_start(self, field=None):
def on_auto_start(self):
"""
Toggles to Auto Start Setting.
"""
@ -914,10 +904,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.service_items[item]['service_item'].will_auto_start = \
not self.service_items[item]['service_item'].will_auto_start
def on_service_item_edit_form(self, field=None):
def on_service_item_edit_form(self):
"""
Opens a dialog to edit the service item and update the service display if changes are saved.
:param field:
"""
item = self.find_service_item()[0]
self.service_item_edit_form.set_service_item(self.service_items[item]['service_item'])
@ -990,11 +979,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
prev_item_last_slide = service_iterator.value()
service_iterator += 1
def on_set_item(self, message, field=None):
def on_set_item(self, message):
"""
Called by a signal to select a specific item and make it live usually from remote.
:param field:
:param message: The data passed in from a remove message
"""
self.log_debug(message)
@ -1060,10 +1047,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.service_manager.collapsed(item.parent())
self.service_manager_list.setCurrentItem(item.parent())
def on_collapse_all(self, field=None):
def on_collapse_all(self):
"""
Collapse all the service items.
:param field:
"""
for item in self.service_items:
item['expanded'] = False
@ -1080,10 +1066,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if item.childCount():
self.service_items[pos - 1]['expanded'] = False
def on_expand_all(self, field=None):
def on_expand_all(self):
"""
Collapse all the service items.
:param field:
"""
for item in self.service_items:
item['expanded'] = True
@ -1100,10 +1085,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if item.childCount():
self.service_items[pos - 1]['expanded'] = True
def on_service_top(self, field=None):
def on_service_top(self):
"""
Move the current ServiceItem to the top of the list.
:param field:
"""
item, child = self.find_service_item()
if item < len(self.service_items) and item != -1:
@ -1113,10 +1097,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(0, child)
self.set_modified()
def on_service_up(self, field=None):
def on_service_up(self):
"""
Move the current ServiceItem one position up in the list.
:param field:
"""
item, child = self.find_service_item()
if item > 0:
@ -1126,10 +1109,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(item - 1, child)
self.set_modified()
def on_service_down(self, field=None):
def on_service_down(self):
"""
Move the current ServiceItem one position down in the list.
:param field:
"""
item, child = self.find_service_item()
if item < len(self.service_items) and item != -1:
@ -1139,10 +1121,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(item + 1, child)
self.set_modified()
def on_service_end(self, field=None):
def on_service_end(self):
"""
Move the current ServiceItem to the bottom of the list.
:param field:
"""
item, child = self.find_service_item()
if item < len(self.service_items) and item != -1:
@ -1152,7 +1133,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(len(self.service_items) - 1, child)
self.set_modified()
def on_delete_from_service(self, field=None):
def on_delete_from_service(self):
"""
Remove the current ServiceItem from the list.
:param field:
@ -1378,7 +1359,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.drop_position = -1
self.set_modified()
def make_preview(self, field=None):
def make_preview(self):
"""
Send the current item to the Preview slide controller
:param field:
@ -1403,7 +1384,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
else:
return self.service_items[item]['service_item']
def on_double_click_live(self, field=None):
def on_double_click_live(self):
"""
Send the current item to the Live slide controller but triggered by a tablewidget click event.
:param field:
@ -1411,7 +1392,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.list_double_clicked = True
self.make_live()
def on_single_click_preview(self, field=None):
def on_single_click_preview(self):
"""
If single click previewing is enabled, and triggered by a tablewidget click event,
start a timeout to verify a double-click hasn't triggered.
@ -1463,7 +1444,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
'is missing or inactive'))
self.application.set_normal_cursor()
def remote_edit(self, field=None):
def remote_edit(self):
"""
Triggers a remote edit to a plugin to allow item to be edited.
:param field:
@ -1475,7 +1456,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if new_item:
self.add_service_item(new_item, replace=True)
def on_service_item_rename(self, field=None):
def on_service_item_rename(self):
"""
Opens a dialog to rename the service item.
@ -1493,7 +1474,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(item, -1)
self.set_modified()
def create_custom(self, field=None):
def create_custom(self):
"""
Saves the current text item as a custom slide
:param field:
@ -1613,7 +1594,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.renderer.set_service_theme(self.service_theme)
self.regenerate_service_items()
def on_theme_change_action(self, field=None):
def on_theme_change_action(self):
"""
Handles theme change events

View File

@ -545,14 +545,14 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.on_theme_display(False)
self.on_hide_display(False)
def service_previous(self, field=None):
def service_previous(self):
"""
Live event to select the previous service item from the service manager.
"""
self.keypress_queue.append(ServiceItemAction.Previous)
self._process_queue()
def service_next(self, field=None):
def service_next(self):
"""
Live event to select the next service item from the service manager.
"""
@ -1103,7 +1103,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
else:
Registry().execute('live_display_show')
def on_slide_selected(self, field=None):
def on_slide_selected(self):
"""
Slide selected in controller
Note for some reason a dummy field is required. Nothing is passed!
@ -1244,7 +1244,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.preview_widget.change_slide(row)
self.slide_selected()
def on_slide_selected_previous(self, field=None):
def on_slide_selected_previous(self):
"""
Go to the previous slide.
"""
@ -1372,7 +1372,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
if event.timerId() == self.timer_id:
self.on_slide_selected_next(self.play_slides_loop.isChecked())
def on_edit_song(self, field=None):
def on_edit_song(self):
"""
From the preview display requires the service Item to be editied
"""
@ -1381,16 +1381,16 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
if new_item:
self.add_service_item(new_item)
def on_preview_add_to_service(self, field=None):
def on_preview_add_to_service(self):
"""
From the preview display request the Item to be added to service
"""
if self.service_item:
self.service_manager.add_service_item(self.service_item)
def on_preview_double_click(self, field=None):
def on_preview_double_click(self):
"""
Triggered when a preview slide item is doubleclicked
Triggered when a preview slide item is double clicked
"""
if self.service_item:
if Settings().value('advanced/double click live') and Settings().value('core/auto unblank'):

View File

@ -384,7 +384,7 @@ class BibleMediaItem(MediaManagerItem):
This initialises the given bible, which means that its book names and their chapter numbers is added to the
combo boxes on the 'Select' Tab. This is not of any importance of the 'Search' Tab.
:param last_book_id: The "book reference id" of the book which is chosen at the moment. (int)
:param last_book: The "book reference id" of the book which is chosen at the moment. (int)
:return: None
"""
log.debug('initialise_advanced_bible {bible}, {ref}'.format(bible=self.bible, ref=last_book))
@ -574,6 +574,7 @@ class BibleMediaItem(MediaManagerItem):
Update the second bible. If changing from single to dual bible modes as if the user wants to clear the search
results, if not revert to the previously selected bible
:param: selection not required by part of the signature
:return: None
"""
new_selection = self.second_combo_box.currentData()
@ -1005,14 +1006,17 @@ class BibleMediaItem(MediaManagerItem):
}[self.settings.display_style]
return '{{su}}{bracket[0]}{verse_text}{bracket[1]}{{/su}}&nbsp;'.format(verse_text=verse_text, bracket=bracket)
def search(self, string, showError):
def search(self, string, show_error=True):
"""
Search for some Bible verses (by reference).
:param string: search string
:param show_error: do we show the error
:return: the results of the search
"""
if self.bible is None:
return []
reference = self.plugin.manager.parse_ref(self.bible.name, string)
search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError)
search_results = self.plugin.manager.get_verses(self.bible.name, reference, show_error)
if search_results:
verse_text = ' '.join([verse.text for verse in search_results])
return [[string, verse_text]]

View File

@ -1,307 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import logging
import re
import zipfile
from xml.etree import ElementTree
from openlp.core.common import is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.display.screens import ScreenList
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
if is_win():
from ctypes import cdll
from ctypes.wintypes import RECT
log = logging.getLogger(__name__)
class PptviewController(PresentationController):
"""
Class to control interactions with PowerPoint Viewer Presentations. It creates the runtime Environment , Loads the
and Closes the Presentation. As well as triggering the correct activities based on the users input
"""
log.info('PPTViewController loaded')
def __init__(self, plugin):
"""
Initialise the class
"""
log.debug('Initialising')
self.process = None
super(PptviewController, self).__init__(plugin, 'Powerpoint Viewer', PptviewDocument)
self.supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm']
def check_available(self):
"""
PPT Viewer is able to run on this machine.
"""
log.debug('check_available')
if not is_win():
return False
return self.check_installed()
if is_win():
def check_installed(self):
"""
Check the viewer is installed.
"""
log.debug('Check installed')
try:
self.start_process()
return self.process.CheckInstalled()
except OSError:
return False
def start_process(self):
"""
Loads the PPTVIEWLIB library.
"""
if self.process:
return
log.debug('start PPTView')
dll_path = AppLocation.get_directory(AppLocation.AppDir) \
/ 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll'
self.process = cdll.LoadLibrary(str(dll_path))
if log.isEnabledFor(logging.DEBUG):
self.process.SetDebug(1)
def kill(self):
"""
Called at system exit to clean up any running presentations
"""
log.debug('Kill pptviewer')
while self.docs:
self.docs[0].close_presentation()
class PptviewDocument(PresentationDocument):
"""
Class which holds information and controls a single presentation.
"""
def __init__(self, controller, document_path):
"""
Constructor, store information about the file and initialise.
:param openlp.core.common.path.Path document_path: File path to the document to load
:rtype: None
"""
log.debug('Init Presentation PowerPoint')
super().__init__(controller, document_path)
self.presentation = None
self.ppt_id = None
self.blanked = False
self.hidden = False
def load_presentation(self):
"""
Called when a presentation is added to the SlideController. It builds the environment, starts communication with
the background PptView task started earlier.
"""
log.debug('LoadPresentation')
temp_path = self.get_temp_folder()
size = ScreenList().current['size']
rect = RECT(size.x(), size.y(), size.right(), size.bottom())
preview_path = temp_path / 'slide'
# Ensure that the paths are null terminated
file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0'
preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0'
if not temp_path.is_dir():
temp_path.mkdir(parents=True)
self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16)
if self.ppt_id >= 0:
self.create_thumbnails()
self.stop_presentation()
return True
else:
return False
def create_thumbnails(self):
"""
PPTviewLib creates large BMP's, but we want small PNG's for consistency. Convert them here.
"""
log.debug('create_thumbnails')
if self.check_thumbnails():
return
log.debug('create_thumbnails proceeding')
for idx in range(self.get_slide_count()):
path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=idx + 1)
self.convert_thumbnail(path, idx + 1)
def create_titles_and_notes(self):
"""
Extracts the titles and notes from the zipped file
and writes the list of titles (one per slide)
to 'titles.txt'
and the notes to 'slideNotes[x].txt'
in the thumbnails directory
"""
titles = None
notes = None
# let's make sure we have a valid zipped presentation
if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)):
namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
# open the file
with zipfile.ZipFile(str(self.file_path)) as zip_file:
# find the presentation.xml to get the slide count
with zip_file.open('ppt/presentation.xml') as pres:
tree = ElementTree.parse(pres)
nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces)
# initialize the lists
titles = ['' for i in range(len(nodes))]
notes = ['' for i in range(len(nodes))]
# loop thru the file list to find slides and notes
for zip_info in zip_file.infolist():
node_type = ''
index = -1
list_to_add = None
# check if it is a slide
match = re.search(r'slides/slide(.+)\.xml', zip_info.filename)
if match:
index = int(match.group(1)) - 1
node_type = 'ctrTitle'
list_to_add = titles
# or a note
match = re.search(r'notesSlides/notesSlide(.+)\.xml', zip_info.filename)
if match:
index = int(match.group(1)) - 1
node_type = 'body'
list_to_add = notes
# if it is one of our files, index shouldn't be -1
if index >= 0:
with zip_file.open(zip_info) as zipped_file:
tree = ElementTree.parse(zipped_file)
text = ''
nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t",
namespaces=namespaces)
# if we found any content
if nodes and len(nodes) > 0:
for node in nodes:
if len(text) > 0:
text += '\n'
text += node.text
# Let's remove the \n from the titles and
# just add one at the end
if node_type == 'ctrTitle':
text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n'
list_to_add[index] = text
# now let's write the files
self.save_titles_and_notes(titles, notes)
def close_presentation(self):
"""
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
shut down.
"""
log.debug('ClosePresentation')
if self.controller.process:
self.controller.process.ClosePPT(self.ppt_id)
self.ppt_id = -1
self.controller.remove_doc(self)
def is_loaded(self):
"""
Returns true if a presentation is loaded.
"""
if self.ppt_id < 0:
return False
if self.get_slide_count() < 0:
return False
return True
def is_active(self):
"""
Returns true if a presentation is currently active.
"""
return self.is_loaded() and not self.hidden
def blank_screen(self):
"""
Blanks the screen.
"""
self.controller.process.Blank(self.ppt_id)
self.blanked = True
def unblank_screen(self):
"""
Unblanks (restores) the presentation.
"""
self.controller.process.Unblank(self.ppt_id)
self.blanked = False
def is_blank(self):
"""
Returns true if screen is blank.
"""
log.debug('is blank OpenOffice')
return self.blanked
def stop_presentation(self):
"""
Stops the current presentation and hides the output.
"""
self.hidden = True
self.controller.process.Stop(self.ppt_id)
def start_presentation(self):
"""
Starts a presentation from the beginning.
"""
if self.hidden:
self.hidden = False
self.controller.process.Resume(self.ppt_id)
else:
self.controller.process.RestartShow(self.ppt_id)
def get_slide_number(self):
"""
Returns the current slide number.
"""
return self.controller.process.GetCurrentSlide(self.ppt_id)
def get_slide_count(self):
"""
Returns total number of slides.
"""
return self.controller.process.GetSlideCount(self.ppt_id)
def goto_slide(self, slide_no):
"""
Moves to a specific slide in the presentation.
:param slide_no: The slide the text is required for, starting at 1
"""
self.controller.process.GotoSlide(self.ppt_id, slide_no)
def next_step(self):
"""
Triggers the next effect of slide on the running presentation.
"""
self.controller.process.NextStep(self.ppt_id)
def previous_step(self):
"""
Triggers the previous slide on the running presentation.
"""
self.controller.process.PrevStep(self.ppt_id)

View File

@ -1,121 +0,0 @@
PPTVIEWLIB - Control PowerPoint Viewer 2003/2007 (for openlp.org)
Copyright (C) 2008-2018 Jonathan Corwin (j@corwin.co.uk)
This library wrappers the free Microsoft PowerPoint Viewer (2003/2007) program,
allowing it to be more easily controlled from another program.
The PowerPoint Viewer must already be installed on the destination machine, and is
freely available at microsoft.com.
The full Microsoft Office PowerPoint and PowerPoint Viewer 97 have a COM interface allowing
automation. This ability was removed from the 2003+ viewer offerings.
To developers: I am not a C/C++ or Win32 API programmer as you can probably tell.
The code and API of this DLL could certainly do with some tidying up, and the
error trapping, where it exists, is very basic. I'll happily accept patches!
This library is covered by the GPL (http://www.gnu.org/licenses/)
It is NOT covered by the LGPL, so can only be used in GPL compatable programs.
(http://www.gnu.org/licenses/why-not-lgpl.html)
This README.TXT must be distributed with the pptviewlib.dll
This library has a limit of 50 PowerPoints which can be opened simultaneously.
This project can be built with the free Microsoft Visual C++ 2008 Express Edition.
USAGE
-----
BOOL CheckInstalled(void);
Returns TRUE if PowerPointViewer is installed. FALSE if not.
int OpenPPT(char *filename, HWND hParentWnd, RECT rect, char *previewpath);
Opens the PowerPoint file, counts the number of slides, sizes and positions accordingly
and creates preview images of each slide. Note PowerPoint Viewer only allows the
slideshow to be resized whilst it is being loaded. It can be moved at any time however.
The only way to count the number of slides is to step through the entire show. Therefore
there will be a delay whilst opening large presentations for the first time.
For pre XP/2003 systems, the slideshow will flicker as the screen snapshots are taken.
filename: The PowerPoint file to be opened. Full path
hParentWnd: The window which will become the parent of the slideshow window.
Can be NULL.
rect: The location/dimensions of the slideshow window.
If all properties of this structure are zero, the dimensions of the hParentWnd
are used.
previewpath If specified, the prefix to use for snapshot images of each slide, in the
form: previewpath + n + ".bmp", where n is the slide number.
A file called previewpath + "info.txt" will also be created containing information
about the PPT file, to speed up future openings of the unmodified file.
Note it is up the calling program to directly access these images if they
are required.
RETURNS: An unique identifier to pass to other methods in this library.
If < 0, then the PPT failed to open.
If >=0, ClosePPT must be called when the PPT is no longer being used
or when the calling program is closed to release resources/hooks.
void ClosePPT(int id);
Closes the presentation, releasing any resources and hooks.
id: The value returned from OpenPPT.
int GetCurrentSlide(int id);
Returns the current slide number (from 1)
id: The value returned from OpenPPT.
int GetSlideCount(int id);
Returns the total number of slides.
id: The value returned from OpenPPT.
void NextStep(int id);
Advances one step (animation) through the slideshow.
id: The value returned from OpenPPT.
void PrevStep(int id);
Goes backwards one step (animation) through the slideshow.
id: The value returned from OpenPPT.
void GotoSlide(int id, int slideno);
Goes directly to a specific slide in the slideshow
id: The value returned from OpenPPT.
slideno: The number of the slide (from 1) to go directly to.
If the slide has already been displayed, then the completed slide with animations performed
will be shown. This is how the PowerPoint Viewer works so have no control over this.
void RestartShow(int id);
Restarts the show from the beginning. To reset animations, behind the scenes it
has to travel to the end and step backwards though the entire show. Therefore
for large presentations there might be a delay.
id: The value returned from OpenPPT.
void Blank(int id);
Blanks the screen, colour black.
id: The value returned from OpenPPT.
void Unblank(int id)
Unblanks the screen, restoring it to it's pre-blank state.
id: The value returned from OpenPPT.
void Stop(int id)
Moves the slideshow off the screen. (There is no concept of stop show in the PowerPoint Viewer)
id: The value returned from OpenPPT.
void Resume(int id)
Moves the slideshow display back onto the screen following a Stop()
id: The value returned from OpenPPT.

View File

@ -1,210 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import sys
from ctypes import *
from ctypes.wintypes import RECT
from PyQt5 import QtWidgets
class PPTViewer(QtWidgets.QWidget):
"""
Standalone Test Harness for the pptviewlib library
"""
def __init__(self, parent=None):
super(PPTViewer, self).__init__(parent)
self.pptid = -1
self.setWindowTitle('PowerPoint Viewer Test')
ppt_label = QtWidgets.QLabel('Open PowerPoint file')
slide_label = QtWidgets.QLabel('Go to slide #')
self.pptEdit = QtWidgets.QLineEdit()
self.slideEdit = QtWidgets.QLineEdit()
x_label = QtWidgets.QLabel('X pos')
y_label = QtWidgets.QLabel('Y pos')
width_label = QtWidgets.QLabel('Width')
height_label = QtWidgets.QLabel('Height')
self.xEdit = QtWidgets.QLineEdit('100')
self.yEdit = QtWidgets.QLineEdit('100')
self.widthEdit = QtWidgets.QLineEdit('900')
self.heightEdit = QtWidgets.QLineEdit('700')
self.total = QtWidgets.QLabel()
ppt_btn = QtWidgets.QPushButton('Open')
ppt_dlg_btn = QtWidgets.QPushButton('...')
folder_label = QtWidgets.QLabel('Slide .bmp path')
self.folderEdit = QtWidgets.QLineEdit('slide')
slide_btn = QtWidgets.QPushButton('Go')
prev = QtWidgets.QPushButton('Prev')
next = QtWidgets.QPushButton('Next')
blank = QtWidgets.QPushButton('Blank')
unblank = QtWidgets.QPushButton('Unblank')
restart = QtWidgets.QPushButton('Restart')
close = QtWidgets.QPushButton('Close')
resume = QtWidgets.QPushButton('Resume')
stop = QtWidgets.QPushButton('Stop')
grid = QtWidgets.QGridLayout()
row = 0
grid.addWidget(folder_label, 0, 0)
grid.addWidget(self.folderEdit, 0, 1)
row += 1
grid.addWidget(x_label, row, 0)
grid.addWidget(self.xEdit, row, 1)
grid.addWidget(y_label, row, 2)
grid.addWidget(self.yEdit, row, 3)
row += 1
grid.addWidget(width_label, row, 0)
grid.addWidget(self.widthEdit, row, 1)
grid.addWidget(height_label, row, 2)
grid.addWidget(self.heightEdit, row, 3)
row += 1
grid.addWidget(ppt_label, row, 0)
grid.addWidget(self.pptEdit, row, 1)
grid.addWidget(ppt_dlg_btn, row, 2)
grid.addWidget(ppt_btn, row, 3)
row += 1
grid.addWidget(slide_label, row, 0)
grid.addWidget(self.slideEdit, row, 1)
grid.addWidget(slide_btn, row, 2)
row += 1
grid.addWidget(prev, row, 0)
grid.addWidget(next, row, 1)
row += 1
grid.addWidget(blank, row, 0)
grid.addWidget(unblank, row, 1)
row += 1
grid.addWidget(restart, row, 0)
grid.addWidget(close, row, 1)
row += 1
grid.addWidget(stop, row, 0)
grid.addWidget(resume, row, 1)
ppt_btn.clicked.connect(self.openClick)
ppt_dlg_btn.clicked.connect(self.openDialog)
slide_btn.clicked.connect(self.gotoClick)
prev.clicked.connect(self.prevClick)
next.clicked.connect(self.nextClick)
blank.clicked.connect(self.blankClick)
unblank.clicked.connect(self.unblankClick)
restart.clicked.connect(self.restartClick)
close.clicked.connect(self.closeClick)
stop.clicked.connect(self.stopClick)
resume.clicked.connect(self.resumeClick)
self.setLayout(grid)
self.resize(300, 150)
def prevClick(self):
if self.pptid < 0:
return
self.pptdll.PrevStep(self.pptid)
self.updateCurrSlide()
app.processEvents()
def nextClick(self):
if self.pptid < 0:
return
self.pptdll.NextStep(self.pptid)
self.updateCurrSlide()
app.processEvents()
def blankClick(self):
if self.pptid < 0:
return
self.pptdll.Blank(self.pptid)
app.processEvents()
def unblankClick(self):
if self.pptid < 0:
return
self.pptdll.Unblank(self.pptid)
app.processEvents()
def restartClick(self):
if self.pptid < 0:
return
self.pptdll.RestartShow(self.pptid)
self.updateCurrSlide()
app.processEvents()
def stopClick(self):
if self.pptid < 0:
return
self.pptdll.Stop(self.pptid)
app.processEvents()
def resumeClick(self):
if self.pptid < 0:
return
self.pptdll.Resume(self.pptid)
app.processEvents()
def closeClick(self):
if self.pptid < 0:
return
self.pptdll.ClosePPT(self.pptid)
self.pptid = -1
app.processEvents()
def openClick(self):
oldid = self.pptid
rect = RECT(int(self.xEdit.text()), int(self.yEdit.text()),
int(self.widthEdit.text()), int(self.heightEdit.text()))
filename = str(self.pptEdit.text().replace('/', '\\'))
folder = str(self.folderEdit.text().replace('/', '\\'))
print(filename, folder)
self.pptid = self.pptdll.OpenPPT(filename, None, rect, folder)
print('id: ' + str(self.pptid))
if oldid >= 0:
self.pptdll.ClosePPT(oldid)
slides = self.pptdll.GetSlideCount(self.pptid)
print('slidecount: ' + str(slides))
self.total.setNum(self.pptdll.GetSlideCount(self.pptid))
self.updateCurrSlide()
def updateCurrSlide(self):
if self.pptid < 0:
return
slide = str(self.pptdll.GetCurrentSlide(self.pptid))
print('currslide: ' + slide)
self.slideEdit.setText(slide)
app.processEvents()
def gotoClick(self):
if self.pptid < 0:
return
print(self.slideEdit.text())
self.pptdll.GotoSlide(self.pptid, int(self.slideEdit.text()))
self.updateCurrSlide()
app.processEvents()
def openDialog(self):
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
if __name__ == '__main__':
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
pptdll.SetDebug(1)
print('Begin...')
app = QtWidgets.QApplication(sys.argv)
window = PPTViewer()
window.pptdll = pptdll
window.show()
sys.exit(app.exec())

View File

@ -1,920 +0,0 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2018 OpenLP Developers *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#include <direct.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "pptviewlib.h"
// Because of the callbacks used by SetWindowsHookEx, the memory used needs to
// be sharable across processes (the callbacks are done from a different
// process) Therefore use data_seg with RWS memory.
//
// See http://msdn.microsoft.com/en-us/library/aa366551(VS.85).aspx for
// alternative method of holding memory, removing fixed limits which would allow
// dynamic number of items, rather than a fixed number. Use a Local\ mapping,
// since global has UAC issues in Vista.
#pragma data_seg(".PPTVIEWLIB")
PPTVIEW pptView[MAX_PPTS] = {NULL};
HHOOK globalHook = NULL;
BOOL debug = FALSE;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.PPTVIEWLIB,RWS")
HINSTANCE hInstance = NULL;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall,
LPVOID lpReserved)
{
hInstance = (HINSTANCE)hModule;
switch(ulReasonForCall)
{
case DLL_PROCESS_ATTACH:
DEBUG(L"PROCESS_ATTACH\n");
break;
case DLL_THREAD_ATTACH:
//DEBUG(L"THREAD_ATTACH\n");
break;
case DLL_THREAD_DETACH:
//DEBUG(L"THREAD_DETACH\n");
break;
case DLL_PROCESS_DETACH:
// Clean up... hopefully there is only the one process attached?
// We'll find out soon enough during tests!
DEBUG(L"PROCESS_DETACH\n");
for (int i = 0; i < MAX_PPTS; i++)
ClosePPT(i);
break;
}
return TRUE;
}
DllExport void SetDebug(BOOL onOff)
{
printf("SetDebug\n");
debug = onOff;
DEBUG(L"enabled\n");
}
DllExport BOOL CheckInstalled()
{
wchar_t cmdLine[MAX_PATH * 2];
DEBUG(L"CheckInstalled\n");
BOOL found = GetPPTViewerPath(cmdLine, sizeof(cmdLine));
if(found)
{
DEBUG(L"Exe: %s\n", cmdLine);
}
return found;
}
// Open the PointPoint, count the slides and take a snapshot of each slide
// for use in previews
// previewpath is a prefix for the location to put preview images of each slide.
// "<n>.bmp" will be appended to complete the path. E.g. "c:\temp\slide" would
// create "c:\temp\slide1.bmp" slide2.bmp, slide3.bmp etc.
// It will also create a *info.txt containing information about the ppt
DllExport int OpenPPT(wchar_t *filename, HWND hParentWnd, RECT rect,
wchar_t *previewPath)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
wchar_t cmdLine[MAX_PATH * 2];
int id;
DEBUG(L"OpenPPT start: %s; %s\n", filename, previewPath);
DEBUG(L"OpenPPT start: %u; %i, %i, %i, %i\n", hParentWnd, rect.top,
rect.left, rect.bottom, rect.right);
if (GetPPTViewerPath(cmdLine, sizeof(cmdLine)) == FALSE)
{
DEBUG(L"OpenPPT: GetPPTViewerPath failed\n");
return -1;
}
id = -1;
for (int i = 0; i < MAX_PPTS; i++)
{
if (pptView[i].state == PPT_CLOSED)
{
id = i;
break;
}
}
if (id < 0)
{
DEBUG(L"OpenPPT: Too many PPTs\n");
return -1;
}
memset(&pptView[id], 0, sizeof(PPTVIEW));
wcscpy_s(pptView[id].filename, MAX_PATH, filename);
wcscpy_s(pptView[id].previewPath, MAX_PATH, previewPath);
pptView[id].state = PPT_CLOSED;
pptView[id].slideCount = 0;
pptView[id].currentSlide = 0;
pptView[id].firstSlideSteps = 0;
pptView[id].lastSlideSteps = 0;
pptView[id].guess = 0;
pptView[id].hParentWnd = hParentWnd;
pptView[id].hWnd = NULL;
pptView[id].hWnd2 = NULL;
for (int i = 0; i < MAX_SLIDES; i++)
{
pptView[id].slideNos[i] = 0;
}
if (hParentWnd != NULL && rect.top == 0 && rect.bottom == 0
&& rect.left == 0 && rect.right == 0)
{
LPRECT windowRect = NULL;
GetWindowRect(hParentWnd, windowRect);
pptView[id].rect.top = 0;
pptView[id].rect.left = 0;
pptView[id].rect.bottom = windowRect->bottom - windowRect->top;
pptView[id].rect.right = windowRect->right - windowRect->left;
}
else
{
pptView[id].rect.top = rect.top;
pptView[id].rect.left = rect.left;
pptView[id].rect.bottom = rect.bottom;
pptView[id].rect.right = rect.right;
}
wcscat_s(cmdLine, MAX_PATH * 2, L" /F /S \"");
wcscat_s(cmdLine, MAX_PATH * 2, filename);
wcscat_s(cmdLine, MAX_PATH * 2, L"\"");
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
BOOL gotInfo = GetPPTInfo(id);
/*
* I'd really like to just hook on the new threadid. However this always
* gives error 87. Perhaps I'm hooking to soon? No idea... however can't
* wait since I need to ensure I pick up the WM_CREATE as this is the only
* time the window can be resized in such away the content scales correctly
*
* hook = SetWindowsHookEx(WH_CBT,CbtProc,hInstance,pi.dwThreadId);
*/
if (globalHook != NULL)
{
UnhookWindowsHookEx(globalHook);
}
globalHook = SetWindowsHookEx(WH_CBT, CbtProc, hInstance, NULL);
if (globalHook == 0)
{
DEBUG(L"OpenPPT: SetWindowsHookEx failed\n");
ClosePPT(id);
return -1;
}
pptView[id].state = PPT_STARTED;
Sleep(10);
if (!CreateProcess(NULL, cmdLine, NULL, NULL, FALSE, 0, 0, NULL, &si, &pi))
{
DEBUG(L"OpenPPT: CreateProcess failed: %s\n", cmdLine);
ClosePPT(id);
return -1;
}
pptView[id].dwProcessId = pi.dwProcessId;
pptView[id].dwThreadId = pi.dwThreadId;
pptView[id].hThread = pi.hThread;
pptView[id].hProcess = pi.hProcess;
while (pptView[id].state == PPT_STARTED)
Sleep(10);
if (gotInfo)
{
DEBUG(L"OpenPPT: Info loaded, no refresh\n");
pptView[id].state = PPT_LOADED;
Resume(id);
}
else
{
DEBUG(L"OpenPPT: Get info\n");
pptView[id].steps = 0;
int steps = 0;
while (pptView[id].state == PPT_OPENED)
{
if (steps <= pptView[id].steps)
{
Sleep(100);
DEBUG(L"OpenPPT: Step %d/%d\n", steps, pptView[id].steps);
steps++;
NextStep(id);
}
Sleep(10);
}
DEBUG(L"OpenPPT: Slides %d, Steps %d, first slide steps %d\n",
pptView[id].slideCount, pptView[id].steps,
pptView[id].firstSlideSteps);
for(int i = 1; i <= pptView[id].slideCount; i++)
{
DEBUG(L"OpenPPT: Slide %d = %d\n", i, pptView[id].slideNos[i]);
}
SavePPTInfo(id);
if (pptView[id].state == PPT_CLOSING
|| pptView[id].slideCount <= 0)
{
ClosePPT(id);
id=-1;
}
else
{
RestartShow(id);
}
}
if (id >= 0)
{
if (pptView[id].msgHook != NULL)
{
UnhookWindowsHookEx(pptView[id].msgHook);
}
pptView[id].msgHook = NULL;
}
DEBUG(L"OpenPPT: Exit: id=%i\n", id);
return id;
}
// Load information about the ppt from an info.txt file.
// Format:
// version
// filedate
// filesize
// slidecount
// first slide steps
BOOL GetPPTInfo(int id)
{
struct _stat fileStats;
wchar_t info[MAX_PATH];
FILE* pFile;
wchar_t buf[100];
DEBUG(L"GetPPTInfo: start\n");
if (_wstat(pptView[id].filename, &fileStats) != 0)
{
return FALSE;
}
swprintf_s(info, MAX_PATH, L"%sinfo.txt", pptView[id].previewPath);
int err = _wfopen_s(&pFile, info, L"r");
if (err != 0)
{
DEBUG(L"GetPPTInfo: file open failed - %d\n", err);
return FALSE;
}
fgetws(buf, 100, pFile); // version == 1
fgetws(buf, 100, pFile);
if (fileStats.st_mtime != _wtoi(buf))
{
DEBUG(L"GetPPTInfo: date changed\n");
fclose (pFile);
return FALSE;
}
fgetws(buf, 100, pFile);
if (fileStats.st_size != _wtoi(buf))
{
DEBUG(L"GetPPTInfo: size changed\n");
fclose (pFile);
return FALSE;
}
fgetws(buf, 100, pFile); // slidecount
int slideCount = _wtoi(buf);
fgetws(buf, 100, pFile); // first slide steps
int firstSlideSteps = _wtoi(buf);
// check all the preview images still exist
for (int i = 1; i <= slideCount; i++)
{
swprintf_s(info, MAX_PATH, L"%s%i.bmp", pptView[id].previewPath, i);
if (GetFileAttributes(info) == INVALID_FILE_ATTRIBUTES)
{
DEBUG(L"GetPPTInfo: bmp not found\n");
return FALSE;
}
}
fclose(pFile);
pptView[id].slideCount = slideCount;
pptView[id].firstSlideSteps = firstSlideSteps;
DEBUG(L"GetPPTInfo: exit ok\n");
return TRUE;
}
BOOL SavePPTInfo(int id)
{
struct _stat fileStats;
wchar_t info[MAX_PATH];
FILE* pFile;
DEBUG(L"SavePPTInfo: start\n");
if (_wstat(pptView[id].filename, &fileStats) != 0)
{
DEBUG(L"SavePPTInfo: stat of %s failed\n", pptView[id].filename);
return FALSE;
}
swprintf_s(info, MAX_PATH, L"%sinfo.txt", pptView[id].previewPath);
int err = _wfopen_s(&pFile, info, L"w");
if (err != 0)
{
DEBUG(L"SavePPTInfo: fopen of %s failed%i\n", info, err);
return FALSE;
}
fprintf(pFile, "1\n");
fprintf(pFile, "%u\n", fileStats.st_mtime);
fprintf(pFile, "%u\n", fileStats.st_size);
fprintf(pFile, "%u\n", pptView[id].slideCount);
fprintf(pFile, "%u\n", pptView[id].firstSlideSteps);
fclose(pFile);
DEBUG(L"SavePPTInfo: exit ok\n");
return TRUE;
}
// Get the path of the PowerPoint viewer from the registry
BOOL GetPPTViewerPath(wchar_t *pptViewerPath, int stringSize)
{
wchar_t cwd[MAX_PATH];
DEBUG(L"GetPPTViewerPath: start\n");
if(GetPPTViewerPathFromReg(pptViewerPath, stringSize))
{
if(_waccess(pptViewerPath, 0) != -1)
{
DEBUG(L"GetPPTViewerPath: exit registry\n");
return TRUE;
}
}
// This is where it gets ugly. PPT2007 it seems no longer stores its
// location in the registry. So we have to use the defaults which will
// upset those who like to put things somewhere else
// Viewer 2007 in 64bit Windows:
if(_waccess(L"C:\\Program Files (x86)\\Microsoft Office\\Office12\\PPTVIEW.EXE",
0) != -1)
{
wcscpy_s(
L"C:\\Program Files (x86)\\Microsoft Office\\Office12\\PPTVIEW.EXE",
stringSize, pptViewerPath);
DEBUG(L"GetPPTViewerPath: exit 64bit 2007\n");
return TRUE;
}
// Viewer 2007 in 32bit Windows:
if(_waccess(L"C:\\Program Files\\Microsoft Office\\Office12\\PPTVIEW.EXE", 0)
!= -1)
{
wcscpy_s(L"C:\\Program Files\\Microsoft Office\\Office12\\PPTVIEW.EXE",
stringSize, pptViewerPath);
DEBUG(L"GetPPTViewerPath: exit 32bit 2007\n");
return TRUE;
}
// Give them the opportunity to place it in the same folder as the app
_wgetcwd(cwd, MAX_PATH);
wcscat_s(cwd, MAX_PATH, L"\\PPTVIEW.EXE");
if(_waccess(cwd, 0) != -1)
{
wcscpy_s(pptViewerPath, stringSize, cwd);
DEBUG(L"GetPPTViewerPath: exit local\n");
return TRUE;
}
DEBUG(L"GetPPTViewerPath: exit fail\n");
return FALSE;
}
BOOL GetPPTViewerPathFromReg(wchar_t *pptViewerPath, int stringSize)
{
HKEY hKey;
DWORD dwType, dwSize;
LRESULT lResult;
// The following registry settings are for, respectively, (I think)
// PPT Viewer 2007 (older versions. Latest not in registry) & PPT Viewer 2010
// PPT Viewer 2003 (recent versions)
// PPT Viewer 2003 (older versions)
// PPT Viewer 97
if ((RegOpenKeyExW(HKEY_CLASSES_ROOT,
L"PowerPointViewer.Show.12\\shell\\Show\\command", 0, KEY_READ, &hKey)
!= ERROR_SUCCESS)
&& (RegOpenKeyExW(HKEY_CLASSES_ROOT,
L"PowerPointViewer.Show.11\\shell\\Show\\command", 0, KEY_READ, &hKey)
!= ERROR_SUCCESS)
&& (RegOpenKeyExW(HKEY_CLASSES_ROOT,
L"Applications\\PPTVIEW.EXE\\shell\\open\\command", 0, KEY_READ, &hKey)
!= ERROR_SUCCESS)
&& (RegOpenKeyExW(HKEY_CLASSES_ROOT,
L"Applications\\PPTVIEW.EXE\\shell\\Show\\command", 0, KEY_READ, &hKey)
!= ERROR_SUCCESS))
{
return FALSE;
}
dwType = REG_SZ;
dwSize = (DWORD)stringSize;
lResult = RegQueryValueEx(hKey, NULL, NULL, &dwType, (LPBYTE)pptViewerPath,
&dwSize);
RegCloseKey(hKey);
if (lResult != ERROR_SUCCESS)
{
return FALSE;
}
// remove "%1" from end of key value
pptViewerPath[wcslen(pptViewerPath) - 4] = '\0';
return TRUE;
}
// Unhook the Windows hook
void Unhook(int id)
{
DEBUG(L"Unhook: start %d\n", id);
if (pptView[id].hook != NULL)
{
UnhookWindowsHookEx(pptView[id].hook);
}
if (pptView[id].msgHook != NULL)
{
UnhookWindowsHookEx(pptView[id].msgHook);
}
pptView[id].hook = NULL;
pptView[id].msgHook = NULL;
DEBUG(L"Unhook: exit ok\n");
}
// Close the PowerPoint viewer, release resources
DllExport void ClosePPT(int id)
{
DEBUG(L"ClosePPT: start%d\n", id);
pptView[id].state = PPT_CLOSED;
Unhook(id);
if (pptView[id].hWnd == 0)
{
TerminateThread(pptView[id].hThread, 0);
}
else
{
PostMessage(pptView[id].hWnd, WM_CLOSE, 0, 0);
}
CloseHandle(pptView[id].hThread);
CloseHandle(pptView[id].hProcess);
memset(&pptView[id], 0, sizeof(PPTVIEW));
DEBUG(L"ClosePPT: exit ok\n");
return;
}
// Moves the show back onto the display
DllExport void Resume(int id)
{
DEBUG(L"Resume: %d\n", id);
MoveWindow(pptView[id].hWnd, pptView[id].rect.left,
pptView[id].rect.top,
pptView[id].rect.right - pptView[id].rect.left,
pptView[id].rect.bottom - pptView[id].rect.top, TRUE);
Unblank(id);
}
// Moves the show off the screen so it can't be seen
DllExport void Stop(int id)
{
DEBUG(L"Stop:%d\n", id);
MoveWindow(pptView[id].hWnd, -32000, -32000,
pptView[id].rect.right - pptView[id].rect.left,
pptView[id].rect.bottom - pptView[id].rect.top, TRUE);
}
// Return the total number of slides
DllExport int GetSlideCount(int id)
{
DEBUG(L"GetSlideCount:%d\n", id);
if (pptView[id].state == 0)
{
return -1;
}
else
{
return pptView[id].slideCount;
}
}
// Return the number of the slide currently viewing
DllExport int GetCurrentSlide(int id)
{
DEBUG(L"GetCurrentSlide:%d\n", id);
if (pptView[id].state == 0)
{
return -1;
}
else
{
return pptView[id].currentSlide;
}
}
// Take a step forwards through the show
DllExport void NextStep(int id)
{
DEBUG(L"NextStep:%d (%d)\n", id, pptView[id].currentSlide);
if (pptView[id].currentSlide > pptView[id].slideCount) return;
if (pptView[id].currentSlide < pptView[id].slideCount)
{
pptView[id].guess = pptView[id].currentSlide + 1;
}
PostMessage(pptView[id].hWnd2, WM_MOUSEWHEEL, MAKEWPARAM(0, -WHEEL_DELTA),
0);
}
// Take a step backwards through the show
DllExport void PrevStep(int id)
{
DEBUG(L"PrevStep:%d (%d)\n", id, pptView[id].currentSlide);
if (pptView[id].currentSlide > 1)
{
pptView[id].guess = pptView[id].currentSlide - 1;
}
PostMessage(pptView[id].hWnd2, WM_MOUSEWHEEL, MAKEWPARAM(0, WHEEL_DELTA),
0);
}
// Blank the show (black screen)
DllExport void Blank(int id)
{
// B just toggles blank on/off. However pressing any key unblanks.
// So send random unmapped letter first (say 'A'), then we can
// better guarantee B will blank instead of trying to guess
// whether it was already blank or not.
DEBUG(L"Blank:%d\n", id);
HWND h1 = GetForegroundWindow();
HWND h2 = GetFocus();
SetForegroundWindow(pptView[id].hWnd);
SetFocus(pptView[id].hWnd);
// slight pause, otherwise event triggering this call may grab focus back!
Sleep(50);
keybd_event((int)'A', 0, 0, 0);
keybd_event((int)'A', 0, KEYEVENTF_KEYUP, 0);
keybd_event((int)'B', 0, 0, 0);
keybd_event((int)'B', 0, KEYEVENTF_KEYUP, 0);
SetForegroundWindow(h1);
SetFocus(h2);
}
// Unblank the show
DllExport void Unblank(int id)
{
DEBUG(L"Unblank:%d\n", id);
// Pressing any key resumes.
// For some reason SendMessage works for unblanking, but not blanking.
SendMessage(pptView[id].hWnd2, WM_CHAR, 'A', 0);
}
// Go directly to a slide
DllExport void GotoSlide(int id, int slideNo)
{
DEBUG(L"GotoSlide %i %i:\n", id, slideNo);
// Did try WM_KEYDOWN/WM_CHAR/WM_KEYUP with SendMessage but didn't work
// perhaps I was sending to the wrong window? No idea.
// Anyway fall back to keybd_event, which is OK as long we makesure
// the slideshow has focus first
char ch[10];
if (slideNo < 0) return;
pptView[id].guess = slideNo;
_itoa_s(slideNo, ch, 10, 10);
HWND h1 = GetForegroundWindow();
HWND h2 = GetFocus();
SetForegroundWindow(pptView[id].hWnd);
SetFocus(pptView[id].hWnd);
// slight pause, otherwise event triggering this call may grab focus back!
Sleep(50);
for (int i=0; i<10; i++)
{
if (ch[i] == '\0') break;
keybd_event((BYTE)ch[i], 0, 0, 0);
keybd_event((BYTE)ch[i], 0, KEYEVENTF_KEYUP, 0);
}
keybd_event(VK_RETURN, 0, 0, 0);
keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
SetForegroundWindow(h1);
SetFocus(h2);
}
// Restart the show from the beginning
DllExport void RestartShow(int id)
{
// If we just go direct to slide one, then it remembers that all other
// slides have been animated, so ends up just showing the completed slides
// of those slides that have been animated next time we advance.
// Only way I've found to get around this is to step backwards all the way
// through. Lets move the window out of the way first so the audience
// doesn't see this.
DEBUG(L"RestartShow:%d\n", id);
Stop(id);
GotoSlide(id, pptView[id].slideCount);
for (int i=0; i <= pptView[id].steps - pptView[id].lastSlideSteps; i++)
{
PrevStep(id);
Sleep(10);
}
int i = 0;
while ((pptView[id].currentSlide > 1) && (i++ < 30000))
{
Sleep(10);
}
Resume(id);
}
// This hook is started with the PPTVIEW.EXE process and waits for the
// WM_CREATEWND message. At this point (and only this point) can the
// window be resized to the correct size.
// Release the hook as soon as we're complete to free up resources
LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam)
{
HHOOK hook = globalHook;
if (nCode == HCBT_CREATEWND)
{
wchar_t csClassName[32];
HWND hCurrWnd = (HWND)wParam;
DWORD retProcId = NULL;
GetClassName(hCurrWnd, csClassName, sizeof(csClassName));
if ((wcscmp(csClassName, L"paneClassDC") == 0)
||(wcscmp(csClassName, L"screenClass") == 0))
{
int id = -1;
DWORD windowThread = GetWindowThreadProcessId(hCurrWnd, NULL);
for (int i=0; i < MAX_PPTS; i++)
{
if (pptView[i].dwThreadId == windowThread)
{
id = i;
break;
}
}
if (id >= 0)
{
if (wcscmp(csClassName, L"paneClassDC") == 0)
{
pptView[id].hWnd2 = hCurrWnd;
}
else
{
pptView[id].hWnd = hCurrWnd;
CBT_CREATEWND* cw = (CBT_CREATEWND*)lParam;
if (pptView[id].hParentWnd != NULL)
{
cw->lpcs->hwndParent = pptView[id].hParentWnd;
}
cw->lpcs->cy = pptView[id].rect.bottom
- pptView[id].rect.top;
cw->lpcs->cx = pptView[id].rect.right
- pptView[id].rect.left;
cw->lpcs->y = -32000;
cw->lpcs->x = -32000;
}
if ((pptView[id].hWnd != NULL) && (pptView[id].hWnd2 != NULL))
{
UnhookWindowsHookEx(globalHook);
globalHook = NULL;
pptView[id].hook = SetWindowsHookEx(WH_CALLWNDPROC,
CwpProc, hInstance, pptView[id].dwThreadId);
pptView[id].msgHook = SetWindowsHookEx(WH_GETMESSAGE,
GetMsgProc, hInstance, pptView[id].dwThreadId);
Sleep(10);
pptView[id].state = PPT_OPENED;
}
}
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
// This hook exists whilst the slideshow is loading but only listens on the
// slideshows thread. It listens out for mousewheel events
LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
HHOOK hook = NULL;
MSG *pMSG = (MSG *)lParam;
DWORD windowThread = GetWindowThreadProcessId(pMSG->hwnd, NULL);
int id = -1;
for (int i = 0; i < MAX_PPTS; i++)
{
if (pptView[i].dwThreadId == windowThread)
{
id = i;
hook = pptView[id].msgHook;
break;
}
}
if (id >= 0 && nCode == HC_ACTION && wParam == PM_REMOVE
&& pMSG->message == WM_MOUSEWHEEL)
{
if (pptView[id].state != PPT_LOADED)
{
if (pptView[id].currentSlide == 1)
{
pptView[id].firstSlideSteps++;
}
pptView[id].steps++;
pptView[id].lastSlideSteps++;
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
// This hook exists whilst the slideshow is running but only listens on the
// slideshows thread. It listens out for slide changes, message WM_USER+22.
LRESULT CALLBACK CwpProc(int nCode, WPARAM wParam, LPARAM lParam){
CWPSTRUCT *cwp;
cwp = (CWPSTRUCT *)lParam;
HHOOK hook = NULL;
wchar_t filename[MAX_PATH];
DWORD windowThread = GetWindowThreadProcessId(cwp->hwnd, NULL);
int id = -1;
for (int i = 0; i < MAX_PPTS; i++)
{
if (pptView[i].dwThreadId == windowThread)
{
id = i;
hook = pptView[id].hook;
break;
}
}
if ((id >= 0) && (nCode == HC_ACTION))
{
if (cwp->message == WM_USER + 22)
{
if (pptView[id].state != PPT_LOADED)
{
if ((pptView[id].currentSlide > 0)
&& (pptView[id].previewPath != NULL
&& wcslen(pptView[id].previewPath) > 0))
{
swprintf_s(filename, MAX_PATH, L"%s%i.bmp",
pptView[id].previewPath,
pptView[id].currentSlide);
CaptureAndSaveWindow(cwp->hwnd, filename);
}
if (((cwp->wParam == 0)
|| (pptView[id].slideNos[1] == cwp->wParam))
&& (pptView[id].currentSlide > 0))
{
pptView[id].state = PPT_LOADED;
pptView[id].currentSlide = pptView[id].slideCount + 1;
}
else
{
if (cwp->wParam > 0)
{
pptView[id].currentSlide = pptView[id].currentSlide + 1;
pptView[id].slideNos[pptView[id].currentSlide]
= cwp->wParam;
pptView[id].slideCount = pptView[id].currentSlide;
pptView[id].lastSlideSteps = 0;
}
}
}
else
{
if (cwp->wParam > 0)
{
if(pptView[id].guess > 0
&& pptView[id].slideNos[pptView[id].guess] == 0)
{
pptView[id].currentSlide = 0;
}
for(int i = 1; i <= pptView[id].slideCount; i++)
{
if(pptView[id].slideNos[i] == cwp->wParam)
{
pptView[id].currentSlide = i;
break;
}
}
if(pptView[id].currentSlide == 0)
{
pptView[id].slideNos[pptView[id].guess] = cwp->wParam;
pptView[id].currentSlide = pptView[id].guess;
}
pptView[id].guess = 0;
}
}
}
if ((pptView[id].state != PPT_CLOSED)
&&(cwp->message == WM_CLOSE || cwp->message == WM_QUIT))
{
pptView[id].state = PPT_CLOSING;
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
VOID CaptureAndSaveWindow(HWND hWnd, wchar_t* filename)
{
HBITMAP hBmp;
if ((hBmp = CaptureWindow(hWnd)) == NULL)
{
return;
}
RECT client;
GetClientRect(hWnd, &client);
UINT uiBytesPerRow = 3 * client.right; // RGB takes 24 bits
UINT uiRemainderForPadding;
if ((uiRemainderForPadding = uiBytesPerRow % sizeof(DWORD)) > 0)
uiBytesPerRow += (sizeof(DWORD) - uiRemainderForPadding);
UINT uiBytesPerAllRows = uiBytesPerRow * client.bottom;
PBYTE pDataBits;
if ((pDataBits = new BYTE[uiBytesPerAllRows]) != NULL)
{
BITMAPINFOHEADER bmi = {0};
BITMAPFILEHEADER bmf = {0};
// Prepare to get the data out of HBITMAP:
bmi.biSize = sizeof(bmi);
bmi.biPlanes = 1;
bmi.biBitCount = 24;
bmi.biHeight = client.bottom;
bmi.biWidth = client.right;
// Get it:
HDC hDC = GetDC(hWnd);
GetDIBits(hDC, hBmp, 0, client.bottom, pDataBits, (BITMAPINFO*) &bmi,
DIB_RGB_COLORS);
ReleaseDC(hWnd, hDC);
// Fill the file header:
bmf.bfOffBits = sizeof(bmf) + sizeof(bmi);
bmf.bfSize = bmf.bfOffBits + uiBytesPerAllRows;
bmf.bfType = 0x4D42;
// Writing:
FILE* pFile;
int err = _wfopen_s(&pFile, filename, L"wb");
if (err == 0)
{
fwrite(&bmf, sizeof(bmf), 1, pFile);
fwrite(&bmi, sizeof(bmi), 1, pFile);
fwrite(pDataBits, sizeof(BYTE), uiBytesPerAllRows, pFile);
fclose(pFile);
}
delete [] pDataBits;
}
DeleteObject(hBmp);
}
HBITMAP CaptureWindow(HWND hWnd)
{
HDC hDC;
BOOL bOk = FALSE;
HBITMAP hImage = NULL;
hDC = GetDC(hWnd);
RECT rcClient;
GetClientRect(hWnd, &rcClient);
if ((hImage = CreateCompatibleBitmap(hDC, rcClient.right, rcClient.bottom))
!= NULL)
{
HDC hMemDC;
HBITMAP hDCBmp;
if ((hMemDC = CreateCompatibleDC(hDC)) != NULL)
{
hDCBmp = (HBITMAP)SelectObject(hMemDC, hImage);
HMODULE hLib = LoadLibrary(L"User32");
// PrintWindow works for windows outside displayable area
// but was only introduced in WinXP. BitBlt requires the window to
// be topmost and within the viewable area of the display
if (GetProcAddress(hLib, "PrintWindow") == NULL)
{
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE);
BitBlt(hMemDC, 0, 0, rcClient.right, rcClient.bottom, hDC, 0,
0, SRCCOPY);
SetWindowPos(hWnd, HWND_NOTOPMOST, -32000, -32000, 0, 0,
SWP_NOSIZE);
}
else
{
PrintWindow(hWnd, hMemDC, 0);
}
SelectObject(hMemDC, hDCBmp);
DeleteDC(hMemDC);
bOk = TRUE;
}
}
ReleaseDC(hWnd, hDC);
if (!bOk)
{
if (hImage)
{
DeleteObject(hImage);
hImage = NULL;
}
}
return hImage;
}

View File

@ -1,80 +0,0 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2018 OpenLP Developers *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
#define DllExport extern "C" __declspec( dllexport )
#define DEBUG(...) if (debug) wprintf(__VA_ARGS__)
enum PPTVIEWSTATE {PPT_CLOSED, PPT_STARTED, PPT_OPENED, PPT_LOADED,
PPT_CLOSING};
DllExport int OpenPPT(wchar_t *filename, HWND hParentWnd, RECT rect,
wchar_t *previewPath);
DllExport BOOL CheckInstalled();
DllExport void ClosePPT(int id);
DllExport int GetCurrentSlide(int id);
DllExport int GetSlideCount(int id);
DllExport void NextStep(int id);
DllExport void PrevStep(int id);
DllExport void GotoSlide(int id, int slide_no);
DllExport void RestartShow(int id);
DllExport void Blank(int id);
DllExport void Unblank(int id);
DllExport void Stop(int id);
DllExport void Resume(int id);
DllExport void SetDebug(BOOL onOff);
LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK CwpProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
BOOL GetPPTViewerPath(wchar_t *pptViewerPath, int stringSize);
BOOL GetPPTViewerPathFromReg(wchar_t *pptViewerPath, int stringSize);
HBITMAP CaptureWindow(HWND hWnd);
VOID SaveBitmap(wchar_t* filename, HBITMAP hBmp) ;
VOID CaptureAndSaveWindow(HWND hWnd, wchar_t* filename);
BOOL GetPPTInfo(int id);
BOOL SavePPTInfo(int id);
void Unhook(int id);
#define MAX_PPTS 16
#define MAX_SLIDES 256
struct PPTVIEW
{
HHOOK hook;
HHOOK msgHook;
HWND hWnd;
HWND hWnd2;
HWND hParentWnd;
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
RECT rect;
int slideCount;
int currentSlide;
int firstSlideSteps;
int lastSlideSteps;
int steps;
int guess;
wchar_t filename[MAX_PATH];
wchar_t previewPath[MAX_PATH];
int slideNos[MAX_SLIDES];
PPTVIEWSTATE state;
};

View File

@ -1,202 +0,0 @@
<?xml version="1.0" encoding="Windows-1252"?>
<VisualStudioProject
ProjectType="Visual C++"
Version="9.00"
Name="pptviewlib"
ProjectGUID="{04CC20D1-DC5A-4189-8181-4011E3C21DCF}"
RootNamespace="pptviewlib"
Keyword="Win32Proj"
TargetFrameworkVersion="196613"
>
<Platforms>
<Platform
Name="Win32"
/>
</Platforms>
<ToolFiles>
</ToolFiles>
<Configurations>
<Configuration
Name="Debug|Win32"
OutputDirectory="$(SolutionDir)$(ConfigurationName)"
IntermediateDirectory="$(ConfigurationName)"
ConfigurationType="2"
CharacterSet="1"
>
<Tool
Name="VCPreBuildEventTool"
/>
<Tool
Name="VCCustomBuildTool"
/>
<Tool
Name="VCXMLDataGeneratorTool"
/>
<Tool
Name="VCWebServiceProxyGeneratorTool"
/>
<Tool
Name="VCMIDLTool"
/>
<Tool
Name="VCCLCompilerTool"
Optimization="0"
PreprocessorDefinitions="WIN32;_DEBUG;_WINDOWS;_USRDLL;PPTVIEWLIB_EXPORTS"
MinimalRebuild="true"
BasicRuntimeChecks="3"
RuntimeLibrary="3"
UsePrecompiledHeader="0"
WarningLevel="3"
DebugInformationFormat="4"
/>
<Tool
Name="VCManagedResourceCompilerTool"
/>
<Tool
Name="VCResourceCompilerTool"
/>
<Tool
Name="VCPreLinkEventTool"
/>
<Tool
Name="VCLinkerTool"
LinkIncremental="2"
ModuleDefinitionFile=""
GenerateDebugInformation="true"
SubSystem="2"
TargetMachine="1"
/>
<Tool
Name="VCALinkTool"
/>
<Tool
Name="VCManifestTool"
/>
<Tool
Name="VCXDCMakeTool"
/>
<Tool
Name="VCBscMakeTool"
/>
<Tool
Name="VCFxCopTool"
/>
<Tool
Name="VCAppVerifierTool"
/>
<Tool
Name="VCPostBuildEventTool"
/>
</Configuration>
<Configuration
Name="Release|Win32"
OutputDirectory="$(SolutionDir)$(ConfigurationName)"
IntermediateDirectory="$(ConfigurationName)"
ConfigurationType="2"
CharacterSet="1"
WholeProgramOptimization="1"
>
<Tool
Name="VCPreBuildEventTool"
/>
<Tool
Name="VCCustomBuildTool"
/>
<Tool
Name="VCXMLDataGeneratorTool"
/>
<Tool
Name="VCWebServiceProxyGeneratorTool"
/>
<Tool
Name="VCMIDLTool"
/>
<Tool
Name="VCCLCompilerTool"
Optimization="2"
EnableIntrinsicFunctions="true"
PreprocessorDefinitions="WIN32;NDEBUG;_WINDOWS;_USRDLL;PPTVIEWLIB_EXPORTS"
RuntimeLibrary="2"
EnableFunctionLevelLinking="true"
UsePrecompiledHeader="0"
WarningLevel="3"
DebugInformationFormat="3"
/>
<Tool
Name="VCManagedResourceCompilerTool"
/>
<Tool
Name="VCResourceCompilerTool"
/>
<Tool
Name="VCPreLinkEventTool"
/>
<Tool
Name="VCLinkerTool"
LinkIncremental="1"
GenerateDebugInformation="true"
SubSystem="2"
OptimizeReferences="2"
EnableCOMDATFolding="2"
TargetMachine="1"
/>
<Tool
Name="VCALinkTool"
/>
<Tool
Name="VCManifestTool"
/>
<Tool
Name="VCXDCMakeTool"
/>
<Tool
Name="VCBscMakeTool"
/>
<Tool
Name="VCFxCopTool"
/>
<Tool
Name="VCAppVerifierTool"
/>
<Tool
Name="VCPostBuildEventTool"
/>
</Configuration>
</Configurations>
<References>
</References>
<Files>
<Filter
Name="Source Files"
Filter="cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx"
UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"
>
<File
RelativePath=".\pptviewlib.cpp"
>
</File>
<File
RelativePath=".\README.TXT"
>
</File>
</Filter>
<Filter
Name="Header Files"
Filter="h;hpp;hxx;hm;inl;inc;xsd"
UniqueIdentifier="{93995380-89BD-4b04-88EB-625FBE52EBFB}"
>
<File
RelativePath=".\pptviewlib.h"
>
</File>
</Filter>
<Filter
Name="Resource Files"
Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav"
UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}"
>
</Filter>
</Files>
<Globals>
</Globals>
</VisualStudioProject>

View File

@ -44,7 +44,6 @@ __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
'presentations/pdf_program': None,
'presentations/Impress': QtCore.Qt.Checked,
'presentations/Powerpoint': QtCore.Qt.Checked,
'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
'presentations/Pdf': QtCore.Qt.Checked,
'presentations/presentations files': [],
'presentations/thumbnail_scheme': '',
@ -57,7 +56,7 @@ __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
class PresentationPlugin(Plugin):
"""
This plugin allowed a Presentation to be opened, controlled and displayed on the output display. The plugin controls
third party applications such as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer.
third party applications such as OpenOffice.org Impress, and Microsoft PowerPoint.
"""
log = logging.getLogger('PresentationPlugin')

View File

@ -321,9 +321,12 @@ class SongMediaItem(MediaManagerItem):
:param search_results: A tuple containing (songbook entry, book name, song title, song id)
:return: None
"""
def get_songbook_key(result):
"""Get the key to sort by"""
return (get_natural_key(result[1]), get_natural_key(result[0]), get_natural_key(result[2]))
def get_songbook_key(text_array):
"""
Get the key to sort by
:param text_array: the result text to be processed.
"""
return get_natural_key(text_array[1]), get_natural_key(text_array[0]), get_natural_key(text_array[2])
log.debug('display results Book')
self.list_view.clear()
@ -373,7 +376,7 @@ class SongMediaItem(MediaManagerItem):
"""
def get_theme_key(song):
"""Get the key to sort by"""
return (get_natural_key(song.theme_name), song.sort_key)
return get_natural_key(song.theme_name), song.sort_key
log.debug('display results Themes')
self.list_view.clear()
@ -396,7 +399,7 @@ class SongMediaItem(MediaManagerItem):
"""
def get_cclinumber_key(song):
"""Get the key to sort by"""
return (get_natural_key(song.ccli_number), song.sort_key)
return get_natural_key(song.ccli_number), song.sort_key
log.debug('display results CCLI number')
self.list_view.clear()
@ -460,6 +463,8 @@ class SongMediaItem(MediaManagerItem):
"""
Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator
to say which type of display is required.
:param song_id: the id of the song
:param preview: show we preview after the update
"""
log.debug('on_remote_edit for song {song}'.format(song=song_id))
song_id = int(song_id)
@ -721,7 +726,8 @@ class SongMediaItem(MediaManagerItem):
self.generate_footer(item, song)
return item
def _authors_match(self, song, authors):
@staticmethod
def _authors_match(song, authors):
"""
Checks whether authors from a song in the database match the authors of the song to be imported.
@ -738,11 +744,12 @@ class SongMediaItem(MediaManagerItem):
# List must be empty at the end
return not author_list
def search(self, string, show_error):
def search(self, string, show_error=True):
"""
Search for some songs
:param string: The string to show
:param show_error: Is this an error?
:return: the results of the search
"""
search_results = self.search_entire(string)
return [[song.id, song.title, song.alternate_title] for song in search_results]

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core.common.registry import Registry
from openlp.core.api.endpoint.controller import controller_text
class TestController(TestCase):
"""
Test the Remote plugin deploy functions
"""
def setUp(self):
"""
Setup for tests
"""
Registry.create()
self.registry = Registry()
self.mocked_live_controller = MagicMock()
Registry().register('live_controller', self.mocked_live_controller)
def test_controller_text(self):
"""
Remote Deploy tests - test the dummy zip file is processed correctly
"""
# GIVEN: A mocked service with a dummy service item
self.mocked_live_controller.service_item = MagicMock()
# WHEN: I trigger the method
ret = controller_text("SomeText")
# THEN: I get a basic set of results
results = ret['results']
assert isinstance(results['item'], MagicMock)
assert len(results['slides']) == 0

View File

@ -167,7 +167,7 @@ class TestMediaItem(TestCase, TestMixin):
# THEN: It should be a subclass of :class:`MediaManagerItem`
assert isinstance(self.media_item, MediaManagerItem)
def test_steup_item(self):
def test_setup_item(self):
"""
Test the setup_item method
"""

View File

@ -1,226 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This module contains tests for the pptviewcontroller module of the Presentations plugin.
"""
import shutil
from tempfile import mkdtemp
from unittest import TestCase, skipIf
from unittest.mock import MagicMock, patch
from openlp.core.common import is_win
from openlp.core.common.path import Path
from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH
class TestPptviewController(TestCase, TestMixin):
"""
Test the PptviewController Class
"""
def setUp(self):
"""
Set up the patches and mocks need for all tests.
"""
self.setup_application()
self.build_settings()
self.mock_plugin = MagicMock()
self.temp_folder = mkdtemp()
self.mock_plugin.settings_section = self.temp_folder
def tearDown(self):
"""
Stop the patches
"""
self.destroy_settings()
shutil.rmtree(self.temp_folder)
def test_constructor(self):
"""
Test the Constructor from the PptViewController
"""
# GIVEN: No presentation controller
controller = None
# WHEN: The presentation controller object is created
controller = PptviewController(plugin=self.mock_plugin)
# THEN: The name of the presentation controller should be correct
assert 'Powerpoint Viewer' == controller.name, 'The name of the presentation controller should be correct'
def test_check_available(self):
"""
Test check_available / check_installed
"""
# GIVEN: A mocked dll loader and a controller
with patch('ctypes.cdll.LoadLibrary') as mocked_load_library:
mocked_process = MagicMock()
mocked_process.CheckInstalled.return_value = True
mocked_load_library.return_value = mocked_process
controller = PptviewController(plugin=self.mock_plugin)
# WHEN: check_available is called
available = controller.check_available()
# THEN: On windows it should return True, on other platforms False
if is_win():
assert available is True, 'check_available should return True on windows.'
else:
assert available is False, 'check_available should return False when not on windows.'
class TestPptviewDocument(TestCase):
"""
Test the PptviewDocument Class
"""
def setUp(self):
"""
Set up the patches and mocks need for all tests.
"""
self.pptview_document_create_thumbnails_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
self.pptview_document_stop_presentation_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.stop_presentation')
self.presentation_document_get_temp_folder_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument.get_temp_folder')
self.presentation_document_setup_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
self.rect_patcher = MagicMock()
self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start()
self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start()
self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
self.mock_rect = self.rect_patcher.start()
self.mock_screen_list = self.screen_list_patcher.start()
self.mock_controller = MagicMock()
self.mock_presentation = MagicMock()
self.temp_folder = mkdtemp()
self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
def tearDown(self):
"""
Stop the patches
"""
self.pptview_document_create_thumbnails_patcher.stop()
self.pptview_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop()
self.rect_patcher.stop()
self.screen_list_patcher.stop()
shutil.rmtree(self.temp_folder)
@skipIf(not is_win(), 'Not Windows')
def test_load_presentation_succesful(self):
"""
Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
"""
# GIVEN: A reset mocked_os
self.mock_controller.process.OpenPPT.return_value = 0
instance = PptviewDocument(self.mock_controller, self.mock_presentation)
instance.file_path = 'test\path.ppt'
# WHEN: The temporary directory exists and OpenPPT returns successfully (not -1)
result = instance.load_presentation()
# THEN: PptviewDocument.load_presentation should return True
assert result is True
@skipIf(not is_win(), 'Not Windows')
def test_load_presentation_un_succesful(self):
"""
Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
not successfully opened
"""
# GIVEN: A reset mock_os_isdir
self.mock_controller.process.OpenPPT.return_value = -1
instance = PptviewDocument(self.mock_controller, self.mock_presentation)
instance.file_path = 'test\path.ppt'
# WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
with patch.object(instance, 'get_temp_folder') as mocked_get_folder:
mocked_get_folder.return_value = MagicMock(spec=Path)
result = instance.load_presentation()
# THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
assert result is False
def test_create_titles_and_notes(self):
"""
Test PowerpointController.create_titles_and_notes
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = RESOURCE_PATH / 'presentations' / 'test.pptx'
doc.save_titles_and_notes = MagicMock()
# WHEN reading the titles and notes
doc.create_titles_and_notes()
# THEN save_titles_and_notes should have been called once with empty arrays
doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'],
['Notes for slide 1', 'Inserted', 'Notes for slide 2',
'Notes \nfor slide 4', 'Notes for slide 3'])
def test_create_titles_and_notes_nonexistent_file(self):
"""
Test PowerpointController.create_titles_and_notes with nonexistent file
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
with patch('builtins.open') as mocked_open, \
patch.object(Path, 'exists') as mocked_path_exists, \
patch('openlp.plugins.presentations.lib.presentationcontroller.create_paths') as \
mocked_dir_exists:
mocked_path_exists.return_value = False
mocked_dir_exists.return_value = False
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = Path('Idontexist.pptx')
doc.save_titles_and_notes = MagicMock()
# WHEN: Reading the titles and notes
doc.create_titles_and_notes()
# THEN: File existens should have been checked, and not have been opened.
doc.save_titles_and_notes.assert_called_once_with(None, None)
mocked_path_exists.assert_called_with()
assert mocked_open.call_count == 0, 'There should be no calls to open a file.'
def test_create_titles_and_notes_invalid_file(self):
"""
Test PowerpointController.create_titles_and_notes with invalid file
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file
with patch('builtins.open') as mocked_open, \
patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
mocked_is_zf.return_value = False
mocked_open.filesize = 10
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = RESOURCE_PATH / 'presentations' / 'test.ppt'
doc.save_titles_and_notes = MagicMock()
# WHEN: reading the titles and notes
doc.create_titles_and_notes()
# THEN:
doc.save_titles_and_notes.assert_called_once_with(None, None)
assert mocked_is_zf.call_count == 1, 'is_zipfile should have been called once'