← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~raoul-snyman/openlp/powerpointmac into lp:openlp

 

Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/powerpointmac into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)
  Samuel Mehrbrodt (sam92)

For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/powerpointmac/+merge/225762

Update: I've moved tests around, and rewritten the functional tests from scratch (the existing tests were actually interface tests)

This is pretty much a straight copy of Dmitriy's mac presentations branch, but only the PowerPoint bit (thanks to Jonathan Springer for separating them).

I'm not too sure what needs to be done, hence this merge proposal. It works with PPT on Mac.
-- 
https://code.launchpad.net/~raoul-snyman/openlp/powerpointmac/+merge/225762
Your team OpenLP Core is requested to review the proposed merge of lp:~raoul-snyman/openlp/powerpointmac into lp:openlp.
=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
--- openlp/plugins/presentations/lib/messagelistener.py	2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/messagelistener.py	2014-07-06 20:44:24 +0000
@@ -98,7 +98,7 @@
             return True
         if not self.doc.is_loaded():
             if not self.doc.load_presentation():
-                log.warn('Failed to activate %s' % self.doc.filepath)
+                log.warn('Failed to activate %s' % self.doc.file_path)
                 return False
         if self.is_live:
             self.doc.start_presentation()
@@ -109,7 +109,7 @@
         if self.doc.is_active():
             return True
         else:
-            log.warn('Failed to activate %s' % self.doc.filepath)
+            log.warn('Failed to activate %s' % self.doc.file_path)
             return False
 
     def slide(self, slide):

=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py	2014-04-12 20:19:22 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2014-07-06 20:44:24 +0000
@@ -27,8 +27,9 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-This modul is for controlling powerpiont. PPT API documentation:
-`http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
+This module is for controlling Microsoft PowerPoint on Windows.
+
+See the PPT API documentation: `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
 """
 import os
 import logging

=== added file 'openlp/plugins/presentations/lib/powerpointmaccontroller.py'
--- openlp/plugins/presentations/lib/powerpointmaccontroller.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/powerpointmaccontroller.py	2014-07-06 20:44:24 +0000
@@ -0,0 +1,412 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2012 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Eric Ludin, Edwin Lunando, Brian T. Meyer,    #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky                                             #
+# --------------------------------------------------------------------------- #
+# 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 os
+import sys
+import logging
+import shutil
+
+if sys.platform == 'darwin':
+    from appscript import ApplicationNotFoundError, CantLaunchApplicationError, CommandError, app, k
+
+from openlp.core.common import Settings
+from openlp.core.lib import ScreenList
+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
+
+
+log = logging.getLogger(__name__)
+
+
+class PowerPointMacController(PresentationController):
+    """
+    Class to control interactions with PowerPoint 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('PowerpointController loaded')
+
+    def __init__(self, plugin):
+        """
+        Initialise the class
+        """
+        log.debug('Initialising')
+        super(PowerPointMacController, self).__init__(plugin, 'PowerPoint for Mac', PowerPointMacDocument)
+        self.supports = ['ppt', 'pps', 'pptx', 'ppsx']
+        self.process = None
+
+    def check_available(self):
+        """
+        PowerPoint is able to run on this machine
+        """
+        log.debug('check_available')
+        result = False
+        if sys.platform == 'darwin':
+            try:
+                self.process = app(id='com.microsoft.powerpoint')
+                result = True
+            except ApplicationNotFoundError:
+                pass
+            finally:
+                self.kill()
+        return result
+
+    # Check if OpenLP is running on Mac OS, and then declare these methods
+    # Not sure why we do this (surely there's a better way)
+    if sys.platform == 'darwin':
+
+        def start_process(self):
+            """
+            Loads PowerPoint process
+            """
+            log.debug('start_process')
+            if not self.process:
+                self.process = app(id='com.microsoft.powerpoint')
+                if not self.process.isrunning():
+                    self.process.relaunchmode = 'limited'  # 'always'
+                    self.process.launch()
+                    self.apply_app_settings()
+
+        def kill(self):
+            """
+            Called at system exit to clean up any running presentations
+            """
+            log.debug('Kill Powerpoint')
+            while self.docs and self.process.isrunning():
+                self.docs[0].close_presentation()
+            if self.process is None or not self.process.isrunning():
+                return
+            try:
+                total = self.process.presentations()
+                if self.process and total != k.missing_value and len(total) > 0:
+                    return
+                self.process.quit(saving=k.ask)
+            except CantLaunchApplicationError:
+                log.debug('Kill Keynote failed')
+            self.process = None
+
+        def apply_app_settings(self):
+            """
+            Apply settings for PowerPoint
+            14\Options\Options\Save graphics screen heigth = 240
+            14\Options\Options\Save graphics screen width = 320
+            14\Options\Options\Save only current slide graphics = 0
+
+            """
+            # TODO: add settings for different versions of MS PPT
+            pass
+
+
+class PowerPointMacDocument(PresentationDocument):
+    """
+    Class which holds information and controls a single presentation
+    """
+
+    def __init__(self, controller, presentation):
+        """
+        Constructor, store information about the file and initialise
+        """
+        log.debug('Init Presentation Powerpoint')
+        super(PowerPointMacDocument, self).__init__(controller, presentation)
+        self.presentation = None
+
+    def load_presentation(self):
+        """
+        Called when a presentation is added to the SlideController.
+        Opens the PowerPoint file using the process created earlier.
+        """
+        log.debug('load_presentation')
+        if not self.controller.process or not self.controller.process.isrunning():
+            self.controller.start_process()
+        try:
+            self.controller.process.open(self.file_path)
+        except CommandError:
+            log.debug('PPT open failed')
+            return False
+        presentations = self.controller.process.presentations()
+        for presentation in presentations:
+            full_name = presentation.full_name()
+            full_name = full_name.replace('Macintosh HD', '')
+            full_name = full_name.replace(':', '/')
+            if self.file_path == full_name:
+                self.presentation = presentation
+                self.create_thumbnails()
+                return True
+        return False
+
+    def create_thumbnails(self):
+        """
+        Create the thumbnail images for the current presentation.
+        """
+        log.debug('create_thumbnails')
+        if self.check_thumbnails():
+            return
+        thumbnail_folder = self.get_thumbnail_folder()
+        temp_dir = self.get_temp_folder()
+        self.presentation.save(in_=temp_dir, as_=k.save_as_PNG)
+        slide_no = 0
+        self.move_thumbnails(thumbnail_folder, slide_no, temp_dir)
+
+    def move_thumbnails(self, thumbnail_folder, slide_no, temp_dir):
+        """
+        Move the thumbnail images from subdirectories to right path.
+
+        :param thumbnail_folder:
+        :param slide_no:
+        :param temp_dir:
+        """
+        if not os.path.isdir(temp_dir):
+            return
+        for filename in os.listdir(temp_dir):
+            full_filename = os.path.join(temp_dir, filename)
+            if os.path.isdir(full_filename):
+                self.move_thumbnails(thumbnail_folder, slide_no, full_filename)
+            elif os.path.isfile(full_filename) and filename.endswith('.png') and not filename == 'icon.png':
+                slide_no += 1
+                new_name = os.path.join(thumbnail_folder, self.controller.thumbnail_prefix + str(slide_no) + '.png')
+                if full_filename == new_name \
+                        or full_filename.startswith(os.path.join(thumbnail_folder, self.controller.thumbnail_prefix)):
+                    continue
+                try:
+                    os.renames(full_filename, new_name)
+                except:
+                    open(new_name, 'w').write(open(full_filename, 'r').read())
+                    os.unlink(full_filename)
+        if temp_dir != thumbnail_folder:
+            try:
+                shutil.rmtree(temp_dir)
+            except:
+                pass
+
+    def close_presentation(self):
+        """
+        Close presentation and clean up objects. This is triggered by a new
+        object being added to SlideController or OpenLP being shut down.
+        """
+        log.debug('close_presentation')
+        if self.presentation:
+            try:
+                self.presentation.close()
+            except CommandError:
+                log.debug('Could not close the presentation')
+        self.presentation = None
+        self.controller.remove_doc(self)
+
+    def is_loaded(self):
+        """
+        Returns ``True`` if a presentation is loaded.
+        """
+        log.debug('is_loaded')
+        try:
+            if not self.controller.process.isrunning():
+                return False
+            if len(self.controller.process.document_windows()) == 0:
+                return False
+            presentations = self.controller.process.presentations()
+            if len(presentations) == 0:
+                return False
+        except:
+            return False
+        for presentation in presentations:
+            full_name = presentation.full_name()
+            full_name = full_name.replace('Macintosh HD', '')
+            full_name = full_name.replace(':', '/')
+            if self.file_path == full_name:
+                return True
+        return False
+
+    def is_active(self):
+        """
+        Returns ``True`` if a presentation is currently active.
+        """
+        log.debug('is_active')
+        if not self.is_loaded():
+            return False
+        try:
+            slide_show_window = self.presentation.slide_show_window
+            if slide_show_window is None:
+                return False
+            slide_state = self.presentation.slide_show_window.slideshow_view.slide_state()
+            if slide_state is None or slide_state == k.missing_value:
+                return False
+        except:
+            return False
+        return True
+
+    def unblank_screen(self):
+        """
+        Unblanks (restores) the presentation.
+        """
+        log.debug('unblank_screen')
+        if self.is_blank():
+            self.presentation.slide_show_window.slideshow_view.slide_state.set(k.slide_show_state_running)
+            self.presentation.slide_show_window.slideshow_view.go_to_next_slide()
+            self.presentation.slide_show_window.slideshow_view.go_to_previous_slide()
+
+    def blank_screen(self):
+        """
+        Blanks the screen.
+        """
+        log.debug('blank_screen')
+        self.presentation.slide_show_window.slideshow_view.slide_state.set(k.slide_show_state_black_screen)
+
+    def is_blank(self):
+        """
+        Returns ``True`` if screen is blank.
+        """
+        log.debug('is_blank')
+        if self.is_active() and self.presentation.slide_show_window.slideshow_view.slide_state() \
+                is k.slide_show_state_black_screen:
+            return True
+        else:
+            return False
+
+    def stop_presentation(self):
+        """
+        Stops the current presentation and hides the output.
+        """
+        log.debug('stop_presentation')
+        self.presentation.slide_show_window.slideshow_view.exit_slide_show()
+
+    def start_presentation(self):
+        """
+        Starts a presentation from the beginning.
+        """
+        log.debug('start_presentation')
+        rect = ScreenList().current['size']
+        ppt_settings = self.presentation.slide_show_settings
+        openlp_settings = Settings("openlp.org", "OpenLP")
+        show_presenter_view = openlp_settings.value(self.controller.plugin.settings_section + '/show presenter view')
+        override_position = openlp_settings.value('core/override position')
+        if show_presenter_view:
+            ppt_settings.show_type.set(k.slide_show_type_presenter)
+        elif override_position:
+            ppt_settings.show_type.set(k.slide_show_type_window)
+        else:
+            ppt_settings.show_type.set(k.slide_show_type_kiosk)
+        ppt_window = ppt_settings.run_slide_show()
+        # TODO: set slideshow state = running
+        #if ppt_window.slideshow_view.slide_state() != appscript.k.slide_show_state_running:
+        #   ppt_window.slideshow_view.slide_state.set(appscript.k.slide_show_state_running)
+        if not ppt_window or show_presenter_view:
+            return
+        if override_position:
+            top = float(rect.y())
+            height = float(rect.height())
+            left_position = float(rect.x())
+            width = float(rect.width())
+            ppt_window.top.set(top)
+            ppt_window.height.set(height)
+            ppt_window.left_position.set(left_position)
+            ppt_window.width.set(width)
+        else:
+            # TODO: set output monitor for fullscreen mode
+            pass
+
+    def get_slide_number(self):
+        """
+        Returns the current slide number.
+        """
+        log.debug('get_slide_number')
+        return self.presentation.slide_show_window.slideshow_view.current_show_position()
+
+    def get_slide_count(self):
+        """
+        Returns total number of slides.
+        """
+        log.debug('get_slide_count')
+        return len(self.presentation.slides())
+
+    def goto_slide(self, slideno):
+        """
+        Moves to a specific slide in the presentation.
+
+        :param slideno:
+        """
+        log.debug('goto_slide')
+        # FIXME: this works. but needs to fix this code
+        while self.get_slide_number() != slideno:
+            if self.get_slide_number() < slideno:
+                self.presentation.slide_show_window.slideshow_view.go_to_next_slide()
+            else:
+                self.presentation.slide_show_window.slideshow_view.go_to_previous_slide()
+        self.controller.process.activate()
+
+    def next_step(self):
+        """
+        Triggers the next effect of slide on the running presentation.
+        """
+        log.debug('next_step')
+        # TODO: check if slideshow stopped then restart it from current position
+        if self.is_active():
+            self.presentation.slide_show_window.slideshow_view.go_to_next_slide()
+            self.controller.process.activate()
+            if self.get_slide_number() > self.get_slide_count():
+                self.previous_step()
+
+    def previous_step(self):
+        """
+        Triggers the previous slide on the running presentation.
+        """
+        log.debug('previous_step')
+        # TODO: check if slideshow stopped then restart it from current position
+        if self.is_active():
+            self.presentation.slide_show_window.slideshow_view.go_to_previous_slide()
+            self.controller.process.activate()
+
+    def get_slide_text(self, slide_no):
+        """
+        Returns the text on the slide.
+
+        :param slide_no: The slide the text is required for, starting at 1.
+        """
+        return _get_text_from_shapes(self.presentation.slides[slide_no - 1].shapes)
+
+    def get_slide_notes(self, slide_no):
+        """
+        Returns the text on the slide.
+
+        :param slide_no: The slide the notes are required for, starting at 1.
+        """
+        return _get_text_from_shapes(
+            self.presentation.slides[slide_no].notes_page.shapes())
+
+
+def _get_text_from_shapes(shapes):
+    """
+    Returns any text extracted from the shapes on a presentation slide.
+
+    ``shapes``
+        A set of shapes to search for text.
+    """
+    text = ''
+    for idx in range(len(shapes)):
+        shape = shapes[idx + 1]
+        if shape.has_text_frame():
+            text += shape.text_frame.text_range.content() + '\n'
+    return text

=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py	2014-04-12 20:19:22 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py	2014-07-06 20:44:24 +0000
@@ -27,6 +27,8 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
+import sys
+
 from PyQt4 import QtGui
 
 from openlp.core.common import Settings, UiStrings, translate
@@ -39,6 +41,7 @@
     """
     PresentationsTab is the Presentations settings tab in the settings dialog.
     """
+
     def __init__(self, parent, title, visible_title, controllers, icon_path):
         """
         Constructor
@@ -74,6 +77,11 @@
         self.override_app_check_box = QtGui.QCheckBox(self.advanced_group_box)
         self.override_app_check_box.setObjectName('override_app_check_box')
         self.advanced_layout.addWidget(self.override_app_check_box)
+        self.show_presenter_view_check_box = QtGui.QCheckBox(self.advanced_group_box)
+        self.show_presenter_view_check_box.setObjectName(u'show_presenter_view_check_box')
+        self.advanced_layout.addWidget(self.show_presenter_view_check_box)
+        if not sys.platform.startswith('darwin'):
+            self.show_presenter_view_check_box.setVisible(False)
         self.left_layout.addWidget(self.advanced_group_box)
         # Pdf options
         self.pdf_group_box = QtGui.QGroupBox(self.left_column)
@@ -117,6 +125,8 @@
         self.pdf_group_box.setTitle(translate('PresentationPlugin.PresentationTab', 'PDF options'))
         self.override_app_check_box.setText(
             translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
+        self.show_presenter_view_check_box.setText(translate('PresentationPlugin.PresentationTab',
+                                                             'Show Presenter View'))
         self.pdf_program_check_box.setText(
             translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
 
@@ -135,6 +145,7 @@
             checkbox = self.presenter_check_boxes[controller.name]
             checkbox.setChecked(Settings().value(self.settings_section + '/' + controller.name))
         self.override_app_check_box.setChecked(Settings().value(self.settings_section + '/override app'))
+        self.show_presenter_view_check_box.setChecked(Settings().value(self.settings_section + '/show presenter view'))
         # load pdf-program settings
         enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
         self.pdf_program_check_box.setChecked(enable_pdf_program)
@@ -168,6 +179,10 @@
         if Settings().value(setting_key) != self.override_app_check_box.checkState():
             Settings().setValue(setting_key, self.override_app_check_box.checkState())
             changed = True
+        setting_key = self.settings_section + '/show presenter view'
+        if Settings().value(setting_key) != self.show_presenter_view_check_box.checkState():
+            Settings().setValue(setting_key, self.show_presenter_view_check_box.isChecked())
+            changed = True
         # Save pdf-settings
         pdf_program = self.pdf_program_path.text()
         enable_pdf_program = self.pdf_program_check_box.checkState()

=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2014-04-12 20:19:22 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2014-07-06 20:44:24 +0000
@@ -31,6 +31,7 @@
 formats.
 """
 import os
+import sys
 import logging
 
 from PyQt4 import QtCore
@@ -42,14 +43,19 @@
 
 log = logging.getLogger(__name__)
 
-
 __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
                         'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
                         'presentations/pdf_program': '',
                         'presentations/Impress': QtCore.Qt.Checked,
                         'presentations/Powerpoint': QtCore.Qt.Checked,
                         'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
+                        'presentations/PowerPoint for Mac': QtCore.Qt.Checked,
+                        'presentations/Keynote': QtCore.Qt.Checked,
                         'presentations/Pdf': QtCore.Qt.Checked,
+                        'presentations/show presenter view': QtCore.Qt.Checked,
+                        'PresentationModeUseSecondary': '',
+                        'PresentationModeEnableFeedbackDisplay': False,
+                        'PresentationModePlayWellWithOthers': False,
                         'presentations/presentations files': []
                         }
 
@@ -127,6 +133,10 @@
         controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
         for filename in os.listdir(controller_dir):
             if filename.endswith('controller.py') and not filename == 'presentationcontroller.py':
+                if not sys.platform.startswith('darwin') and filename == 'powerpointmaccontroller.py':
+                    continue
+                if not sys.platform.startswith('win') and filename == 'powerpointcontroller.py':
+                    continue
                 path = os.path.join(controller_dir, filename)
                 if os.path.isfile(path):
                     module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
@@ -146,10 +156,10 @@
         Return information about this plugin.
         """
         about_text = translate('PresentationPlugin', '<strong>Presentation '
-                               'Plugin</strong><br />The presentation plugin provides the '
-                               'ability to show presentations using a number of different '
-                               'programs. The choice of available presentation programs is '
-                               'available to the user in a drop down box.')
+                                                     'Plugin</strong><br />The presentation plugin provides the '
+                                                     'ability to show presentations using a number of different '
+                                                     'programs. The choice of available presentation programs is '
+                                                     'available to the user in a drop down box.')
         return about_text
 
     def set_plugin_text_strings(self):

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2014-03-31 17:57:15 +0000
+++ scripts/check_dependencies.py	2014-07-06 20:44:24 +0000
@@ -47,6 +47,7 @@
     nose = None
 
 IS_WIN = sys.platform.startswith('win')
+IS_OSX = sys.platform.startswith('darwin')
 
 
 VERS = {
@@ -57,6 +58,8 @@
     # pyenchant 1.6 required on Windows
     'enchant': '1.6' if IS_WIN else '1.3'
 }
+if IS_OSX:
+    VERS.update({'appscript': '1.0.0'})
 
 # pywin32
 WIN32_MODULES = [
@@ -67,6 +70,11 @@
     'icu',
 ]
 
+# OSX
+OSX_MODULES = [
+    'appscript',
+    'mactypes', 
+]
 MODULES = [
     'PyQt4',
     'PyQt4.QtCore',
@@ -178,6 +186,12 @@
         check_vers(enchant.__version__, VERS['enchant'], 'enchant')
     except ImportError:
         print_vers_fail(VERS['enchant'], 'enchant')
+    if IS_OSX:
+        try:
+            import appscript
+            check_vers(appscript.__version__, VERS['appscript'], 'appscript')
+        except ImportError:
+            print_vers_fail(VERS['appscript'], 'appscript')
 
 
 def print_enchant_backends_and_languages():
@@ -229,6 +243,10 @@
         print('Checking for Windows specific modules...')
         for m in WIN32_MODULES:
             check_module(m)
+    if IS_OSX:
+        print('Checking for OS X specific modules...')
+        for m in OSX_MODULES:
+            check_module(m)
     verify_versions()
     print_qt_image_formats()
     print_enchant_backends_and_languages()

=== modified file 'tests/functional/__init__.py'
--- tests/functional/__init__.py	2014-03-14 22:08:44 +0000
+++ tests/functional/__init__.py	2014-07-06 20:44:24 +0000
@@ -42,9 +42,9 @@
 from PyQt4 import QtGui
 
 if sys.version_info[1] >= 3:
-    from unittest.mock import MagicMock, patch, mock_open, call
+    from unittest.mock import MagicMock, PropertyMock, patch, mock_open, call
 else:
-    from mock import MagicMock, patch, mock_open, call
+    from mock import MagicMock, PropertyMock, patch, mock_open, call
 
 # Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a  QApplication.
 application = QtGui.QApplication([])

=== added file 'tests/functional/openlp_plugins/presentations/test_powerpointmaccontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointmaccontroller.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointmaccontroller.py	2014-07-06 20:44:24 +0000
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# 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 PowerpointController class of the Presentations plugin.
+"""
+import sys
+from unittest import TestCase, SkipTest
+
+if sys.platform != 'darwin':
+    raise SkipTest('Not OS X, skipping PowerPoint for Mac tests')
+
+from appscript import ApplicationNotFoundError
+
+from openlp.plugins.presentations.lib.powerpointmaccontroller import PowerPointMacController, PowerPointMacDocument
+from tests.functional import MagicMock, PropertyMock, patch
+
+# TODO: write tests for classes
+# - PowerpointController
+# - PowerpointDocument
+
+
+class TestPowerpointController(TestCase):
+    """
+    Test the PowerpointController class
+    """
+    def setUp(self):
+        """
+        Set up the PowerpointController
+        """
+        presentation_controller = MagicMock()
+        presentation_controller.settings_section = 'presentations'
+        self.powerpoint_controller = PowerPointMacController(presentation_controller)
+
+    def check_available_true_test(self):
+        """
+        Test the check_available() method calls the correct underlying methods and returns True
+        """
+        # GIVEN: Various appscript methods mocked out and set not to raise exceptions
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.app') as mocked_app, \
+                patch('openlp.plugins.presentations.lib.powerpointmaccontroller.PowerPointMacController.kill') as mocked_kill:
+            mocked_application = MagicMock()
+            mocked_app.return_value = mocked_application
+
+            # WHEN: The check_available() method is called
+            result = self.powerpoint_controller.check_available()
+
+            # THEN: The controller should be available
+            self.assertTrue(result, 'The result of check_available() should be True')
+            mocked_app.assert_called_with(id='com.microsoft.powerpoint')
+            mocked_kill.assert_called_with()
+            self.assertIs(mocked_application, self.powerpoint_controller.process, 'The process attribute should be the '
+                                                                                  'mocked out return value of app()')
+
+    def check_available_false_test(self):
+        """
+        Test when an underlying function call in check_available() raises an exception, then the result should be False
+        """
+        # GIVEN: Various appscript methods mocked out and set to raise an exception
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.app') as mocked_app:
+            mocked_app.side_effect = ApplicationNotFoundError(name='OpenLP')
+
+            # WHEN: The check_available() method is called
+            result = self.powerpoint_controller.check_available()
+
+            # THEN: The controller should be available
+            self.assertFalse(result, 'The result of check_available() should be True')
+            mocked_app.assert_called_with(id='com.microsoft.powerpoint')
+
+    def start_process_not_running_test(self):
+        """
+        Test that when start_process() is called and PowerPoint is not running, it is started
+        """
+        # GIVEN: A None process attribute and a mocked out app() method
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.app') as mocked_app:
+            mocked_process = MagicMock()
+            mocked_process.isrunning.return_value = False
+            #mocked_process.launch.return_value = None
+            mocked_app.return_value = mocked_process
+            self.powerpoint_controller.apply_app_settings = MagicMock()
+
+            # WHEN: The start_process() method is called
+            self.powerpoint_controller.start_process()
+
+            # THEN: The process should not be None
+            mocked_app.assert_called_with(id='com.microsoft.powerpoint')
+            mocked_process.isrunning.assert_called_with()
+            self.assertIsNotNone(self.powerpoint_controller.process, 'The PowerPoint process should have been created')
+            mocked_process.launch.assert_called_with()
+            self.powerpoint_controller.apply_app_settings.assert_called_with()
+
+    def start_process_is_running_test(self):
+        """
+        Test that when start_process() is called and PowerPoint is running then nothing else happens
+        """
+        # GIVEN: A None process attribute and a mocked out app() method
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.app') as mocked_app:
+            mocked_process = MagicMock()
+            mocked_process.isrunning.return_value = True
+            #mocked_process.launch.return_value = None
+            mocked_app.return_value = mocked_process
+            self.powerpoint_controller.apply_app_settings = MagicMock()
+
+            # WHEN: The start_process() method is called
+            self.powerpoint_controller.start_process()
+
+            # THEN: The process should not be None
+            mocked_app.assert_called_with(id='com.microsoft.powerpoint')
+            mocked_process.isrunning.assert_called_with()
+            self.assertIsNotNone(self.powerpoint_controller.process, 'The PowerPoint process should have been created')
+            self.assertEqual(0, mocked_process.launch.call_count, 'The launch method should not hae been called')
+            self.assertEqual(0, self.powerpoint_controller.apply_app_settings.call_count,
+                             'The apply_app_settings method should not have been called')
+
+    def load_presentation_test(self):
+        """
+        Test loading a document in PPT
+        """
+        pass
+
+    def close_presentation_test(self):
+        """
+        Test closing a document in PPT
+        """
+        pass
+
+    def kill_with_no_more_docs_test(self):
+        """
+        Test running the kill() method when the first document is successfully closed
+        """
+        # GIVEN: A PowerPoint process and a document
+        mocked_process = MagicMock()
+        mocked_process.isrunning.side_effect = [True, False, False]
+        mocked_doc = MagicMock()
+        self.powerpoint_controller.process = mocked_process
+        self.powerpoint_controller.docs = [mocked_doc]
+
+        # WHEN: kill() is called
+        self.powerpoint_controller.kill()
+
+        # THEN: close_presentation() should only be called once, and nothing else in the method should have been called
+        mocked_doc.close_presentation.assert_called_once_with()
+        self.assertEqual(0, mocked_process.presentations.call_count,
+                         'The presentations() method should not have been called')
+
+    def kill_runs_on_docs_more_than_once_test(self):
+        """
+        Test running the kill() method with multiple documents to close
+        """
+        # GIVEN: A PowerPoint process and a document
+        mocked_process = MagicMock()
+        mocked_process.isrunning.side_effect = [True, True, True, False, False]
+        mocked_doc = MagicMock()
+        self.powerpoint_controller.process = mocked_process
+        self.powerpoint_controller.docs = [mocked_doc]
+
+        # WHEN: kill() is called
+        self.powerpoint_controller.kill()
+
+        # THEN: close_presentation() should only be called once, and nothing else in the method should have been called
+        self.assertEqual(3, mocked_doc.close_presentation.call_count,
+                         'The presentations() method should not have been called')
+        self.assertEqual(0, mocked_process.presentations.call_count,
+                         'The presentations() method should not have been called')
+
+    def kill_with_process_missing_value_gt0_test(self):
+        """
+        Test running the kill() method with total != k.missing_value returns correctly
+        """
+        # GIVEN: A PowerPoint process and a mocked out "k"
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.k') as mocked_k:
+            mocked_k.missing_value = [None]
+            mocked_process = MagicMock()
+            mocked_process.isrunning.side_effect = [False, True]
+            mocked_process.presentations.return_value = [MagicMock(), MagicMock()]
+            mocked_doc = MagicMock()
+            self.powerpoint_controller.process = mocked_process
+            self.powerpoint_controller.docs = [mocked_doc]
+
+            # WHEN: kill() is called
+            self.powerpoint_controller.kill()
+
+            # THEN: process.quit() should not have been called
+            self.assertEqual(0, mocked_process.quit.call_count, 'The quit() method should not have been called')
+
+    def kill_calls_quit_test(self):
+        """
+        Test running the kill() method calls quit
+        """
+        # GIVEN: A PowerPoint process and a mocked out "k"
+        with patch('openlp.plugins.presentations.lib.powerpointmaccontroller.k') as mocked_k:
+            mocked_k.missing_value = []
+            mocked_k.ask = 1
+            mocked_process = MagicMock()
+            mocked_process.isrunning.side_effect = [False, True]
+            mocked_process.presentations.return_value = []
+            mocked_doc = MagicMock()
+            self.powerpoint_controller.process = mocked_process
+            self.powerpoint_controller.docs = [mocked_doc]
+
+            # WHEN: kill() is called
+            self.powerpoint_controller.kill()
+
+            # THEN: process.quit() should have been called and eventually process is set to None
+            mocked_process.quit.assert_called_with(saving=mocked_k.ask)
+            self.assertIsNone(self.powerpoint_controller.process, 'process should be None')
\ No newline at end of file

=== added directory 'tests/interfaces/openlp_plugins/presentations'
=== added file 'tests/interfaces/openlp_plugins/presentations/__init__.py'
--- tests/interfaces/openlp_plugins/presentations/__init__.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_plugins/presentations/__init__.py	2014-07-06 20:44:24 +0000
@@ -0,0 +1,1 @@
+__author__ = 'raoul'

=== added file 'tests/interfaces/openlp_plugins/presentations/test_powerpointmaccontroller.py'
--- tests/interfaces/openlp_plugins/presentations/test_powerpointmaccontroller.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_plugins/presentations/test_powerpointmaccontroller.py	2014-07-06 20:44:24 +0000
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# 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 PowerpointController class of the Presentations plugin.
+"""
+
+from unittest import TestCase, skipIf
+
+from tests.functional import MagicMock
+import os
+from openlp.plugins.presentations.lib.powerpointmaccontroller import PowerPointMacController, PowerPointMacDocument
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+# TODO: write tests for classes
+# - PowerpointController
+# - PowerpointDocument
+
+
+class TestPowerpointController(TestCase):
+    """
+    Test the PowerpointController class
+    """
+    def setUp(self):
+        """
+        Set up the PowerpointController
+        """
+        presentation_controller = MagicMock()
+        presentation_controller.settings_section = 'presentations'
+        self.powerpoint_controller = PowerPointMacController(presentation_controller)
+        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+        self.doc = PowerPointMacDocument(self.powerpoint_controller, self.file_name)
+        self.doc.presentation_deleted()
+
+    def tearDown(self):
+        self.doc.controller.kill()
+
+    def verify_installation_test(self):
+        """
+        Test that PowerPoint for Mac is installed, and sets the controller to available
+        """
+        # GIVEN: A boolean value set to false (to confirm the test works)
+        is_installed = False
+
+        # WHEN: The check_available() method is called
+        is_installed = self.powerpoint_controller.check_available()
+
+        # THEN: The controller should be available
+        self.assertTrue(is_installed, 'The result of check_available() should be True')
+
+    def start_process_test(self):
+        """
+        Test the start_process() of PPT
+        """
+        # GIVEN: A boolean value set to true
+        # WHEN: We "convert" it to a bool
+        self.powerpoint_controller.start_process()
+        # THEN: The process should not be None
+        assert self.powerpoint_controller.process is not None, 'The result of start_process() should be True'
+
+    def load_presentation_test(self):
+        """
+        Test loading a document in PPT
+        """
+        # GIVEN: the filename
+        # WHEN: loading the filename
+        self.doc.load_presentation()
+        result = self.doc.is_loaded()
+        # THEN: result should be true
+        assert result is True, 'The PPT should load document'
+        assert len(self.doc.controller.docs) != 0, 'The powerpoint_controller.docs should have one document.'
+
+    def close_presentation_test(self):
+        """
+        Test closing a document in PPT
+        """
+        # GIVEN: loading the filename
+        self.doc.load_presentation()
+        # WHEN: closing the filename
+        self.doc.close_presentation()
+        result = self.doc.is_loaded()
+        # THEN: result should be true
+        assert result is False, 'The PPT should close document'
+        assert len(self.doc.controller.docs) == 0, 'The controller should have 0 documents.'
+
+    def kill_test(self):
+        """
+        Test running the kill() method with an PowerpointController
+        """
+        # GIVEN: A PowerpointController instance and a list of documents
+        ppt_document = MagicMock()
+        self.powerpoint_controller.start_process()
+        self.powerpoint_controller.docs.append(ppt_document)
+        # WHEN: we run kill()
+        self.powerpoint_controller.kill()
+        # THEN: all docs should be deleted from controller
+        assert len(self.powerpoint_controller.docs) == 0, 'The powerpoint_controller should close all documents.'
+        assert self.powerpoint_controller.process in None, \
+            'The powerpoint_controller should shout down PPT application.'

=== added file 'tests/resources/presentations/test.pptx'
Binary files tests/resources/presentations/test.pptx	1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.pptx	2014-07-06 20:44:24 +0000 differ

Follow ups