← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~patrick-zakweb/openlp/image-previews into lp:openlp

 

mohij has proposed merging lp:~patrick-zakweb/openlp/image-previews into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
  Andreas Preikschat (googol)

For more details, see:
https://code.launchpad.net/~patrick-zakweb/openlp/image-previews/+merge/169616

This branch does not add any new features. It refactors the preview list widget to be in a separate file with a relatively clean interface. This cleans up the slidecontroller.py a little and allows to add alternative implementations of the slide preview widget later on.

I tested:
-preview
-live
-live with images
-switching songs via cursor keys
-switching songs via double clicking in service manager
-switching slides via shortcuts
-resizing screen (There is a resize bug both, in trunk and in this branch, but I don't want to create a fix before merging this branch, since several changes in slidecontroller.py are necessary and I don't like having to merge branches)
-resizing controller area

One thing I am not clear about is the import statements.
When adding the listpreviewwidget to __init__.py and importing via
from openlp.core.ui import ListPreviewWidget
I always get:

Traceback (most recent call last):
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp.py", line 40, in <module>
    from openlp.core import main
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/__init__.py", line 33, in <module>
    import core
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/core/__init__.py", line 46, in <module>
    from openlp.core.lib import Settings, ScreenList, UiStrings, Registry, check_directory_exists
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/core/lib/__init__.py", line 392, in <module>
    from renderer import Renderer
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/core/lib/renderer.py", line 37, in <module>
    from openlp.core.ui import MainDisplay
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/core/ui/__init__.py", line 89, in <module>
    from slidecontroller import SlideController, DisplayController
  File "/home/patrick/Documents/openlp/repo/icon-preview-list/openlp/core/ui/slidecontroller.py", line 44, in <module>
    from openlp.core.ui import ListPreviewWidget
ImportError: cannot import name ListPreviewWidget

Process finished with exit code 1

No idea what is wrong with that.
Any hints appreciated.
----------------------------------------------------------------------------------------------------------
After looking for testable classes I ended up with my own...
I factored out some code of test_serviceitem.py because I did not want to duplicate code. This forced me to turn the "tests" folder into a package. I am not sure whether this is a good idea, because the name "tests" is non openlp specific. I am not sure where else I should put common test logic though. Any opinions appreciated.
-----------------------------------------------------------------------------------------------------------
- Merge master.
-Tested naviagation with mouse and keyboard in both, live and preview.
-----------------------------------------------------------------------------------------------------------
- Remove superfluous comment.
-- 
https://code.launchpad.net/~patrick-zakweb/openlp/image-previews/+merge/169616
Your team OpenLP Core is subscribed to branch lp:openlp.
=== added file 'openlp/core/ui/listpreviewwidget.py'
--- openlp/core/ui/listpreviewwidget.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/listpreviewwidget.py	2013-06-15 10:58:51 +0000
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2013 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2013 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                          #
+###############################################################################
+"""
+The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
+It is based on a QTableWidget but represents its contents in list form.
+"""
+
+from PyQt4 import QtCore, QtGui
+
+from openlp.core.lib import ImageSource, Registry, ServiceItem
+
+
+class ListPreviewWidget(QtGui.QTableWidget):
+    def __init__(self, parent, screen_ratio):
+        """
+        Initializes the widget to default state.
+        An empty ServiceItem is used per default.
+        One needs to call replace_service_manager_item() to make this widget display something.
+        """
+        super(QtGui.QTableWidget, self).__init__(parent)
+        # Set up the widget.
+        self.setColumnCount(1)
+        self.horizontalHeader().setVisible(False)
+        self.setColumnWidth(0, parent.width())
+        self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+        self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+        self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
+        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.setAlternatingRowColors(True)
+        # Initialize variables.
+        self.service_item = ServiceItem()
+        self.screen_ratio = screen_ratio
+
+    def resizeEvent(self, QResizeEvent):
+        """
+        Overloaded method from QTableWidget. Will recalculate the layout.
+        """
+        self.__recalculate_layout()
+
+    def __recalculate_layout(self):
+        """
+        Recalculates the layout of the table widget. It will set height and width
+        of the table cells. QTableWidget does not adapt the cells to the widget size on its own.
+        """
+        self.setColumnWidth(0, self.viewport().width())
+        if self.service_item:
+            # Sort out songs, bibles, etc.
+            if self.service_item.is_text():
+                self.resizeRowsToContents()
+            else:
+                # Sort out image heights.
+                for framenumber in range(len(self.service_item.get_frames())):
+                    height = self.viewport().width() / self.screen_ratio
+                    self.setRowHeight(framenumber, height)
+
+    def screen_size_changed(self, screen_ratio):
+        """
+        To be called whenever the live screen size changes.
+        Because this makes a layout recalculation necessary.
+        """
+        self.screen_ratio = screen_ratio
+        self.__recalculate_layout()
+
+    def replace_service_item(self, service_item, width, slideNumber):
+        """
+        Replaces the current preview items with the ones in service_item.
+        Displays the given slide.
+        """
+        self.service_item = service_item
+        self.clear()
+        self.setRowCount(0)
+        self.setColumnWidth(0, width)
+        row = 0
+        text = []
+        for framenumber, frame in enumerate(self.service_item.get_frames()):
+            self.setRowCount(self.slide_count() + 1)
+            item = QtGui.QTableWidgetItem()
+            slideHeight = 0
+            if self.service_item.is_text():
+                if frame[u'verseTag']:
+                    # These tags are already translated.
+                    verse_def = frame[u'verseTag']
+                    verse_def = u'%s%s' % (verse_def[0], verse_def[1:])
+                    two_line_def = u'%s\n%s' % (verse_def[0], verse_def[1:])
+                    row = two_line_def
+                else:
+                    row += 1
+                item.setText(frame[u'text'])
+            else:
+                label = QtGui.QLabel()
+                label.setMargin(4)
+                if self.service_item.is_media():
+                    label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
+                else:
+                    label.setScaledContents(True)
+                if self.service_item.is_command():
+                    label.setPixmap(QtGui.QPixmap(frame[u'image']))
+                else:
+                    image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin)
+                    label.setPixmap(QtGui.QPixmap.fromImage(image))
+                self.setCellWidget(framenumber, 0, label)
+                slideHeight = width / self.screen_ratio
+                row += 1
+            text.append(unicode(row))
+            self.setItem(framenumber, 0, item)
+            if slideHeight:
+                self.setRowHeight(framenumber, slideHeight)
+        self.setVerticalHeaderLabels(text)
+        if self.service_item.is_text():
+            self.resizeRowsToContents()
+        self.setColumnWidth(0, self.viewport().width())
+        self.setFocus()
+        self.change_slide(slideNumber)
+
+    def change_slide(self, slide):
+        """
+        Switches to the given row.
+        """
+        if slide >= self.slide_count():
+            slide = self.slide_count() - 1
+        # Scroll to next item if possible.
+        if slide + 1 < self.slide_count():
+            self.scrollToItem(self.item(slide + 1, 0))
+        self.selectRow(slide)
+
+    def current_slide_number(self):
+        """
+        Returns the position of the currently active item. Will return -1 if the widget is empty.
+        """
+        return super(ListPreviewWidget, self).currentRow()
+
+    def slide_count(self):
+        """
+        Returns the number of slides this widget holds.
+        """
+        return super(ListPreviewWidget, self).rowCount()
+
+    def _get_image_manager(self):
+        """
+        Adds the image manager to the class dynamically.
+        """
+        if not hasattr(self, u'_image_manager'):
+            self._image_manager = Registry().get(u'image_manager')
+        return self._image_manager
+
+    image_manager = property(_get_image_manager)
+

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2013-05-19 07:35:07 +0000
+++ openlp/core/ui/servicemanager.py	2013-06-15 10:58:51 +0000
@@ -1369,7 +1369,7 @@
                     self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0)
                     next_item = self.service_manager_list.topLevelItem(item)
                     self.service_manager_list.setCurrentItem(next_item)
-                    self.live_controller.preview_list_widget.setFocus()
+                    self.live_controller.preview_widget.setFocus()
         else:
             critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
                 translate('OpenLP.ServiceManager',

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2013-06-03 18:47:25 +0000
+++ openlp/core/ui/slidecontroller.py	2013-06-15 10:58:51 +0000
@@ -41,6 +41,7 @@
 from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
 from openlp.core.lib.ui import create_action
 from openlp.core.utils.actions import ActionList, CategoryOrder
+from openlp.core.ui.listpreviewwidget import ListPreviewWidget
 
 log = logging.getLogger(__name__)
 
@@ -159,18 +160,8 @@
         self.controller_layout.setSpacing(0)
         self.controller_layout.setMargin(0)
         # Controller list view
-        self.preview_list_widget = QtGui.QTableWidget(self.controller)
-        self.preview_list_widget.setColumnCount(1)
-        self.preview_list_widget.horizontalHeader().setVisible(False)
-        self.preview_list_widget.setColumnWidth(0, self.controller.width())
-        self.preview_list_widget.is_live = self.is_live
-        self.preview_list_widget.setObjectName(u'preview_list_widget')
-        self.preview_list_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
-        self.preview_list_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
-        self.preview_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
-        self.preview_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-        self.preview_list_widget.setAlternatingRowColors(True)
-        self.controller_layout.addWidget(self.preview_list_widget)
+        self.preview_widget = ListPreviewWidget(self, self.ratio)
+        self.controller_layout.addWidget(self.preview_widget)
         # Build the full toolbar
         self.toolbar = OpenLPToolbar(self)
         size_toolbar_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
@@ -352,7 +343,7 @@
                 {u'key': u'O', u'configurable': True, u'text': translate('OpenLP.SlideController', 'Go to "Other"')}
             ]
             shortcuts.extend([{u'key': unicode(number)} for number in range(10)])
-            self.preview_list_widget.addActions([create_action(self,
+            self.controller.addActions([create_action(self,
                 u'shortcutAction_%s' % s[u'key'], text=s.get(u'text'),
                 can_shortcuts=True,
                 context=QtCore.Qt.WidgetWithChildrenShortcut,
@@ -360,7 +351,7 @@
                 triggers=self._slideShortcutActivated) for s in shortcuts])
             self.shortcutTimer.timeout.connect(self._slideShortcutActivated)
         # Signals
-        self.preview_list_widget.clicked.connect(self.onSlideSelected)
+        self.preview_widget.clicked.connect(self.onSlideSelected)
         if self.is_live:
             # Need to use event as called across threads and UI is updated
             QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
@@ -368,13 +359,13 @@
             self.toolbar.set_widget_visible(self.loop_list, False)
             self.toolbar.set_widget_visible(self.wide_menu, False)
         else:
-            self.preview_list_widget.doubleClicked.connect(self.onGoLiveClick)
+            self.preview_widget.doubleClicked.connect(self.onGoLiveClick)
             self.toolbar.set_widget_visible([u'editSong'], False)
         if self.is_live:
             self.setLiveHotkeys(self)
-            self.__addActionsToWidget(self.preview_list_widget)
+            self.__addActionsToWidget(self.controller)
         else:
-            self.preview_list_widget.addActions([self.nextItem, self.previous_item])
+            self.controller.addActions([self.nextItem, self.previous_item])
         Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
         Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
         Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
@@ -433,7 +424,7 @@
         if len(matches) == 1:
             self.shortcutTimer.stop()
             self.current_shortcut = u''
-            self.__checkUpdateSelectedSlide(self.slideList[matches[0]])
+            self.preview_widget.change_slide(self.slideList[matches[0]])
             self.slideSelected()
         elif sender_name != u'shortcutTimer':
             # Start the time as we did not have any match.
@@ -443,7 +434,7 @@
             if self.current_shortcut in keys:
                 # We had more than one match for example "V1" and "V10", but
                 # "V1" was the slide we wanted to go.
-                self.__checkUpdateSelectedSlide(self.slideList[self.current_shortcut])
+                self.preview_widget.change_slide(self.slideList[self.current_shortcut])
                 self.slideSelected()
            # Reset the shortcut.
             self.current_shortcut = u''
@@ -538,6 +529,7 @@
             self.ratio = 1
         self.media_controller.setup_display(self.display, False)
         self.preview_size_changed()
+        self.preview_widget.screen_size_changed(self.ratio)
         self.preview_display.setup()
         service_item = ServiceItem()
         self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
@@ -575,17 +567,6 @@
             self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
             self.preview_display.screen = {
                 u'size': self.preview_display.geometry()}
-        # Make sure that the frames have the correct size.
-        self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width())
-        if self.service_item:
-            # Sort out songs, bibles, etc.
-            if self.service_item.is_text():
-                self.preview_list_widget.resizeRowsToContents()
-            else:
-                # Sort out image heights.
-                width = self.main_window.controlSplitter.sizes()[self.split]
-                for framenumber in range(len(self.service_item.get_frames())):
-                    self.preview_list_widget.setRowHeight(framenumber, width / self.ratio)
         self.onControllerSizeChanged(self.controller.width())
 
     def onControllerSizeChanged(self, width):
@@ -710,7 +691,7 @@
         Replacement item following a remote edit
         """
         if item == self.service_item:
-            self._process_item(item, self.preview_list_widget.currentRow())
+            self._process_item(item, self.preview_widget.current_slide_number())
 
     def addServiceManagerItem(self, item, slideno):
         """
@@ -726,7 +707,7 @@
             slidenum = 0
         # If service item is the same as the current one, only change slide
         if slideno >= 0 and item == self.service_item:
-            self.__checkUpdateSelectedSlide(slidenum)
+            self.preview_widget.change_slide(slidenum)
             self.slideSelected()
         else:
             self._process_item(item, slidenum)
@@ -753,10 +734,6 @@
             self._resetBlank()
         Registry().execute(u'%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slideno])
         self.slideList = {}
-        width = self.main_window.controlSplitter.sizes()[self.split]
-        self.preview_list_widget.clear()
-        self.preview_list_widget.setRowCount(0)
-        self.preview_list_widget.setColumnWidth(0, width)
         if self.is_live:
             self.song_menu.menu().clear()
             self.display.audio_player.reset()
@@ -781,9 +758,8 @@
                 self.setAudioItemsVisibility(True)
         row = 0
         text = []
+        width = self.main_window.controlSplitter.sizes()[self.split]
         for framenumber, frame in enumerate(self.service_item.get_frames()):
-            self.preview_list_widget.setRowCount(self.preview_list_widget.rowCount() + 1)
-            item = QtGui.QTableWidgetItem()
             slideHeight = 0
             if self.service_item.is_text():
                 if frame[u'verseTag']:
@@ -799,36 +775,15 @@
                 else:
                     row += 1
                     self.slideList[unicode(row)] = row - 1
-                item.setText(frame[u'text'])
             else:
-                label = QtGui.QLabel()
-                label.setMargin(4)
-                if service_item.is_media():
-                    label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
-                else:
-                    label.setScaledContents(True)
-                if self.service_item.is_command():
-                    label.setPixmap(QtGui.QPixmap(frame[u'image']))
-                else:
-                    # If current slide set background to image
-                    if framenumber == slideno:
-                        self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'],
-                            ImageSource.ImagePlugin)
-                    image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin)
-                    label.setPixmap(QtGui.QPixmap.fromImage(image))
-                self.preview_list_widget.setCellWidget(framenumber, 0, label)
                 slideHeight = width * (1 / self.ratio)
                 row += 1
                 self.slideList[unicode(row)] = row - 1
-            text.append(unicode(row))
-            self.preview_list_widget.setItem(framenumber, 0, item)
-            if slideHeight:
-                self.preview_list_widget.setRowHeight(framenumber, slideHeight)
-        self.preview_list_widget.setVerticalHeaderLabels(text)
-        if self.service_item.is_text():
-            self.preview_list_widget.resizeRowsToContents()
-        self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width())
-        self.__updatePreviewSelection(slideno)
+                # If current slide set background to image
+                if not self.service_item.is_command() and framenumber == slideno:
+                    self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'],
+                        ImageSource.ImagePlugin)
+        self.preview_widget.replace_service_item(self.service_item, width, slideno)
         self.enable_tool_bar(service_item)
         # Pass to display for viewing.
         # Postpone image build, we need to do this later to avoid the theme
@@ -838,7 +793,6 @@
         if service_item.is_media():
             self.onMediaStart(service_item)
         self.slideSelected(True)
-        self.preview_list_widget.setFocus()
         if old_item:
             # Close the old item after the new one is opened
             # This avoids the service theme/desktop flashing on screen
@@ -850,16 +804,6 @@
                 self.onMediaClose()
         Registry().execute(u'slidecontroller_%s_started' % self.type_prefix, [service_item])
 
-    def __updatePreviewSelection(self, slideno):
-        """
-        Utility method to update the selected slide in the list.
-        """
-        if slideno > self.preview_list_widget.rowCount():
-            self.preview_list_widget.selectRow(
-                self.preview_list_widget.rowCount() - 1)
-        else:
-            self.__checkUpdateSelectedSlide(slideno)
-
     # Screen event methods
     def on_slide_selected_index(self, message):
         """
@@ -872,7 +816,7 @@
             Registry().execute(u'%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
             self.updatePreview()
         else:
-            self.__checkUpdateSelectedSlide(index)
+            self.preview_widget.change_slide(index)
             self.slideSelected()
 
     def mainDisplaySetBackground(self):
@@ -1015,9 +959,9 @@
         Generate the preview when you click on a slide.
         if this is the Live Controller also display on the screen
         """
-        row = self.preview_list_widget.currentRow()
+        row = self.preview_widget.current_slide_number()
         self.selected_row = 0
-        if -1 < row < self.preview_list_widget.rowCount():
+        if -1 < row < self.preview_widget.slide_count():
             if self.service_item.is_command():
                 if self.is_live and not start:
                     Registry().execute(u'%s_slide' % self.service_item.name.lower(),
@@ -1035,7 +979,7 @@
                     self.service_item.bg_image_bytes = None
             self.updatePreview()
             self.selected_row = row
-            self.__checkUpdateSelectedSlide(row)
+            self.preview_widget.change_slide(row)
         Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row)
         self.display.setFocus()
 
@@ -1043,7 +987,7 @@
         """
         The slide has been changed. Update the slidecontroller accordingly
         """
-        self.__checkUpdateSelectedSlide(row)
+        self.preview_widget.change_slide(row)
         self.updatePreview()
         Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row)
 
@@ -1089,8 +1033,8 @@
         if self.service_item.is_command() and self.is_live:
             self.updatePreview()
         else:
-            row = self.preview_list_widget.currentRow() + 1
-            if row == self.preview_list_widget.rowCount():
+            row = self.preview_widget.current_slide_number() + 1
+            if row == self.preview_widget.slide_count():
                 if wrap is None:
                     if self.slide_limits == SlideLimits.Wrap:
                         row = 0
@@ -1098,12 +1042,12 @@
                         self.serviceNext()
                         return
                     else:
-                        row = self.preview_list_widget.rowCount() - 1
+                        row = self.preview_widget.slide_count() - 1
                 elif wrap:
                     row = 0
                 else:
-                    row = self.preview_list_widget.rowCount() - 1
-            self.__checkUpdateSelectedSlide(row)
+                    row = self.preview_widget.slide_count() - 1
+            self.preview_widget.change_slide(row)
             self.slideSelected()
 
     def on_slide_selected_previous(self):
@@ -1116,27 +1060,19 @@
         if self.service_item.is_command() and self.is_live:
             self.updatePreview()
         else:
-            row = self.preview_list_widget.currentRow() - 1
+            row = self.preview_widget.current_slide_number() - 1
             if row == -1:
                 if self.slide_limits == SlideLimits.Wrap:
-                    row = self.preview_list_widget.rowCount() - 1
+                    row = self.preview_widget.slide_count() - 1
                 elif self.is_live and self.slide_limits == SlideLimits.Next:
                     self.keypress_queue.append(ServiceItemAction.PreviousLastSlide)
                     self._process_queue()
                     return
                 else:
                     row = 0
-            self.__checkUpdateSelectedSlide(row)
+            self.preview_widget.change_slide(row)
             self.slideSelected()
 
-    def __checkUpdateSelectedSlide(self, row):
-        """
-        Check if this slide has been updated
-        """
-        if row + 1 < self.preview_list_widget.rowCount():
-            self.preview_list_widget.scrollToItem(self.preview_list_widget.item(row + 1, 0))
-        self.preview_list_widget.selectRow(row)
-
     def onToggleLoop(self):
         """
         Toggles the loop state.
@@ -1151,7 +1087,7 @@
         """
         Start the timer loop running and store the timer id
         """
-        if self.preview_list_widget.rowCount() > 1:
+        if self.preview_widget.slide_count() > 1:
             self.timer_id = self.startTimer(int(self.delay_spin_box.value()) * 1000)
 
     def on_stop_loop(self):
@@ -1261,8 +1197,8 @@
         """
         If preview copy slide item to live controller from Preview Controller
         """
-        row = self.preview_list_widget.currentRow()
-        if -1 < row < self.preview_list_widget.rowCount():
+        row = self.preview_widget.current_slide_number()
+        if -1 < row < self.preview_widget.slide_count():
             if self.service_item.from_service:
                 self.service_manager.preview_live(self.service_item.unique_identifier, row)
             else:

=== added file 'tests/__init__.py'
=== modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py	2013-05-25 05:48:08 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py	2013-06-15 10:58:51 +0000
@@ -2,11 +2,12 @@
     Package to test the openlp.core.lib package.
 """
 import os
-import cPickle
 from unittest import TestCase
 from mock import MagicMock, patch
 
 from openlp.core.lib import ItemCapabilities, ServiceItem, Registry
+from tests.utils.osdinteraction import read_service_from_file
+from tests.utils.constants import TEST_RESOURCES_PATH
 
 
 VERSE = u'The Lord said to {r}Noah{/r}: \n'\
@@ -18,8 +19,6 @@
         'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
 FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
 
-TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources'))
-
 
 class TestServiceItem(TestCase):
 
@@ -78,7 +77,7 @@
         service_item.name = u'test'
 
         # WHEN: adding image to a service item
-        test_image = os.path.join(TEST_PATH, u'church.jpg')
+        test_image = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
         service_item.add_from_image(test_image, u'Image Title')
 
         # THEN: We should get back a valid service item
@@ -133,8 +132,8 @@
         service_item.name = u'test'
 
         # WHEN: adding image to a service item
-        test_file = os.path.join(TEST_PATH, u'church.jpg')
-        service_item.add_from_command(TEST_PATH, u'church.jpg', test_file)
+        test_file = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
+        service_item.add_from_command(TEST_RESOURCES_PATH, u'church.jpg', test_file)
 
         # THEN: We should get back a valid service item
         assert service_item.is_valid is True, u'The new service item should be valid'
@@ -151,7 +150,7 @@
         assert len(service) == 2, u'The saved service should have two parts'
         assert service[u'header'][u'name'] == u'test', u'A test plugin should be returned'
         assert service[u'data'][0][u'title'] == u'church.jpg', u'The first title name should be "church,jpg"'
-        assert service[u'data'][0][u'path'] == TEST_PATH, u'The path should match the input path'
+        assert service[u'data'][0][u'path'] == TEST_RESOURCES_PATH, u'The path should match the input path'
         assert service[u'data'][0][u'image'] == test_file, u'The image should match the full path to image'
 
         # WHEN validating a service item
@@ -170,13 +169,12 @@
         """
         Test the Service Item - adding a custom slide from a saved service
         """
-        # GIVEN: A new service item and a mocked add icon function
+        # GIVEN: A new service item
         service_item = ServiceItem(None)
-        service_item.add_icon = MagicMock()
 
         # WHEN: adding a custom from a saved Service
-        line = self.convert_file_service_item(u'serviceitem_custom_1.osd')
-        service_item.set_from_service(line)
+        service = read_service_from_file(u'serviceitem_custom_1.osd')
+        service_item.set_from_service(service[0])
 
         # THEN: We should get back a valid service item
         assert service_item.is_valid is True, u'The new service item should be valid'
@@ -195,18 +193,17 @@
         """
         Test the Service Item - adding an image from a saved service
         """
-        # GIVEN: A new service item and a mocked add icon function
+        # GIVEN: A new service item
         image_name = u'image_1.jpg'
-        test_file = os.path.join(TEST_PATH, image_name)
+        test_file = os.path.join(TEST_RESOURCES_PATH, image_name)
         frame_array = {u'path': test_file, u'title': image_name}
 
         service_item = ServiceItem(None)
-        service_item.add_icon = MagicMock()
 
         # WHEN: adding an image from a saved Service and mocked exists
-        line = self.convert_file_service_item(u'serviceitem_image_1.osd')
+        service = read_service_from_file(u'serviceitem_image_1.osd')
         with patch('os.path.exists'):
-            service_item.set_from_service(line, TEST_PATH)
+            service_item.set_from_service(service[0], TEST_RESOURCES_PATH)
 
         # THEN: We should get back a valid service item
         assert service_item.is_valid is True, u'The new service item should be valid'
@@ -229,7 +226,7 @@
         """
         Test the Service Item - adding an image from a saved local service
         """
-        # GIVEN: A new service item and a mocked add icon function
+        # GIVEN: A new service item
         image_name1 = u'image_1.jpg'
         image_name2 = u'image_2.jpg'
         test_file1 = os.path.join(u'/home/openlp', image_name1)
@@ -238,12 +235,11 @@
         frame_array2 = {u'path': test_file2, u'title': image_name2}
 
         service_item = ServiceItem(None)
-        service_item.add_icon = MagicMock()
 
         # WHEN: adding an image from a saved Service and mocked exists
-        line = self.convert_file_service_item(u'serviceitem_image_2.osd')
+        service = read_service_from_file(u'serviceitem_image_2.osd')
         with patch('os.path.exists'):
-            service_item.set_from_service(line)
+            service_item.set_from_service(service[0])
 
         # THEN: We should get back a valid service item
         assert service_item.is_valid is True, u'The new service item should be valid'
@@ -286,16 +282,3 @@
         assert service_item.title is None, u'The title should be set to a value'
         assert service_item.is_capable(ItemCapabilities.HasDetailedTitleDisplay) is False, \
             u'The Capability should have been removed'
-
-    def convert_file_service_item(self, name):
-        service_file = os.path.join(TEST_PATH, name)
-        try:
-            open_file = open(service_file, u'r')
-            items = cPickle.load(open_file)
-            first_line = items[0]
-        except IOError:
-            first_line = u''
-        finally:
-            open_file.close()
-        return first_line
-

=== added file 'tests/interfaces/openlp_core_ui/test_listpreviewwidget.py'
--- tests/interfaces/openlp_core_ui/test_listpreviewwidget.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_core_ui/test_listpreviewwidget.py	2013-06-15 10:58:51 +0000
@@ -0,0 +1,88 @@
+"""
+    Package to test the openlp.core.ui.listpreviewwidget.
+"""
+
+from unittest import TestCase
+from mock import MagicMock, patch
+
+from PyQt4 import QtGui
+
+from openlp.core.lib import Registry, ServiceItem
+from openlp.core.ui import listpreviewwidget
+from tests.utils.osdinteraction import read_service_from_file
+
+class TestListPreviewWidget(TestCase):
+
+    def setUp(self):
+        """
+        Create the UI.
+        """
+        Registry.create()
+        self.app = QtGui.QApplication([])
+        self.main_window = QtGui.QMainWindow()
+        self.image = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
+        self.image_manager = MagicMock()
+        self.image_manager.get_image.return_value = self.image
+        Registry().register(u'image_manager', self.image_manager)
+        self.preview_widget = listpreviewwidget.ListPreviewWidget(self.main_window, 2)
+
+    def tearDown(self):
+        """
+        Delete all the C++ objects at the end so that we don't have a segfault.
+        """
+        del self.preview_widget
+        del self.main_window
+        del self.app
+
+    def initial_slide_count_test(self):
+        """
+        Test the inital slide count.
+        """
+        # GIVEN: A new ListPreviewWidget instance.
+        # WHEN: No SlideItem has been added yet.
+        # THEN: The count of items should be zero.
+        self.assertEqual(self.preview_widget.slide_count(), 0,
+            u'The slide list should be empty.')
+
+    def initial_slide_number_test(self):
+        """
+        Test the inital slide number.
+        """
+        # GIVEN: A new ListPreviewWidget instance.
+        # WHEN: No SlideItem has been added yet.
+        # THEN: The number of the current item should be -1.
+        self.assertEqual(self.preview_widget.current_slide_number(), -1,
+            u'The slide number should be -1.')
+
+    def replace_service_item_test(self):
+        """
+        Test item counts and current number with a service item.
+        """
+        # GIVEN: A ServiceItem with two frames.
+        service_item = ServiceItem(None)
+        service = read_service_from_file(u'serviceitem_image_2.osd')
+        with patch('os.path.exists'):
+            service_item.set_from_service(service[0])
+        # WHEN: Added to the preview widget.
+        self.preview_widget.replace_service_item(service_item, 1, 1)
+        # THEN: The slide count and number should fit.
+        self.assertEqual(self.preview_widget.slide_count(), 2,
+                         u'The slide count should be 2.')
+        self.assertEqual(self.preview_widget.current_slide_number(), 1,
+                         u'The current slide number should  be 1.')
+
+    def change_slide_test(self):
+        """
+        Test the change_slide method.
+        """
+        # GIVEN: A ServiceItem with two frames content.
+        service_item = ServiceItem(None)
+        service = read_service_from_file(u'serviceitem_image_2.osd')
+        with patch('os.path.exists'):
+            service_item.set_from_service(service[0])
+        # WHEN: Added to the preview widget and switched to the second frame.
+        self.preview_widget.replace_service_item(service_item, 1, 0)
+        self.preview_widget.change_slide(1)
+        # THEN: The current_slide_number should reflect the change.
+        self.assertEqual(self.preview_widget.current_slide_number(), 1,
+                         u'The current slide number should  be 1.')

=== added directory 'tests/utils'
=== added file 'tests/utils/__init__.py'
=== added file 'tests/utils/constants.py'
--- tests/utils/constants.py	1970-01-01 00:00:00 +0000
+++ tests/utils/constants.py	2013-06-15 10:58:51 +0000
@@ -0,0 +1,5 @@
+
+import os
+
+OPENLP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..'))
+TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources')

=== added file 'tests/utils/osdinteraction.py'
--- tests/utils/osdinteraction.py	1970-01-01 00:00:00 +0000
+++ tests/utils/osdinteraction.py	2013-06-15 10:58:51 +0000
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2013 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2013 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                          #
+###############################################################################
+"""
+The :mod:`osdinteraction` provides miscellaneous functions for interacting with
+OSD files.
+"""
+
+import os
+import cPickle
+
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+
+def read_service_from_file(file_name):
+    """
+    Reads an OSD file and returns the first service item found therein.
+    @param file_name: File name of an OSD file residing in the tests/resources folder.
+    @return: The service contained in the file.
+    """
+    service_file = os.path.join(TEST_RESOURCES_PATH, file_name)
+    with open(service_file, u'r') as open_file:
+        service = cPickle.load(open_file)
+    return service


References