← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~arjan-i/openlp/images_groups into lp:openlp

 

Arjan Schrijver has proposed merging lp:~arjan-i/openlp/images_groups into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
  Andreas Preikschat (googol)
  Raoul Snyman (raoul-snyman)

For more details, see:
https://code.launchpad.net/~arjan-i/openlp/images_groups/+merge/151525

This branch implements the 'images groups' feature. It has the following features:
- convert old 'images files' setting to SQLite database
- existing images are loaded into a special 'Imported' group
- groups can be nested
- images can be moved between groups by drag&drop
- new images can be added directly to a group by drag&drop, or by clicking the 'Add images' button
- image groups can be used as service items with multiple images


Okay, ready for another try.
All previous comments and ideas have been implemented/fixed.
Only thing missing now is loading images in a separate thread, but maybe I can implement that post-merge?
-- 
https://code.launchpad.net/~arjan-i/openlp/images_groups/+merge/151525
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2013-03-01 17:45:17 +0000
+++ openlp/core/lib/__init__.py	2013-03-04 14:42:22 +0000
@@ -391,6 +391,7 @@
 from screen import ScreenList
 from settings import Settings
 from listwidgetwithdnd import ListWidgetWithDnD
+from treewidgetwithdnd import TreeWidgetWithDnD
 from formattingtags import FormattingTags
 from spelltextedit import SpellTextEdit
 from plugin import PluginStatus, StringContent, Plugin

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2013-02-19 09:56:36 +0000
+++ openlp/core/lib/mediamanageritem.py	2013-03-04 14:42:22 +0000
@@ -338,7 +338,7 @@
             self.validateAndLoad(files)
         self.application.set_normal_cursor()
 
-    def loadFile(self, files):
+    def loadFile(self, data):
         """
         Turn file from Drag and Drop into an array so the Validate code can run it.
 
@@ -347,7 +347,7 @@
         """
         new_files = []
         error_shown = False
-        for file_name in files:
+        for file_name in data['files']:
             file_type = file_name.split(u'.')[-1]
             if file_type.lower() not in self.onNewFileMasks:
                 if not error_shown:
@@ -357,9 +357,15 @@
             else:
                 new_files.append(file_name)
         if new_files:
-            self.validateAndLoad(new_files)
-
-    def validateAndLoad(self, files):
+            self.validateAndLoad(new_files, data['target'])
+
+    def dnd_move_internal(self, target):
+        """
+        Handle internal moving of media manager items
+        """
+        pass
+
+    def validateAndLoad(self, files, target_group=None):
         """
         Process a list for files either from the File Dialog or from Drag and
         Drop
@@ -374,16 +380,17 @@
             full_list.append(self.listView.item(count).data(QtCore.Qt.UserRole))
         duplicates_found = False
         files_added = False
-        for file in files:
-            filename = os.path.split(unicode(file))[1]
+        for file_path in files:
+            filename = os.path.split(unicode(file_path))[1]
             if filename in names:
                 duplicates_found = True
             else:
                 files_added = True
                 full_list.append(file)
         if full_list and files_added:
-            self.listView.clear()
-            self.loadList(full_list)
+            if target_group is None:
+                self.listView.clear()
+            self.loadList(full_list, target_group)
             last_dir = os.path.split(unicode(files[0]))[0]
             Settings().setValue(self.settingsSection + u'/last directory', last_dir)
             Settings().setValue(u'%s/%s files' % (self.settingsSection, self.settingsSection), self.getFileList())
@@ -416,7 +423,7 @@
             count += 1
         return file_list
 
-    def loadList(self, list):
+    def loadList(self, list, target_group):
         """
         Load a list. Needs to be implemented by the plugin.
         """

=== modified file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py	2013-02-07 08:42:17 +0000
+++ openlp/core/lib/plugin.py	2013-03-04 14:42:22 +0000
@@ -216,6 +216,12 @@
         if self.mediaItemClass:
             self.mediaItem = self.mediaItemClass(self.main_window.mediaDockManager.media_dock, self, self.icon)
 
+    def upgrade_settings(self, settings):
+        """
+        Upgrade the settings of this plugin.
+        """
+        pass
+
     def addImportMenuItem(self, importMenu):
         """
         Create a menu item and add it to the "Import" menu.
@@ -300,24 +306,10 @@
         # FIXME: Remove after 2.2 release.
         # This is needed to load the list of images/media/presentation from the config saved
         # before the settings rewrite.
-        if self.mediaItemClass is not None:
-            # We need QSettings instead of Settings here to bypass our central settings dict.
-            # Do NOT do this anywhere else!
-            settings = QtCore.QSettings()
-            settings.beginGroup(self.settingsSection)
-            if settings.contains(u'%s count' % self.name):
-                list_count = int(settings.value(u'%s count' % self.name, 0))
-                loaded_list = []
-                if list_count:
-                    for counter in range(list_count):
-                        item = settings.value(u'%s %d' % (self.name, counter), u'')
-                        if item:
-                            loaded_list.append(item)
-                        settings.remove(u'%s %d' % (self.name, counter))
-                settings.remove(u'%s count' % self.name)
-                # Now save the list to the config using our Settings class.
-                Settings().setValue(u'%s/%s files' % (self.settingsSection, self.name), loaded_list)
-            settings.endGroup()
+        if self.mediaItemClass is not None and self.name != u'images':
+            loaded_list = Settings().get_files_from_config(self)
+            # Now save the list to the config using our Settings class.
+            Settings().setValue(u'%s/%s files' % (self.settingsSection, self.name), loaded_list)
 
     def usesTheme(self, theme):
         """

=== modified file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py	2013-02-22 07:15:07 +0000
+++ openlp/core/lib/pluginmanager.py	2013-03-04 14:42:22 +0000
@@ -183,6 +183,17 @@
             if plugin.status is not PluginStatus.Disabled:
                 plugin.addToolsMenuItem(self.main_window.tools_menu)
 
+    def hook_upgrade_plugin_settings(self, settings):
+        """
+        Loop through all the plugins and give them an opportunity to upgrade their settings.
+
+        ``settings``
+            The Settings object containing the old settings.
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.upgrade_settings(settings)
+
     def initialise_plugins(self):
         """
         Loop through all the plugins and give them an opportunity to

=== modified file 'openlp/core/lib/settings.py'
--- openlp/core/lib/settings.py	2013-02-21 21:26:24 +0000
+++ openlp/core/lib/settings.py	2013-03-04 14:42:22 +0000
@@ -439,3 +439,32 @@
         if isinstance(default_value, int):
             return int(setting)
         return setting
+
+    def get_files_from_config(self, plugin):
+        """
+        This removes the settings needed for old way we saved files (e. g. the image paths for the image plugin). A list
+        of file paths are returned.
+
+        **Note**: Only a list of paths is returned; this does not convert anything!
+
+        ``plugin``
+            The Plugin object.The caller has to convert/save the list himself; o
+        """
+        files_list = []
+        # We need QSettings instead of Settings here to bypass our central settings dict.
+        # Do NOT do this anywhere else!
+        settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
+        settings.beginGroup(plugin.settingsSection)
+        if settings.contains(u'%s count' % plugin.name):
+            # Get the count.
+            list_count = int(settings.value(u'%s count' % plugin.name, 0))
+            if list_count:
+                for counter in range(list_count):
+                    # The keys were named e. g.: "image 0"
+                    item = settings.value(u'%s %d' % (plugin.name, counter), u'')
+                    if item:
+                        files_list.append(item)
+                    settings.remove(u'%s %d' % (plugin.name, counter))
+            settings.remove(u'%s count' % plugin.name)
+        settings.endGroup()
+        return files_list

=== added file 'openlp/core/lib/treewidgetwithdnd.py'
--- openlp/core/lib/treewidgetwithdnd.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/treewidgetwithdnd.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,137 @@
+# -*- 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                          #
+###############################################################################
+"""
+Extend QTreeWidget to handle drag and drop functionality
+"""
+import os
+
+from PyQt4 import QtCore, QtGui
+
+from openlp.core.lib import Registry
+
+
+class TreeWidgetWithDnD(QtGui.QTreeWidget):
+    """
+    Provide a tree widget to store objects and handle drag and drop events
+    """
+    def __init__(self, parent=None, name=u''):
+        """
+        Initialise the tree widget
+        """
+        QtGui.QTreeWidget.__init__(self, parent)
+        self.mimeDataText = name
+        self.allow_internal_dnd = False
+        self.header().close()
+        self.defaultIndentation = self.indentation()
+        self.setIndentation(0)
+        self.setAnimated(True)
+        assert(self.mimeDataText)
+
+    def activateDnD(self):
+        """
+        Activate DnD of widget
+        """
+        self.setAcceptDrops(True)
+        self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
+        Registry().register_function((u'%s_dnd' % self.mimeDataText), self.parent().loadFile)
+        Registry().register_function((u'%s_dnd_internal' % self.mimeDataText), self.parent().dnd_move_internal)
+
+    def mouseMoveEvent(self, event):
+        """
+        Drag and drop event does not care what data is selected
+        as the recipient will use events to request the data move
+        just tell it what plugin to call
+        """
+        if event.buttons() != QtCore.Qt.LeftButton:
+            event.ignore()
+            return
+        if not self.selectedItems():
+            event.ignore()
+            return
+        drag = QtGui.QDrag(self)
+        mimeData = QtCore.QMimeData()
+        drag.setMimeData(mimeData)
+        mimeData.setText(self.mimeDataText)
+        drag.start(QtCore.Qt.CopyAction)
+
+    def dragEnterEvent(self, event):
+        if event.mimeData().hasUrls():
+            event.accept()
+        elif self.allow_internal_dnd:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        QtGui.QTreeWidget.dragMoveEvent(self, event)
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        elif self.allow_internal_dnd:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        """
+        Receive drop event, check if it is a file or internal object and process it if it is.
+
+        ``event``
+            Handle of the event pint passed
+        """
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            files = []
+            for url in event.mimeData().urls():
+                localFile = url.toLocalFile()
+                if os.path.isfile(localFile):
+                    files.append(localFile)
+                elif os.path.isdir(localFile):
+                    listing = os.listdir(localFile)
+                    for file_name in listing:
+                        files.append(os.path.join(localFile, file_name))
+            Registry().execute(u'%s_dnd' % self.mimeDataText, {'files': files, 'target': self.itemAt(event.pos())})
+        elif self.allow_internal_dnd:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            Registry().execute(u'%s_dnd_internal' % self.mimeDataText, self.itemAt(event.pos()))
+        else:
+            event.ignore()
+
+    # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple.
+    def addItem(self, item):
+        self.addTopLevelItem(item)
+
+    def count(self):
+        return self.topLevelItemCount()
+
+    def item(self, index):
+        return self.topLevelItem(index)

=== modified file 'openlp/core/lib/uistrings.py'
--- openlp/core/lib/uistrings.py	2013-02-01 19:58:18 +0000
+++ openlp/core/lib/uistrings.py	2013-03-04 14:42:22 +0000
@@ -58,6 +58,7 @@
         """
         self.About = translate('OpenLP.Ui', 'About')
         self.Add = translate('OpenLP.Ui', '&Add')
+        self.AddGroup = translate('OpenLP.Ui', 'Add group')
         self.Advanced = translate('OpenLP.Ui', 'Advanced')
         self.AllFiles = translate('OpenLP.Ui', 'All Files')
         self.Automatic = translate('OpenLP.Ui', 'Automatic')

=== modified file 'openlp/core/resources.py'
--- openlp/core/resources.py	2012-12-29 20:56:56 +0000
+++ openlp/core/resources.py	2013-03-04 14:42:22 +0000
@@ -54736,6 +54736,156 @@
 \x89\x3e\x62\x80\x70\x10\xc3\x84\x9d\xb0\x11\x66\xa2\x8b\xe8\x6c\
 \x7e\xaa\x0f\xd9\x9d\x89\x07\xa1\xb5\x4b\xa0\x00\x00\x00\x00\x49\
 \x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x06\x1a\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\
+\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdb\x04\x1e\
+\x15\x05\x2e\x4a\x27\x58\x31\x00\x00\x05\x9a\x49\x44\x41\x54\x58\
+\xc3\xed\x97\x3d\xa8\x6d\x57\x11\xc7\x7f\xb3\xd6\xda\xfb\xdc\xeb\
+\x7b\xde\x24\x48\x22\x1a\x7d\x88\x36\x0a\x06\x21\x29\x83\x4d\x0a\
+\x31\x58\xa4\x30\xa4\xb1\x10\x3b\x1b\x4b\x8b\x80\x8d\x8d\x76\xd6\
+\x16\x06\xb4\x13\x2c\x6d\x44\x50\xb1\x08\x04\x12\x25\x8f\x18\x62\
+\x11\xcd\xcb\x33\x79\x1f\xbe\xfb\xee\xbb\x79\xf7\x9e\x8f\xfd\xb1\
+\x66\x2c\x66\xd6\xdd\xe7\x11\x04\x0b\x25\x8d\x07\x0e\x7b\x9f\x75\
+\x67\xad\x99\xf9\xcf\x7f\xfe\x6b\x2e\x7c\xd4\x9f\x17\x5e\xfc\xdd\
+\x7f\x6c\xfb\xfc\x0f\x7e\xff\x5f\xf7\x2f\x00\xcf\xbf\xf8\x87\xd7\
+\x86\x71\xf7\x94\x98\xa0\xe2\x8b\x19\x98\x01\xc1\x48\x29\xd3\x77\
+\xe5\x2f\xbf\xfa\xd1\x33\x4f\xfc\x4f\x02\xf8\xda\x77\x7f\x69\xdf\
+\xfe\xd6\x37\xb8\x75\x67\x64\xae\xa0\x06\x25\xf9\x5f\x0f\x8a\x60\
+\xba\xe1\xb7\x7f\x7c\xdd\x72\x29\x82\x08\x02\x18\x60\xe6\x36\x62\
+\x60\x02\x28\x68\x1c\x2a\xe2\xe7\x08\x90\x80\x1a\x4f\xff\x18\x66\
+\xc3\xb3\xbf\xfe\xc9\x73\xbf\x29\x00\xc3\x70\x9f\x57\xdf\x3c\xe5\
+\x9f\xa7\x5b\x4a\x1c\x2e\xe2\x0e\x0c\xf8\xec\x27\x7b\xbe\xfc\xc4\
+\x57\xa4\x64\xc1\x30\xb2\xb8\xc3\x92\x05\x80\x94\x84\x2c\x20\x22\
+\x74\xc9\xf7\x20\x42\x4e\x90\x04\x04\x7f\x37\x20\x27\xa8\x92\xec\
+\xe5\x57\xde\xf8\x1e\xe0\x01\xe8\xbc\xe5\xde\xe9\x39\xd9\x26\x66\
+\x15\x77\x1e\x99\x18\x70\xfd\xe6\x8e\x69\xae\xd4\x71\xc3\x6e\xd8\
+\xa1\xb5\xe2\x38\x84\xb3\x28\x15\x91\x29\x0a\x29\x81\xa9\x41\xf2\
+\xcc\x67\x83\x2c\x50\xab\xf0\xa9\xc7\x1e\x92\x5b\x37\xaf\xf5\x00\
+\x05\xc0\x52\x66\x33\x6c\x40\xf5\x02\xd6\xe4\xe7\xf8\x66\x55\xe6\
+\xdd\x39\xc7\x77\xde\xe3\xf8\xd6\x0d\xc6\x71\x70\x38\x5b\xa0\x06\
+\xd6\x42\x12\x2e\x02\x13\x96\xf7\x8b\x48\x13\xdc\xbc\xf6\x71\xee\
+\xdf\x7b\x6f\xba\x08\x60\x7b\x7e\xc6\xbd\xe3\xf7\x31\x53\x72\x38\
+\x6e\x28\x60\x80\x56\xce\xcf\x3e\xe0\xc6\xbb\x6f\xf3\xf3\x97\x7e\
+\x48\xea\x0a\x27\x67\x20\xe9\xdf\x30\xcb\x3e\xcc\xb4\x07\x02\x52\
+\xc0\x78\xf6\xee\xc9\x8f\x5f\x2b\x00\xff\x78\xe7\x2d\xee\x1c\xaf\
+\x31\x53\xa4\x65\xd0\x08\x06\xa8\x29\x75\x1c\x78\xec\xd1\x47\xf8\
+\x60\x53\xf8\xf3\x55\x63\x00\xfa\x46\xc6\xe6\x27\x08\x99\x62\xad\
+\xe2\xb0\xcf\xe6\x99\x2a\x0b\x49\x53\x16\xce\x8e\xaf\x3f\x55\xbe\
+\xff\xb3\x3f\xfd\xed\xab\x4f\x3f\x69\xd3\xc4\x42\xef\x7d\xec\xf6\
+\xd6\xd6\x6b\xb8\xfa\x57\x85\x64\x94\x60\xbe\x98\x13\x2d\x1b\x4c\
+\x41\x4e\x0c\xaa\x2c\x9d\xd2\x08\x88\x41\xd7\xce\x96\xcc\xe9\xc9\
+\x6d\xca\xdb\xd7\xee\x7f\xfe\xd2\x0a\xd6\xb3\xd2\x45\xc6\x3b\x83\
+\x55\x23\x54\x64\x61\x06\x2a\x70\x98\x8d\xd9\x84\x3e\x81\x6a\xb4\
+\x97\xc1\x18\x4f\x42\x3f\x52\x20\x31\x0b\x88\x7a\x5b\x57\xf3\xaf\
+\x02\x1d\x30\x6e\xd7\x94\x79\xda\x52\x13\xf4\xd9\xfb\x76\x06\xfa\
+\x88\xb2\xc6\x7b\xb5\xa5\x8e\x93\x09\x09\xd8\xd5\x05\x28\x88\x00\
+\x80\x12\x90\x27\x1c\x19\x0b\xcc\xc7\xba\x00\xaa\xc0\x60\x5e\xda\
+\x92\x80\x5a\x61\xae\xae\x7e\x29\x04\x44\x81\x95\xc0\xa4\xd0\x27\
+\x18\x14\x0e\xc5\x9f\xb6\x87\x4c\x8d\xa0\x4b\x70\x20\x05\xc9\x7a\
+\x81\xad\xfa\x99\x35\x02\x9b\x82\x0b\x86\x97\x22\x1b\x94\x6a\x0b\
+\x5c\xd5\xa0\x33\x18\xcd\x0f\xab\xea\xb5\x5c\xcf\x70\x00\x6c\x65\
+\xc9\x0c\xf5\x92\x68\x70\x00\x83\x09\x18\xe2\xf0\x4d\x38\x6b\x28\
+\xcc\xb1\xae\xc1\x8b\x39\x92\x49\x62\x30\xab\x1b\xa9\xc2\x4e\xdd\
+\xb0\x6a\x1c\xee\x9c\x63\x17\x81\xaa\xfa\xdf\x30\x18\x23\x43\x55\
+\x0f\xfa\x80\x65\x1d\xf5\x73\x35\x6c\xab\xb9\xe3\xaa\xfe\x2c\x16\
+\x92\xdf\x7a\x7e\xa7\xd6\x74\x82\x5d\x75\xd2\x98\x3e\xd8\x3a\x16\
+\xc8\x0c\xba\x68\xfc\x2e\xea\x71\x39\xc1\x46\x97\x0b\x46\x81\x83\
+\x58\x6b\x5c\x19\xcd\xcb\x71\x98\x22\x51\x33\x0a\x06\xd3\xec\x3b\
+\x2c\x36\xf6\x51\x8e\x29\xd8\xdb\xca\xb1\xdf\xa5\x29\xd8\x7e\x88\
+\x97\xe2\x6c\x0e\x1d\x08\xc8\x33\x8e\xc4\x7e\x57\x2b\x70\x20\xb0\
+\x9b\x21\x97\x40\x55\x82\x91\xad\x47\x25\xe0\x9e\xc3\xb9\x9a\x93\
+\x34\xc5\xcd\x36\x86\xad\x0a\xf4\x61\x3b\x05\x52\xd9\x9c\xcc\x12\
+\xb6\x35\x6c\x4d\x7c\xbf\x85\xed\x8c\xb7\x66\x6a\x25\xb8\x94\x60\
+\x3b\x39\x4b\xa7\x80\xb4\x34\xb2\xd8\xc2\x70\x31\x38\xcc\x8e\x58\
+\x0d\xdb\x96\xe1\x4a\x3c\x91\xdc\xa4\x37\x04\x48\x15\x74\xf6\x00\
+\xa7\x38\xb7\x8f\x8e\x50\x85\x64\x06\x75\x8e\x0b\x45\x9d\x48\x16\
+\x41\xec\x14\x56\xc9\x37\xd5\x20\xd7\x34\x7b\xe4\x35\x6c\x5b\x07\
+\x6d\xd4\xed\xba\xa8\x93\x2a\x8c\xcd\xd6\xbc\xac\x5d\x70\x60\xab\
+\x0e\x7d\x0f\x94\x24\x89\x22\x60\x25\x5f\xd4\xec\xa1\xe4\xd1\xae\
+\x22\xbb\x51\x96\x16\xc2\xa0\xcb\x4b\x7d\x2f\x25\x6f\xbd\x23\x60\
+\x4b\x74\x4e\x5a\xa4\x7c\x15\x02\x56\xcd\x33\xcf\x02\x97\x81\x41\
+\x60\x22\x21\x57\x5e\xf8\xa9\xe5\xf2\x09\xa6\xb8\x88\x9a\x1e\xb4\
+\xcb\xe3\xe2\x82\x0b\x58\x1b\x7c\xd2\x32\x8d\xf1\xed\xa2\x53\x62\
+\x5d\x42\x03\xaa\x2d\x17\x95\xed\xdd\x92\x39\x65\xb6\xf7\xae\x53\
+\x1e\x7f\xe4\x88\xd5\xd1\x97\x28\x36\x2f\x63\xd6\xfe\xf5\x1d\x07\
+\xda\x5e\x20\xcd\x20\x45\xab\xee\xdf\xbe\x79\x6f\x14\xab\xb1\x37\
+\x45\x77\xec\x8f\x6c\x2a\x99\xd3\xdd\x6d\x8a\xaa\x32\x0d\x03\xa3\
+\xd5\xc5\x09\x0f\x0e\x24\xda\xe4\xb3\x21\xc1\xa2\x80\x31\xf4\x7c\
+\xc8\xb6\x44\x7b\xb6\x69\xa8\x49\x71\x17\xa8\xe4\x94\x18\xe7\x4a\
+\xf9\xe2\x95\xcf\xf0\x85\x2b\x4f\x82\x98\xf7\x6f\xe8\xbf\x8a\x6b\
+\xff\x5a\x21\x37\x41\xcf\x20\xd5\x33\x6f\x53\xb3\x6a\x9b\xfb\xfc\
+\x3b\xed\x4d\xa0\xa2\xb0\xca\x4e\xd8\x91\xa5\xcd\x27\x83\x55\x16\
+\xfe\x7e\x79\x4d\xb9\x79\x77\xcd\xd1\x25\x98\x6c\x66\x32\xc8\x0a\
+\xa5\x38\xc9\xba\xbd\x8c\x14\xbf\x94\x74\xef\x9e\x50\x85\x2e\x39\
+\x49\x53\x75\x67\x63\x68\x44\x8a\xc9\xda\x22\xc8\xa6\xb2\x43\xe8\
+\x47\x97\x0b\x37\x6e\x9f\x53\xcc\x60\xb0\x13\x36\xb5\xba\xf0\xc4\
+\xa6\x55\xf2\x20\xe6\xa8\x49\x1f\xbf\x53\x82\xf3\x1a\xec\x4e\xfe\
+\x2d\x0a\xb9\x77\x2d\x69\xce\x15\xa8\x09\x76\x53\x8c\xe8\x91\x40\
+\xae\xa0\x1d\xec\xa6\x84\xa1\x94\x92\xba\xf7\xc7\xfb\x77\x1f\xcf\
+\xa2\x58\x0e\x19\xed\x84\xd3\x31\x7a\x35\xb9\xba\xad\x81\xbe\x73\
+\x1d\x38\xc0\xe7\xc1\x41\xe1\xa0\x87\xcd\xe8\x0e\x4a\x81\x3a\x04\
+\xfc\x02\xab\xe2\xea\xd3\x65\x17\xa9\x2a\xce\xb2\x71\x0d\x86\x72\
+\xf4\xf0\xa7\xaf\x3a\xa7\x1f\xfe\xdc\x77\x48\xf9\x63\xbe\xd5\x04\
+\xc3\x87\x2b\xd3\xb4\xc7\xfd\x84\x59\x0a\x24\xc5\xdf\x2d\x61\x26\
+\x48\x52\x04\x05\x31\x84\xea\xd5\xc7\x30\x0c\x11\xbf\x47\x93\xe8\
+\xc5\x6f\x41\x31\xdd\x70\xf2\xce\x2f\xe0\xeb\x1f\xe1\x3f\xa6\xdf\
+\xe4\xff\x1f\xfe\x05\xfe\x09\x43\x47\xb1\x8e\x8c\x2b\x00\x00\x00\
+\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
+\x00\x00\x02\xfa\
+\x89\
+\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
+\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\
+\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdb\x05\x08\
+\x12\x0c\x27\xaf\x64\x6c\xaa\x00\x00\x02\x7a\x49\x44\x41\x54\x38\
+\xcb\x75\x93\x4b\x48\x94\x51\x14\xc7\x7f\xe7\x7e\xd7\xc9\xc7\x44\
+\x0f\xe8\x81\x15\x11\xd4\x2a\x2b\xe8\xe1\xae\xd2\x4a\x43\x82\x28\
+\x1a\x83\x08\x6a\xd5\xaa\x5d\x41\x24\x04\x6d\x02\x0b\x5a\x44\x11\
+\xb4\xab\x55\xf4\x58\x48\x60\x14\x49\x2e\x4c\xa4\xa0\x24\x7a\x40\
+\x0a\x16\xbd\x4c\x1c\x2b\xc7\x79\x98\xf3\x7d\xf7\x9e\x16\xdf\x8c\
+\x8c\x50\x77\x73\xe1\xc0\xf9\xfd\x7f\xe7\x70\x2f\xfc\xe7\xa4\xce\
+\x77\xcf\x2d\x5c\xe2\x50\x53\x77\xf3\x87\xa3\xcf\x8e\xba\xa6\xee\
+\xe6\x21\x2e\x71\x08\x40\x52\xe7\x1f\xbe\x70\x62\x1a\x51\x10\x00\
+\x15\x6c\x22\x78\x7f\xef\x5c\x4b\xc3\x6c\xf3\x55\x0e\xb4\x6c\x6d\
+\xe9\xda\xb5\x64\x8b\x9e\x5d\xdb\x29\x17\x47\x3a\xb4\x77\xfc\x95\
+\xf4\x0c\xf4\xa4\xa4\xed\xd4\x6d\x6d\xdd\xdb\x86\xba\x10\x63\x84\
+\x20\xb0\x3c\xee\xed\xd3\x44\x22\x21\x88\x61\x9e\x5b\xc0\xdd\x55\
+\xad\x34\xac\x4c\xea\xc2\xdc\xa8\xf4\xb5\x2b\x3b\xee\x0b\x93\xc9\
+\x7a\x7d\x3b\x36\x3a\x6c\x73\x99\x09\xa6\x0b\x53\xbc\x1e\x99\xc0\
+\x60\x58\xb3\xac\x86\x9d\x8d\x9b\xc4\x23\x28\x50\xab\x4b\xb8\x33\
+\x3c\xc5\x9b\x13\x19\x29\x0b\xf5\xb5\x2b\x80\xc8\x65\x59\x67\xa7\
+\x7e\x8d\x91\xc9\xe6\x89\xa6\xf3\x04\x06\x86\x3e\x67\x71\xa1\x23\
+\x9d\xfe\x46\x2e\x33\x49\x55\x34\x1f\xda\x40\x6e\x08\x75\xbf\x21\
+\xd7\xa1\x24\x3b\x85\xfc\x22\x60\x0d\xc3\x36\x33\x31\xca\x9b\xc1\
+\x17\x7c\xfc\x9a\x26\x30\x02\x08\x7f\xa6\xb3\xec\xdb\xbd\x99\x83\
+\xfb\x0f\x22\xde\xf0\x34\x6b\xb8\xf0\xe5\x18\xf9\x92\x41\x7e\x11\
+\xb0\x02\x52\xe6\x54\x5a\x52\x1d\x37\xf5\xf4\xc9\x23\x64\x0b\x33\
+\x18\x13\x5b\xaa\x0a\x33\x33\x45\x86\x47\x7e\x13\x46\x4a\x35\xb5\
+\x0c\x26\x1f\x70\xdb\x9f\x21\xfc\x91\x23\x51\x9f\xe4\x78\xcd\x35\
+\xd2\xb7\x3e\x61\x35\xa8\xa5\xff\xe5\x0f\xa2\x62\x31\x6e\x2e\xa5\
+\x18\x11\x34\x80\x2a\x81\x22\x05\x36\xe4\xdb\xb8\xa2\x87\xb1\x0b\
+\x12\x44\xf9\x19\xbc\x73\xf4\x98\xeb\x58\x55\xc1\x08\x50\x4a\x0f\
+\x00\x57\x06\x79\x28\x02\x16\x25\x24\x24\xe4\x57\x09\xaf\x54\xf9\
+\x3a\x50\xb0\xaa\x10\x79\xc5\x79\x45\x80\x08\x30\x80\x53\x10\x89\
+\xdf\x46\xb1\x04\x34\x65\xb0\x82\xf5\x4a\xa4\x8a\x55\x55\xbc\x53\
+\x22\x17\x03\xca\x63\x28\x71\xb3\xaf\x78\x8c\x5a\x31\x62\xe4\x3c\
+\xea\x15\x2b\x22\x18\x13\x10\x18\x3f\x3b\x42\x39\xcd\x97\x6e\xa9\
+\x80\xc5\x75\xc5\x04\x01\x06\xc1\x8e\xfd\xcc\xf0\xe8\xf9\x3b\xd4\
+\x47\x73\x92\x4c\x85\x85\xfe\xe3\xaf\xd8\x44\x0d\xdf\xbe\x8f\x63\
+\x57\x2f\x5f\xcc\xa6\x8d\xeb\x71\x51\x38\x9b\x54\xa9\x5a\x59\xf3\
+\x15\xe0\xc4\xbc\x1a\x06\xc6\x9f\x63\xa7\x0a\xc5\x27\xc5\xd0\xb7\
+\x46\xa1\x22\x02\x5e\xe3\x45\x56\x11\x2f\xb1\xac\x14\x96\x40\x56\
+\x00\x55\x1c\x8e\x42\xa8\x5d\xc2\xd2\x6d\x7b\x88\xc2\xed\x80\xa0\
+\x6a\x10\x04\x55\x99\x1d\x5f\x15\x44\x4a\x02\x12\xcb\x89\x78\x14\
+\xa5\xba\xb6\xff\x2f\x62\x10\x26\x9d\x60\xcb\xdf\xa1\x00\x00\x00\
+\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
 \x00\x00\x0a\x90\
 \x89\
 \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@@ -71235,6 +71385,10 @@
 \x00\x69\
 \x00\x63\x00\x6f\x00\x6e\
 \x00\x06\
+\x07\x03\x7d\xc3\
+\x00\x69\
+\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
+\x00\x06\
 \x06\x8f\x92\xc3\
 \x00\x62\
 \x00\x69\x00\x62\x00\x6c\x00\x65\x00\x73\
@@ -71366,6 +71520,15 @@
 \x00\x62\
 \x00\x69\x00\x62\x00\x6c\x00\x65\x00\x73\x00\x5f\x00\x73\x00\x65\x00\x61\x00\x72\x00\x63\x00\x68\x00\x5f\x00\x75\x00\x6e\x00\x6c\
 \x00\x6f\x00\x63\x00\x6b\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x0f\
+\x09\x74\x2a\xa7\
+\x00\x69\
+\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x5f\x00\x67\x00\x72\x00\x6f\x00\x75\x00\x70\x00\x2e\x00\x70\x00\x6e\x00\x67\
+\x00\x13\
+\x0f\x86\x85\x67\
+\x00\x69\
+\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x5f\x00\x6e\x00\x65\x00\x77\x00\x5f\x00\x67\x00\x72\x00\x6f\x00\x75\x00\x70\x00\x2e\x00\x70\
+\x00\x6e\x00\x67\
 \x00\x15\
 \x00\x76\xd9\xc7\
 \x00\x6f\
@@ -71833,148 +71996,151 @@
 "
 
 qt_resource_struct = "\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x12\x00\x00\x00\x01\
-\x00\x00\x00\xe6\x00\x02\x00\x00\x00\x06\x00\x00\x00\x88\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x84\
-\x00\x00\x00\xb4\x00\x02\x00\x00\x00\x08\x00\x00\x00\x7c\
-\x00\x00\x00\xd6\x00\x02\x00\x00\x00\x14\x00\x00\x00\x68\
-\x00\x00\x00\x2c\x00\x02\x00\x00\x00\x02\x00\x00\x00\x66\
-\x00\x00\x00\x14\x00\x02\x00\x00\x00\x02\x00\x00\x00\x64\
-\x00\x00\x00\xf4\x00\x02\x00\x00\x00\x05\x00\x00\x00\x5f\
-\x00\x00\x01\x2e\x00\x02\x00\x00\x00\x09\x00\x00\x00\x56\
-\x00\x00\x00\x8c\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x4c\
-\x00\x00\x00\xc4\x00\x02\x00\x00\x00\x03\x00\x00\x00\x49\
-\x00\x00\x00\x3c\x00\x02\x00\x00\x00\x10\x00\x00\x00\x39\
-\x00\x00\x00\x62\x00\x02\x00\x00\x00\x03\x00\x00\x00\x36\
-\x00\x00\x00\x78\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\
-\x00\x00\x01\x42\x00\x02\x00\x00\x00\x03\x00\x00\x00\x32\
-\x00\x00\x00\x9e\x00\x02\x00\x00\x00\x08\x00\x00\x00\x2a\
-\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x29\
-\x00\x00\x00\x4e\x00\x02\x00\x00\x00\x11\x00\x00\x00\x18\
-\x00\x00\x01\x06\x00\x02\x00\x00\x00\x05\x00\x00\x00\x13\
-\x00\x00\x04\x4c\x00\x00\x00\x00\x00\x01\x00\x0a\xa7\x31\
-\x00\x00\x03\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x23\xa9\
-\x00\x00\x03\xec\x00\x00\x00\x00\x00\x01\x00\x05\x65\x6d\
-\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x08\x06\x4f\
-\x00\x00\x03\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xc4\x8b\
-\x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x10\xd7\x23\
-\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x10\xde\x6e\
-\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x10\xd1\xc7\
-\x00\x00\x11\x66\x00\x00\x00\x00\x00\x01\x00\x10\xf0\xec\
-\x00\x00\x10\xe8\x00\x00\x00\x00\x00\x01\x00\x10\xe6\x84\
-\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x10\xca\x90\
-\x00\x00\x11\xb8\x00\x00\x00\x00\x00\x01\x00\x10\xf6\xf3\
-\x00\x00\x10\xb0\x00\x00\x00\x00\x00\x01\x00\x10\xe1\xe2\
-\x00\x00\x12\x06\x00\x00\x00\x00\x00\x01\x00\x10\xfc\x1f\
-\x00\x00\x0f\x9a\x00\x00\x00\x00\x00\x01\x00\x10\xcd\x2e\
-\x00\x00\x11\x92\x00\x00\x00\x00\x00\x01\x00\x10\xf4\x1f\
-\x00\x00\x11\x3c\x00\x00\x00\x00\x00\x01\x00\x10\xee\x74\
-\x00\x00\x0f\xee\x00\x00\x00\x00\x00\x01\x00\x10\xd4\xa1\
-\x00\x00\x11\xe2\x00\x00\x00\x00\x00\x01\x00\x10\xf9\xd8\
-\x00\x00\x11\x10\x00\x00\x00\x00\x00\x01\x00\x10\xe9\xc9\
-\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x10\xdb\x71\
-\x00\x00\x10\x3e\x00\x00\x00\x00\x00\x01\x00\x10\xd9\x5a\
-\x00\x00\x03\x66\x00\x00\x00\x00\x00\x01\x00\x00\x21\x92\
-\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x0f\x4c\x08\
-\x00\x00\x0c\xb0\x00\x00\x00\x00\x00\x01\x00\x0f\x52\xce\
-\x00\x00\x0b\xb4\x00\x00\x00\x00\x00\x01\x00\x0f\x45\x04\
-\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x0f\x4e\xa8\
-\x00\x00\x0c\xda\x00\x00\x00\x00\x00\x01\x00\x0f\x55\x99\
-\x00\x00\x0b\xfe\x00\x00\x00\x00\x00\x01\x00\x0f\x4a\x1e\
-\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x0f\x47\xab\
-\x00\x00\x0c\x8e\x00\x00\x00\x00\x00\x01\x00\x0f\x50\x2a\
-\x00\x00\x01\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x05\xe6\
-\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x02\xfe\
-\x00\x00\x0e\xc0\x00\x00\x00\x00\x00\x01\x00\x0f\x70\xe7\
-\x00\x00\x0e\xea\x00\x00\x00\x00\x00\x01\x00\x0f\x76\x51\
-\x00\x00\x0f\x1a\x00\x00\x00\x00\x00\x01\x00\x0f\xe4\xda\
-\x00\x00\x0f\x3a\x00\x00\x00\x00\x00\x01\x00\x0f\xeb\x8b\
-\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x11\x03\x33\
-\x00\x00\x12\x2e\x00\x00\x00\x00\x00\x01\x00\x10\xfe\x32\
-\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x11\x2d\x38\
-\x00\x00\x13\x2e\x00\x00\x00\x00\x00\x01\x00\x11\x0b\x3d\
-\x00\x00\x12\xca\x00\x00\x00\x00\x00\x01\x00\x11\x06\x86\
-\x00\x00\x12\x54\x00\x00\x00\x00\x00\x01\x00\x11\x01\x63\
-\x00\x00\x13\xf6\x00\x00\x00\x00\x00\x01\x00\x11\x1e\x13\
-\x00\x00\x13\x94\x00\x00\x00\x00\x00\x01\x00\x11\x15\xe3\
-\x00\x00\x14\xca\x00\x00\x00\x00\x00\x01\x00\x11\x30\x1a\
-\x00\x00\x14\x70\x00\x00\x00\x00\x00\x01\x00\x11\x29\x5b\
-\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x11\x1a\xef\
-\x00\x00\x14\xfc\x00\x00\x00\x00\x00\x01\x00\x11\x33\xd7\
-\x00\x00\x14\x18\x00\x00\x00\x00\x00\x01\x00\x11\x22\x82\
-\x00\x00\x13\x5c\x00\x00\x00\x00\x00\x01\x00\x11\x13\x6d\
-\x00\x00\x14\x46\x00\x00\x00\x00\x00\x01\x00\x11\x26\xd3\
-\x00\x00\x12\xf8\x00\x00\x00\x00\x00\x01\x00\x11\x08\x8d\
-\x00\x00\x0a\x10\x00\x00\x00\x00\x00\x01\x00\x0e\xec\xf7\
-\x00\x00\x0a\x30\x00\x00\x00\x00\x00\x01\x00\x0e\xf0\xd4\
-\x00\x00\x09\xea\x00\x00\x00\x00\x00\x01\x00\x0e\xea\x59\
-\x00\x00\x0d\x9e\x00\x00\x00\x00\x00\x01\x00\x0f\x5f\xb5\
-\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x0f\x6a\x2a\
-\x00\x00\x0e\x96\x00\x00\x00\x00\x00\x01\x00\x0f\x6e\xe3\
-\x00\x00\x0d\x34\x00\x00\x00\x00\x00\x01\x00\x0f\x5b\xc5\
-\x00\x00\x0e\x18\x00\x00\x00\x00\x00\x01\x00\x0f\x66\xf7\
-\x00\x00\x0d\x68\x00\x00\x00\x00\x00\x01\x00\x0f\x5d\xbc\
-\x00\x00\x0d\xd2\x00\x00\x00\x00\x00\x01\x00\x0f\x61\xb3\
-\x00\x00\x0d\xf4\x00\x00\x00\x00\x00\x01\x00\x0f\x63\xc6\
-\x00\x00\x0e\x72\x00\x00\x00\x00\x00\x01\x00\x0f\x6c\x12\
-\x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x0f\x59\x09\
-\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x00\x1b\x20\
-\x00\x00\x02\x72\x00\x00\x00\x00\x00\x01\x00\x00\x11\xc5\
-\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x03\
-\x00\x00\x02\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x15\x7b\
-\x00\x00\x01\xee\x00\x00\x00\x00\x00\x01\x00\x00\x09\x05\
-\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x0f\x9a\
-\x00\x00\x02\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x18\xbd\
-\x00\x00\x03\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x8c\
-\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x72\
-\x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x0d\x51\x59\
-\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x0d\x48\x13\
-\x00\x00\x04\xb8\x00\x00\x00\x00\x00\x01\x00\x0d\x50\x06\
-\x00\x00\x05\x52\x00\x00\x00\x00\x00\x01\x00\x0d\x56\x1f\
-\x00\x00\x05\x20\x00\x00\x00\x00\x00\x01\x00\x0d\x54\x57\
-\x00\x00\x15\x64\x00\x00\x00\x00\x00\x01\x00\x11\x3d\x0b\
-\x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x11\x3f\xdb\
-\x00\x00\x15\x20\x00\x00\x00\x00\x00\x01\x00\x11\x37\x8a\
-\x00\x00\x15\x44\x00\x00\x00\x00\x00\x01\x00\x11\x3a\x88\
-\x00\x00\x06\xde\x00\x00\x00\x00\x00\x01\x00\x0e\xaa\x8e\
-\x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x0e\xbb\x82\
-\x00\x00\x09\xca\x00\x00\x00\x00\x00\x01\x00\x0e\xe6\x66\
-\x00\x00\x09\x98\x00\x00\x00\x00\x00\x01\x00\x0e\xdf\x50\
-\x00\x00\x08\x7e\x00\x00\x00\x00\x00\x01\x00\x0e\xc4\x50\
-\x00\x00\x09\x1e\x00\x00\x00\x00\x00\x01\x00\x0e\xd7\x0f\
-\x00\x00\x09\x48\x00\x00\x00\x00\x00\x01\x00\x0e\xd9\x44\
-\x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x0e\xc0\x33\
-\x00\x00\x08\x4c\x00\x00\x00\x00\x00\x01\x00\x0e\xc2\xb3\
-\x00\x00\x07\x52\x00\x00\x00\x00\x00\x01\x00\x0e\xb4\x35\
-\x00\x00\x07\xa4\x00\x00\x00\x00\x00\x01\x00\x0e\xb8\x76\
-\x00\x00\x07\xec\x00\x00\x00\x00\x00\x01\x00\x0e\xbe\xe0\
-\x00\x00\x08\xd0\x00\x00\x00\x00\x00\x01\x00\x0e\xca\xc5\
-\x00\x00\x08\xa4\x00\x00\x00\x00\x00\x01\x00\x0e\xc8\x62\
-\x00\x00\x09\x6a\x00\x00\x00\x00\x00\x01\x00\x0e\xdd\x48\
-\x00\x00\x06\xb0\x00\x00\x00\x00\x00\x01\x00\x0e\xa3\xd0\
-\x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x0e\xad\x7a\
-\x00\x00\x07\x2e\x00\x00\x00\x00\x00\x01\x00\x0e\xb0\xbc\
-\x00\x00\x07\x82\x00\x00\x00\x00\x00\x01\x00\x0e\xb5\x2e\
-\x00\x00\x08\xee\x00\x00\x00\x00\x00\x01\x00\x0e\xcd\x67\
-\x00\x00\x0a\xc4\x00\x00\x00\x00\x00\x01\x00\x0f\x2a\x0c\
-\x00\x00\x0a\xf8\x00\x00\x00\x00\x00\x01\x00\x0f\x2d\x0f\
-\x00\x00\x0b\x6e\x00\x00\x00\x00\x00\x01\x00\x0f\x3b\x47\
-\x00\x00\x0a\x94\x00\x00\x00\x00\x00\x01\x00\x0f\x27\xb3\
-\x00\x00\x0b\x3e\x00\x00\x00\x00\x00\x01\x00\x0f\x38\x14\
-\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x0f\x3e\x1d\
-\x00\x00\x0a\x52\x00\x00\x00\x00\x00\x01\x00\x0e\xf4\x0e\
-\x00\x00\x0b\x1a\x00\x00\x00\x00\x00\x01\x00\x0f\x30\xe2\
-\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x11\x42\xd4\
-\x00\x00\x16\x2a\x00\x00\x00\x00\x00\x01\x00\x11\x49\x4a\
-\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x11\x46\x36\
-\x00\x00\x16\x52\x00\x00\x00\x00\x00\x01\x00\x11\x4b\xe8\
-\x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x0d\x91\xaf\
-\x00\x00\x05\xb8\x00\x00\x00\x00\x00\x01\x00\x0d\x62\x6f\
-\x00\x00\x05\x88\x00\x00\x00\x00\x00\x01\x00\x0d\x57\xdb\
-\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x0d\x77\xa4\
-\x00\x00\x06\x48\x00\x00\x00\x00\x00\x01\x00\x0d\x95\xcc\
-\x00\x00\x06\x7c\x00\x00\x00\x00\x00\x01\x00\x0e\x5e\x65\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x13\x00\x00\x00\x01\
+\x00\x00\x00\xe6\x00\x02\x00\x00\x00\x06\x00\x00\x00\x8b\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x87\
+\x00\x00\x00\xb4\x00\x02\x00\x00\x00\x08\x00\x00\x00\x7f\
+\x00\x00\x00\xd6\x00\x02\x00\x00\x00\x14\x00\x00\x00\x6b\
+\x00\x00\x00\x2c\x00\x02\x00\x00\x00\x02\x00\x00\x00\x69\
+\x00\x00\x00\x14\x00\x02\x00\x00\x00\x02\x00\x00\x00\x67\
+\x00\x00\x01\x06\x00\x02\x00\x00\x00\x05\x00\x00\x00\x62\
+\x00\x00\x00\xf4\x00\x02\x00\x00\x00\x02\x00\x00\x00\x60\
+\x00\x00\x01\x40\x00\x02\x00\x00\x00\x09\x00\x00\x00\x57\
+\x00\x00\x00\x8c\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x4d\
+\x00\x00\x00\xc4\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4a\
+\x00\x00\x00\x3c\x00\x02\x00\x00\x00\x10\x00\x00\x00\x3a\
+\x00\x00\x00\x62\x00\x02\x00\x00\x00\x03\x00\x00\x00\x37\
+\x00\x00\x00\x78\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\
+\x00\x00\x01\x54\x00\x02\x00\x00\x00\x03\x00\x00\x00\x33\
+\x00\x00\x00\x9e\x00\x02\x00\x00\x00\x08\x00\x00\x00\x2b\
+\x00\x00\x01\x2c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x2a\
+\x00\x00\x00\x4e\x00\x02\x00\x00\x00\x11\x00\x00\x00\x19\
+\x00\x00\x01\x18\x00\x02\x00\x00\x00\x05\x00\x00\x00\x14\
+\x00\x00\x04\x5e\x00\x00\x00\x00\x00\x01\x00\x0a\xa7\x31\
+\x00\x00\x03\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x23\xa9\
+\x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x65\x6d\
+\x00\x00\x04\x2c\x00\x00\x00\x00\x00\x01\x00\x08\x06\x4f\
+\x00\x00\x03\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc4\x8b\
+\x00\x00\x10\x7a\x00\x00\x00\x00\x00\x01\x00\x10\xe0\x3f\
+\x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x10\xe7\x8a\
+\x00\x00\x10\x2a\x00\x00\x00\x00\x00\x01\x00\x10\xda\xe3\
+\x00\x00\x11\xc8\x00\x00\x00\x00\x00\x01\x00\x10\xfa\x08\
+\x00\x00\x11\x4a\x00\x00\x00\x00\x00\x01\x00\x10\xef\xa0\
+\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x10\xd3\xac\
+\x00\x00\x12\x1a\x00\x00\x00\x00\x00\x01\x00\x11\x00\x0f\
+\x00\x00\x11\x12\x00\x00\x00\x00\x00\x01\x00\x10\xea\xfe\
+\x00\x00\x12\x68\x00\x00\x00\x00\x00\x01\x00\x11\x05\x3b\
+\x00\x00\x0f\xfc\x00\x00\x00\x00\x00\x01\x00\x10\xd6\x4a\
+\x00\x00\x11\xf4\x00\x00\x00\x00\x00\x01\x00\x10\xfd\x3b\
+\x00\x00\x11\x9e\x00\x00\x00\x00\x00\x01\x00\x10\xf7\x90\
+\x00\x00\x10\x50\x00\x00\x00\x00\x00\x01\x00\x10\xdd\xbd\
+\x00\x00\x12\x44\x00\x00\x00\x00\x00\x01\x00\x11\x02\xf4\
+\x00\x00\x11\x72\x00\x00\x00\x00\x00\x01\x00\x10\xf2\xe5\
+\x00\x00\x10\xc6\x00\x00\x00\x00\x00\x01\x00\x10\xe4\x8d\
+\x00\x00\x10\xa0\x00\x00\x00\x00\x00\x01\x00\x10\xe2\x76\
+\x00\x00\x03\x78\x00\x00\x00\x00\x00\x01\x00\x00\x21\x92\
+\x00\x00\x0c\x92\x00\x00\x00\x00\x00\x01\x00\x0f\x55\x24\
+\x00\x00\x0d\x12\x00\x00\x00\x00\x00\x01\x00\x0f\x5b\xea\
+\x00\x00\x0c\x16\x00\x00\x00\x00\x00\x01\x00\x0f\x4e\x20\
+\x00\x00\x0c\xba\x00\x00\x00\x00\x00\x01\x00\x0f\x57\xc4\
+\x00\x00\x0d\x3c\x00\x00\x00\x00\x00\x01\x00\x0f\x5e\xb5\
+\x00\x00\x0c\x60\x00\x00\x00\x00\x00\x01\x00\x0f\x53\x3a\
+\x00\x00\x0c\x3a\x00\x00\x00\x00\x00\x01\x00\x0f\x50\xc7\
+\x00\x00\x0c\xf0\x00\x00\x00\x00\x00\x01\x00\x0f\x59\x46\
+\x00\x00\x01\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x05\xe6\
+\x00\x00\x01\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x02\xfe\
+\x00\x00\x0f\x22\x00\x00\x00\x00\x00\x01\x00\x0f\x7a\x03\
+\x00\x00\x0f\x4c\x00\x00\x00\x00\x00\x01\x00\x0f\x7f\x6d\
+\x00\x00\x0f\x7c\x00\x00\x00\x00\x00\x01\x00\x0f\xed\xf6\
+\x00\x00\x0f\x9c\x00\x00\x00\x00\x00\x01\x00\x0f\xf4\xa7\
+\x00\x00\x12\xea\x00\x00\x00\x00\x00\x01\x00\x11\x0c\x4f\
+\x00\x00\x12\x90\x00\x00\x00\x00\x00\x01\x00\x11\x07\x4e\
+\x00\x00\x15\x06\x00\x00\x00\x00\x00\x01\x00\x11\x36\x54\
+\x00\x00\x13\x90\x00\x00\x00\x00\x00\x01\x00\x11\x14\x59\
+\x00\x00\x13\x2c\x00\x00\x00\x00\x00\x01\x00\x11\x0f\xa2\
+\x00\x00\x12\xb6\x00\x00\x00\x00\x00\x01\x00\x11\x0a\x7f\
+\x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x11\x27\x2f\
+\x00\x00\x13\xf6\x00\x00\x00\x00\x00\x01\x00\x11\x1e\xff\
+\x00\x00\x15\x2c\x00\x00\x00\x00\x00\x01\x00\x11\x39\x36\
+\x00\x00\x14\xd2\x00\x00\x00\x00\x00\x01\x00\x11\x32\x77\
+\x00\x00\x14\x22\x00\x00\x00\x00\x00\x01\x00\x11\x24\x0b\
+\x00\x00\x15\x5e\x00\x00\x00\x00\x00\x01\x00\x11\x3c\xf3\
+\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x11\x2b\x9e\
+\x00\x00\x13\xbe\x00\x00\x00\x00\x00\x01\x00\x11\x1c\x89\
+\x00\x00\x14\xa8\x00\x00\x00\x00\x00\x01\x00\x11\x2f\xef\
+\x00\x00\x13\x5a\x00\x00\x00\x00\x00\x01\x00\x11\x11\xa9\
+\x00\x00\x0a\x72\x00\x00\x00\x00\x00\x01\x00\x0e\xf6\x13\
+\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x0e\xf9\xf0\
+\x00\x00\x0a\x4c\x00\x00\x00\x00\x00\x01\x00\x0e\xf3\x75\
+\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x01\x00\x0f\x68\xd1\
+\x00\x00\x0e\x9e\x00\x00\x00\x00\x00\x01\x00\x0f\x73\x46\
+\x00\x00\x0e\xf8\x00\x00\x00\x00\x00\x01\x00\x0f\x77\xff\
+\x00\x00\x0d\x96\x00\x00\x00\x00\x00\x01\x00\x0f\x64\xe1\
+\x00\x00\x0e\x7a\x00\x00\x00\x00\x00\x01\x00\x0f\x70\x13\
+\x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x0f\x66\xd8\
+\x00\x00\x0e\x34\x00\x00\x00\x00\x00\x01\x00\x0f\x6a\xcf\
+\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x0f\x6c\xe2\
+\x00\x00\x0e\xd4\x00\x00\x00\x00\x00\x01\x00\x0f\x75\x2e\
+\x00\x00\x0d\x6e\x00\x00\x00\x00\x00\x01\x00\x0f\x62\x25\
+\x00\x00\x03\x00\x00\x00\x00\x00\x00\x01\x00\x00\x1b\x20\
+\x00\x00\x02\x84\x00\x00\x00\x00\x00\x01\x00\x00\x11\xc5\
+\x00\x00\x02\x28\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x03\
+\x00\x00\x02\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x15\x7b\
+\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x09\x05\
+\x00\x00\x02\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x0f\x9a\
+\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x00\x18\xbd\
+\x00\x00\x03\x50\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x8c\
+\x00\x00\x03\x28\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x72\
+\x00\x00\x05\x9a\x00\x00\x00\x00\x00\x01\x00\x0d\x57\xdb\
+\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x0d\x5d\xf9\
+\x00\x00\x04\xfc\x00\x00\x00\x00\x00\x01\x00\x0d\x51\x59\
+\x00\x00\x04\x8e\x00\x00\x00\x00\x00\x01\x00\x0d\x48\x13\
+\x00\x00\x04\xca\x00\x00\x00\x00\x00\x01\x00\x0d\x50\x06\
+\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x0d\x56\x1f\
+\x00\x00\x05\x32\x00\x00\x00\x00\x00\x01\x00\x0d\x54\x57\
+\x00\x00\x15\xc6\x00\x00\x00\x00\x00\x01\x00\x11\x46\x27\
+\x00\x00\x15\xfa\x00\x00\x00\x00\x00\x01\x00\x11\x48\xf7\
+\x00\x00\x15\x82\x00\x00\x00\x00\x00\x01\x00\x11\x40\xa6\
+\x00\x00\x15\xa6\x00\x00\x00\x00\x00\x01\x00\x11\x43\xa4\
+\x00\x00\x07\x40\x00\x00\x00\x00\x00\x01\x00\x0e\xb3\xaa\
+\x00\x00\x08\x26\x00\x00\x00\x00\x00\x01\x00\x0e\xc4\x9e\
+\x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x0e\xef\x82\
+\x00\x00\x09\xfa\x00\x00\x00\x00\x00\x01\x00\x0e\xe8\x6c\
+\x00\x00\x08\xe0\x00\x00\x00\x00\x00\x01\x00\x0e\xcd\x6c\
+\x00\x00\x09\x80\x00\x00\x00\x00\x00\x01\x00\x0e\xe0\x2b\
+\x00\x00\x09\xaa\x00\x00\x00\x00\x00\x01\x00\x0e\xe2\x60\
+\x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x0e\xc9\x4f\
+\x00\x00\x08\xae\x00\x00\x00\x00\x00\x01\x00\x0e\xcb\xcf\
+\x00\x00\x07\xb4\x00\x00\x00\x00\x00\x01\x00\x0e\xbd\x51\
+\x00\x00\x08\x06\x00\x00\x00\x00\x00\x01\x00\x0e\xc1\x92\
+\x00\x00\x08\x4e\x00\x00\x00\x00\x00\x01\x00\x0e\xc7\xfc\
+\x00\x00\x09\x32\x00\x00\x00\x00\x00\x01\x00\x0e\xd3\xe1\
+\x00\x00\x09\x06\x00\x00\x00\x00\x00\x01\x00\x0e\xd1\x7e\
+\x00\x00\x09\xcc\x00\x00\x00\x00\x00\x01\x00\x0e\xe6\x64\
+\x00\x00\x07\x12\x00\x00\x00\x00\x00\x01\x00\x0e\xac\xec\
+\x00\x00\x07\x64\x00\x00\x00\x00\x00\x01\x00\x0e\xb6\x96\
+\x00\x00\x07\x90\x00\x00\x00\x00\x00\x01\x00\x0e\xb9\xd8\
+\x00\x00\x07\xe4\x00\x00\x00\x00\x00\x01\x00\x0e\xbe\x4a\
+\x00\x00\x09\x50\x00\x00\x00\x00\x00\x01\x00\x0e\xd6\x83\
+\x00\x00\x0b\x26\x00\x00\x00\x00\x00\x01\x00\x0f\x33\x28\
+\x00\x00\x0b\x5a\x00\x00\x00\x00\x00\x01\x00\x0f\x36\x2b\
+\x00\x00\x0b\xd0\x00\x00\x00\x00\x00\x01\x00\x0f\x44\x63\
+\x00\x00\x0a\xf6\x00\x00\x00\x00\x00\x01\x00\x0f\x30\xcf\
+\x00\x00\x0b\xa0\x00\x00\x00\x00\x00\x01\x00\x0f\x41\x30\
+\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x0f\x47\x39\
+\x00\x00\x0a\xb4\x00\x00\x00\x00\x00\x01\x00\x0e\xfd\x2a\
+\x00\x00\x0b\x7c\x00\x00\x00\x00\x00\x01\x00\x0f\x39\xfe\
+\x00\x00\x16\x2a\x00\x00\x00\x00\x00\x01\x00\x11\x4b\xf0\
+\x00\x00\x16\x8c\x00\x00\x00\x00\x00\x01\x00\x11\x52\x66\
+\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x11\x4f\x52\
+\x00\x00\x16\xb4\x00\x00\x00\x00\x00\x01\x00\x11\x55\x04\
+\x00\x00\x06\x7a\x00\x00\x00\x00\x00\x01\x00\x0d\x9a\xcb\
+\x00\x00\x06\x1a\x00\x00\x00\x00\x00\x01\x00\x0d\x6b\x8b\
+\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x0d\x60\xf7\
+\x00\x00\x06\x4a\x00\x00\x00\x00\x00\x01\x00\x0d\x80\xc0\
+\x00\x00\x06\xaa\x00\x00\x00\x00\x00\x01\x00\x0d\x9e\xe8\
+\x00\x00\x06\xde\x00\x00\x00\x00\x00\x01\x00\x0e\x67\x81\
 "
 
 def qInitResources():

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2013-02-26 17:27:30 +0000
+++ openlp/core/ui/mainwindow.py	2013-03-04 14:42:22 +0000
@@ -807,19 +807,11 @@
             return
         setting_sections = []
         # Add main sections.
-        setting_sections.extend([self.generalSettingsSection])
-        setting_sections.extend([self.advancedSettingsSection])
-        setting_sections.extend([self.uiSettingsSection])
-        setting_sections.extend([self.shortcutsSettingsSection])
-        setting_sections.extend([self.serviceManagerSettingsSection])
-        setting_sections.extend([self.themesSettingsSection])
-        setting_sections.extend([self.playersSettingsSection])
-        setting_sections.extend([self.displayTagsSection])
-        setting_sections.extend([self.headerSection])
-        setting_sections.extend([u'crashreport'])
+        setting_sections.extend([self.generalSettingsSection, self.advancedSettingsSection, self.uiSettingsSection,
+            self.shortcutsSettingsSection, self.serviceManagerSettingsSection, self.themesSettingsSection,
+            self.playersSettingsSection, self.displayTagsSection, self.headerSection, u'crashreport'])
         # Add plugin sections.
-        for plugin in self.plugin_manager.plugins:
-            setting_sections.extend([plugin.name])
+        setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins])
         # Copy the settings file to the tmp dir, because we do not want to change the original one.
         temp_directory = os.path.join(unicode(gettempdir()), u'openlp')
         check_directory_exists(temp_directory)
@@ -827,11 +819,13 @@
         shutil.copyfile(import_file_name, temp_config)
         settings = Settings()
         import_settings = Settings(temp_config, Settings.IniFormat)
+        # Convert image files
+        log.info(u'hook upgrade_plugin_settings')
+        self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
         # Remove/rename old settings to prepare the import.
         import_settings.remove_obsolete_settings()
-        # Lets do a basic sanity check. If it contains this string we can
-        # assume it was created by OpenLP and so we'll load what we can
-        # from it, and just silently ignore anything we don't recognise
+        # Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll
+        # load what we can from it, and just silently ignore anything we don't recognise.
         if import_settings.value(u'SettingsImport/type') != u'OpenLP_settings_export':
             QtGui.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Import settings'),
                 translate('OpenLP.MainWindow', 'The file you have selected does not appear to be a valid OpenLP '
@@ -866,9 +860,8 @@
         settings.setValue(u'file_date_imported', now.strftime("%Y-%m-%d %H:%M"))
         settings.endGroup()
         settings.sync()
-        # We must do an immediate restart or current configuration will
-        # overwrite what was just imported when application terminates
-        # normally.   We need to exit without saving configuration.
+        # We must do an immediate restart or current configuration will overwrite what was just imported when
+        # application terminates normally.   We need to exit without saving configuration.
         QtGui.QMessageBox.information(self, translate('OpenLP.MainWindow', 'Import settings'),
             translate('OpenLP.MainWindow', 'OpenLP will now close.  Imported settings will '
                 'be applied the next time you start OpenLP.'),

=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
--- openlp/plugins/custom/lib/mediaitem.py	2013-02-07 08:42:17 +0000
+++ openlp/plugins/custom/lib/mediaitem.py	2013-03-04 14:42:22 +0000
@@ -96,7 +96,7 @@
         self.searchTextEdit.setCurrentSearchType(Settings().value( u'%s/last search type' % self.settingsSection))
         self.config_updated()
 
-    def loadList(self, custom_slides):
+    def loadList(self, custom_slides, target_group=None):
         # Sort out what custom we want to select after loading the list.
         self.saveAutoSelectId()
         self.listView.clear()

=== added directory 'openlp/plugins/images/forms'
=== added file 'openlp/plugins/images/forms/__init__.py'
--- openlp/plugins/images/forms/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/forms/__init__.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,57 @@
+# -*- 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                          #
+###############################################################################
+"""
+Forms in OpenLP are made up of two classes. One class holds all the graphical
+elements, like buttons and lists, and the other class holds all the functional
+code, like slots and loading and saving.
+
+The first class, commonly known as the **Dialog** class, is typically named
+``Ui_<name>Dialog``. It is a slightly modified version of the class that the
+``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
+converting most strings from "" to u'' and using OpenLP's ``translate()``
+function for translating strings.
+
+The second class, commonly known as the **Form** class, is typically named
+``<name>Form``. This class is the one which is instantiated and used. It uses
+dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class
+mentioned above, like so::
+
+    class AuthorsForm(QtGui.QDialog, Ui_AuthorsDialog):
+
+        def __init__(self, parent=None):
+            QtGui.QDialog.__init__(self, parent)
+            self.setupUi(self)
+
+This allows OpenLP to use ``self.object`` for all the GUI elements while keeping
+them separate from the functionality, so that it is easier to recreate the GUI
+from the .ui files later if necessary.
+"""
+
+from addgroupform import AddGroupForm
+from choosegroupform import ChooseGroupForm

=== added file 'openlp/plugins/images/forms/addgroupdialog.py'
--- openlp/plugins/images/forms/addgroupdialog.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/forms/addgroupdialog.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,64 @@
+# -*- 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                          #
+###############################################################################
+
+from PyQt4 import QtGui
+
+from openlp.core.lib import translate
+from openlp.core.lib.ui import create_button_box
+
+
+class Ui_AddGroupDialog(object):
+    def setupUi(self, add_group_dialog):
+        add_group_dialog.setObjectName(u'add_group_dialog')
+        add_group_dialog.resize(300, 10)
+        self.dialog_layout = QtGui.QVBoxLayout(add_group_dialog)
+        self.dialog_layout.setObjectName(u'dialog_layout')
+        self.name_layout = QtGui.QFormLayout()
+        self.name_layout.setObjectName(u'name_layout')
+        self.parent_group_label = QtGui.QLabel(add_group_dialog)
+        self.parent_group_label.setObjectName(u'parent_group_label')
+        self.parent_group_combobox = QtGui.QComboBox(add_group_dialog)
+        self.parent_group_combobox.setObjectName(u'parent_group_combobox')
+        self.name_layout.addRow(self.parent_group_label, self.parent_group_combobox)
+        self.name_label = QtGui.QLabel(add_group_dialog)
+        self.name_label.setObjectName(u'name_label')
+        self.name_edit = QtGui.QLineEdit(add_group_dialog)
+        self.name_edit.setObjectName(u'name_edit')
+        self.name_label.setBuddy(self.name_edit)
+        self.name_layout.addRow(self.name_label, self.name_edit)
+        self.dialog_layout.addLayout(self.name_layout)
+        self.button_box = create_button_box(add_group_dialog, u'button_box', [u'cancel', u'save'])
+        self.dialog_layout.addWidget(self.button_box)
+        self.retranslateUi(add_group_dialog)
+        add_group_dialog.setMaximumHeight(add_group_dialog.sizeHint().height())
+
+    def retranslateUi(self, add_group_dialog):
+        add_group_dialog.setWindowTitle(translate('ImagePlugin.AddGroupForm', 'Add group'))
+        self.parent_group_label.setText(translate('ImagePlugin.AddGroupForm', 'Parent group:'))
+        self.name_label.setText(translate('ImagePlugin.AddGroupForm', 'Group name:'))

=== added file 'openlp/plugins/images/forms/addgroupform.py'
--- openlp/plugins/images/forms/addgroupform.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/forms/addgroupform.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,68 @@
+# -*- 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                          #
+###############################################################################
+
+from PyQt4 import QtGui
+
+from openlp.core.lib import translate
+from openlp.core.lib.ui import critical_error_message_box
+from openlp.plugins.images.forms.addgroupdialog import Ui_AddGroupDialog
+
+
+class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
+    """
+    Class documentation goes here.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        """
+        QtGui.QDialog.__init__(self, parent)
+        self.setupUi(self)
+
+    def exec_(self, clear=True, show_top_level_group=False, selected_group=None):
+        if clear:
+            self.name_edit.clear()
+        self.name_edit.setFocus()
+        if show_top_level_group and not self.parent_group_combobox.top_level_group_added:
+            self.parent_group_combobox.insertItem(0, translate('ImagePlugin.MediaItem', '-- Top-level group --'), 0)
+            self.parent_group_combobox.top_level_group_added = True
+        if selected_group is not None:
+            for i in range(self.parent_group_combobox.count()):
+                if self.parent_group_combobox.itemData(i) == selected_group:
+                    self.parent_group_combobox.setCurrentIndex(i)
+        return QtGui.QDialog.exec_(self)
+
+    def accept(self):
+        if not self.name_edit.text():
+            critical_error_message_box(message=translate('ImagePlugin.AddGroupForm',
+                'You need to type in a group name.'))
+            self.name_edit.setFocus()
+            return False
+        else:
+            return QtGui.QDialog.accept(self)

=== added file 'openlp/plugins/images/forms/choosegroupdialog.py'
--- openlp/plugins/images/forms/choosegroupdialog.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/forms/choosegroupdialog.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,94 @@
+# -*- 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                          #
+###############################################################################
+
+from PyQt4 import QtCore, QtGui
+
+from openlp.core.lib import translate
+from openlp.core.lib.ui import create_button_box
+
+
+class Ui_ChooseGroupDialog(object):
+    """
+    The UI for the "Choose Image Group" form.
+    """
+    def setupUi(self, choose_group_dialog):
+        """
+        Set up the UI.
+
+        ``choose_group_dialog``
+            The form object (not the class).
+        """
+        choose_group_dialog.setObjectName(u'choose_group_dialog')
+        choose_group_dialog.resize(399, 119)
+        self.choose_group_layout = QtGui.QFormLayout(choose_group_dialog)
+        self.choose_group_layout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow)
+        self.choose_group_layout.setMargin(8)
+        self.choose_group_layout.setSpacing(8)
+        self.choose_group_layout.setLabelAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.choose_group_layout.setObjectName(u'choose_group_layout')
+        self.group_question_label = QtGui.QLabel(choose_group_dialog)
+        self.group_question_label.setWordWrap(True)
+        self.group_question_label.setObjectName(u'group_question_label')
+        self.choose_group_layout.setWidget(1, QtGui.QFormLayout.SpanningRole, self.group_question_label)
+        self.nogroup_radio_button = QtGui.QRadioButton(choose_group_dialog)
+        self.nogroup_radio_button.setChecked(True)
+        self.nogroup_radio_button.setObjectName(u'nogroup_radio_button')
+        self.choose_group_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.nogroup_radio_button)
+        self.existing_radio_button = QtGui.QRadioButton(choose_group_dialog)
+        self.existing_radio_button.setChecked(False)
+        self.existing_radio_button.setObjectName(u'existing_radio_button')
+        self.choose_group_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.existing_radio_button)
+        self.group_combobox = QtGui.QComboBox(choose_group_dialog)
+        self.group_combobox.setObjectName(u'group_combobox')
+        self.choose_group_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.group_combobox)
+        self.new_radio_button = QtGui.QRadioButton(choose_group_dialog)
+        self.new_radio_button.setChecked(False)
+        self.new_radio_button.setObjectName(u'new_radio_button')
+        self.choose_group_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.new_radio_button)
+        self.new_group_edit = QtGui.QLineEdit(choose_group_dialog)
+        self.new_group_edit.setObjectName(u'new_group_edit')
+        self.choose_group_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.new_group_edit)
+        self.group_button_box = create_button_box(choose_group_dialog, u'buttonBox', [u'ok'])
+        self.choose_group_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.group_button_box)
+
+        self.retranslateUi(choose_group_dialog)
+        QtCore.QMetaObject.connectSlotsByName(choose_group_dialog)
+
+    def retranslateUi(self, choose_group_dialog):
+        """
+        Translate the UI on the fly.
+
+        ``choose_group_dialog``
+            The form object (not the class).
+        """
+        choose_group_dialog.setWindowTitle(translate('ImagePlugin.ChooseGroupForm', 'Select Image Group'))
+        self.group_question_label.setText(translate('ImagePlugin.ChooseGroupForm', 'Add images to group:'))
+        self.nogroup_radio_button.setText(translate('ImagePlugin.ChooseGroupForm', 'No group'))
+        self.existing_radio_button.setText(translate('ImagePlugin.ChooseGroupForm', 'Existing group'))
+        self.new_radio_button.setText(translate('ImagePlugin.ChooseGroupForm', 'New group'))

=== added file 'openlp/plugins/images/forms/choosegroupform.py'
--- openlp/plugins/images/forms/choosegroupform.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/forms/choosegroupform.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,51 @@
+# -*- 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                          #
+###############################################################################
+
+from PyQt4 import QtGui
+
+from openlp.plugins.images.forms.choosegroupdialog import Ui_ChooseGroupDialog
+
+
+class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog):
+    """
+    Class documentation goes here.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        """
+        QtGui.QDialog.__init__(self, parent)
+        self.setupUi(self)
+
+    def exec_(self, selected_group=None):
+        if selected_group is not None:
+            for i in range(self.group_combobox.count()):
+                if self.group_combobox.itemData(i) == selected_group:
+                    self.group_combobox.setCurrentIndex(i)
+        return QtGui.QDialog.exec_(self)

=== modified file 'openlp/plugins/images/imageplugin.py'
--- openlp/plugins/images/imageplugin.py	2013-02-16 18:24:31 +0000
+++ openlp/plugins/images/imageplugin.py	2013-03-04 14:42:22 +0000
@@ -32,13 +32,16 @@
 import logging
 
 from openlp.core.lib import Plugin, StringContent, Registry, ImageSource, Settings, build_icon, translate
+from openlp.core.lib.db import Manager
 from openlp.plugins.images.lib import ImageMediaItem, ImageTab
+from openlp.plugins.images.lib.db import init_schema, ImageFilenames
 
 log = logging.getLogger(__name__)
 
 __default_settings__ = {
-        u'images/images files': []
-    }
+    u'images/db type': u'sqlite',
+    u'images/background color': u'#000000',
+}
 
 
 class ImagePlugin(Plugin):
@@ -46,6 +49,7 @@
 
     def __init__(self):
         Plugin.__init__(self, u'images', __default_settings__, ImageMediaItem, ImageTab)
+        self.manager = Manager(u'images', init_schema)
         self.weight = -7
         self.iconPath = u':/plugins/plugin_images.png'
         self.icon = build_icon(self.iconPath)
@@ -65,6 +69,26 @@
             'provided by the theme.')
         return about_text
 
+    def app_startup(self):
+        """
+        Perform tasks on application startup
+        """
+        Plugin.app_startup(self)
+        # Convert old settings-based image list to the database
+        files_from_config = Settings().get_files_from_config(self)
+        if len(files_from_config) > 0:
+            log.debug(u'Importing images list from old config: %s' % files_from_config)
+            self.mediaItem.save_new_images_list(files_from_config)
+
+    def upgrade_settings(self, settings):
+        """
+        Upgrade the settings of this plugin.
+        """
+        files_from_config = settings.get_files_from_config(self)
+        if len(files_from_config) > 0:
+            log.debug(u'Importing images list from old config: %s' % files_from_config)
+            self.mediaItem.save_new_images_list(files_from_config)
+
     def setPluginTextStrings(self):
         """
         Called to define all translatable texts of the plugin
@@ -75,8 +99,7 @@
             u'plural': translate('ImagePlugin', 'Images', 'name plural')
         }
         ## Name for MediaDockManager, SettingsManager ##
-        self.textStrings[StringContent.VisibleName] = {u'title': translate('ImagePlugin', 'Images', 'container title')
-        }
+        self.textStrings[StringContent.VisibleName] = {u'title': translate('ImagePlugin', 'Images', 'container title')}
         # Middle Header Bar
         tooltips = {
             u'load': translate('ImagePlugin', 'Load a new image.'),

=== added file 'openlp/plugins/images/lib/db.py'
--- openlp/plugins/images/lib/db.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/images/lib/db.py	2013-03-04 14:42:22 +0000
@@ -0,0 +1,99 @@
+# -*- 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:`db` module provides the database and schema that is the backend for the Images plugin
+"""
+
+from sqlalchemy import Column, ForeignKey, Table, types
+from sqlalchemy.orm import mapper, relation, reconstructor
+
+from openlp.core.lib.db import BaseModel, init_db
+
+
+class ImageGroups(BaseModel):
+    """
+    ImageGroups model
+    """
+    pass
+
+
+class ImageFilenames(BaseModel):
+    """
+    ImageFilenames model
+    """
+    pass
+
+
+def init_schema(url):
+    """
+    Setup the images database connection and initialise the database schema.
+
+    ``url``
+        The database to setup
+
+    The images database contains the following tables:
+
+        * image_groups
+        * image_filenames
+
+    **image_groups Table**
+        This table holds the names of the images groups. It has the following columns:
+
+        * id
+        * parent_id
+        * group_name
+
+    **image_filenames Table**
+        This table holds the filenames of the images and the group they belong to. It has the following columns:
+
+        * id
+        * group_id
+        * filename
+    """
+    session, metadata = init_db(url)
+
+    # Definition of the "image_groups" table
+    image_groups_table = Table(u'image_groups', metadata,
+        Column(u'id', types.Integer(), primary_key=True),
+        Column(u'parent_id', types.Integer()),
+        Column(u'group_name', types.Unicode(128))
+    )
+
+    # Definition of the "image_filenames" table
+    image_filenames_table = Table(u'image_filenames', metadata,
+        Column(u'id', types.Integer(), primary_key=True),
+        Column(u'group_id', types.Integer(), ForeignKey(u'image_groups.id'), default=None),
+        Column(u'filename', types.Unicode(255), nullable=False)
+    )
+
+    mapper(ImageGroups, image_groups_table)
+    mapper(ImageFilenames, image_filenames_table)
+
+    metadata.create_all(checkfirst=True)
+    return session

=== modified file 'openlp/plugins/images/lib/imagetab.py'
--- openlp/plugins/images/lib/imagetab.py	2013-02-07 08:42:17 +0000
+++ openlp/plugins/images/lib/imagetab.py	2013-03-04 14:42:22 +0000
@@ -31,6 +31,7 @@
 
 from openlp.core.lib import SettingsTab, Registry, Settings, UiStrings, translate
 
+
 class ImageTab(SettingsTab):
     """
     ImageTab is the images settings tab in the settings dialog.
@@ -92,4 +93,3 @@
         settings.endGroup()
         if self.initial_color != self.bg_color:
             Registry().execute(u'image_updated')
-

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2013-02-27 11:33:03 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2013-03-04 14:42:22 +0000
@@ -32,13 +32,17 @@
 
 from PyQt4 import QtCore, QtGui
 
-from openlp.core.lib import MediaManagerItem, ItemCapabilities, Registry, ServiceItemContext, Settings, UiStrings, \
-    build_icon, check_item_selected, check_directory_exists, create_thumb, translate, validate_thumb
-from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, ServiceItemContext, Settings, \
+    StringContent, TreeWidgetWithDnD, UiStrings, build_icon, check_directory_exists, check_item_selected, \
+    create_thumb, translate, validate_thumb
+from openlp.core.lib.ui import create_widget_action, critical_error_message_box
 from openlp.core.utils import AppLocation, delete_file, locale_compare, get_images_filter
+from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm
+from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
 
 log = logging.getLogger(__name__)
 
+
 class ImageMediaItem(MediaManagerItem):
     """
     This is the custom media manager item for images.
@@ -50,6 +54,11 @@
         MediaManagerItem.__init__(self, parent, plugin, icon)
         self.quickPreviewAllowed = True
         self.hasSearch = True
+        self.manager = plugin.manager
+        self.choose_group_form = ChooseGroupForm(self)
+        self.add_group_form = AddGroupForm(self)
+        self.fill_groups_combobox(self.choose_group_form.group_combobox)
+        self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
         Registry().register_function(u'live_theme_changed', self.live_theme_changed)
         # Allow DnD from the desktop
         self.listView.activateDnD()
@@ -59,6 +68,8 @@
             'Select Image(s)')
         file_formats = get_images_filter()
         self.onNewFileMasks = u'%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles)
+        self.addGroupAction.setText(UiStrings().AddGroup)
+        self.addGroupAction.setToolTip(UiStrings().AddGroup)
         self.replaceAction.setText(UiStrings().ReplaceBG)
         self.replaceAction.setToolTip(UiStrings().ReplaceLiveBG)
         self.resetAction.setText(UiStrings().ResetBG)
@@ -75,70 +86,342 @@
         log.debug(u'initialise')
         self.listView.clear()
         self.listView.setIconSize(QtCore.QSize(88, 50))
+        self.listView.setIndentation(self.listView.defaultIndentation)
+        self.listView.allow_internal_dnd = True
         self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settingsSection), u'thumbnails')
         check_directory_exists(self.servicePath)
-        self.loadList(Settings().value(self.settingsSection +  u'/images files'), True)
+        # Load images from the database
+        self.loadFullList(
+            self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True)
 
     def addListViewToToolBar(self):
-        MediaManagerItem.addListViewToToolBar(self)
+        """
+        Creates the main widget for listing items the media item is tracking.
+        This method overloads MediaManagerItem.addListViewToToolBar
+        """
+        # Add the List widget
+        self.listView = TreeWidgetWithDnD(self, self.plugin.name)
+        self.listView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
+        self.listView.setAlternatingRowColors(True)
+        self.listView.setObjectName(u'%sTreeView' % self.plugin.name)
+        # Add to pageLayout
+        self.pageLayout.addWidget(self.listView)
+        # define and add the context menu
+        self.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        if self.hasEditIcon:
+            create_widget_action(self.listView,
+                text=self.plugin.getString(StringContent.Edit)[u'title'],
+                icon=u':/general/general_edit.png',
+                triggers=self.onEditClick)
+            create_widget_action(self.listView, separator=True)
+        if self.hasDeleteIcon:
+            create_widget_action(self.listView,
+                text=self.plugin.getString(StringContent.Delete)[u'title'],
+                icon=u':/general/general_delete.png',
+                shortcuts=[QtCore.Qt.Key_Delete], triggers=self.onDeleteClick)
+            create_widget_action(self.listView, separator=True)
+        create_widget_action(self.listView,
+            text=self.plugin.getString(StringContent.Preview)[u'title'],
+            icon=u':/general/general_preview.png',
+            shortcuts=[QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return],
+            triggers=self.onPreviewClick)
+        create_widget_action(self.listView,
+            text=self.plugin.getString(StringContent.Live)[u'title'],
+            icon=u':/general/general_live.png',
+            shortcuts=[QtCore.Qt.ShiftModifier | QtCore.Qt.Key_Enter,
+            QtCore.Qt.ShiftModifier | QtCore.Qt.Key_Return],
+            triggers=self.onLiveClick)
+        create_widget_action(self.listView,
+            text=self.plugin.getString(StringContent.Service)[u'title'],
+            icon=u':/general/general_add.png',
+            shortcuts=[QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal],
+            triggers=self.onAddClick)
+        if self.addToServiceItem:
+            create_widget_action(self.listView, separator=True)
+            create_widget_action(self.listView,
+                text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
+                icon=u':/general/general_add.png',
+                triggers=self.onAddEditClick)
+        self.addCustomContextActions()
+        # Create the context menu and add all actions from the listView.
+        self.menu = QtGui.QMenu()
+        self.menu.addActions(self.listView.actions())
+        self.listView.doubleClicked.connect(self.onDoubleClicked)
+        self.listView.itemSelectionChanged.connect(self.onSelectionChange)
+        self.listView.customContextMenuRequested.connect(self.contextMenu)
         self.listView.addAction(self.replaceAction)
 
+    def addStartHeaderBar(self):
+        self.addGroupAction = self.toolbar.addToolbarAction(u'addGroupAction',
+            icon=u':/images/image_new_group.png', triggers=self.onAddGroupClick)
+
     def addEndHeaderBar(self):
         self.replaceAction = self.toolbar.addToolbarAction(u'replaceAction',
             icon=u':/slides/slide_blank.png', triggers=self.onReplaceClick)
         self.resetAction = self.toolbar.addToolbarAction(u'resetAction',
             icon=u':/system/system_close.png', visible=False, triggers=self.onResetClick)
 
+    def recursivelyDeleteGroup(self, image_group):
+        """
+        Recursively deletes a group and all groups and images in it
+        """
+        images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
+        for image in images:
+            delete_file(os.path.join(self.servicePath, os.path.split(image.filename)[1]))
+            self.manager.delete_object(ImageFilenames, image.id)
+        image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
+        for group in image_groups:
+            self.recursivelyDeleteGroup(group)
+            self.manager.delete_object(ImageGroups, group.id)
+
     def onDeleteClick(self):
         """
         Remove an image item from the list
         """
         # Turn off auto preview triggers.
         self.listView.blockSignals(True)
-        if check_item_selected(self.listView, translate('ImagePlugin.MediaItem','You must select an image to delete.')):
-            row_list = [item.row() for item in self.listView.selectedIndexes()]
-            row_list.sort(reverse=True)
+        if check_item_selected(self.listView, translate('ImagePlugin.MediaItem',
+            'You must select an image or group to delete.')):
+            item_list = self.listView.selectedItems()
             self.application.set_busy_cursor()
-            self.main_window.displayProgressBar(len(row_list))
-            for row in row_list:
-                text = self.listView.item(row)
-                if text:
-                    delete_file(os.path.join(self.servicePath, text.text()))
-                self.listView.takeItem(row)
+            self.main_window.displayProgressBar(len(item_list))
+            for row_item in item_list:
+                if row_item:
+                    item_data = row_item.data(0, QtCore.Qt.UserRole)
+                    if isinstance(item_data, ImageFilenames):
+                        delete_file(os.path.join(self.servicePath, row_item.text(0)))
+                        if item_data.group_id == 0:
+                            self.listView.takeTopLevelItem(self.listView.indexOfTopLevelItem(row_item))
+                        else:
+                            row_item.parent().removeChild(row_item)
+                        self.manager.delete_object(ImageFilenames, row_item.data(0, QtCore.Qt.UserRole).id)
+                    elif isinstance(item_data, ImageGroups):
+                        if QtGui.QMessageBox.question(self.listView.parent(),
+                            translate('ImagePlugin.MediaItem', 'Remove group'),
+                            translate('ImagePlugin.MediaItem',
+                            'Are you sure you want to remove "%s" and everything in it?') % item_data.group_name,
+                            QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
+                            QtGui.QMessageBox.No)) == QtGui.QMessageBox.Yes:
+                            self.recursivelyDeleteGroup(item_data)
+                            self.manager.delete_object(ImageGroups, row_item.data(0, QtCore.Qt.UserRole).id)
+                            if item_data.parent_id is 0:
+                                self.listView.takeTopLevelItem(self.listView.indexOfTopLevelItem(row_item))
+                            else:
+                                row_item.parent().removeChild(row_item)
+                            self.fill_groups_combobox(self.choose_group_form.group_combobox)
+                            self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
                 self.main_window.incrementProgressBar()
-            Settings.setValue(self.settingsSection + u'/images files', self.getFileList())
             self.main_window.finishedProgressBar()
             self.application.set_normal_cursor()
         self.listView.blockSignals(False)
 
-    def loadList(self, images, initialLoad=False):
-        self.application.set_busy_cursor()
-        if not initialLoad:
+    def add_sub_groups(self, group_list, parent_group_id):
+        """
+        Recursively add subgroups to the given parent group in a QTreeWidget
+        """
+        image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
+        image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
+        folder_icon = build_icon(u':/images/image_group.png')
+        for image_group in image_groups:
+            group = QtGui.QTreeWidgetItem()
+            group.setText(0, image_group.group_name)
+            group.setData(0, QtCore.Qt.UserRole, image_group)
+            group.setIcon(0, folder_icon)
+            if parent_group_id is 0:
+                self.listView.addTopLevelItem(group)
+            else:
+                group_list[parent_group_id].addChild(group)
+            group_list[image_group.id] = group
+            self.add_sub_groups(group_list, image_group.id)
+
+    def fill_groups_combobox(self, combobox, parent_group_id=0, prefix=''):
+        """
+        Recursively add groups to the combobox in the 'Add group' dialog
+        """
+        if parent_group_id is 0:
+            combobox.clear()
+            combobox.top_level_group_added = False
+        image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
+        image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
+        for image_group in image_groups:
+            combobox.addItem(prefix + image_group.group_name, image_group.id)
+            self.fill_groups_combobox(combobox, image_group.id, prefix + '   ')
+
+    def expand_group(self, group_id, root_item=None):
+        return_value = False
+        if root_item is None:
+            root_item = self.listView.invisibleRootItem()
+        for i in range(root_item.childCount()):
+            child = root_item.child(i)
+            if self.expand_group(group_id, child):
+                child.setExpanded(True)
+                return_value = True
+        if isinstance(root_item.data(0, QtCore.Qt.UserRole), ImageGroups):
+            if root_item.data(0, QtCore.Qt.UserRole).id == group_id:
+                return True
+        return return_value
+
+    def loadFullList(self, images, initial_load=False, open_group=None):
+        """
+        Replace the list of images and groups in the interface.
+        """
+        if not initial_load:
+            self.application.set_busy_cursor()
             self.main_window.displayProgressBar(len(images))
+        self.listView.clear()
+        # Load the list of groups and add them to the treeView
+        group_items = {}
+        self.add_sub_groups(group_items, parent_group_id=0)
+        if open_group is not None:
+            self.expand_group(open_group.id)
         # Sort the images by its filename considering language specific
         # characters.
-        images.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
+        images.sort(cmp=locale_compare, key=lambda image_object: os.path.split(unicode(image_object.filename))[1])
         for imageFile in images:
-            filename = os.path.split(unicode(imageFile))[1]
+            log.debug(u'Loading image: %s', imageFile.filename)
+            filename = os.path.split(imageFile.filename)[1]
             thumb = os.path.join(self.servicePath, filename)
-            if not os.path.exists(unicode(imageFile)):
+            if not os.path.exists(imageFile.filename):
                 icon = build_icon(u':/general/general_delete.png')
             else:
-                if validate_thumb(unicode(imageFile), thumb):
+                if validate_thumb(imageFile.filename, thumb):
                     icon = build_icon(thumb)
                 else:
-                    icon = create_thumb(unicode(imageFile), thumb)
-            item_name = QtGui.QListWidgetItem(filename)
-            item_name.setIcon(icon)
-            item_name.setToolTip(imageFile)
-            item_name.setData(QtCore.Qt.UserRole, imageFile)
-            self.listView.addItem(item_name)
-            if not initialLoad:
+                    icon = create_thumb(imageFile.filename, thumb)
+            item_name = QtGui.QTreeWidgetItem(filename)
+            item_name.setText(0, filename)
+            item_name.setIcon(0, icon)
+            item_name.setToolTip(0, imageFile.filename)
+            item_name.setData(0, QtCore.Qt.UserRole, imageFile)
+            if imageFile.group_id is 0:
+                self.listView.addTopLevelItem(item_name)
+            else:
+                group_items[imageFile.group_id].addChild(item_name)
+            if not initial_load:
                 self.main_window.incrementProgressBar()
-        if not initialLoad:
+        if not initial_load:
             self.main_window.finishedProgressBar()
         self.application.set_normal_cursor()
 
+    def validateAndLoad(self, files, target_group=None):
+        """
+        Process a list for files either from the File Dialog or from Drag and
+        Drop. This method is overloaded from MediaManagerItem.
+
+        ``files``
+            The files to be loaded.
+        """
+        self.loadList(files, target_group)
+        last_dir = os.path.split(unicode(files[0]))[0]
+        Settings().setValue(self.settingsSection + u'/last directory', last_dir)
+
+    def loadList(self, images, target_group=None, initial_load=False):
+        """
+        Add new images to the database. This method is called when adding images using the Add button or DnD.
+        """
+        if target_group is None:
+            # Find out if a group must be pre-selected
+            preselect_group = None
+            selected_items = self.listView.selectedItems()
+            if len(selected_items) > 0:
+                selected_item = selected_items[0]
+                if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageFilenames):
+                    selected_item = selected_item.parent()
+                if isinstance(selected_item, QtGui.QTreeWidgetItem):
+                    if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageGroups):
+                        preselect_group = selected_item.data(0, QtCore.Qt.UserRole).id
+            # Enable and disable parts of the 'choose group' form
+            if preselect_group is None:
+                self.choose_group_form.nogroup_radio_button.setChecked(True)
+                self.choose_group_form.existing_radio_button.setChecked(False)
+                self.choose_group_form.new_radio_button.setChecked(False)
+            else:
+                self.choose_group_form.nogroup_radio_button.setChecked(False)
+                self.choose_group_form.existing_radio_button.setChecked(True)
+                self.choose_group_form.new_radio_button.setChecked(False)
+            if self.manager.get_object_count(ImageGroups) == 0:
+                self.choose_group_form.existing_radio_button.setDisabled(True)
+                self.choose_group_form.group_combobox.setDisabled(True)
+            else:
+                self.choose_group_form.existing_radio_button.setDisabled(False)
+                self.choose_group_form.group_combobox.setDisabled(False)
+            # Ask which group the images should be saved in
+            if self.choose_group_form.exec_(selected_group=preselect_group):
+                if self.choose_group_form.nogroup_radio_button.isChecked():
+                    # User chose 'No group'
+                    parent_group = ImageGroups()
+                    parent_group.id = 0
+                elif self.choose_group_form.existing_radio_button.isChecked():
+                    # User chose 'Existing group'
+                    group_id = self.choose_group_form.group_combobox.itemData(
+                        self.choose_group_form.group_combobox.currentIndex(), QtCore.Qt.UserRole)
+                    parent_group = self.manager.get_object_filtered(ImageGroups, ImageGroups.id == group_id)
+                elif self.choose_group_form.new_radio_button.isChecked():
+                    # User chose 'New group'
+                    parent_group = ImageGroups()
+                    parent_group.parent_id = 0
+                    parent_group.group_name = self.choose_group_form.new_group_edit.text()
+                    self.manager.save_object(parent_group)
+        else:
+            parent_group = target_group.data(0, QtCore.Qt.UserRole)
+            if isinstance(parent_group, ImageFilenames):
+                if parent_group.group_id == 0:
+                    parent_group = ImageGroups()
+                    parent_group.id = 0
+                else:
+                    parent_group = target_group.parent().data(0, QtCore.Qt.UserRole)
+        # If no valid parent group is found, do nothing
+        if not isinstance(parent_group, ImageGroups):
+            return
+        # Save the new images in the database
+        self.save_new_images_list(images, group_id=parent_group.id, reload_list=False)
+        self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
+            initial_load=initial_load, open_group=parent_group)
+
+    def save_new_images_list(self, images_list, group_id=0, reload_list=True):
+        for filename in images_list:
+            if type(filename) is not str and type(filename) is not unicode:
+                continue
+            log.debug(u'Adding new image: %s', filename)
+            imageFile = ImageFilenames()
+            imageFile.group_id = group_id
+            imageFile.filename = unicode(filename)
+            self.manager.save_object(imageFile)
+        if reload_list:
+            self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
+
+    def dnd_move_internal(self, target):
+        """
+        Handle drag-and-drop moving of images within the media manager
+        """
+        items_to_move = self.listView.selectedItems()
+        # Determine group to move images to
+        target_group = target
+        if target_group is not None and isinstance(target_group.data(0, QtCore.Qt.UserRole), ImageFilenames):
+            target_group = target.parent()
+        # Move to toplevel
+        if target_group is None:
+            target_group = self.listView.invisibleRootItem()
+            target_group.setData(0, QtCore.Qt.UserRole, ImageGroups())
+            target_group.data(0, QtCore.Qt.UserRole).id = 0
+        # Move images in the treeview
+        items_to_save = []
+        for item in items_to_move:
+            if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
+                if isinstance(item.parent(), QtGui.QTreeWidgetItem):
+                    item.parent().removeChild(item)
+                else:
+                    self.listView.invisibleRootItem().removeChild(item)
+                target_group.addChild(item)
+                item.setSelected(True)
+                item_data = item.data(0, QtCore.Qt.UserRole)
+                item_data.group_id = target_group.data(0, QtCore.Qt.UserRole).id
+                items_to_save.append(item_data)
+        target_group.setExpanded(True)
+        target_group.sortChildren(0, QtCore.Qt.AscendingOrder)
+        # Update the group ID's of the images in the database
+        self.manager.save_objects(items_to_save)
+
     def generateSlideData(self, service_item, item=None, xmlVersion=False,
         remote=False, context=ServiceItemContext.Service):
         background = QtGui.QColor(Settings().value(self.settingsSection + u'/background color'))
@@ -148,7 +431,11 @@
             items = self.listView.selectedItems()
             if not items:
                 return False
-        service_item.title = unicode(self.plugin.nameStrings[u'plural'])
+        # Determine service item title
+        if isinstance(items[0].data(0, QtCore.Qt.UserRole), ImageGroups):
+            service_item.title = items[0].text(0)
+        else:
+            service_item.title = unicode(self.plugin.nameStrings[u'plural'])
         service_item.add_capability(ItemCapabilities.CanMaintain)
         service_item.add_capability(ItemCapabilities.CanPreview)
         service_item.add_capability(ItemCapabilities.CanLoop)
@@ -157,8 +444,16 @@
         service_item.theme = -1
         missing_items = []
         missing_items_filenames = []
-        for bitem in items:
-            filename = bitem.data(QtCore.Qt.UserRole)
+        # Expand groups to images
+        for bitem in items:
+            if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None:
+                for index in range(0, bitem.childCount()):
+                    if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames):
+                        items.append(bitem.child(index))
+                items.remove(bitem)
+        # Find missing files
+        for bitem in items:
+            filename = bitem.data(0, QtCore.Qt.UserRole).filename
             if not os.path.exists(filename):
                 missing_items.append(bitem)
                 missing_items_filenames.append(filename)
@@ -181,11 +476,70 @@
             return False
         # Continue with the existing images.
         for bitem in items:
-            filename = bitem.data(QtCore.Qt.UserRole)
+            filename = bitem.data(0, QtCore.Qt.UserRole).filename
             name = os.path.split(filename)[1]
             service_item.add_from_image(filename, name, background)
         return True
 
+    def __checkObject(self, objects, newObject, edit):
+        """
+        Utility method to check for an existing object.
+
+        ``edit``
+            If we edit an item, this should be *True*.
+        """
+        if objects:
+            # If we edit an existing object, we need to make sure that we do
+            # not return False when nothing has changed.
+            if edit:
+                for object in objects:
+                    if object.id != newObject.id:
+                        return False
+                return True
+            else:
+                return False
+        else:
+            return True
+
+    def checkGroupName(self, newGroup, edit=False):
+        """
+        Returns *False* if the given Group already exists, otherwise *True*.
+        """
+        groups = self.manager.get_all_objects(ImageGroups, ImageGroups.group_name == newGroup.group_name)
+        return self.__checkObject(groups, newGroup, edit)
+
+    def onAddGroupClick(self):
+        """
+        Called to add a new group
+        """
+        # Find out if a group must be pre-selected
+        preselect_group = 0
+        selected_items = self.listView.selectedItems()
+        if len(selected_items) > 0:
+            selected_item = selected_items[0]
+            if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageFilenames):
+                selected_item = selected_item.parent()
+            if isinstance(selected_item, QtGui.QTreeWidgetItem):
+                if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageGroups):
+                    preselect_group = selected_item.data(0, QtCore.Qt.UserRole).id
+        # Show 'add group' dialog
+        if self.add_group_form.exec_(show_top_level_group=True, selected_group=preselect_group):
+            new_group = ImageGroups.populate(parent_id=self.add_group_form.parent_group_combobox.itemData(
+                self.add_group_form.parent_group_combobox.currentIndex(), QtCore.Qt.UserRole),
+                group_name=self.add_group_form.name_edit.text())
+            if self.checkGroupName(new_group):
+                if self.manager.save_object(new_group):
+                    self.loadFullList(self.manager.get_all_objects(ImageFilenames,
+                        order_by_ref=ImageFilenames.filename))
+                    self.fill_groups_combobox(self.choose_group_form.group_combobox)
+                    self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
+                else:
+                    critical_error_message_box(
+                        message=translate('ImagePlugin.AddGroupForm', 'Could not add the new group.'))
+            else:
+                critical_error_message_box(
+                    message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
+
     def onResetClick(self):
         """
         Called to reset the Live background with the image selected,
@@ -206,9 +560,8 @@
         if check_item_selected(self.listView,
                 translate('ImagePlugin.MediaItem', 'You must select an image to replace the background with.')):
             background = QtGui.QColor(Settings().value(self.settingsSection + u'/background color'))
-            item = self.listView.selectedIndexes()[0]
-            bitem = self.listView.item(item.row())
-            filename = bitem.data(QtCore.Qt.UserRole)
+            bitem = self.listView.selectedItems()[0]
+            filename = bitem.data(0, QtCore.Qt.UserRole).filename
             if os.path.exists(filename):
                 if self.live_controller.display.directImage(filename, background):
                     self.resetAction.setVisible(True)
@@ -221,11 +574,10 @@
                         'the image file "%s" no longer exists.') % filename)
 
     def search(self, string, showError):
-        files = Settings().value(self.settingsSection + u'/images files')
+        files = self.manager.get_all_objects(ImageFilenames, filter_clause=ImageFilenames.filename.contains(string),
+            order_by_ref=ImageFilenames.filename)
         results = []
-        string = string.lower()
-        for file in files:
-            filename = os.path.split(unicode(file))[1]
-            if filename.lower().find(string) > -1:
-                results.append([file, filename])
+        for file_object in files:
+            filename = os.path.split(unicode(file_object.filename))[1]
+            results.append([file_object.filename, filename])
         return results

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2013-02-07 11:33:47 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2013-03-04 14:42:22 +0000
@@ -254,7 +254,7 @@
                 self.listView.takeItem(row)
             Settings().setValue(self.settingsSection + u'/media files', self.getFileList())
 
-    def loadList(self, media):
+    def loadList(self, media, target_group=None):
         # Sort the media by its filename considering language specific
         # characters.
         media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2013-02-07 11:33:47 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2013-03-04 14:42:22 +0000
@@ -120,7 +120,7 @@
         """
         self.listView.setIconSize(QtCore.QSize(88, 50))
         files = Settings().value(self.settingsSection + u'/presentations files')
-        self.loadList(files, True)
+        self.loadList(files, initialLoad=True)
         self.populate_display_types()
 
     def populate_display_types(self):
@@ -141,7 +141,7 @@
         else:
             self.presentationWidget.hide()
 
-    def loadList(self, files, initialLoad=False):
+    def loadList(self, files, target_group=None, initialLoad=False):
         """
         Add presentations into the media manager
         This is called both on initial load of the plugin to populate with

=== added file 'resources/forms/imagesaddgroupdialog.ui'
--- resources/forms/imagesaddgroupdialog.ui	1970-01-01 00:00:00 +0000
+++ resources/forms/imagesaddgroupdialog.ui	2013-03-04 14:42:22 +0000
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AddGroupDialog</class>
+ <widget class="QDialog" name="AddGroupDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>365</width>
+    <height>119</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Add group</string>
+  </property>
+  <layout class="QFormLayout" name="AddGroupLayout">
+   <property name="fieldGrowthPolicy">
+    <enum>QFormLayout::ExpandingFieldsGrow</enum>
+   </property>
+   <property name="horizontalSpacing">
+    <number>8</number>
+   </property>
+   <property name="verticalSpacing">
+    <number>8</number>
+   </property>
+   <property name="margin">
+    <number>8</number>
+   </property>
+   <item row="1" column="0">
+    <widget class="QLabel" name="ParentGroupLabel">
+     <property name="text">
+      <string>Parent group:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QComboBox" name="ParentGroupComboBox"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="GroupNameLabel">
+     <property name="text">
+      <string>Group name:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QLineEdit" name="GroupNameEdit"/>
+   </item>
+   <item row="3" column="1">
+    <widget class="QDialogButtonBox" name="GroupButtonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="../images/openlp-2.qrc"/>
+ </resources>
+ <connections/>
+</ui>

=== added file 'resources/forms/imageschoosegroupdialog.ui'
--- resources/forms/imageschoosegroupdialog.ui	1970-01-01 00:00:00 +0000
+++ resources/forms/imageschoosegroupdialog.ui	2013-03-04 14:42:22 +0000
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ChooseGroupDialog</class>
+ <widget class="QDialog" name="ChooseGroupDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>440</width>
+    <height>119</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Choose group</string>
+  </property>
+  <layout class="QFormLayout" name="addGroupLayout">
+   <property name="fieldGrowthPolicy">
+    <enum>QFormLayout::ExpandingFieldsGrow</enum>
+   </property>
+   <property name="horizontalSpacing">
+    <number>8</number>
+   </property>
+   <property name="verticalSpacing">
+    <number>8</number>
+   </property>
+   <property name="margin">
+    <number>8</number>
+   </property>
+   <item row="1" column="0" colspan="2">
+    <widget class="QLabel" name="groupQuestionLabel">
+     <property name="text">
+      <string>To which group do you want these images to be added?</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QComboBox" name="groupComboBox"/>
+   </item>
+   <item row="3" column="1">
+    <widget class="QDialogButtonBox" name="groupButtonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="../images/openlp-2.qrc"/>
+ </resources>
+ <connections/>
+</ui>

=== added file 'resources/images/image_group.png'
Binary files resources/images/image_group.png	1970-01-01 00:00:00 +0000 and resources/images/image_group.png	2013-03-04 14:42:22 +0000 differ
=== added file 'resources/images/image_new_group.png'
Binary files resources/images/image_new_group.png	1970-01-01 00:00:00 +0000 and resources/images/image_new_group.png	2013-03-04 14:42:22 +0000 differ
=== modified file 'resources/images/openlp-2.qrc'
--- resources/images/openlp-2.qrc	2012-12-06 19:26:50 +0000
+++ resources/images/openlp-2.qrc	2013-03-04 14:42:22 +0000
@@ -21,6 +21,10 @@
     <file>song_topic_edit.png</file>
     <file>song_book_edit.png</file>
   </qresource>
+  <qresource prefix="images">
+    <file>image_group.png</file>
+    <file>image_new_group.png</file>
+  </qresource>
   <qresource prefix="bibles">
     <file>bibles_search_text.png</file>
     <file>bibles_search_reference.png</file>


Follow ups