← 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:
  Tim Bentley (trb143)
  Raoul Snyman (raoul-snyman)

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

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, images)
  - Fixed bug that was preventing the remote to be updated with correct slide (presentations)
  - Added thumbnails and notes to stage view when available
  - Displays the service notes (general)
Changes to the main display:
  - Display the title of the slide on each item on the slide controller (presentations)
Added option of remote setting tab to choose if thumbnails are displayed

-- 
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/235415
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2014-05-01 17:49:43 +0000
+++ openlp/core/lib/__init__.py	2014-09-21 19:28:31 +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	2014-03-20 19:10:31 +0000
+++ openlp/core/lib/imagemanager.py	2014-09-21 19:28:31 +0000
@@ -106,7 +106,7 @@
     """
     secondary_priority = 0
 
-    def __init__(self, path, source, background):
+    def __init__(self, path, source, background, width=-1, height=-1):
         """
         Create an image for the :class:`ImageManager`'s cache.
 
@@ -115,7 +115,8 @@
             :class:`~openlp.core.lib.ImageSource` class.
         :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
             ratio does not match with the display ratio.
-
+        :param width: The width of the image, defaults to -1 meaning that the screen width will be used.
+        :param height: The height of the image, defaults to -1 meaning that the screen height will be used.
         """
         self.path = path
         self.image = None
@@ -124,6 +125,8 @@
         self.source = source
         self.background = background
         self.timestamp = 0
+        self.width = width
+        self.height = height
         # 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
@@ -210,13 +213,13 @@
                 image.background = background
                 self._reset_image(image)
 
-    def update_image_border(self, path, source, background):
+    def update_image_border(self, path, source, background, width=-1, height=-1):
         """
         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, width, height)]
         if image.source == source:
             image.background = background
             self._reset_image(image)
@@ -237,12 +240,12 @@
         if not self.image_thread.isRunning():
             self.image_thread.start()
 
-    def get_image(self, path, source):
+    def get_image(self, path, source, width=-1, height=-1):
         """
         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, width, height)]
         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
@@ -257,12 +260,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, width=-1, height=-1):
         """
         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, width, height)]
         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
@@ -272,14 +275,14 @@
                 time.sleep(0.1)
         return image.image_bytes
 
-    def add_image(self, path, source, background):
+    def add_image(self, path, source, background, width=-1, height=-1):
         """
         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, width, height) in self._cache:
+            image = Image(path, source, background, width, height)
+            self._cache[(path, source, width, height)] = 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()):
@@ -308,7 +311,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-06-17 07:27:12 +0000
+++ openlp/core/lib/serviceitem.py	2014-09-21 19:28:31 +0000
@@ -39,8 +39,8 @@
 
 from PyQt4 import QtGui
 
-from openlp.core.common import RegistryProperties, Settings, translate
-from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
+from openlp.core.common import RegistryProperties, Settings, translate, AppLocation
+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
 
 log = logging.getLogger(__name__)
 
@@ -112,7 +112,17 @@
             The capability to edit the title of the item
 
     ``IsOptical``
-            .Determines is the service_item is based on an optical device
+            Determines is the service_item is based on an optical device
+
+    ``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
@@ -133,6 +143,9 @@
     CanAutoStartForLive = 16
     CanEditTitle = 17
     IsOptical = 18
+    HasDisplayTitle = 19
+    HasNotes = 20
+    HasThumbnails = 21
 
 
 class ServiceItem(RegistryProperties):
@@ -272,18 +285,23 @@
             self.raw_footer = []
         self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
 
-    def add_from_image(self, path, title, background=None):
+    def add_from_image(self, path, title, background=None, thumbnail=None):
         """
         Add an image slide to the service item.
 
         :param path: The directory in which the image file is located.
         :param title: A title for the slide in the service item.
         :param background:
+        :param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
         """
         if background:
             self.image_border = background
         self.service_item_type = ServiceItemType.Image
-        self._raw_frames.append({'title': title, 'path': path})
+        # If no thumbnail was given we create one
+        if not thumbnail:
+            thumbnail = os.path.join(AppLocation.get_section_data_path('images'), 'thumbnails', os.path.split(path)[1])
+            create_thumb(path, thumbnail, False)
+        self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
         self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
         self._new_item()
 
@@ -301,16 +319,22 @@
         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.
 
         :param path: The title of the slide in the service item.
         :param file_name: The title of the slide in the service item.
         :param image: The command of/for the slide.
+        :param display_title: Title to show in gui/webinterface, optional.
+        :param notes: Notes to show in the webinteface, optional.
         """
         self.service_item_type = ServiceItemType.Command
-        self._raw_frames.append({'title': file_name, 'image': image, 'path': path})
+        # If the item should have a display title but this frame doesn't have one, we make one up
+        if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
+            display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1)
+        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):
@@ -354,7 +378,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, service_item, path=None):
@@ -425,7 +450,8 @@
                     self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
                 elif 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-05-19 19:19:05 +0000
+++ openlp/core/ui/listpreviewwidget.py	2014-09-21 19:28:31 +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-08-23 20:42:10 +0000
+++ openlp/core/ui/servicemanager.py	2014-09-21 19:28:31 +0000
@@ -1281,7 +1281,11 @@
             # 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', ' ')
+                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-07-12 23:47:53 +0000
+++ openlp/core/ui/slidecontroller.py	2014-09-21 19:28:31 +0000
@@ -873,6 +873,7 @@
         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()
@@ -1042,8 +1043,8 @@
                         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()
 
@@ -1055,6 +1056,7 @@
         """
         self.preview_widget.change_slide(row)
         self.update_preview()
+        self.selected_row = row
 
     def update_preview(self):
         """

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2014-05-06 19:53:08 +0000
+++ openlp/plugins/bibles/lib/manager.py	2014-09-21 19:28:31 +0000
@@ -85,7 +85,7 @@
             BibleFormat.CSV,
             BibleFormat.OpenSong,
             BibleFormat.WebDownload,
-            BibleFormar.Zefania,
+            BibleFormat.Zefania,
         ]
 
 

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2014-04-24 02:57:02 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2014-09-21 19:28:31 +0000
@@ -551,6 +551,7 @@
         service_item.add_capability(ItemCapabilities.CanLoop)
         service_item.add_capability(ItemCapabilities.CanAppend)
         service_item.add_capability(ItemCapabilities.CanEditTitle)
+        service_item.add_capability(ItemCapabilities.HasThumbnails)
         # force a nonexistent theme
         service_item.theme = -1
         missing_items_file_names = []
@@ -589,7 +590,7 @@
         # Continue with the existing images.
         for filename in images_file_names:
             name = os.path.split(filename)[1]
-            service_item.add_from_image(filename, name, background)
+            service_item.add_from_image(filename, name, background, os.path.join(self.service_path, name))
         return True
 
     def check_group_exists(self, new_group):

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2014-08-27 23:18:06 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2014-09-21 19:28:31 +0000
@@ -65,7 +65,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__)
@@ -257,6 +257,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):
@@ -450,22 +451,46 @@
 
         :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)

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2014-06-05 16:25:37 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2014-09-21 19:28:31 +0000
@@ -288,13 +288,14 @@
                             os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
                         doc.load_presentation()
                     i = 1
-                    image_file = 'mainslide%03d.png' % i
-                    image = os.path.join(doc.get_temp_folder(), image_file)
+                    image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
+                    thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
                     while os.path.isfile(image):
-                        service_item.add_from_image(image, name)
+                        service_item.add_from_image(image, name, thumbnail=thumbnail)
                         i += 1
-                        image_file = 'mainslide%03d.png' % i
-                        image = os.path.join(doc.get_temp_folder(), image_file)
+                        image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
+                        thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
+                    service_item.add_capability(ItemCapabilities.HasThumbnails)
                     doc.close_presentation()
                     return True
                 else:
@@ -323,8 +324,21 @@
                     i = 1
                     img = doc.get_thumbnail_path(i, True)
                     if img:
+                        # Get titles and notes
+                        titles, notes = doc.get_titles_and_notes()
+                        service_item.add_capability(ItemCapabilities.HasDisplayTitle)
+                        if notes.count('') != len(notes):
+                            service_item.add_capability(ItemCapabilities.HasNotes)
+                        service_item.add_capability(ItemCapabilities.HasThumbnails)
                         while img:
-                            service_item.add_from_command(path, name, img)
+                            # Use title and note if available
+                            title = ''
+                            if titles and len(titles) >= i:
+                                title = titles[i - 1]
+                            note = ''
+                            if notes and len(notes) >= i:
+                                note = notes[i - 1]
+                            service_item.add_from_command(path, name, img, title, note)
                             i += 1
                             img = doc.get_thumbnail_path(i, True)
                         doc.close_presentation()

=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py	2014-08-27 23:18:06 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2014-09-21 19:28:31 +0000
@@ -27,7 +27,7 @@
 # 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
@@ -37,16 +37,17 @@
 
 if is_win():
     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 openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
 from openlp.core.common import trace_error_handler
 from .presentationcontroller import PresentationController, PresentationDocument
 
-
 log = logging.getLogger(__name__)
 
 
@@ -136,6 +137,7 @@
             self.controller.process.Presentations.Open(self.file_path, False, False, True)
             self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
             self.create_thumbnails()
+            self.create_titles_and_notes()
             # Powerpoint 2013 pops up when loading a file, so we minimize it again
             if self.presentation.Application.Version == u'15.0':
                 try:
@@ -392,6 +394,28 @@
         """
         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)
+
     def show_error_msg(self):
         """
         Stop presentation and display an error message.
@@ -410,8 +434,8 @@
     :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 == 2:  # 2 from is enum PpPlaceholderType.ppPlaceholderBody
+            if shape.HasTextFrame and shape.TextFrame.HasText:
+                text += shape.TextFrame.TextRange.Text + '\n'
     return text

=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py	2014-08-27 23:18:06 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py	2014-09-21 19:28:31 +0000
@@ -29,6 +29,11 @@
 
 import logging
 import os
+import logging
+import zipfile
+import re
+from xml.etree import ElementTree
+
 
 from openlp.core.common import is_win
 
@@ -127,14 +132,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()
@@ -154,6 +159,68 @@
             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)
+                # 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)
+
     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-09-21 19:28:31 +0000 differ
=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2014-05-02 06:42:17 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2014-09-21 19:28:31 +0000
@@ -293,6 +293,49 @@
         """
         return ''
 
+    def get_titles_and_notes(self):
+        """
+        Reads the titles from the titles file and
+        the notes files and returns the content in 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):
     """
@@ -427,3 +470,12 @@
 
     def close_presentation(self):
         pass
+
+
+class TextType(object):
+    """
+    Type Enumeration for Types of Text to request
+    """
+    Title = 0
+    SlideText = 1
+    Notes = 2

=== 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-09-21 19:28:31 +0000
@@ -87,16 +87,30 @@
         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"];
-          if (text != "") text = text + ": ";
-          text = text + data.results.slides[idx]["text"];
+          var indexInt = parseInt(idx,10);
+          var slide = data.results.slides[idx];
+          var text = slide["tag"];
+          if (text != "") {
+            text = 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 />');
-          var li = $("<li data-icon=\"false\">").append(
-            $("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text));
-          if (data.results.slides[idx]["selected"]) {
+          if (slide["img"]) {
+            text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails88x88/") + "'>";
+          }
+          var li = $("<li data-icon=\"false\">").append($("<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;

=== modified file 'openlp/plugins/remotes/html/stage.js'
--- openlp/plugins/remotes/html/stage.js	2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/stage.js	2014-09-21 19:28:31 +0000
@@ -102,7 +102,21 @@
     $("#verseorder span").removeClass("currenttag");
     $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
     var slide = OpenLP.currentSlides[OpenLP.currentSlide];
-    var text = slide["text"];
+    var text = "";
+    // use title if available
+    if (slide["title"]) {
+        text = slide["title"];
+    } else {
+        text = slide["text"];
+    }
+    // use thumbnail if available
+    if (slide["img"]) {
+        text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
+    }
+    // use notes if available
+    if (slide["notes"]) {
+        text += '<br />' + slide["notes"];
+    }
     text = text.replace(/\n/g, "<br />");
     $("#currentslide").html(text);
     text = "";
@@ -110,7 +124,11 @@
       for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
         if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
             text = text + "<p class=\"nextslide\">";
-        text = text + OpenLP.currentSlides[idx]["text"];
+        if (OpenLP.currentSlides[idx]["title"]) {
+            text = text + OpenLP.currentSlides[idx]["title"];
+        } else {
+            text = text + OpenLP.currentSlides[idx]["text"];
+        }
         if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
             text = text + "</p>";
         else

=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2014-04-19 05:09:54 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2014-09-21 19:28:31 +0000
@@ -125,7 +125,7 @@
 from PyQt4 import QtCore
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate
-from openlp.core.lib import PluginStatus, StringContent, image_to_byte
+from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities
 
 log = logging.getLogger(__name__)
 FILE_TYPES = {
@@ -159,6 +159,7 @@
             ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
             ('^/(main)$', {'function': self.serve_file, 'secure': False}),
             (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
+            (r'^/(\w+)/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}),
@@ -328,7 +329,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):
@@ -380,6 +382,42 @@
         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', 'images']
+        # -1 means use the default dimension in ImageManager
+        width = -1
+        height = -1
+        if dimensions:
+            match = re.search('(\d+)x(\d+)', dimensions)
+            if match:
+                # let's make sure that the dimensions are within reason
+                width = sorted([10, int(match.group(1)), 1000])[1]
+                height = sorted([10, int(match.group(2)), 1000])[1]
+        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, width, height)
+                        ext, content_type = self.get_content_type(full_path)
+                        image = self.image_manager.get_image(full_path, just_file_name, width, height)
+                        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.
@@ -458,6 +496,7 @@
         if current_item:
             for index, frame in enumerate(current_item.get_frames()):
                 item = {}
+                # Handle text (songs, custom)
                 if current_item.is_text():
                     if frame['verseTag']:
                         item['tag'] = str(frame['verseTag'])
@@ -465,11 +504,33 @@
                         item['tag'] = str(index + 1)
                     item['text'] = str(frame['text'])
                     item['html'] = str(frame['html'])
+                # Handle images, unless a custom thumbnail is given or if thumbnails is disabled
+                elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'):
+                    item['tag'] = str(index + 1)
+                    thumbnail_path = os.path.sep + os.path.join('images', 'thumbnails', frame['title'])
+                    item['img'] = urllib.request.pathname2url(thumbnail_path)
+                    item['text'] = str(frame['title'])
+                    item['html'] = str(frame['title'])
                 else:
+                    # Handle presentation etc.
                     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) and \
+                            Settings().value('remotes/thumbnails'):
+                        # If the file is under our app directory tree send the portion after the match
+                        data_path = AppLocation.get_data_path()
+                        print(frame)
+                        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
+                print(item)
                 data.append(item)
         json_data = {'results': {'slides': data}}
         if current_item:

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2014-04-19 05:26:49 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2014-09-21 19:28:31 +0000
@@ -144,6 +144,7 @@
             try:
                 self.httpd = server_class((address, port), CustomHandler)
                 log.debug("Server started for class %s %s %d" % (server_class, address, port))
+                break
             except OSError:
                 log.debug("failed to start http server thread state %d %s" %
                           (loop, self.http_thread.isRunning()))
@@ -151,6 +152,8 @@
                 time.sleep(0.1)
             except:
                 log.error('Failed to start server ')
+                loop += 1
+                time.sleep(0.1)
 
     def stop_server(self):
         """

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2014-04-20 20:16:08 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2014-09-21 19:28:31 +0000
@@ -62,6 +62,9 @@
         self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
         self.twelve_hour_check_box.setObjectName('twelve_hour_check_box')
         self.server_settings_layout.addRow(self.twelve_hour_check_box)
+        self.thumbnails_check_box = QtGui.QCheckBox(self.server_settings_group_box)
+        self.thumbnails_check_box.setObjectName('thumbnails_check_box')
+        self.server_settings_layout.addRow(self.thumbnails_check_box)
         self.left_layout.addWidget(self.server_settings_group_box)
         self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
         self.http_settings_group_box.setObjectName('http_settings_group_box')
@@ -163,6 +166,7 @@
         self.left_layout.addStretch()
         self.right_layout.addStretch()
         self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
+        self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
         self.address_edit.textChanged.connect(self.set_urls)
         self.port_spin_box.valueChanged.connect(self.set_urls)
         self.https_port_spin_box.valueChanged.connect(self.set_urls)
@@ -176,6 +180,8 @@
         self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
         self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
         self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
+        self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
+                                                    'Show thumbnails of non-text slides in remote and stage view.'))
         self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
         self.qr_description_label.setText(
             translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/'
@@ -240,6 +246,8 @@
         self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
         self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
         self.twelve_hour_check_box.setChecked(self.twelve_hour)
+        self.thumbnails = Settings().value(self.settings_section + '/thumbnails')
+        self.thumbnails_check_box.setChecked(self.thumbnails)
         local_data = AppLocation.get_directory(AppLocation.DataDir)
         if not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.crt')) or \
                 not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.key')):
@@ -271,6 +279,7 @@
         Settings().setValue(self.settings_section + '/https enabled', self.https_settings_group_box.isChecked())
         Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
         Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
+        Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
         Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
         Settings().setValue(self.settings_section + '/user id', self.user_id.text())
         Settings().setValue(self.settings_section + '/password', self.password.text())
@@ -285,6 +294,15 @@
         if check_state == QtCore.Qt.Checked:
             self.twelve_hour = True
 
+    def on_thumbnails_check_box_changed(self, check_state):
+        """
+        Toggle the thumbnail check box.
+        """
+        self.thumbnails = False
+        # we have a set value convert to True/False
+        if check_state == QtCore.Qt.Checked:
+            self.thumbnails = True
+
     def https_changed(self):
         """
         Invert the HTTP group box based on Https group settings

=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py	2014-04-20 20:16:08 +0000
+++ openlp/plugins/remotes/remoteplugin.py	2014-09-21 19:28:31 +0000
@@ -44,7 +44,8 @@
     'remotes/user id': 'openlp',
     'remotes/password': 'password',
     'remotes/authentication enabled': False,
-    'remotes/ip address': '0.0.0.0'
+    'remotes/ip address': '0.0.0.0',
+    'remotes/thumbnails': True
 }
 
 

=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py	2014-04-02 18:51:21 +0000
+++ tests/functional/openlp_core_common/test_applocation.py	2014-09-21 19:28:31 +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-06-04 04:54:44 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py	2014-09-21 19:28:31 +0000
@@ -69,16 +69,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')
@@ -89,6 +90,38 @@
             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, 80, 80)
+
+        # WHEN: the image is retrieved
+        image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80)
+
+        # THEN: The return should be of type image
+        self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
+
+        # WHEN: adding the same image with different dimensions
+        self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100)
+
+        # 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, 80, 80)
+
+        # 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', 120, 120)
+        self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension')
+
     def process_cache_test(self):
         """
         Test the process_cache method
@@ -151,7 +184,7 @@
 
         :param image: The name of the image. E. g. ``image1``
         """
-        return self.image_manager._cache[(TEST_PATH, image)].priority
+        return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority
 
     def mocked_resize_image(self, *args):
         """

=== modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py	2014-03-17 20:59:58 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py	2014-09-21 19:28:31 +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'\
@@ -113,20 +111,25 @@
         # GIVEN: A new service item and a mocked add icon function
         image_name = 'image_1.jpg'
         test_file = os.path.join(TEST_PATH, image_name)
-        frame_array = {'path': test_file, 'title': image_name}
+        thumb_file = os.path.normpath(os.path.join('/path/thumbnails', image_name))
+        frame_array = {'path': test_file, 'title': image_name, 'image': thumb_file}
 
         service_item = ServiceItem(None)
         service_item.add_icon = MagicMock()
 
         # WHEN: adding an image from a saved Service and mocked exists
         line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj')
-        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
+        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\
+                patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb,\
+                patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
+                mocked_get_section_data_path:
             mocked_exists.return_value = True
+            mocked_get_section_data_path.return_value = os.path.normpath('/path/')
             service_item.set_from_service(line, TEST_PATH)
 
         # 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,10 +156,12 @@
         # 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)
-        frame_array1 = {'path': test_file1, 'title': image_name1}
-        frame_array2 = {'path': test_file2, 'title': 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))
+        thumb_file1 = os.path.normpath(os.path.join('/path/thumbnails', image_name1))
+        thumb_file2 = os.path.normpath(os.path.join('/path/thumbnails', image_name2))
+        frame_array1 = {'path': test_file1, 'title': image_name1, 'image': thumb_file1}
+        frame_array2 = {'path': test_file2, 'title': image_name2, 'image': thumb_file2}
 
         service_item = ServiceItem(None)
         service_item.add_icon = MagicMock()
@@ -168,8 +173,12 @@
         line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj')
         line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1)
 
-        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
+        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists, \
+                patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb, \
+                patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
+                mocked_get_section_data_path:
             mocked_exists.return_value = True
+            mocked_get_section_data_path.return_value = os.path.normpath('/path/')
             service_item2.set_from_service(line2)
             service_item.set_from_service(line)
 
@@ -207,6 +216,44 @@
         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')
+
     def service_item_load_optical_media_from_service_test(self):
         """
         Test the Service Item - load an optical media item

=== modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
--- tests/functional/openlp_core_ui/test_thememanager.py	2014-08-21 13:07:02 +0000
+++ tests/functional/openlp_core_ui/test_thememanager.py	2014-09-21 19:28:31 +0000
@@ -57,17 +57,18 @@
         # GIVEN: A new ThemeManager instance.
         theme_manager = ThemeManager()
         theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes')
-        zipfile.ZipFile.__init__ = MagicMock()
-        zipfile.ZipFile.__init__.return_value = None
-        zipfile.ZipFile.write = MagicMock()
-
-        # WHEN: The theme is exported
-        theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
-
-        # THEN: The zipfile should be created at the given path
-        zipfile.ZipFile.__init__.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
-        zipfile.ZipFile.write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'),
-                                                 os.path.join('Default', 'Default.xml'))
+        with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
+                patch('zipfile.ZipFile.write') as mocked_zipfile_write:
+            mocked_zipfile_init.return_value = None
+
+            # WHEN: The theme is exported
+            theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
+
+            # THEN: The zipfile should be created at the given path
+            mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
+            mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes',
+                                                                 'Default', 'Default.xml'),
+                                                    os.path.join('Default', 'Default.xml'))
 
     def initial_theme_manager_test(self):
         """

=== 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-09-21 19:28:31 +0000
@@ -0,0 +1,229 @@
+# -*- 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
+import shutil
+from tempfile import mkdtemp
+
+from tests.functional import patch, MagicMock
+from tests.utils.constants import TEST_RESOURCES_PATH
+from tests.helpers.testmixin import TestMixin
+
+from openlp.plugins.presentations.lib.impresscontroller import \
+    ImpressController, ImpressDocument, TextType
+
+
+class TestImpressController(TestCase, TestMixin):
+    """
+    Test the ImpressController Class
+    """
+
+    def setUp(self):
+        """
+        Set up the patches and mocks need for all tests.
+        """
+        self.get_application()
+        self.build_settings()
+        self.mock_plugin = MagicMock()
+        self.temp_folder = mkdtemp()
+        self.mock_plugin.settings_section = self.temp_folder
+
+    def tearDown(self):
+        """
+        Stop the patches
+        """
+        self.destroy_settings()
+        shutil.rmtree(self.temp_folder)
+
+    def constructor_test(self):
+        """
+        Test the Constructor from the ImpressController
+        """
+        # GIVEN: No presentation controller
+        controller = None
+
+        # WHEN: The presentation controller object is created
+        controller = ImpressController(plugin=self.mock_plugin)
+
+        # THEN: The name of the presentation controller should be correct
+        self.assertEqual('Impress', controller.name,
+                         'The name of the presentation controller should be correct')
+
+
+class TestImpressDocumnt(TestCase):
+    """
+    Test the ImpressDocument Class
+    """
+    def setUp(self):
+        mocked_plugin = MagicMock()
+        mocked_plugin.settings_section = 'presentations'
+        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', '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):
+        """
+        Helper function, creates a mock libreoffice document.
+
+        :param page_count: Number of pages in the document
+        :param note_count: Number of note pages in the document
+        :param text_count: Number of text pages in the document
+        """
+        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):
+        """
+        Helper function.
+        """
+        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

=== modified file 'tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	2014-07-03 11:21:12 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	2014-09-21 19:28:31 +0000
@@ -30,16 +30,20 @@
 Functional tests to test the PowerPointController class and related methods.
 """
 import os
-if os.name == 'nt':
-    import pywintypes
 import shutil
 from unittest import TestCase
 from tempfile import mkdtemp
 
 from tests.functional import patch, MagicMock
 from tests.helpers.testmixin import TestMixin
-
-from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument,\
+    _get_text_from_shapes
+from openlp.core.common import is_win
+
+if is_win():
+    import pywintypes
 
 
 class TestPowerpointController(TestCase, TestMixin):
@@ -79,7 +83,7 @@
                          'The name of the presentation controller should be correct')
 
 
-class TestPowerpointDocument(TestCase):
+class TestPowerpointDocument(TestCase, TestMixin):
     """
     Test the PowerpointDocument Class
     """
@@ -88,6 +92,11 @@
         """
         Set up the patches and mocks need for all tests.
         """
+        self.get_application()
+        self.build_settings()
+        self.mock_plugin = MagicMock()
+        self.temp_folder = mkdtemp()
+        self.mock_plugin.settings_section = self.temp_folder
         self.powerpoint_document_stop_presentation_patcher = patch(
             'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation')
         self.presentation_document_get_temp_folder_patcher = patch(
@@ -100,6 +109,8 @@
         self.mock_controller = MagicMock()
         self.mock_presentation = MagicMock()
         self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
+        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+        self.real_controller = PowerpointController(self.mock_plugin)
 
     def tearDown(self):
         """
@@ -108,12 +119,14 @@
         self.powerpoint_document_stop_presentation_patcher.stop()
         self.presentation_document_get_temp_folder_patcher.stop()
         self.presentation_document_setup_patcher.stop()
+        self.destroy_settings()
+        shutil.rmtree(self.temp_folder)
 
     def show_error_msg_test(self):
         """
         Test the PowerpointDocument.show_error_msg() method gets called on com exception
         """
-        if os.name == 'nt':
+        if is_win():
             # GIVEN: A PowerpointDocument with mocked controller and presentation
             with patch('openlp.plugins.presentations.lib.powerpointcontroller.critical_error_message_box') as \
                     mocked_critical_error_message_box:
@@ -129,3 +142,95 @@
                                                                      'integration and the presentation will be stopped.'
                                                                      ' Restart the presentation if you wish to '
                                                                      'present it.')
+
+    # add _test to the following if necessary
+    def verify_loading_document(self):
+        """
+        Test loading a document in PowerPoint
+        """
+        if is_win() and self.real_controller.check_available():
+            # GIVEN: A PowerpointDocument and a presentation
+            doc = PowerpointDocument(self.real_controller, self.file_name)
+
+            # WHEN: loading the filename
+            doc.load_presentation()
+            result = doc.is_loaded()
+
+            # THEN: result should be true
+            self.assertEqual(result, True, 'The result should be True')
+        else:
+            self.skipTest('Powerpoint not available, skipping test.')
+
+    def create_titles_and_notes_test(self):
+        """
+        Test creating the titles from PowerPoint
+        """
+        if is_win() and self.real_controller.check_available():
+            # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+            self.doc = PowerpointDocument(self.real_controller, 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'], [' ', ' '])
+        else:
+            self.skipTest('Powerpoint not available, skipping test.')
+
+    def create_titles_and_notes_with_no_slides_test(self):
+        """
+        Test creating the titles from PowerPoint when it returns no slides
+        """
+        if is_win() and self.real_controller.check_available():
+            # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+            doc = PowerpointDocument(self.real_controller, self.file_name)
+            doc.save_titles_and_notes = MagicMock()
+            doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+            pres = MagicMock()
+            pres.Slides = []
+            doc.presentation = pres
+
+            # WHEN reading the titles and notes
+            doc.create_titles_and_notes()
+
+            # THEN the save should have been called exactly once with empty titles and notes
+            doc.save_titles_and_notes.assert_called_once_with([], [])
+        else:
+            self.skipTest('Powerpoint not available, skipping test.')
+
+    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')

=== modified file 'tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-04-20 20:19:21 +0000
+++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-09-21 19:28:31 +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,          #
@@ -31,16 +31,19 @@
 """
 import os
 import shutil
-if os.name == 'nt':
-    from ctypes import cdll
 
 from tempfile import mkdtemp
 from unittest import TestCase
 
 from tests.functional import MagicMock, patch
 from tests.helpers.testmixin import TestMixin
+from tests.utils.constants import TEST_RESOURCES_PATH
 
 from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
+from openlp.core.common import is_win
+
+if is_win():
+    from ctypes import cdll
 
 
 class TestPptviewController(TestCase, TestMixin):
@@ -98,7 +101,7 @@
             available = controller.check_available()
 
             # THEN: On windows it should return True, on other platforms False
-            if os.name == 'nt':
+            if is_win():
                 self.assertTrue(available, 'check_available should return True on windows.')
             else:
                 self.assertFalse(available, 'check_available should return False when not on windows.')
@@ -130,7 +133,7 @@
         """
         Set up the patches and mocks need for all tests.
         """
-        self.os_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os')
+        self.os_isdir_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.isdir')
         self.pptview_document_create_thumbnails_patcher = patch(
             'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
         self.pptview_document_stop_presentation_patcher = patch(
@@ -141,46 +144,45 @@
             'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
         self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
         self.rect_patcher = MagicMock()
-
-        self.mock_os = self.os_patcher.start()
+        self.mock_os_isdir = self.os_isdir_patcher.start()
         self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start()
         self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start()
         self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
         self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
         self.mock_rect = self.rect_patcher.start()
         self.mock_screen_list = self.screen_list_patcher.start()
-
         self.mock_controller = MagicMock()
         self.mock_presentation = MagicMock()
-
-        self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
+        self.temp_folder = mkdtemp()
+        self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
 
     def tearDown(self):
         """
         Stop the patches
         """
-        self.os_patcher.stop()
+        self.os_isdir_patcher.stop()
         self.pptview_document_create_thumbnails_patcher.stop()
         self.pptview_document_stop_presentation_patcher.stop()
         self.presentation_document_get_temp_folder_patcher.stop()
         self.presentation_document_setup_patcher.stop()
         self.rect_patcher.stop()
         self.screen_list_patcher.stop()
+        shutil.rmtree(self.temp_folder)
 
     def load_presentation_succesfull_test(self):
         """
         Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
         """
         # GIVEN: A reset mocked_os
-        self.mock_os.reset()
+        self.mock_os_isdir.reset()
 
         # WHEN: The temporary directory exists and OpenPPT returns successfully (not -1)
-        self.mock_os.path.isdir.return_value = True
+        self.mock_os_isdir.return_value = True
         self.mock_controller.process.OpenPPT.return_value = 0
         instance = PptviewDocument(self.mock_controller, self.mock_presentation)
         instance.file_path = 'test\path.ppt'
 
-        if os.name == 'nt':
+        if is_win():
             result = instance.load_presentation()
 
             # THEN: PptviewDocument.load_presentation should return True
@@ -191,17 +193,78 @@
         Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
         not successfully opened
         """
-        # GIVEN: A reset mocked_os
-        self.mock_os.reset()
+        # GIVEN: A reset mock_os_isdir
+        self.mock_os_isdir.reset()
 
         # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
-        self.mock_os.path.isdir.return_value = False
-        self.mock_controller.process.OpenPPT.return_value = -1
-        instance = PptviewDocument(self.mock_controller, self.mock_presentation)
-        instance.file_path = 'test\path.ppt'
-        if os.name == 'nt':
-            result = instance.load_presentation()
-
-            # THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False
-            self.mock_os.makedirs.assert_called_once_with('temp folder')
-            self.assertFalse(result)
+        with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs:
+            self.mock_os_isdir.return_value = False
+            self.mock_controller.process.OpenPPT.return_value = -1
+            instance = PptviewDocument(self.mock_controller, self.mock_presentation)
+            instance.file_path = 'test\path.ppt'
+            if is_win():
+                result = instance.load_presentation()
+
+                # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
+                mock_makedirs.assert_called_once_with(self.temp_folder)
+                self.assertFalse(result)
+
+    def create_titles_and_notes_test(self):
+        """
+        Test PowerpointController.create_titles_and_notes
+        """
+        # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
+        doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+        doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+        doc.save_titles_and_notes = MagicMock()
+
+        # WHEN reading the titles and notes
+        doc.create_titles_and_notes()
+
+        # THEN save_titles_and_notes should have been called once with empty arrays
+        doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'],
+                                                          ['Notes for slide 1', 'Inserted', 'Notes for slide 2',
+                                                           'Notes \nfor slide 4', 'Notes for slide 3'])
+
+    def 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
+            doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+            doc.file_path = 'Idontexist.pptx'
+            doc.save_titles_and_notes = MagicMock()
+
+            # WHEN: Reading the titles and notes
+            doc.create_titles_and_notes()
+
+            # THEN: File existens should have been checked, and not have been opened.
+            doc.save_titles_and_notes.assert_called_once_with(None, None)
+            mocked_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') as mocked_open, \
+                patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
+            mocked_is_zf.return_value = False
+            mocked_open.filesize = 10
+            doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+            doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
+            doc.save_titles_and_notes = MagicMock()
+
+            # WHEN: reading the titles and notes
+            doc.create_titles_and_notes()
+
+            # THEN:
+            doc.save_titles_and_notes.assert_called_once_with(None, None)
+            self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once')

=== 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-09-21 19:28:31 +0000
@@ -1,158 +1,166 @@
-# -*- 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_remotetab.py'
--- tests/functional/openlp_plugins/remotes/test_remotetab.py	2014-03-14 22:08:44 +0000
+++ tests/functional/openlp_plugins/remotes/test_remotetab.py	2014-09-21 19:28:31 +0000
@@ -48,7 +48,8 @@
     'remotes/user id': 'openlp',
     'remotes/password': 'password',
     'remotes/authentication enabled': False,
-    'remotes/ip address': '0.0.0.0'
+    'remotes/ip address': '0.0.0.0',
+    'remotes/thumbnails': True
 }
 ZERO_URL = '0.0.0.0'
 TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))

=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py	2014-05-07 20:38:34 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py	2014-09-21 19:28:31 +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, 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
 
@@ -186,3 +188,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/resources/presentations/test.ppt'
Binary files tests/resources/presentations/test.ppt	1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.ppt	2014-09-21 19:28:31 +0000 differ
=== added file 'tests/resources/presentations/test.pptx'
Binary files tests/resources/presentations/test.pptx	1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.pptx	2014-09-21 19:28:31 +0000 differ

References