← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/better-remote into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/better-remote into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/211631

Continuation of Felipes work.
Changes to remote control:
  - Displays the title of the slide (presentations)
  - Displays the presenter's notes (presentations)
  - Displays a thumbnail for each slide (presentations)
  - Added settings page for the remote to choose if thumbnails are displayed (presentations)
    - defaults to no display
    - persisted on cookies
  - Fixed bug that was preventing the remote to be updated with correct slide (presentations)
  - Displays the service notes (general)
Changes to the main display:
  - Display the title of the slide on each item on the slide controller (presentations)
-- 
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/211631
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/better-remote into lp:openlp.
=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py	2013-12-24 08:56:50 +0000
+++ openlp/core/common/applocation.py	2014-03-18 21:03:35 +0000
@@ -76,8 +76,8 @@
             return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0])
         elif dir_type == AppLocation.PluginsDir:
             app_path = os.path.abspath(os.path.split(sys.argv[0])[0])
-            return get_frozen_path(os.path.join(app_path, 'plugins'),
-                os.path.join(os.path.split(openlp.__file__)[0], 'plugins'))
+            return os.path.normpath(get_frozen_path(os.path.join(app_path, 'plugins'),
+                os.path.join(os.path.split(openlp.__file__)[0], 'plugins')))
         elif dir_type == AppLocation.VersionDir:
             return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0])
         elif dir_type == AppLocation.LanguageDir:

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2013-12-24 08:56:50 +0000
+++ openlp/core/common/settings.py	2014-03-18 21:03:35 +0000
@@ -386,7 +386,7 @@
         """
         if self.group():
             key = self.group() + '/' + key
-        return Settings.__default_settings__[key]
+        return Settings.__default_settings__.get(key, '')
 
     def remove_obsolete_settings(self):
         """
@@ -424,9 +424,9 @@
         """
         # if group() is not empty the group has not been specified together with the key.
         if self.group():
-            default_value = Settings.__default_settings__[self.group() + '/' + key]
+            default_value = Settings.__default_settings__.get(self.group() + '/' + key, '')
         else:
-            default_value = Settings.__default_settings__[key]
+            default_value = Settings.__default_settings__.get(key, '')
         setting = super(Settings, self).value(key, default_value)
         return self._convert_value(setting, default_value)
 

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2014-03-11 18:58:49 +0000
+++ openlp/core/lib/__init__.py	2014-03-18 21:03:35 +0000
@@ -145,11 +145,13 @@
     return button_icon
 
 
-def image_to_byte(image):
+def image_to_byte(image, base_64=True):
     """
     Resize an image to fit on the current screen for the web and returns it as a byte stream.
 
     :param image: The image to converted.
+    :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
+        To preserve original intention, this defaults to True
     """
     log.debug('image_to_byte - start')
     byte_array = QtCore.QByteArray()
@@ -158,6 +160,8 @@
     buffie.open(QtCore.QIODevice.WriteOnly)
     image.save(buffie, "PNG")
     log.debug('image_to_byte - end')
+    if not base_64:
+        return byte_array
     # convert to base64 encoding so does not get missed!
     return bytes(byte_array.toBase64()).decode('utf-8')
 

=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	2013-12-28 21:33:38 +0000
+++ openlp/core/lib/imagemanager.py	2014-03-18 21:03:35 +0000
@@ -36,6 +36,7 @@
 import os
 import time
 import queue
+import re
 
 from PyQt4 import QtCore
 
@@ -106,7 +107,7 @@
     """
     secondary_priority = 0
 
-    def __init__(self, path, source, background):
+    def __init__(self, path, source, background, dimensions=''):
         """
         Create an image for the :class:`ImageManager`'s cache.
 
@@ -128,6 +129,15 @@
         self.source = source
         self.background = background
         self.timestamp = 0
+        match = re.search('(\d+)x(\d+)', dimensions)
+        if match:
+            # let's make sure that the dimensions are within reason
+            self.width = sorted([10, int(match.group(1)), 1000])[1]
+            self.height = sorted([10, int(match.group(2)), 1000])[1]
+        else:
+            # -1 means use the default dimension in ImageManager
+            self.width = -1
+            self.height = -1
         # FIXME: We assume that the path exist. The caller has to take care that it exists!
         if os.path.exists(path):
             self.timestamp = os.stat(path).st_mtime
@@ -218,13 +228,13 @@
                 image.background = background
                 self._reset_image(image)
 
-    def update_image_border(self, path, source, background):
+    def update_image_border(self, path, source, background, dimensions=''):
         """
         Border has changed so update the image affected.
         """
         log.debug('update_image_border')
         # Mark the image as dirty for a rebuild by setting the image and byte stream to None.
-        image = self._cache[(path, source)]
+        image = self._cache[(path, source, dimensions)]
         if image.source == source:
             image.background = background
             self._reset_image(image)
@@ -245,12 +255,12 @@
         if not self.image_thread.isRunning():
             self.image_thread.start()
 
-    def get_image(self, path, source):
+    def get_image(self, path, source, dimensions=''):
         """
         Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
         """
         log.debug('getImage %s' % path)
-        image = self._cache[(path, source)]
+        image = self._cache[(path, source, dimensions)]
         if image.image is None:
             self._conversion_queue.modify_priority(image, Priority.High)
             # make sure we are running and if not give it a kick
@@ -265,12 +275,12 @@
             self._conversion_queue.modify_priority(image, Priority.Low)
         return image.image
 
-    def get_image_bytes(self, path, source):
+    def get_image_bytes(self, path, source, dimensions=''):
         """
         Returns the byte string for an image. If not present wait for the background thread to process it.
         """
         log.debug('get_image_bytes %s' % path)
-        image = self._cache[(path, source)]
+        image = self._cache[(path, source, dimensions)]
         if image.image_bytes is None:
             self._conversion_queue.modify_priority(image, Priority.Urgent)
             # make sure we are running and if not give it a kick
@@ -280,14 +290,14 @@
                 time.sleep(0.1)
         return image.image_bytes
 
-    def add_image(self, path, source, background):
+    def add_image(self, path, source, background, dimensions=''):
         """
         Add image to cache if it is not already there.
         """
         log.debug('add_image %s' % path)
-        if not (path, source) in self._cache:
-            image = Image(path, source, background)
-            self._cache[(path, source)] = image
+        if not (path, source, dimensions) in self._cache:
+            image = Image(path, source, background, dimensions)
+            self._cache[(path, source, dimensions)] = image
             self._conversion_queue.put((image.priority, image.secondary_priority, image))
         # Check if the there are any images with the same path and check if the timestamp has changed.
         for image in list(self._cache.values()):
@@ -316,7 +326,10 @@
         image = self._conversion_queue.get()[2]
         # Generate the QImage for the image.
         if image.image is None:
-            image.image = resize_image(image.path, self.width, self.height, image.background)
+            # Let's see if the image was requested with specific dimensions
+            width = self.width if image.width == -1 else image.width
+            height = self.height if image.height == -1 else image.height
+            image.image = resize_image(image.path, width, height, image.background)
             # Set the priority to Lowest and stop here as we need to process more important images first.
             if image.priority == Priority.Normal:
                 self._conversion_queue.modify_priority(image, Priority.Lowest)

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2014-03-16 21:25:23 +0000
+++ openlp/core/lib/serviceitem.py	2014-03-18 21:03:35 +0000
@@ -108,6 +108,16 @@
     ``CanAutoStartForLive``
             The capability to ignore the do not play if display blank flag.
 
+    ``HasDisplayTitle``
+            The item contains 'displaytitle' on every frame which should be
+            preferred over 'title' when displaying the item
+
+    ``HasNotes``
+            The item contains 'notes'
+
+    ``HasThumbnails``
+            The item has related thumbnails available
+
     """
     CanPreview = 1
     CanEdit = 2
@@ -125,6 +135,9 @@
     CanWordSplit = 14
     HasBackgroundAudio = 15
     CanAutoStartForLive = 16
+    HasDisplayTitle = 17
+    HasNotes = 18
+    HasThumbnails = 19
 
 
 class ServiceItem(RegistryProperties):
@@ -303,7 +316,7 @@
         self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
         self._new_item()
 
-    def add_from_command(self, path, file_name, image):
+    def add_from_command(self, path, file_name, image, display_title=None, notes=None):
         """
         Add a slide from a command.
 
@@ -317,7 +330,8 @@
             The command of/for the slide.
         """
         self.service_item_type = ServiceItemType.Command
-        self._raw_frames.append({'title': file_name, 'image': image, 'path': path})
+        self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
+                                 'display_title': display_title, 'notes': notes})
         self._new_item()
 
     def get_service_repr(self, lite_save):
@@ -362,7 +376,8 @@
                 service_data = [slide['title'] for slide in self._raw_frames]
         elif self.service_item_type == ServiceItemType.Command:
             for slide in self._raw_frames:
-                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']})
+                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
+                                     'display_title': slide['display_title'], 'notes': slide['notes']})
         return {'header': service_header, 'data': service_data}
 
     def set_from_service(self, serviceitem, path=None):
@@ -434,7 +449,8 @@
                     self.title = text_image['title']
                 if path:
                     self.has_original_files = False
-                    self.add_from_command(path, text_image['title'], text_image['image'])
+                    self.add_from_command(path, text_image['title'], text_image['image'],
+                                          text_image.get('display_title',''), text_image.get('notes', ''))
                 else:
                     self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
         self._new_item()

=== modified file 'openlp/core/ui/listpreviewwidget.py'
--- openlp/core/ui/listpreviewwidget.py	2014-03-16 21:25:23 +0000
+++ openlp/core/ui/listpreviewwidget.py	2014-03-18 21:03:35 +0000
@@ -94,8 +94,8 @@
         Displays the given slide.
         """
         self.service_item = service_item
+        self.setRowCount(0)
         self.clear()
-        self.setRowCount(0)
         self.setColumnWidth(0, width)
         row = 0
         text = []

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2014-03-16 21:25:23 +0000
+++ openlp/core/ui/servicemanager.py	2014-03-18 21:03:35 +0000
@@ -1267,10 +1267,17 @@
             tree_widget_item.setToolTip(0, '<br>'.join(tips))
             tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
             tree_widget_item.setSelected(item['selected'])
-            # Add the children to their parent tree_widget_item.
+            # Add the children to their parent tree_widget)item.
             for count, frame in enumerate(service_item_from_item.get_frames()):
                 child = QtGui.QTreeWidgetItem(tree_widget_item)
-                text = frame['title'].replace('\n', ' ')
+                # prefer to use a display_title
+                if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
+                    text = frame['display_title'].replace('\n', ' ')
+                    # oops, it is missing, let's make one up
+                    if len(text.strip()) == 0:
+                        text = '[slide ' + str(count+1) + ']'
+                else:
+                    text = frame['title'].replace('\n', ' ')
                 child.setText(0, text[:40])
                 child.setData(0, QtCore.Qt.UserRole, count)
                 if service_item == item_count:

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2014-03-13 20:08:47 +0000
+++ openlp/core/ui/slidecontroller.py	2014-03-18 21:03:35 +0000
@@ -850,12 +850,17 @@
 
         :param message: remote message to be processed.
         """
-        index = int(message[0])
+        index = 0
+        if len(message) == 0 or message[0] == 'undefined':
+            return
+        else:
+            index = int(message[0])
         if not self.service_item:
             return
         if self.service_item.is_command():
             Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
             self.update_preview()
+            self.selected_row = index
         else:
             self.preview_widget.change_slide(index)
             self.slide_selected()
@@ -1025,10 +1030,12 @@
                         self.display.image(to_display)
                     # reset the store used to display first image
                     self.service_item.bg_image_bytes = None
-            self.update_preview()
             self.selected_row = row
+            self.update_preview()
             self.preview_widget.change_slide(row)
         self.display.setFocus()
+        if self.type_prefix == 'live':
+            Registry().execute('websock_send', '')
 
     def on_slide_change(self, row):
         """
@@ -1038,7 +1045,7 @@
         """
         self.preview_widget.change_slide(row)
         self.update_preview()
-        Registry().execute('slidecontroller_%s_changed' % self.type_prefix, row)
+        self.selected_row = row
 
     def update_preview(self):
         """

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2014-03-18 21:03:35 +0000
@@ -61,7 +61,7 @@
 
 from openlp.core.lib import ScreenList
 from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
-from .presentationcontroller import PresentationController, PresentationDocument
+from .presentationcontroller import PresentationController, PresentationDocument, TextType
 
 
 log = logging.getLogger(__name__)
@@ -253,6 +253,7 @@
         self.presentation.Display = ScreenList().current['number'] + 1
         self.control = None
         self.create_thumbnails()
+        self.create_titles_and_notes()
         return True
 
     def create_thumbnails(self):
@@ -448,22 +449,47 @@
 
         :param slide_no: The slide the notes are required for, starting at 1
         """
-        return self.__get_text_from_page(slide_no, True)
+        return self.__get_text_from_page(slide_no, TextType.Notes)
 
-    def __get_text_from_page(self, slide_no, notes=False):
+    def __get_text_from_page(self, slide_no, text_type=TextType.SlideText):
         """
         Return any text extracted from the presentation page.
 
         :param slide_no: The slide the notes are required for, starting at 1
         :param notes: A boolean. If set the method searches the notes of the slide.
+        :param text_type: A TextType. Enumeration of the types of supported text.
         """
         text = ''
+        if TextType.Title <= text_type <= TextType.Notes:
+            pages = self.document.getDrawPages()
+            if 0 < slide_no <= pages.getCount():
+                page = pages.getByIndex(slide_no - 1)
+                if text_type == TextType.Notes:
+                    page = page.getNotesPage()
+                for index in range(page.getCount()):
+                    shape = page.getByIndex(index)
+                    shape_type = shape.getShapeType()
+                    if shape.supportsService("com.sun.star.drawing.Text"):
+                        # if they requested title, make sure it is the title
+                        if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape":
+                            text += shape.getString() + '\n'
+        return text
+
+    def create_titles_and_notes(self):
+        """
+        Writes the list of titles (one per slide) 
+        to 'titles.txt' 
+        and the notes to 'slideNotes[x].txt'
+        in the thumbnails directory
+        """
+        titles = []
+        notes = []
         pages = self.document.getDrawPages()
-        page = pages.getByIndex(slide_no - 1)
-        if notes:
-            page = page.getNotesPage()
-        for index in range(page.getCount()):
-            shape = page.getByIndex(index)
-            if shape.supportsService("com.sun.star.drawing.Text"):
-                text += shape.getString() + '\n'
-        return text
+        for slide_no in range(1, pages.getCount() + 1):
+            titles.append(self.__get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
+            note = self.__get_text_from_page(slide_no, TextType.Notes)
+            if len(note) == 0:
+                note = ' '
+            notes.append(note)
+        self.save_titles_and_notes(titles, notes)
+        return

=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py	2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2014-03-18 21:03:35 +0000
@@ -27,22 +27,24 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-This modul is for controlling powerpiont. PPT API documentation:
+This module is for controlling powerpoint. PPT API documentation:
 `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
 """
 import os
 import logging
+from .ppinterface import constants
 
 if os.name == 'nt':
     from win32com.client import Dispatch
+    import win32com
     import winreg
     import win32ui
     import pywintypes
 
 from openlp.core.lib import ScreenList
+from openlp.core.common import Registry
 from .presentationcontroller import PresentationController, PresentationDocument
 
-
 log = logging.getLogger(__name__)
 
 
@@ -83,6 +85,8 @@
             log.debug('start_process')
             if not self.process:
                 self.process = Dispatch('PowerPoint.Application')
+                self.events = PowerpointEvents(self.process)
+                self.events.controller = self
             self.process.Visible = True
             self.process.WindowState = 2
 
@@ -135,6 +139,7 @@
             return False
         self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
         self.create_thumbnails()
+        self.create_titles_and_notes()
         return True
 
     def create_thumbnails(self):
@@ -319,6 +324,29 @@
         """
         return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
 
+    def create_titles_and_notes(self):
+        """
+        Writes the list of titles (one per slide) 
+        to 'titles.txt' 
+        and the notes to 'slideNotes[x].txt'
+        in the thumbnails directory
+        """
+        titles = []
+        notes = []
+        for slide in self.presentation.Slides:
+            try:
+                text = slide.Shapes.Title.TextFrame.TextRange.Text
+            except Exception as e:
+                log.exception(e)
+                text = ''
+            titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n')
+            note = _get_text_from_shapes(slide.NotesPage.Shapes)
+            if len(note) == 0:
+                note = ' '
+            notes.append(note)
+        self.save_titles_and_notes(titles, notes)
+        return
+
 
 def _get_text_from_shapes(shapes):
     """
@@ -327,8 +355,33 @@
     :param shapes: A set of shapes to search for text.
     """
     text = ''
-    for index in range(shapes.Count):
-        shape = shapes(index + 1)
-        if shape.HasTextFrame:
-            text += shape.TextFrame.TextRange.Text + '\n'
+    for shape in shapes:
+        if shape.PlaceholderFormat.Type == constants.ppPlaceholderBody:
+            if shape.HasTextFrame and shape.TextFrame.HasText:
+                text += shape.TextFrame.TextRange.Text + '\n'
     return text
+
+if os.name == "nt":
+    ppE = win32com.client.getevents("PowerPoint.Application")
+
+    class PowerpointEvents(ppE):
+        def OnSlideShowBegin(self, hwnd):
+            #print("SS Begin")
+            return
+
+        def OnSlideShowEnd(self, pres):
+            #print("SS End")
+            return
+
+        def OnSlideShowNextSlide(self, hwnd):
+            Registry().execute('slidecontroller_live_change', hwnd.View.CurrentShowPosition - 1)
+            #print('Slide change:',hwnd.View.CurrentShowPosition)
+            return
+
+        def OnSlideShowOnNext(self, hwnd):
+            #print("SS Advance")
+            return
+
+        def OnSlideShowOnPrevious(self, hwnd):
+            #print("SS GoBack")
+            return

=== added file 'openlp/plugins/presentations/lib/ppinterface.py'
--- openlp/plugins/presentations/lib/ppinterface.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/ppinterface.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,330 @@
+# -*- 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                          #
+###############################################################################
+"""
+These declarations have been extracted from the interface file created by makepy
+"""
+class constants:
+    ppPlaceholderBody             =2          # from enum PpPlaceholderType
+
+import os
+if os.name=='nt':
+    import pythoncom, pywintypes
+    from win32com.client import Dispatch,DispatchBaseClass,CoClassBaseClass, \
+        CLSIDToClass
+    from pywintypes import IID
+    import win32com
+
+
+    # The following 3 lines may need tweaking for the particular server
+    # Candidates are pythoncom.Missing, .Empty and .ArgNotFound
+    defaultNamedOptArg=pythoncom.Empty
+    defaultNamedNotOptArg=pythoncom.Empty
+    defaultUnnamedArg=pythoncom.Empty
+
+
+    CLSID = IID('{91493440-5A91-11CF-8700-00AA0060263B}')
+    MajorVersion = 2
+    MinorVersion = 11
+    LibraryFlags = 8
+    LCID = 0x0
+
+    class EApplication:
+        CLSID = CLSID_Sink = IID('{914934C2-5A91-11CF-8700-00AA0060263B}')
+        coclass_clsid = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+        _public_methods_ = [] # For COM Server support
+        _dispid_to_func_ = {
+                 2029 : "OnProtectedViewWindowActivate",
+                 2015 : "OnPresentationPrint",
+                 2013 : "OnSlideShowNextSlide",
+                 2011 : "OnSlideShowBegin",
+                 2001 : "OnWindowSelectionChange",
+                 2005 : "OnPresentationSave",
+                 2020 : "OnAfterNewPresentation",
+                 2014 : "OnSlideShowEnd",
+                 2028 : "OnProtectedViewWindowBeforeClose",
+                 2025 : "OnPresentationBeforeClose",
+                 2018 : "OnPresentationBeforeSave",
+                 2010 : "OnWindowDeactivate",
+                 2021 : "OnAfterPresentationOpen",
+                 2027 : "OnProtectedViewWindowBeforeEdit",
+                 2026 : "OnProtectedViewWindowOpen",
+                 2023 : "OnSlideShowOnNext",
+                 2012 : "OnSlideShowNextBuild",
+                 2002 : "OnWindowBeforeRightClick",
+                 2030 : "OnProtectedViewWindowDeactivate",
+                 2016 : "OnSlideSelectionChanged",
+                 2004 : "OnPresentationClose",
+                 2017 : "OnColorSchemeChanged",
+                 2019 : "OnSlideShowNextClick",
+                 2006 : "OnPresentationOpen",
+                 2003 : "OnWindowBeforeDoubleClick",
+                 2031 : "OnPresentationCloseFinal",
+                 2032 : "OnAfterDragDropOnSlide",
+                 2033 : "OnAfterShapeSizeChange",
+                 2009 : "OnWindowActivate",
+                 2022 : "OnPresentationSync",
+                 2007 : "OnNewPresentation",
+                 2024 : "OnSlideShowOnPrevious",
+                 2008 : "OnPresentationNewSlide",
+            }
+
+        def __init__(self, oobj = None):
+            if oobj is None:
+                self._olecp = None
+            else:
+                import win32com.server.util
+                from win32com.server.policy import EventHandlerPolicy
+                cpc=oobj._oleobj_.QueryInterface(pythoncom.IID_IConnectionPointContainer)
+                cp=cpc.FindConnectionPoint(self.CLSID_Sink)
+                cookie=cp.Advise(win32com.server.util.wrap(self, usePolicy=EventHandlerPolicy))
+                self._olecp,self._olecp_cookie = cp,cookie
+        def __del__(self):
+            try:
+                self.close()
+            except pythoncom.com_error:
+                pass
+        def close(self):
+            if self._olecp is not None:
+                cp,cookie,self._olecp,self._olecp_cookie = self._olecp,self._olecp_cookie,None,None
+                cp.Unadvise(cookie)
+        def _query_interface_(self, iid):
+            import win32com.server.util
+            if iid==self.CLSID_Sink: return win32com.server.util.wrap(self)
+
+    class _Application(DispatchBaseClass):
+        CLSID = IID('{91493442-5A91-11CF-8700-00AA0060263B}')
+        coclass_clsid = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+
+        def Activate(self):
+            return self._oleobj_.InvokeTypes(2033, LCID, 1, (24, 0), (),)
+
+        # Result is of type FileDialog
+        # The method FileDialog is actually a property, but must be used as a method to correctly pass the arguments
+        def FileDialog(self, Type=defaultNamedNotOptArg):
+            ret = self._oleobj_.InvokeTypes(2045, LCID, 2, (9, 0), ((3, 1),),Type
+                )
+            if ret is not None:
+                ret = Dispatch(ret, 'FileDialog', '{000C0362-0000-0000-C000-000000000046}')
+            return ret
+
+        def GetOptionFlag(self, Option=defaultNamedNotOptArg, Persist=False):
+            return self._oleobj_.InvokeTypes(2043, LCID, 1, (11, 0), ((3, 1), (11, 49)),Option
+                , Persist)
+
+        def Help(self, HelpFile='vbapp10.chm', ContextID=0):
+            return self._ApplyTypes_(2020, 1, (24, 32), ((8, 49), (3, 49)), 'Help', None,HelpFile
+                , ContextID)
+
+        def LaunchPublishSlidesDialog(self, SlideLibraryUrl=defaultNamedNotOptArg):
+            return self._oleobj_.InvokeTypes(2054, LCID, 1, (24, 0), ((8, 1),),SlideLibraryUrl
+                )
+
+        def LaunchSendToPPTDialog(self, SlideUrls=defaultNamedNotOptArg):
+            return self._oleobj_.InvokeTypes(2055, LCID, 1, (24, 0), ((16396, 1),),SlideUrls
+                )
+
+        # Result is of type Theme
+        def OpenThemeFile(self, themeFileName=defaultNamedNotOptArg):
+            ret = self._oleobj_.InvokeTypes(2069, LCID, 1, (9, 0), ((8, 1),),themeFileName
+                )
+            if ret is not None:
+                ret = Dispatch(ret, 'OpenThemeFile', '{D9D60EB3-D4B4-4991-9C16-75585B3346BB}')
+            return ret
+
+        def PPFileDialog(self, Type=defaultNamedNotOptArg):
+            ret = self._oleobj_.InvokeTypes(2023, LCID, 1, (13, 0), ((3, 1),),Type
+                )
+            if ret is not None:
+                # See if this IUnknown is really an IDispatch
+                try:
+                    ret = ret.QueryInterface(pythoncom.IID_IDispatch)
+                except pythoncom.error:
+                    return ret
+                ret = Dispatch(ret, 'PPFileDialog', None)
+            return ret
+
+        def Quit(self):
+            return self._oleobj_.InvokeTypes(2021, LCID, 1, (24, 0), (),)
+
+        def Run(self, *args):
+            return self._get_good_object_(self._oleobj_.Invoke(*((2022,0,1,1)+args)),'Run')
+
+        def SetOptionFlag(self, Option=defaultNamedNotOptArg, State=defaultNamedNotOptArg, Persist=False):
+            return self._oleobj_.InvokeTypes(2044, LCID, 1, (24, 0), ((3, 1), (11, 1), (11, 49)),Option
+                , State, Persist)
+
+        def SetPerfMarker(self, Marker=defaultNamedNotOptArg):
+            return self._oleobj_.InvokeTypes(2051, LCID, 1, (24, 0), ((3, 1),),Marker
+                )
+
+        def StartNewUndoEntry(self):
+            return self._oleobj_.InvokeTypes(2067, LCID, 1, (24, 0), (),)
+
+        _prop_map_get_ = {
+            "Active": (2032, 2, (3, 0), (), "Active", None),
+            "ActiveEncryptionSession": (2058, 2, (3, 0), (), "ActiveEncryptionSession", None),
+            # Method 'ActivePresentation' returns object of type 'Presentation'
+            "ActivePresentation": (2005, 2, (13, 0), (), "ActivePresentation", '{91493444-5A91-11CF-8700-00AA0060263B}'),
+            "ActivePrinter": (2016, 2, (8, 0), (), "ActivePrinter", None),
+            # Method 'ActiveProtectedViewWindow' returns object of type 'ProtectedViewWindow'
+            "ActiveProtectedViewWindow": (2064, 2, (9, 0), (), "ActiveProtectedViewWindow", '{BA72E55A-4FF5-48F4-8215-5505F990966F}'),
+            # Method 'ActiveWindow' returns object of type 'DocumentWindow'
+            "ActiveWindow": (2004, 2, (9, 0), (), "ActiveWindow", '{91493457-5A91-11CF-8700-00AA0060263B}'),
+            # Method 'AddIns' returns object of type 'AddIns'
+            "AddIns": (2018, 2, (9, 0), (), "AddIns", '{91493460-5A91-11CF-8700-00AA0060263B}'),
+            # Method 'AnswerWizard' returns object of type 'AnswerWizard'
+            "AnswerWizard": (2034, 2, (9, 0), (), "AnswerWizard", '{000C0360-0000-0000-C000-000000000046}'),
+            # Method 'Assistance' returns object of type 'IAssistance'
+            "Assistance": (2057, 2, (9, 0), (), "Assistance", '{4291224C-DEFE-485B-8E69-6CF8AA85CB76}'),
+            # Method 'Assistant' returns object of type 'Assistant'
+            "Assistant": (2010, 2, (9, 0), (), "Assistant", '{000C0322-0000-0000-C000-000000000046}'),
+            # Method 'AutoCorrect' returns object of type 'AutoCorrect'
+            "AutoCorrect": (2052, 2, (9, 0), (), "AutoCorrect", '{914934ED-5A91-11CF-8700-00AA0060263B}'),
+            "AutomationSecurity": (2047, 2, (3, 0), (), "AutomationSecurity", None),
+            "Build": (2013, 2, (8, 0), (), "Build", None),
+            # Method 'COMAddIns' returns object of type 'COMAddIns'
+            "COMAddIns": (2035, 2, (9, 0), (), "COMAddIns", '{000C0339-0000-0000-C000-000000000046}'),
+            "Caption": (2009, 2, (8, 0), (), "Caption", None),
+            "ChartDataPointTrack": (2070, 2, (11, 0), (), "ChartDataPointTrack", None),
+            # Method 'CommandBars' returns object of type 'CommandBars'
+            "CommandBars": (2007, 2, (13, 0), (), "CommandBars", '{55F88893-7708-11D1-ACEB-006008961DA5}'),
+            "Creator": (2017, 2, (3, 0), (), "Creator", None),
+            # Method 'DefaultWebOptions' returns object of type 'DefaultWebOptions'
+            "DefaultWebOptions": (2037, 2, (9, 0), (), "DefaultWebOptions", '{914934CD-5A91-11CF-8700-00AA0060263B}'),
+            "Dialogs": (2003, 2, (13, 0), (), "Dialogs", None),
+            "DisplayAlerts": (2049, 2, (3, 0), (), "DisplayAlerts", None),
+            "DisplayDocumentInformationPanel": (2056, 2, (11, 0), (), "DisplayDocumentInformationPanel", None),
+            "DisplayGridLines": (2046, 2, (3, 0), (), "DisplayGridLines", None),
+            "DisplayGuides": (2071, 2, (3, 0), (), "DisplayGuides", None),
+            "FeatureInstall": (2042, 2, (3, 0), (), "FeatureInstall", None),
+            # Method 'FileConverters' returns object of type 'FileConverters'
+            "FileConverters": (2059, 2, (9, 0), (), "FileConverters", '{92D41A50-F07E-4CA4-AF6F-BEF486AA4E6F}'),
+            # Method 'FileFind' returns object of type 'IFind'
+            "FileFind": (2012, 2, (9, 0), (), "FileFind", '{000C0337-0000-0000-C000-000000000046}'),
+            # Method 'FileSearch' returns object of type 'FileSearch'
+            "FileSearch": (2011, 2, (9, 0), (), "FileSearch", '{000C0332-0000-0000-C000-000000000046}'),
+            "FileValidation": (2068, 2, (3, 0), (), "FileValidation", None),
+            "HWND": (2031, 2, (3, 0), (), "HWND", None),
+            "Height": (2028, 2, (4, 0), (), "Height", None),
+            "IsSandboxed": (2065, 2, (11, 0), (), "IsSandboxed", None),
+            # Method 'LanguageSettings' returns object of type 'LanguageSettings'
+            "LanguageSettings": (2038, 2, (9, 0), (), "LanguageSettings", '{000C0353-0000-0000-C000-000000000046}'),
+            "Left": (2025, 2, (4, 0), (), "Left", None),
+            "Marker": (2041, 2, (13, 0), (), "Marker", None),
+            # Method 'MsoDebugOptions' returns object of type 'MsoDebugOptions'
+            "MsoDebugOptions": (2039, 2, (9, 0), (), "MsoDebugOptions", '{000C035A-0000-0000-C000-000000000046}'),
+            "Name": (0, 2, (8, 0), (), "Name", None),
+            # Method 'NewPresentation' returns object of type 'NewFile'
+            "NewPresentation": (2048, 2, (9, 0), (), "NewPresentation", '{000C0936-0000-0000-C000-000000000046}'),
+            "OperatingSystem": (2015, 2, (8, 0), (), "OperatingSystem", None),
+            # Method 'Options' returns object of type 'Options'
+            "Options": (2053, 2, (9, 0), (), "Options", '{914934EE-5A91-11CF-8700-00AA0060263B}'),
+            "Path": (2008, 2, (8, 0), (), "Path", None),
+            # Method 'Presentations' returns object of type 'Presentations'
+            "Presentations": (2001, 2, (9, 0), (), "Presentations", '{91493462-5A91-11CF-8700-00AA0060263B}'),
+            "ProductCode": (2036, 2, (8, 0), (), "ProductCode", None),
+            # Method 'ProtectedViewWindows' returns object of type 'ProtectedViewWindows'
+            "ProtectedViewWindows": (2063, 2, (9, 0), (), "ProtectedViewWindows", '{BA72E559-4FF5-48F4-8215-5505F990966F}'),
+            # Method 'ResampleMediaTasks' returns object of type 'ResampleMediaTasks'
+            "ResampleMediaTasks": (2066, 2, (9, 0), (), "ResampleMediaTasks", '{BA72E554-4FF5-48F4-8215-5505F990966F}'),
+            "ShowStartupDialog": (2050, 2, (3, 0), (), "ShowStartupDialog", None),
+            "ShowWindowsInTaskbar": (2040, 2, (3, 0), (), "ShowWindowsInTaskbar", None),
+            # Method 'SlideShowWindows' returns object of type 'SlideShowWindows'
+            "SlideShowWindows": (2006, 2, (9, 0), (), "SlideShowWindows", '{91493456-5A91-11CF-8700-00AA0060263B}'),
+            # Method 'SmartArtColors' returns object of type 'SmartArtColors'
+            "SmartArtColors": (2062, 2, (9, 0), (), "SmartArtColors", '{000C03CD-0000-0000-C000-000000000046}'),
+            # Method 'SmartArtLayouts' returns object of type 'SmartArtLayouts'
+            "SmartArtLayouts": (2060, 2, (9, 0), (), "SmartArtLayouts", '{000C03C9-0000-0000-C000-000000000046}'),
+            # Method 'SmartArtQuickStyles' returns object of type 'SmartArtQuickStyles'
+            "SmartArtQuickStyles": (2061, 2, (9, 0), (), "SmartArtQuickStyles", '{000C03CB-0000-0000-C000-000000000046}'),
+            "Top": (2026, 2, (4, 0), (), "Top", None),
+            # Method 'VBE' returns object of type 'VBE'
+            "VBE": (2019, 2, (9, 0), (), "VBE", '{0002E166-0000-0000-C000-000000000046}'),
+            "Version": (2014, 2, (8, 0), (), "Version", None),
+            "Visible": (2030, 2, (3, 0), (), "Visible", None),
+            "Width": (2027, 2, (4, 0), (), "Width", None),
+            "WindowState": (2029, 2, (3, 0), (), "WindowState", None),
+            # Method 'Windows' returns object of type 'DocumentWindows'
+            "Windows": (2002, 2, (9, 0), (), "Windows", '{91493455-5A91-11CF-8700-00AA0060263B}'),
+        }
+        _prop_map_put_ = {
+            "AutomationSecurity": ((2047, LCID, 4, 0),()),
+            "Caption": ((2009, LCID, 4, 0),()),
+            "ChartDataPointTrack": ((2070, LCID, 4, 0),()),
+            "DisplayAlerts": ((2049, LCID, 4, 0),()),
+            "DisplayDocumentInformationPanel": ((2056, LCID, 4, 0),()),
+            "DisplayGridLines": ((2046, LCID, 4, 0),()),
+            "DisplayGuides": ((2071, LCID, 4, 0),()),
+            "FeatureInstall": ((2042, LCID, 4, 0),()),
+            "FileValidation": ((2068, LCID, 4, 0),()),
+            "Height": ((2028, LCID, 4, 0),()),
+            "Left": ((2025, LCID, 4, 0),()),
+            "ShowStartupDialog": ((2050, LCID, 4, 0),()),
+            "ShowWindowsInTaskbar": ((2040, LCID, 4, 0),()),
+            "Top": ((2026, LCID, 4, 0),()),
+            "Visible": ((2030, LCID, 4, 0),()),
+            "Width": ((2027, LCID, 4, 0),()),
+            "WindowState": ((2029, LCID, 4, 0),()),
+        }
+        # Default property for this class is 'Name'
+        def __call__(self):
+            return self._ApplyTypes_(*(0, 2, (8, 0), (), "Name", None))
+        def __str__(self, *args):
+            return str(self.__call__(*args))
+        def __int__(self, *args):
+            return int(self.__call__(*args))
+        def __iter__(self):
+            "Return a Python iterator for this object"
+            try:
+                ob = self._oleobj_.InvokeTypes(-4,LCID,3,(13, 10),())
+            except pythoncom.error:
+                raise TypeError("This object does not support enumeration")
+            return win32com.client.util.Iterator(ob, None)
+
+    # This CoClass is known by the name 'PowerPoint.Application.15'
+    class Application(CoClassBaseClass): # A CoClass
+        CLSID = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+        coclass_sources = [
+            EApplication,
+        ]
+        default_source = EApplication
+        coclass_interfaces = [
+            _Application,
+        ]
+        default_interface = _Application
+
+
+    CLSIDToClassMap = {
+        '{91493441-5A91-11CF-8700-00AA0060263B}' : Application,
+        '{91493442-5A91-11CF-8700-00AA0060263B}' : _Application,
+        '{914934C2-5A91-11CF-8700-00AA0060263B}' : EApplication,
+    }
+
+    CLSIDToPackageMap = {}
+    win32com.client.CLSIDToClass.RegisterCLSIDsFromDict( CLSIDToClassMap )

=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py	2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py	2014-03-18 21:03:35 +0000
@@ -29,6 +29,11 @@
 
 import logging
 import os
+import logging
+import zipfile
+import re
+from xml.etree import ElementTree
+
 
 if os.name == 'nt':
     from ctypes import cdll
@@ -124,14 +129,14 @@
         temp_folder = self.get_temp_folder()
         size = ScreenList().current['size']
         rect = RECT(size.x(), size.y(), size.right(), size.bottom())
-        file_path = os.path.normpath(self.file_path)
+        self.file_path = os.path.normpath(self.file_path)
         preview_path = os.path.join(temp_folder, 'slide')
         # Ensure that the paths are null terminated
-        file_path = file_path.encode('utf-16-le') + b'\0'
+        self.file_path = self.file_path.encode('utf-16-le') + b'\0'
         preview_path = preview_path.encode('utf-16-le') + b'\0'
         if not os.path.isdir(temp_folder):
             os.makedirs(temp_folder)
-        self.ppt_id = self.controller.process.OpenPPT(file_path, None, rect, preview_path)
+        self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path)
         if self.ppt_id >= 0:
             self.create_thumbnails()
             self.stop_presentation()
@@ -151,6 +156,70 @@
             path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(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
+        filename = os.path.normpath(self.file_path)
+        # let's make sure we have a valid zipped presentation
+        if os.path.exists(filename) and zipfile.is_zipfile(filename):
+            namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main";,
+                          "a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
+            # open the file
+            with zipfile.ZipFile(filename) 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)
+                #print("slide count: " + str(len(nodes)))
+                # 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("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("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)
+        return
+
     def close_presentation(self):
         """
         Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being

=== added file 'openlp/plugins/presentations/lib/pptviewlib/test.pptx'
Binary files openlp/plugins/presentations/lib/pptviewlib/test.pptx	1970-01-01 00:00:00 +0000 and openlp/plugins/presentations/lib/pptviewlib/test.pptx	2014-03-18 21:03:35 +0000 differ
=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2014-03-17 07:14:51 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2014-03-18 21:03:35 +0000
@@ -292,6 +292,49 @@
         """
         return ''
 
+    def get_titles_and_notes(self):
+        """
+        Reads the titles from the titles file and 
+        the notes files and returns the contents
+        in a two lists
+        """
+        titles = []
+        notes = []
+        titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
+        if os.path.exists(titles_file):
+            try:
+                with open(titles_file) as fi:
+                    titles = fi.read().splitlines()
+            except:
+                log.exception('Failed to open/read existing titles file')
+                titles = []
+        for slide_no, title in enumerate(titles, 1):
+            notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
+            note = ''
+            if os.path.exists(notes_file):
+                try:
+                    with open(notes_file) as fn:
+                        note = fn.read()
+                except:
+                    log.exception('Failed to open/read notes file')
+                    note = ''
+            notes.append(note)
+        return titles, notes
+
+    def save_titles_and_notes(self, titles, notes):
+        """
+        Performs the actual persisting of titles to the titles.txt
+        and notes to the slideNote%.txt
+        """
+        if titles:
+            titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
+            with open(titles_file, mode='w') as fo:
+                fo.writelines(titles)
+        if notes:
+            for slide_no, note in enumerate(notes, 1):
+                notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
+                with open(notes_file, mode='w') as fn:
+                    fn.write(note)
 
 class PresentationController(object):
     """
@@ -426,3 +469,25 @@
 
     def close_presentation(self):
         pass
+<<<<<<< TREE
+=======
+
+    def _get_plugin_manager(self):
+        """
+        Adds the plugin manager to the class dynamically
+        """
+        if not hasattr(self, '_plugin_manager'):
+            self._plugin_manager = Registry().get('plugin_manager')
+        return self._plugin_manager
+
+    plugin_manager = property(_get_plugin_manager)
+
+
+class TextType(object):
+    """
+    Type Enumeration for Types of Text to request
+    """
+    Title = 0
+    SlideText = 1
+    Notes = 2
+>>>>>>> MERGE-SOURCE

=== added file 'openlp/plugins/remotes/html/WebSocketEvents.js'
--- openlp/plugins/remotes/html/WebSocketEvents.js	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/WebSocketEvents.js	2014-03-18 21:03:35 +0000
@@ -0,0 +1,113 @@
+/******************************************************************************
+ * 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                                             *
+ * --------------------------------------------------------------------------- *
+ * 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                          *
+ ******************************************************************************/
+
+/* Thanks to Ismael Celis for the original idea */
+
+var wsEventEngine = function(url, polling_function, polling_interval)
+{
+    this.polling_handle = null;
+    this.polling_interval = polling_interval;
+    this.polling_function = polling_function;
+    this.retry_handle = null;
+    this.callbacks = {};
+
+    this.fallback = function(){
+        this.kill_polling();
+        if(this.polling_function)
+            this.polling_handle = window.setInterval(this.polling_function, this.polling_interval);
+        this.kill_retries();
+        var theEngine = this;
+        this.retry_handle = window.setInterval(function(){theEngine.setup();}, 10000);
+    }
+
+    this.kill_polling = function(){
+        if(this.polling_handle)
+            window.clearInterval(this.polling_handle);
+        this.polling_handle = null;
+    }
+
+    this.kill_retries = function(){
+        if(this.retry_handle)
+            window.clearInterval(this.retry_handle);
+    }
+
+    this.bind = function(event_name, callback){
+        this.callbacks[event_name] = this.callbacks[event_name] || [];
+        this.callbacks[event_name].push(callback);
+        return this;
+    }
+
+    this.send = function(event_name, event_data){
+        var payload = JSON.stringify({ event: event_name, data: event_data });
+        this.websocket.send(payload);
+        return this;
+    }
+
+    this.dispatch = function(event_name, message){
+        var chain = this.callbacks[event_name];
+        if(typeof chain == 'undefined') return; // no callbacks
+        for(var i = 0; i < chain.length; i++)
+            chain[i](message);
+    }
+
+    this.setup = function(){
+        this.websocket = new WebSocket(url);
+        this.websocket.engine = this;
+
+        this.websocket.onmessage = function(websocket_msg){
+            if(this.engine.polling_function)
+                this.engine.polling_function();
+            if( websocket_msg.data.length > 0 ){
+                try{
+                    var json = JSON.parse(websocket_msg.data);
+                    this.engine.dispatch(json.event, json.data);
+                }
+                catch(err){
+                }
+            }
+        }
+
+        this.websocket.onclose = function(){
+            this.engine.dispatch('close', null);
+            this.engine.fallback();
+        }
+
+        this.websocket.onopen = function(){
+            this.engine.dispatch('open', null);
+            this.engine.kill_polling();
+            this.engine.kill_retries();
+        }
+
+    }
+
+    if('WebSocket' in window){
+        this.setup();
+    }
+    else{
+        this.fallback();
+    }
+
+}
\ No newline at end of file

=== modified file 'openlp/plugins/remotes/html/index.html'
--- openlp/plugins/remotes/html/index.html	2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/index.html	2014-03-18 21:03:35 +0000
@@ -35,6 +35,7 @@
   <link rel="stylesheet" href="/files/openlp.css" />
   <link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
   <script type="text/javascript" src="/files/jquery.js"></script>
+  <script type="text/javascript" src="/files/WebSocketEvents.js"></script>
   <script type="text/javascript" src="/files/openlp.js"></script>
   <script type="text/javascript" src="/files/jquery.mobile.js"></script>
   <script type="text/javascript">
@@ -120,6 +121,21 @@
       <a href="#" id="controller-previous" data-role="button" data-icon="arrow-l">${prev}</a>
       <a href="#" id="controller-next" data-role="button" data-icon="arrow-r" data-iconpos="right">${next}</a>
     </div>
+    <div data-role="controlgroup" data-type="horizontal" style="float:left">
+        <a href="#settings" id="controller-settings" data-role="button" data-icon="gear" data-rel="dialog">${settings}</a>
+    </div>
+  </div>
+</div>
+<div data-role="page" id="settings">
+  <div data-role="header" data-position="inline" data-theme="b">
+    <h1>${settings}</h1>
+  </div>
+  <div data-role="content">
+    Display thumbnails:
+    <div data-role="controlgroup" data-type="horizontal">
+        <a href="#" id="display-thumbnails" data-role="button">Yes</a>
+        <a href="#" id="dont-display-thumbnails" data-role="button">No</a>
+    </div>
   </div>
 </div>
 <div data-role="page" id="alerts">

=== modified file 'openlp/plugins/remotes/html/openlp.js'
--- openlp/plugins/remotes/html/openlp.js	2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/openlp.js	2014-03-18 21:03:35 +0000
@@ -87,16 +87,26 @@
         var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
         ul.html("");
         for (idx in data.results.slides) {
-          var text = data.results.slides[idx]["tag"];
+          var indexInt = parseInt(idx,10);
+          var slide = data.results.slides[idx];
+          var text = slide["tag"];
           if (text != "") text = text + ": ";
-          text = text + data.results.slides[idx]["text"];
+          if (slide["title"])
+            text += slide["title"]
+          else
+            text += slide["text"];
+          if (slide["notes"])
+            text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["notes"] + "</div>");
           text = text.replace(/\n/g, '<br />');
+          if (slide["img"] && OpenLP.showThumbnails)
+            text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails80x80/") + "'>";
           var li = $("<li data-icon=\"false\">").append(
-            $("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text));
-          if (data.results.slides[idx]["selected"]) {
+            $("<a href=\"#\">").html(text));
+          if (slide["selected"]) {
             li.attr("data-theme", "e");
           }
           li.children("a").click(OpenLP.setSlide);
+          li.find("*").attr("value", indexInt );
           ul.append(li);
         }
         OpenLP.currentItem = data.results.item;
@@ -241,6 +251,17 @@
       }
     );
   },
+  displayThumbnails: function (event) {
+    event.preventDefault();
+    var target = $(event.target);
+    OpenLP.showThumbnails = target.text() == "No" ? false : true;
+    var dt = new Date();
+    dt.setTime(dt.getTime() + 365 * 24 * 60 * 60 * 1000);
+    document.cookie = "displayThumbs=" + OpenLP.showThumbnails + "; expires=" +
+      dt.toGMTString() + "; path=/";
+    OpenLP.loadController();
+    $("#settings").dialog("close");
+  },
   search: function (event) {
     event.preventDefault();
     var query = OpenLP.escapeString($("#search-text").val())
@@ -320,12 +341,24 @@
   },
   escapeString: function (string) {
     return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")
-  }
+  },
+  showThumbnails: false
 }
 // Initial jQueryMobile options
 $(document).bind("mobileinit", function(){
   $.mobile.defaultDialogTransition = "none";
   $.mobile.defaultPageTransition = "none";
+  var cookies = document.cookie;
+  if( cookies )
+  {
+    var allcookies = cookies.split(";")
+    for(ii = 0; ii < allcookies.length; ii++)
+    {
+      var parts = allcookies[ii].split("=");
+      if(parts.length == 2 && parts[0] == "displayThumbs")
+        OpenLP.showThumbnails = (parts[1]=='true');
+    }
+  }
 });
 // Service Manager
 $("#service-manager").live("pagebeforeshow", OpenLP.loadService);
@@ -345,6 +378,8 @@
 $("#controller-theme").live("click", OpenLP.themeDisplay);
 $("#controller-desktop").live("click", OpenLP.desktopDisplay);
 $("#controller-show").live("click", OpenLP.showDisplay);
+$("#display-thumbnails").live("click", OpenLP.displayThumbnails);
+$("#dont-display-thumbnails").live("click", OpenLP.displayThumbnails);
 // Alerts
 $("#alert-submit").live("click", OpenLP.showAlert);
 // Search
@@ -363,5 +398,6 @@
 $("#search").live("pageinit", function (event) {
   OpenLP.getSearchablePlugins();
 });
-setInterval("OpenLP.pollServer();", 500);
-OpenLP.pollServer();
+//setInterval("OpenLP.pollServer();", 30000);
+//OpenLP.pollServer();
+var ws = new wsEventEngine("ws://" + window.location.hostname + ":8888/Test", OpenLP.pollServer, 500);

=== modified file 'openlp/plugins/remotes/lib/__init__.py'
--- openlp/plugins/remotes/lib/__init__.py	2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/lib/__init__.py	2014-03-18 21:03:35 +0000
@@ -30,5 +30,6 @@
 from .remotetab import RemoteTab
 from .httprouter import HttpRouter
 from .httpserver import OpenLPServer
+from .websocket import WebSocketManager
 
-__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']
+__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter','WebSocketManager']

=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2014-03-16 21:25:23 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2014-03-18 21:03:35 +0000
@@ -124,8 +124,13 @@
 from mako.template import Template
 from PyQt4 import QtCore
 
+<<<<<<< TREE
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate
 from openlp.core.lib import PluginStatus, StringContent, image_to_byte
+=======
+from openlp.core.common import Registry, AppLocation, Settings, translate
+from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities
+>>>>>>> MERGE-SOURCE
 
 log = logging.getLogger(__name__)
 FILE_TYPES = {
@@ -159,6 +164,7 @@
             ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
             ('^/(main)$', {'function': self.serve_file, 'secure': False}),
             (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
+            (r'^/(.*)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
             (r'^/api/poll$', {'function': self.poll, 'secure': False}),
             (r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
             (r'^/main/image$', {'function': self.main_image, 'secure': False}),
@@ -334,7 +340,8 @@
             'no_results': translate('RemotePlugin.Mobile', 'No Results'),
             'options': translate('RemotePlugin.Mobile', 'Options'),
             'service': translate('RemotePlugin.Mobile', 'Service'),
-            'slides': translate('RemotePlugin.Mobile', 'Slides')
+            'slides': translate('RemotePlugin.Mobile', 'Slides'),
+            'settings': translate('RemotePlugin.Mobile', 'Settings'),
         }
 
     def serve_file(self, file_name=None):
@@ -388,6 +395,35 @@
         content_type = FILE_TYPES.get(ext, 'text/plain')
         return ext, content_type
 
+    def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
+        """
+        Serve an image file. If not found return 404.
+        """
+        log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
+        supported_controllers = ['presentations']
+        if not dimensions:
+            dimensions = ''
+        content = ''
+        content_type = None
+        if controller_name and file_name:
+            if controller_name in supported_controllers:
+                full_path = urllib.parse.unquote(file_name)
+                if not '..' in full_path:  # no hacking please
+                    full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
+                                                              'thumbnails/' + full_path))
+                    if os.path.exists(full_path):
+                        path, just_file_name = os.path.split(full_path)
+                        self.image_manager.add_image(full_path, just_file_name, None, dimensions)
+                        ext, content_type = self.get_content_type(full_path)
+                        image = self.image_manager.get_image(full_path, just_file_name, dimensions)
+                        content = image_to_byte(image, False)
+        if len(content) == 0:
+            return self.do_not_found()
+        self.send_response(200)
+        self.send_header('Content-type', content_type)
+        self.end_headers()
+        return content
+
     def poll(self):
         """
         Poll OpenLP to determine the current slide number and item name.
@@ -475,9 +511,21 @@
                     item['html'] = str(frame['html'])
                 else:
                     item['tag'] = str(index + 1)
+                    if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
+                        item['title'] = str(frame['display_title'])
+                    if current_item.is_capable(ItemCapabilities.HasNotes):
+                        item['notes'] = str(frame['notes'])
+                    if current_item.is_capable(ItemCapabilities.HasThumbnails):
+                        # If the file is under our app directory tree send the 
+                        # portion after the match
+                        data_path = AppLocation.get_data_path()
+                        if frame['image'][0:len(data_path)] == data_path:
+                            item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
                     item['text'] = str(frame['title'])
                     item['html'] = str(frame['title'])
                 item['selected'] = (self.live_controller.selected_row == index)
+                if current_item.notes:
+                    item['notes'] = item.get('notes', '') + '\n' + current_item.notes
                 data.append(item)
         json_data = {'results': {'slides': data}}
         if current_item:
@@ -601,3 +649,55 @@
             item_id = plugin.media_item.create_item_from_id(id)
             plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
         self.do_http_success()
+<<<<<<< TREE
+=======
+
+    def _get_service_manager(self):
+        """
+        Adds the service manager to the class dynamically
+        """
+        if not hasattr(self, '_service_manager'):
+            self._service_manager = Registry().get('service_manager')
+        return self._service_manager
+
+    service_manager = property(_get_service_manager)
+
+    def _get_live_controller(self):
+        """
+        Adds the live controller to the class dynamically
+        """
+        if not hasattr(self, '_live_controller'):
+            self._live_controller = Registry().get('live_controller')
+        return self._live_controller
+
+    live_controller = property(_get_live_controller)
+
+    def _get_plugin_manager(self):
+        """
+        Adds the plugin manager to the class dynamically
+        """
+        if not hasattr(self, '_plugin_manager'):
+            self._plugin_manager = Registry().get('plugin_manager')
+        return self._plugin_manager
+
+    plugin_manager = property(_get_plugin_manager)
+
+    def _get_alerts_manager(self):
+        """
+        Adds the alerts manager to the class dynamically
+        """
+        if not hasattr(self, '_alerts_manager'):
+            self._alerts_manager = Registry().get('alerts_manager')
+        return self._alerts_manager
+
+    alerts_manager = property(_get_alerts_manager)
+
+    def _get_image_manager(self):
+        """
+        Adds the image manager to the class dynamically
+        """
+        if not hasattr(self, '_image_manager'):
+            self._image_manager = Registry().get('image_manager')
+        return self._image_manager
+
+    image_manager = property(_get_image_manager)>>>>>>> MERGE-SOURCE

=== added file 'openlp/plugins/remotes/lib/websocket.py'
--- openlp/plugins/remotes/lib/websocket.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/lib/websocket.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,330 @@
+# -*- 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                          #
+###############################################################################
+
+"""
+Simple implementation of RFC 6455 for websocket protocol in a very simple and focused manner, just for the purposes
+of this application
+"""
+
+import logging
+import re
+import socketserver
+import threading
+import time
+import socket
+import base64
+import uuid
+
+from base64 import b64encode
+from hashlib import sha1
+
+HOST, PORT = '', 8888
+WEB_SOCKETS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.encode('utf-8')
+WEB_SOCKETS_RESPONSE_TEMPLATE = (
+    'HTTP/1.1 101 Switching Protocols',
+    'Connection: Upgrade',
+    'Sec-WebSocket-Accept: {key}',
+    'Upgrade: websocket',
+    '',
+    '',
+)
+WEB_SOCKETS_HANDSHAKE_ERROR = 'Error: Handshake'.encode('utf-8')
+WEB_SOCKET_CLIENT_HEADERS = (
+        "GET / HTTP/1.1",
+        "Upgrade: websocket",
+        "Connection: Upgrade",
+        "Host: {host}:{port}",
+        "Origin: null",
+        "Sec-WebSocket-Key: {key}",
+        "Sec-WebSocket-Version: 13",
+        "",
+        "",
+)
+
+
+class ThreadedWebSocketHandler(socketserver.BaseRequestHandler):
+    """
+    ThreadedWebSocketHandler implements the upgrade handshake and continues to serve the socket
+    """
+    def handle(self):
+        """
+        Called once per connection, the connection will not be added to the list of clients
+        until the handshake has succeeded
+        """
+        has_upgraded = False
+        data_buffer = ''
+        while True:
+            data_string = ''
+            data_received = ''
+            try:
+                data_received = self.request.recv(1024)
+            except Exception as e:
+                #print(self.client_address, e.errno, e.strerror)
+                if e.errno == 10053 or e.errno == 10054:
+                    self.server.remove_client(self)
+                    break
+            if len(data_received) > 0:
+                #print(" data_received: ", data_received)
+                if has_upgraded:
+                    data_string = ThreadedWebSocketHandler.decode_websocket_message(data_received)
+                else:
+                    data_string = data_received.decode('utf-8', 'ignore')
+            if len(data_string) > 0:
+                #print(" from: ", self.client_address, " data: ", data_string, " upgraded: ", has_upgraded)
+                if not has_upgraded:
+                    data_buffer += data_string
+                    #print("x", data_buffer, "x")
+                    if data_buffer[0] != 'G':
+                        #print("return error")
+                        self.request.send(WEB_SOCKETS_HANDSHAKE_ERROR)
+                        break
+                    match = re.search('Sec-WebSocket-Key:\s+(.*?)[\n\r]+', data_buffer)
+                    #print("match: ", match)
+                    if match:
+                        received_key = (match.groups()[0].strip()).encode('utf-8')
+                        generated_key = sha1(received_key + WEB_SOCKETS_GUID).digest()
+                        response_key = b64encode(generated_key).decode('utf-8')
+                        response = ('\r\n'.join(WEB_SOCKETS_RESPONSE_TEMPLATE).format(key=response_key)).encode('utf-8')
+                        #print(response)
+                        self.request.send(response)
+                        has_upgraded = True
+                        data_buffer = ''
+                        self.server.add_client(self)
+
+    @staticmethod
+    def decode_websocket_message(byte_array):
+        """
+        decode_websocket_message decodes the messages sent from a websocket client according to RFC 6455
+        :param byte_array: an array of bytes as received from the socket
+        :return: returns a string
+        """
+        data_length = byte_array[1] & 127
+        index_first_mask = 2
+        if data_length == 126:
+            index_first_mask = 4
+        elif data_length == 127:
+            index_first_mask = 10
+        masks = [m for m in byte_array[index_first_mask: index_first_mask + 4]]
+        index_first_data_byte = index_first_mask + 4
+        decoded_chars = []
+        index = index_first_data_byte
+        secondary_index = 0
+        while index < len(byte_array):
+            char = chr(byte_array[index] ^ masks[secondary_index % 4])
+            #print(char)
+            decoded_chars.append(char)
+            index += 1
+            secondary_index += 1
+        return ''.join(decoded_chars)
+
+    @staticmethod
+    def encode_websocket_message(message):
+        """
+        encode_websocket_message encodes a message prior to sending to a websocket client according to RFC 6455
+        :param message: string to be encoded
+        :return: the message encoded into a byte array
+        """
+        frame_head = bytearray(2)
+        frame_head[0] = ThreadedWebSocketHandler.set_bit(frame_head[0], 7)
+        frame_head[0] = ThreadedWebSocketHandler.set_bit(frame_head[0], 0)
+        assert(len(message) < 126)
+        frame_head[1] = len(message)
+        frame = frame_head + message.encode('utf-8')
+        return frame
+
+    @staticmethod
+    def decode_client_websocket_message(received_broadcast):
+        """
+        Helper to decode messages from the client side for testing purposes
+        :param received_broadcast: the byte array received from the server
+        :return: a decoded string
+        """
+        decoded_broadcast = ''
+        if received_broadcast[0] == 129:
+            for c in received_broadcast[2:]:
+                decoded_broadcast += chr(c)
+        return decoded_broadcast
+
+
+    @staticmethod
+    def set_bit(int_type, offset):
+        """
+        set_bit -- helper for bit operation
+        :param int_type: the original value
+        :param offset: which bit to set
+        :return: the modified value
+        """
+        return int_type | (1 << offset)
+
+    def finish(self):
+        """
+        finish is called when the connection is done
+        """
+        #print("finish:", self.client_address)
+#        with self.server.lock:
+#           self.server.remove_client(self)
+        pass
+
+
+class ThreadedWebSocketServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
+    """
+    ThreadedWebSocketServer overrides the standard implementation to add a client list
+    """
+    daemon_threads = True
+    allow_reuse_address = True
+
+    def __init__(self, host_port, handler):
+        super().__init__(host_port, handler)
+        self.clients = {}
+        self.lock = threading.Lock()
+
+    def add_client(self, client):
+        """
+        add_client inserts a reference to the client handler object into the server's list of clients
+        :param client: reference to the client handler
+        """
+        with self.lock:
+            self.clients[client.client_address] = client
+        #print("added: ", client.client_address)
+        #print(self.clients.keys())
+
+    def remove_client(self, client):
+        """
+        remove_client is called by the client handler when the client disconnects
+        :param client: reference to the client handler
+        """
+        with self.lock:
+            if client.client_address in self.clients.keys():
+                self.clients.pop(client.client_address)
+                #print("removed: ", client.client_address)
+
+    def send_to_all_clients(self, msg):
+        """
+        send_to_all_clients sends the same message to all the connected clients
+        :param msg: string to be sent to all connected clients
+        """
+        #print('send_to_all_clients')
+        #print(self.clients.keys())
+        with self.lock:
+            for client in self.clients.values():
+                #print("send_to:", client.client_address)
+                client.request.send(ThreadedWebSocketHandler.encode_websocket_message(msg))
+
+
+class WebSocketManager():
+    """
+    WebSocketManager implements the external interface to the WebSocket engine
+    """
+
+    def __init__(self):
+        self.server = None
+        self.server_thread = None
+
+    def start(self):
+        """
+        start
+        starts the WebSocket engine
+        """
+        self.server = ThreadedWebSocketServer((HOST, PORT), ThreadedWebSocketHandler)
+        self.server_thread = socketserver.threading.Thread(target=self.server.serve_forever)
+        self.server_thread.start()
+        #print("started the WebSocket server")
+
+    def stop(self):
+        """
+        stop
+        stops the WebSocket engine
+        """
+        self.server.shutdown()
+        self.server.server_close()
+        #print("stopped the WebSocket server")
+
+    def send(self, msg):
+        """
+        sends a message to all clients via the websocket server
+        :param msg: string to send
+        """
+        #print(self.server.clients.keys())
+        self.server.send_to_all_clients(msg)
+
+if __name__ == "__main__":
+    #   The following code is helpful to test the server using a browser
+    #   Just paste the following code into an html file
+    #<html>
+    #<head>
+    #<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js";></script>
+    #</head>
+    #<body>
+    #   <div id="results">start:</div>
+    #</body>
+    #<script type="text/javascript">
+    #   appendMessage("testing...");
+    #   var ws = new WebSocket('ws://localhost:8888/Pres')
+    #   ws.onmessage = function(e){
+    #	    appendMessage(e.data)
+	#   }
+    #   ws.onopen = function(){
+	#       appendMessage("open");
+	#       this.send("test send");
+    #   }
+    #   ws.onclose = function(){
+	#       appendMessage("closed");
+    #   }
+    #   function appendMessage(str)
+    #   {
+    #       $("#results").html($("#results").html() + "<br />" + str);
+    #   }
+    #</script>
+    #</html>
+
+    manager = WebSocketManager()
+    manager.start()
+    # Create a socket (SOCK_STREAM means a TCP socket)
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    # Fake a handshake
+    uid = uuid.uuid4()
+    key = base64.encodebytes(uid.bytes).strip()
+    data = ('\r\n'.join(WEB_SOCKET_CLIENT_HEADERS).format(host='localhost', port='8888', key=key)).encode('utf-8')
+    received = None
+    try:
+        # Connect to server and send data
+        sock.connect(('localhost', PORT))
+        sock.send(data)
+        received = sock.recv(1024)
+        time.sleep(5)
+        manager.send("broadcast")
+        print("received: ", ThreadedWebSocketHandler.decode_client_websocket_message(sock.recv(1024)))
+        time.sleep(2)
+        manager.send("\r\njust before kill")
+        print("received: ", ThreadedWebSocketHandler.decode_client_websocket_message(sock.recv(1024)))
+        time.sleep(2)
+    finally:
+        sock.close()
+        manager.stop()
+

=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py	2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/remoteplugin.py	2014-03-18 21:03:35 +0000
@@ -31,7 +31,8 @@
 import time
 
 from openlp.core.lib import Plugin, StringContent, translate, build_icon
-from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
+from openlp.core.common import Registry
+from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer, WebSocketManager
 
 log = logging.getLogger(__name__)
 
@@ -59,6 +60,7 @@
         self.icon = build_icon(self.icon_path)
         self.weight = -1
         self.server = None
+        self.websocketserver = None
 
     def initialise(self):
         """
@@ -67,6 +69,9 @@
         log.debug('initialise')
         super(RemotesPlugin, self).initialise()
         self.server = OpenLPServer()
+        self.websocketserver = WebSocketManager()
+        self.websocketserver.start()
+        Registry().register_function('websock_send', self.websocketserver.send)
 
     def finalise(self):
         """
@@ -77,6 +82,9 @@
         if self.server:
             self.server.stop_server()
             self.server = None
+        if self.websocketserver:
+            self.websocketserver.stop()
+            self.websocketserver = None
 
     def about(self):
         """

=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py	2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_core_common/test_applocation.py	2014-03-18 21:03:35 +0000
@@ -162,9 +162,9 @@
                 patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \
                 patch('openlp.core.common.applocation.os.path.split') as mocked_split, \
                 patch('openlp.core.common.applocation.sys') as mocked_sys:
-            mocked_abspath.return_value = 'plugins/dir'
+            mocked_abspath.return_value = os.path.join('plugins','dir')
             mocked_split.return_value = ['openlp']
-            mocked_get_frozen_path.return_value = 'plugins/dir'
+            mocked_get_frozen_path.return_value = os.path.join('plugins','dir')
             mocked_sys.frozen = 1
             mocked_sys.argv = ['openlp']
 
@@ -172,7 +172,7 @@
             directory = AppLocation.get_directory(AppLocation.PluginsDir)
 
             # THEN: The correct directory should be returned
-            self.assertEqual('plugins/dir', directory, 'Directory should be "plugins/dir"')
+            self.assertEqual(os.path.join('plugins','dir'), directory, 'Directory should be "plugins/dir"')
 
     def get_frozen_path_in_unfrozen_app_test(self):
         """

=== modified file 'tests/functional/openlp_core_lib/test_image_manager.py'
--- tests/functional/openlp_core_lib/test_image_manager.py	2014-03-14 22:08:44 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py	2014-03-18 21:03:35 +0000
@@ -63,16 +63,17 @@
         Test the Image Manager setup basic functionality
         """
         # GIVEN: the an image add to the image manager
-        self.image_manager.add_image(TEST_PATH, 'church.jpg', None)
+        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+        self.image_manager.add_image(full_path, 'church.jpg', None)
 
         # WHEN the image is retrieved
-        image = self.image_manager.get_image(TEST_PATH, 'church.jpg')
+        image = self.image_manager.get_image(full_path, 'church.jpg')
 
         # THEN returned record is a type of image
         self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
 
         # WHEN: The image bytes are requested.
-        byte_array = self.image_manager.get_image_bytes(TEST_PATH, 'church.jpg')
+        byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg')
 
         # THEN: Type should be a str.
         self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str')
@@ -82,3 +83,37 @@
         with self.assertRaises(KeyError) as context:
             self.image_manager.get_image(TEST_PATH, 'church1.jpg')
         self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
+
+    def different_dimension_image_test(self):
+        """
+        Test the Image Manager with dimensions
+        """
+        # GIVEN: add an image with specific dimensions
+        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+        self.image_manager.add_image(full_path, 'church.jpg', None, '80x80')
+
+        # WHEN: the image is retrieved
+        image = self.image_manager.get_image(full_path, 'church.jpg', '80x80')
+
+        # THEN: The return should be of type image
+        self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
+        #print(len(self.image_manager._cache))
+
+        # WHEN: adding the same image with different dimensions
+        self.image_manager.add_image(full_path, 'church.jpg', None, '100x100')
+
+        # THEN: the cache should contain two pictures
+        self.assertEqual(len(self.image_manager._cache), 2,
+                         'Image manager should consider two dimensions of the same picture as different')
+
+        # WHEN: adding the same image with first dimensions
+        self.image_manager.add_image(full_path, 'church.jpg', None, '80x80')
+
+        # THEN: the cache should still contain only two pictures
+        self.assertEqual(len(self.image_manager._cache), 2, 'Same dimensions should not be added again')
+
+        # WHEN: calling with correct image, but wrong dimensions
+        with self.assertRaises(KeyError) as context:
+            self.image_manager.get_image(full_path, 'church.jpg', '120x120')
+        self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension')
+

=== modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py	2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py	2014-03-18 21:03:35 +0000
@@ -32,13 +32,11 @@
 import os
 from unittest import TestCase
 
-
 from tests.functional import MagicMock, patch
 from tests.utils import assert_length, convert_file_service_item
 
 from openlp.core.common import Registry
-from openlp.core.lib import ItemCapabilities, ServiceItem
-
+from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
 
 VERSE = 'The Lord said to {r}Noah{/r}: \n'\
         'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
@@ -126,7 +124,7 @@
 
         # THEN: We should get back a valid service item
         self.assertTrue(service_item.is_valid, 'The new service item should be valid')
-        self.assertEqual(test_file, service_item.get_rendered_frame(0),
+        self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)),
                          'The first frame should match the path to the image')
         self.assertEqual(frame_array, service_item.get_frames()[0],
                          'The return should match frame array1')
@@ -153,8 +151,8 @@
         # GIVEN: A new service item and a mocked add icon function
         image_name1 = 'image_1.jpg'
         image_name2 = 'image_2.jpg'
-        test_file1 = os.path.join('/home/openlp', image_name1)
-        test_file2 = os.path.join('/home/openlp', image_name2)
+        test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
+        test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
         frame_array1 = {'path': test_file1, 'title': image_name1}
         frame_array2 = {'path': test_file2, 'title': image_name2}
 
@@ -206,3 +204,41 @@
                         'This service item should be able to be run in a can be made to Loop')
         self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend),
                         'This service item should be able to have new items added to it')
+
+    def add_from_command_for_a_presentation_test(self):
+        """
+        Test the Service Item - adding a presentation
+        """
+        # GIVEN: A service item, a mocked icon and presentation data
+        service_item = ServiceItem(None)
+        presentation_name = 'test.pptx'
+        image = MagicMock()
+        display_title = 'DisplayTitle'
+        notes = 'Note1\nNote2\n'
+        frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH,
+                 'display_title': display_title, 'notes': notes}
+
+        # WHEN: adding presentation to service_item
+        service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes)
+
+        # THEN: verify that it is setup as a Command and that the frame data matches
+        self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
+        self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
+
+    def add_from_comamnd_without_display_title_and_notes_test(self):
+        """
+        Test the Service Item - add from command, but not presentation
+        """
+        # GIVEN: A new service item, a mocked icon and image data
+        service_item = ServiceItem(None)
+        image_name = 'test.img'
+        image = MagicMock()
+        frame = {'title': image_name, 'image': image, 'path': TEST_PATH,
+                 'display_title': None, 'notes': None}
+
+        # WHEN: adding image to service_item
+        service_item.add_from_command(TEST_PATH, image_name, image)
+
+        # THEN: verify that it is setup as a Command and that the frame data matches
+        self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
+        self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')

=== added file 'tests/functional/openlp_plugins/presentations/test_impresscontroller.py'
--- tests/functional/openlp_plugins/presentations/test_impresscontroller.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,175 @@
+# -*- 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                          #
+###############################################################################
+"""
+Functional tests to test the Impress class and related methods.
+"""
+from unittest import TestCase
+import os
+from mock import MagicMock
+from openlp.plugins.presentations.lib.impresscontroller import \
+    ImpressController, ImpressDocument, TextType
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+class TestLibModule(TestCase):
+
+    def setUp(self):
+        mocked_plugin = MagicMock()
+        mocked_plugin.settings_section = 'presentations'
+        self.file_name = os.path.join(TEST_PATH, 'test.pptx')
+        self.ppc = ImpressController(mocked_plugin)
+        self.doc = ImpressDocument(self.ppc, self.file_name)
+
+    def create_titles_and_notes_test(self):
+        """
+        Test ImpressDocument.create_titles_and_notes
+        """
+        # GIVEN: mocked PresentationController.save_titles_and_notes with
+        # 0 pages and the LibreOffice Document
+        self.doc.save_titles_and_notes = MagicMock()
+        self.doc.document = MagicMock()
+        self.doc.document.getDrawPages.return_value = MagicMock()
+        self.doc.document.getDrawPages().getCount.return_value = 0
+
+        # WHEN reading the titles and notes
+        self.doc.create_titles_and_notes()
+
+        # THEN save_titles_and_notes should have been called with empty arrays
+        self.doc.save_titles_and_notes.assert_called_once_with([], [])
+
+        # GIVEN: reset mock and set it to 2 pages
+        self.doc.save_titles_and_notes.reset_mock()
+        self.doc.document.getDrawPages().getCount.return_value = 2
+
+        # WHEN: a new call to create_titles_and_notes
+        self.doc.create_titles_and_notes()
+
+        # THEN: save_titles_and_notes should have been called once with
+        # two arrays of two elements
+        self.doc.save_titles_and_notes.assert_called_once_with(['\n', '\n'], [' ', ' '])
+
+    def get_text_from_page_out_of_bound_test(self):
+        """
+        Test ImpressDocument.__get_text_from_page with out-of-bounds index
+        """
+        # GIVEN: mocked LibreOffice Document with one slide,
+        # two notes and three texts
+        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+        # WHEN: __get_text_from_page is called with an index of 0x00
+        result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Notes)
+
+        # THEN: the result should be an empty string
+        self.assertEqual(result, '', 'Result should be an empty string')
+
+        # WHEN: regardless of the type of text, index 0x00 is out of bounds
+        result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Title)
+
+        # THEN: result should be an empty string
+        self.assertEqual(result, '', 'Result should be an empty string')
+
+        # WHEN: when called with 2, it should also be out of bounds
+        result = self.doc._ImpressDocument__get_text_from_page(2, TextType.SlideText)
+
+        # THEN: result should be an empty string ... and, getByIndex should
+        # have never been called
+        self.assertEqual(result, '', 'Result should be an empty string')
+        self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
+                         'There should be no call to getByIndex')
+
+    def get_text_from_page_wrong_type_test(self):
+        """
+        Test ImpressDocument.__get_text_from_page with wrong TextType
+        """
+        # GIVEN: mocked LibreOffice Document with one slide, two notes and
+        # three texts
+        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+        # WHEN: called with TextType 3
+        result = self.doc._ImpressDocument__get_text_from_page(1, 3)
+
+        # THEN: result should be an empty string
+        self.assertEqual(result, '', 'Result should be and empty string')
+        self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
+                         'There should be no call to getByIndex')
+
+    def get_text_from_page_valid_params_test(self):
+        """
+        Test ImpressDocument.__get_text_from_page with valid parameters
+        """
+        # GIVEN: mocked LibreOffice Document with one slide,
+        # two notes and three texts
+        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+        # WHEN: __get_text_from_page is called to get the Notes
+        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Notes)
+
+        # THEN: result should be 'Note\nNote\n'
+        self.assertEqual(result, 'Note\nNote\n', 'Result should be \'Note\\n\' times the count of notes in the page')
+
+        # WHEN: get the Title
+        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Title)
+
+        # THEN: result should be 'Title\n'
+        self.assertEqual(result, 'Title\n', 'Result should be exactly \'Title\\n\'')
+
+        # WHEN: get all text
+        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.SlideText)
+
+        # THEN: result should be 'Title\nString\nString\n'
+        self.assertEqual(result, 'Title\nString\nString\n', 'Result should be exactly \'Title\\nString\\nString\\n\'')
+
+    def _mock_a_LibreOffice_document(self, page_count, note_count, text_count):
+        pages = MagicMock()
+        page = MagicMock()
+        pages.getByIndex.return_value = page
+        notes_page = MagicMock()
+        notes_page.getCount.return_value = note_count
+        shape = MagicMock()
+        shape.supportsService.return_value = True
+        shape.getString.return_value = 'Note'
+        notes_page.getByIndex.return_value = shape
+        page.getNotesPage.return_value = notes_page
+        page.getCount.return_value = text_count
+        page.getByIndex.side_effect = self._get_page_shape_side_effect
+        pages.getCount.return_value = page_count
+        document = MagicMock()
+        document.getDrawPages.return_value = pages
+        document.getByIndex.return_value = page
+        return document
+
+    def _get_page_shape_side_effect(*args):
+        page_shape = MagicMock()
+        page_shape.supportsService.return_value = True
+        if args[1] == 0:
+            page_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
+            page_shape.getString.return_value = 'Title'
+        else:
+            page_shape.getString.return_value = 'String'
+        return page_shape

=== added file 'tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,144 @@
+# -*- 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                          #
+###############################################################################
+"""
+Functional tests to test the PowerPointController class and related methods.
+"""
+from unittest import TestCase
+import os
+from mock import MagicMock
+from openlp.plugins.presentations.lib.powerpointcontroller import \
+    PowerpointController, PowerpointDocument, _get_text_from_shapes
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+
+class TestLibModule(TestCase):
+
+    def setUp(self):
+        mocked_plugin = MagicMock()
+        mocked_plugin.settings_section = 'presentations'
+        self.ppc = PowerpointController(mocked_plugin)
+        self.file_name = os.path.join(TEST_PATH, "test.pptx")
+        self.doc = PowerpointDocument(self.ppc, self.file_name)
+
+    # add _test    to the name to enable
+    def verify_installation(self):
+        """
+        Test the installation of Powerpoint
+        """
+        # GIVEN: A boolean value set to true
+        # WHEN: We "convert" it to a bool
+        is_installed = self.ppc.check_available()
+
+        # THEN: We should get back a True bool
+        self.assertEqual(is_installed, True, 'The result should be True')
+
+    # add _test to the following if necessary
+    def verify_loading_document(self):
+        """
+        Test loading a document in PowerPoint
+        """             
+        # GIVEN: the filename
+        print(self.file_name)
+
+        # WHEN: loading the filename
+        self.doc = PowerpointDocument(self.ppc, self.file_name)
+        self.doc.load_presentation()
+        result = self.doc.is_loaded()
+
+        # THEN: result should be true
+        self.assertEqual(result, True, 'The result should be True')
+
+    def create_titles_and_notes_test(self):
+        """
+        Test creating the titles from PowerPoint
+        """
+        # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+        self.doc = PowerpointDocument(self.ppc, self.file_name)
+        self.doc.save_titles_and_notes = MagicMock()
+        self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+        slide = MagicMock()
+        slide.Shapes.Title.TextFrame.TextRange.Text = 'SlideText'
+        pres = MagicMock()
+        pres.Slides = [slide, slide]
+        self.doc.presentation = pres
+
+        # WHEN reading the titles and notes
+        self.doc.create_titles_and_notes()
+
+        # THEN the save should have been called exactly once with 2 titles and 2 notes
+        self.doc.save_titles_and_notes.assert_called_once_with(['SlideText\n', 'SlideText\n'], [' ', ' '])
+
+    def create_titles_and_notes_with_no_slides_test(self):
+        """
+        Test creating the titles from PowerPoint when it returns no slides
+        """
+        # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+        self.doc = PowerpointDocument(self.ppc, self.file_name)
+        self.doc.save_titles_and_notes = MagicMock()
+        self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+        pres = MagicMock()
+        pres.Slides = []
+        self.doc.presentation = pres
+
+        # WHEN reading the titles and notes
+        self.doc.create_titles_and_notes()
+
+        # THEN the save should have been called exactly once with empty titles and notes
+        self.doc.save_titles_and_notes.assert_called_once_with([], [])
+
+    def get_text_from_shapes_test(self):
+        """
+        Test getting text from powerpoint shapes 
+        """
+        # GIVEN: mocked shapes
+        shape = MagicMock()
+        shape.PlaceholderFormat.Type = 2
+        shape.HasTextFrame = shape.TextFrame.HasText = True
+        shape.TextFrame.TextRange.Text = 'slideText'
+        shapes = [shape, shape]
+
+        # WHEN: getting the text
+        result = _get_text_from_shapes(shapes)
+
+        # THEN: it should return the text
+        self.assertEqual(result, 'slideText\nslideText\n', 'result should match \'slideText\nslideText\n\'')
+
+    def get_text_from_shapes_with_no_shapes_test(self):
+        """
+        Test getting text from powerpoint shapes with no shapes
+        """
+        # GIVEN: empty shapes array
+        shapes = []
+
+        # WHEN: getting the text
+        result = _get_text_from_shapes(shapes)
+
+        # THEN: it should not fail but return empty string
+        self.assertEqual(result, '', 'result should be empty')

=== added file 'tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,151 @@
+# -*- 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                          #
+###############################################################################
+"""
+Functional tests to test the PptviewController class and related methods.
+"""
+from unittest import TestCase, SkipTest
+import os
+
+if os.name != 'nt':
+    raise SkipTest('Not Windows, skipping test')
+
+from mock import MagicMock, patch, mock_open
+from openlp.plugins.presentations.lib.pptviewcontroller import PptviewController, PptviewDocument
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+class TestLibModule(TestCase):
+
+    def setUp(self):
+        mocked_plugin = MagicMock()
+        mocked_plugin.settings_section = 'presentations'
+        self.ppc = PptviewController(mocked_plugin)
+        self.file_name = os.path.join(TEST_PATH, "test.pptx")
+        self.doc = PptviewDocument(self.ppc, self.file_name)
+
+    #add _test to the function name to enable test
+    def verify_installation(self):
+        """
+        Test the installation of PowerpointViewer
+        """
+        # GIVEN: A boolean value set to true
+        # WHEN: We "convert" it to a bool
+        is_installed = self.ppc.check_available()
+        # THEN: We should get back a True bool
+        self.assertEqual(is_installed, True, 'The result should be True')
+
+    # add _test to the following if necessary to enable test
+    # I don't have powerpointviewer to verify
+    def verify_loading_document(self):
+        """
+        Test loading a document in PowerpointViewer
+        """             
+        # GIVEN: the filename
+        print(self.file_name)
+        # WHEN: loading the filename
+        self.doc = PptviewDocument(self.ppc, self.file_name)
+        self.doc.load_presentation()
+        result = self.doc.is_loaded()
+        # THEN: result should be true
+        self.assertEqual(result, True, 'The result should be True')
+
+    # disabled
+    def verify_titles(self):
+        """
+        Test reading the titles from PowerpointViewer
+        """
+        # GIVEN:
+        self.doc = PptviewDocument(self.ppc, self.file_name)
+        self.doc.create_titles_and_notes()
+        # WHEN reading the titles and notes
+        titles, notes = self.doc.get_titles_and_notes()
+        print("titles: ".join(titles))
+        print("notes: ".join(notes))
+        # THEN there should be exactly 5 titles and 5 notes
+        self.assertEqual(len(titles), 5, 'There should be five titles')
+        self.assertEqual(len(notes), 5, 'There should be five notes')
+
+    def create_titles_and_notes_test(self):
+        """
+        Test PowerpointController.create_titles_and_notes
+        """
+        # GIVEN: mocked PresentationController.save_titles_and_notes 
+        self.doc.save_titles_and_notes = MagicMock()
+
+        # WHEN reading the titles and notes
+        self.doc.create_titles_and_notes()
+
+        # THEN save_titles_and_notes should have been called once with empty arrays
+        self.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 create_titles_and_notes_nonexistent_file_test(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('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
+            patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
+                mocked_dir_exists:
+            mocked_exists.return_value = False
+            mocked_dir_exists.return_value = False
+            self.doc = PptviewDocument(self.ppc, 'Idontexist.pptx')
+            self.doc.save_titles_and_notes = MagicMock()
+
+            # WHEN: reading the titles and notes
+            self.doc.create_titles_and_notes()
+
+            # THEN:
+            self.doc.save_titles_and_notes.assert_called_once_with(None, None)
+            mocked_exists.assert_any_call('Idontexist.pptx')
+            self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file')
+        
+    def create_titles_and_notes_invalid_file_test(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', mock_open(read_data='this is a test')) as mocked_open, \
+             patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
+             patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
+            mocked_is_zf.return_value = False
+            mocked_exists.return_value = True
+            mocked_open.filesize = 10
+            self.doc = PptviewDocument(self.ppc, os.path.join(TEST_PATH, "test.ppt"))
+            self.doc.save_titles_and_notes = MagicMock()
+
+            # WHEN: reading the titles and notes
+            self.doc.create_titles_and_notes()
+
+            # THEN:
+            self.doc.save_titles_and_notes.assert_called_once_with(None, None)
+            self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once')
+            

=== modified file 'tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-03-08 21:23:47 +0000
+++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-03-18 21:03:35 +0000
@@ -4,8 +4,8 @@
 ###############################################################################
 # OpenLP - Open Source Lyrics Projection                                      #
 # --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2013 Raoul Snyman                                        #
-# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan      #
+# 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,          #

=== modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2014-03-18 21:03:35 +0000
@@ -1,158 +1,169 @@
-# -*- 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 Presentation Controller.
-"""
-from unittest import TestCase
-
-from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
-from tests.functional import MagicMock, patch
-
-
-class TestPresentationController(TestCase):
-    """
-    Test the PresentationController.
-    """
-    # TODO: Items left to test
-    #   PresentationController
-    #       __init__
-    #       enabled
-    #       is_available
-    #       check_available
-    #       start_process
-    #       kill
-    #       add_document
-    #       remove_doc
-    #       close_presentation
-    #       _get_plugin_manager
-
-    def constructor_test(self):
-        """
-        Test the Constructor
-        """
-        # GIVEN: No presentation controller
-        controller = None
-
-        # WHEN: The presentation controller object is created
-        mock_plugin = MagicMock()
-        mock_plugin.settings_section = ''
-        controller = PresentationController(plugin=mock_plugin)
-
-        # THEN: The name of the presentation controller should be correct
-        self.assertEqual('PresentationController', controller.name,
-                         'The name of the presentation controller should be correct')
-
-
-class TestPresentationDocument(TestCase):
-    """
-    Test the PresentationDocument Class
-    """
-    # TODO: Items left to test
-    #   PresentationDocument
-    #       __init__
-    #       load_presentation
-    #       presentation_deleted
-    #       get_file_name
-    #       get_thumbnail_folder
-    #       get_temp_folder
-    #       check_thumbnails
-    #       close_presentation
-    #       is_active
-    #       is_loaded
-    #       blank_screen
-    #       unblank_screen
-    #       is_blank
-    #       stop_presentation
-    #       start_presentation
-    #       get_slide_number
-    #       get_slide_count
-    #       goto_slide
-    #       next_step
-    #       previous_step
-    #       convert_thumbnail
-    #       get_thumbnail_path
-    #       poll_slidenumber
-    #       get_slide_text
-    #       get_slide_notes
-
-    def setUp(self):
-        """
-        Set up the patches and mocks need for all tests.
-        """
-        self.check_directory_exists_patcher = \
-            patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists')
-        self.get_thumbnail_folder_patcher = \
-            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
-        self._setup_patcher = \
-            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
-
-        self.mock_check_directory_exists = self.check_directory_exists_patcher.start()
-        self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
-        self.mock_setup = self._setup_patcher.start()
-
-        self.mock_controller = MagicMock()
-
-        self.mock_get_thumbnail_folder.return_value = 'returned/path/'
-
-    def tearDown(self):
-        """
-        Stop the patches
-        """
-        self.check_directory_exists_patcher.stop()
-        self.get_thumbnail_folder_patcher.stop()
-        self._setup_patcher.stop()
-
-    def initialise_presentation_document_test(self):
-        """
-        Test the PresentationDocument __init__ method when initialising the PresentationDocument Class
-        """
-        # GIVEN: A reset mock_setup and mocked controller
-        self.mock_setup.reset()
-
-        # WHEN: Creating an instance of PresentationDocument
-        PresentationDocument(self.mock_controller, 'Name')
-
-        # THEN: PresentationDocument.__init__ should have been called with the correct arguments
-        self.mock_setup.assert_called_once_with('Name')
-
-    def presentation_document_setup_test(self):
-        """
-        Test the PresentationDocument _setup method when initialising the PresentationDocument Class
-        """
-        self._setup_patcher.stop()
-
-        # GIVEN: A  mocked controller, patched check_directory_exists_patcher and patched get_thumbnail_folder method
-
-        # WHEN: Creating an instance of PresentationDocument
-        PresentationDocument(self.mock_controller, 'Name')
-
-        # THEN: check_directory_exists should have been called with the correct arguments
-        self.mock_check_directory_exists.assert_called_once_with('returned/path/')
-
-        self._setup_patcher.start()
+# -*- 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                          #
+###############################################################################
+"""
+Functional tests to test the PresentationController and PresentationDocument
+classes and related methods.
+"""
+from unittest import TestCase
+import os
+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
+from tests.functional import MagicMock, patch, mock_open
+
+FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder'
+
+class TestPresentationController(TestCase):
+    """
+    Test the PresentationController.
+    """
+
+    def setUp(self):
+        mocked_plugin = MagicMock()
+        mocked_plugin.settings_section = 'presentations'
+        self.presentation = PresentationController(mocked_plugin)
+        self.document = PresentationDocument(self.presentation, '')
+
+
+    def constructor_test(self):
+        """
+        Test the Constructor
+        """
+        # GIVEN: A mocked plugin
+
+        # WHEN: The PresentationController is created
+
+        # THEN: The name of the presentation controller should be correct
+        self.assertEqual('PresentationController', self.presentation.name,
+                         'The name of the presentation controller should be correct')
+
+    def save_titles_and_notes_test(self):
+        """
+        Test PresentationDocument.save_titles_and_notes method with two valid lists
+        """
+        # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
+        mocked_open = mock_open()
+        with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+            titles = ['uno', 'dos']
+            notes = ['one', 'two']
+
+            # WHEN: calling save_titles_and_notes
+            mocked_get_thumbnail_folder.return_value = 'test'
+            self.document.save_titles_and_notes(titles, notes)
+
+            # THEN: the last call to open should have been for slideNotes2.txt
+            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w')
+            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w')
+            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w')
+            self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
+            mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
+            mocked_open().write.assert_called_any('one')
+            mocked_open().write.assert_called_any('two')
+
+    def save_titles_and_notes_with_None_test(self):
+        """
+        Test PresentationDocument.save_titles_and_notes method with no data
+        """
+        # GIVEN: None and an empty list and a mocked open and get_thumbnail_folder
+        with patch('builtins.open') as mocked_open, patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+            titles = None
+            notes = None
+
+            # WHEN: calling save_titles_and_notes
+            mocked_get_thumbnail_folder.return_value = 'test'
+            self.document.save_titles_and_notes(titles, notes)
+
+            # THEN: No file should have been created
+            self.assertEqual(mocked_open.call_count, 0, 'No file should be created')
+
+
+    def get_titles_and_notes_test(self):
+        """
+        Test PresentationDocument.get_titles_and_notes method
+        """
+        # GIVEN: A mocked open, get_thumbnail_folder and exists
+
+        with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \
+            patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+            patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+            mocked_get_thumbnail_folder.return_value = 'test'
+            mocked_exists.return_value = True
+
+            # WHEN: calling get_titles_and_notes
+            result_titles, result_notes = self.document.get_titles_and_notes()
+
+            # THEN: it should return two items for the titles and two empty strings for the notes
+            self.assertIs(type(result_titles), list, 'result_titles should be of type list')
+            self.assertEqual(len(result_titles), 2, 'There should be two items in the titles')
+            self.assertIs(type(result_notes), list, 'result_notes should be of type list')
+            self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
+            self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened')
+            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'))
+            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'))
+            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'))
+            self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
+
+    def get_titles_and_notes_with_file_not_found_test(self):
+        """
+        Test PresentationDocument.get_titles_and_notes method with file not found
+        """
+        # GIVEN: A mocked open, get_thumbnail_folder and exists
+        with patch('builtins.open') as mocked_open, \
+             patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+             patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+            mocked_get_thumbnail_folder.return_value = 'test'
+            mocked_exists.return_value = False
+
+            #WHEN: calling get_titles_and_notes
+            result_titles, result_notes = self.document.get_titles_and_notes()
+
+            # THEN: it should return two empty lists
+            self.assertIs(type(result_titles), list, 'result_titles should be of type list')
+            self.assertEqual(len(result_titles), 0, 'there be no titles')
+            self.assertIs(type(result_notes), list, 'result_notes should be a list')
+            self.assertEqual(len(result_notes), 0, 'but the list should be empty')
+            self.assertEqual(mocked_open.call_count, 0, 'No calls to open files')
+            self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
+
+    def get_titles_and_notes_with_file_error_test(self):
+        """
+        Test PresentationDocument.get_titles_and_notes method with file errors
+        """
+        # GIVEN: A mocked open, get_thumbnail_folder and exists
+        with patch('builtins.open') as mocked_open, \
+             patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+             patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+            mocked_get_thumbnail_folder.return_value = 'test'
+            mocked_exists.return_value = True
+            mocked_open.side_effect = IOError()
+
+            # WHEN: calling get_titles_and_notes
+            result_titles, result_notes = self.document.get_titles_and_notes()
+
+            # THEN: it should return two empty lists
+            self.assertIs(type(result_titles), list, 'result_titles should be a list')
+

=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py	2014-03-14 22:08:44 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py	2014-03-18 21:03:35 +0000
@@ -30,10 +30,12 @@
 This module contains tests for the lib submodule of the Remotes plugin.
 """
 import os
+import urllib.request
 from unittest import TestCase
 
-from openlp.core.common import Settings
+from openlp.core.common import Settings, Registry
 from openlp.plugins.remotes.lib.httpserver import HttpRouter
+from urllib.parse import urlparse
 from tests.functional import MagicMock, patch, mock_open
 from tests.helpers.testmixin import TestMixin
 
@@ -111,7 +113,7 @@
         Test the get_content_type logic
         """
         # GIVEN: a set of files and their corresponding types
-        headers = [ ['test.html', 'text/html'], ['test.css', 'text/css'],
+        headers = [['test.html', 'text/html'], ['test.css', 'text/css'],
             ['test.js', 'application/javascript'], ['test.jpg', 'image/jpeg'],
             ['test.gif', 'image/gif'], ['test.ico', 'image/x-icon'],
             ['test.png', 'image/png'], ['test.whatever', 'text/plain'],
@@ -157,7 +159,7 @@
         self.router.html_dir = os.path.normpath('test/dir')
         self.router.template_vars = MagicMock()
         with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
-                patch('builtins.open', mock_open(read_data='123')):
+            patch('builtins.open', mock_open(read_data='123')):
             mocked_exists.return_value = True
 
             # WHEN: call serve_file with an existing html file
@@ -167,3 +169,86 @@
             self.router.send_response.assert_called_once_with(200)
             self.router.send_header.assert_called_once_with('Content-type', 'text/html')
             self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+
+    def serve_thumbnail_without_params_test(self):
+        """
+        Test the serve_thumbnail routine without params
+        """
+        self.router.send_response = MagicMock()
+        self.router.send_header = MagicMock()
+        self.router.end_headers = MagicMock()
+        self.router.wfile = MagicMock()
+        self.router.serve_thumbnail()
+        self.router.send_response.assert_called_once_with(404)
+        self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
+        self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+
+    def serve_thumbnail_with_invalid_params_test(self):
+        """
+        Test the serve_thumbnail routine with invalid params
+        """
+        # GIVEN: Mocked send_header, send_response, end_headers and wfile
+        self.router.send_response = MagicMock()
+        self.router.send_header = MagicMock()
+        self.router.end_headers = MagicMock()
+        self.router.wfile = MagicMock()
+
+        # WHEN: pass a bad controller
+        self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png')
+
+        # THEN: a 404 should be returned
+        self.assertEqual(len(self.router.send_header.mock_calls), 1, 'One header')
+        self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response')
+        self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket')
+        self.router.send_response.assert_called_once_with(404)
+
+        # WHEN: pass a bad filename
+        self.router.send_response.reset_mock()
+        self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png')
+
+        # THEN: return a 404
+        self.router.send_response.assert_called_once_with(404)
+
+        # WHEN: a dangerous URL is passed
+        self.router.send_response.reset_mock()
+        self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png')
+
+        # THEN: return a 404
+        self.router.send_response.assert_called_once_with(404)
+
+    def serve_thumbnail_with_valid_params_test(self):
+        """
+        Test the serve_thumbnail routine with valid params
+        """
+        # GIVEN: Mocked send_header, send_response, end_headers and wfile
+        self.router.send_response = MagicMock()
+        self.router.send_header = MagicMock()
+        self.router.end_headers = MagicMock()
+        self.router.wfile = MagicMock()
+        mocked_image_manager = MagicMock()
+        Registry.create()
+        Registry().register('image_manager', mocked_image_manager)
+        file_name = 'another%20test/slide1.png'
+        full_path = os.path.normpath(os.path.join('thumbnails',file_name))
+        width = 120
+        height = 90
+        with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
+            patch('builtins.open', mock_open(read_data='123')), \
+            patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
+            patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
+            mocked_exists.return_value = True
+            mocked_image_to_byte.return_value = '123'
+            mocked_location.get_section_data_path.return_value = ''
+
+            # WHEN: pass good controller and filename
+            result = self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name)
+
+            # THEN: a file should be returned
+            self.assertEqual(self.router.send_header.call_count, 1, 'One header')
+            self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
+            self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+            mocked_exists.assert_called_with(urllib.parse.unquote(full_path))
+            self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once')
+            mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'),
+                                                   'slide1.png', None, '120x90')
+            mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), 'slide1.png', '120x90')

=== added file 'tests/functional/openlp_plugins/remotes/test_websocket.py'
--- tests/functional/openlp_plugins/remotes/test_websocket.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_websocket.py	2014-03-18 21:03:35 +0000
@@ -0,0 +1,123 @@
+# -*- 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 WebSockets
+"""
+import base64
+import uuid
+import socket
+import time
+from unittest import TestCase
+
+from openlp.plugins.remotes.lib.websocket import WebSocketManager, ThreadedWebSocketHandler, \
+    WEB_SOCKET_CLIENT_HEADERS
+from tests.functional import MagicMock, patch, mock_open
+
+
+class TestWebSockets(TestCase):
+    """
+    Test the functions in the :mod:`lib` module.
+    """
+
+    def setUp(self):
+        """
+        Setup the WebSocketsManager
+        """
+        self.manager = WebSocketManager()
+        self.manager.start()
+
+    def tearDown(self):
+        self.manager.stop()
+
+    def attempt_to_talk_with_no_handshake_test(self):
+        """
+        Test the websocket without handshaking first
+        """
+        # GIVEN: A default configuration
+
+        # WHEN: attempts to talk without upgrading to websocket
+        # Create a socket (SOCK_STREAM means a TCP socket)
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        data = bytes('No upgrade', 'utf-8')
+        received = None
+        try:
+            # Connect to server and send data
+            sock.connect(('localhost', 8888))
+            sock.send(data)
+            # Receive data from the server and shut down
+            received = sock.recv(1024)
+        finally:
+            sock.close()
+
+        # THEN:
+        self.assertIs(isinstance(self.manager, WebSocketManager), True,
+                      'It should be an object of WebSocketsManager type')
+        self.assertRegexpMatches(received.decode('utf-8'), '.*Error:.*', 'Mismatch')
+
+    def handshake_and_talk_test(self):
+        """
+        Test the websocket handshake
+        """
+        # GIVEN: A default configuration
+
+        # WHEN: upgrade to websocket and then talk
+        print("starting the websocket server")
+        print("started")
+        # Create a socket (SOCK_STREAM means a TCP socket)
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        # Fake a handshake
+        uid = uuid.uuid4()
+        key = base64.encodebytes(uid.bytes).strip()
+        data = bytes('\r\n'.join(WEB_SOCKET_CLIENT_HEADERS).format(host='localhost', port='8888', key=key), 'utf-8')
+        received = None
+        try:
+            # Connect to server and send data
+            sock.connect(('localhost', 8888))
+            print("connected")
+            sock.send(data)
+            #print("data sent: ", data.decode('utf-8'))
+            # Receive data from the server and shut down
+            time.sleep(1)
+            received = sock.recv(1024)
+            print("data received: ", received.decode('utf-8'))
+            time.sleep(1)
+            self.manager.send('broadcast')
+            time.sleep(1)
+            received_broadcast = sock.recv(1024)
+            print(received_broadcast)
+            decoded_broadcast = ThreadedWebSocketHandler.decode_client_websocket_message(received_broadcast)
+        finally:
+            time.sleep(1)
+            sock.close()
+
+        # THEN:
+        self.assertIs(isinstance(self.manager, WebSocketManager), True,
+                      'It should be an object of WebSocketsManager type')
+        self.assertRegexpMatches(received.decode('utf-8'), '.*Upgrade: websocket.*', 'Handshake failed')
+        self.assertRegexpMatches(decoded_broadcast, '.*broadcast', 'WebSocket did not receive correct string')

=== added file 'tests/resources/test.pptx'
Binary files tests/resources/test.pptx	1970-01-01 00:00:00 +0000 and tests/resources/test.pptx	2014-03-18 21:03:35 +0000 differ

Follow ups